This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/apps/api/src/app/container.ts
T
xcsandClaude Fable 5 f3397a60ef E04 修到零被拒线,harness 补七刀:跨进程租约与广播桥、原子写、引用墙、对话区按序号排
活动侧(E04 2.1.0 → 2.1.9,I01 1.2.2):
- 覆盖闭合成为机检:基座新增 distinct 读数,「计划盖住的客户数 = 候选行数」钉进
  S3 收步 / S4 哨兵 / 放行 L2;人加的候选客户不再被整单无视
- 「关键人待识别」占位句式全链退役:查无联系人先发数据录入卡收人;契约新增 refersTo,
  计划/回执的 contact_code 只许引用真实存在的联系人(安装期核引用目标)
- 台账更新确认摆全落库面:客户档案/联系人档案两张落库预览(touched: 数据源,
  正式现值 + 草稿叠加),可补改、可加行(自增主键由基座续号);姓名升必填
- 更新明细:update_key 位置键 + 「同一处只留一条」L2;依据写人话不带机器键;
  标题旁不摆单号;判据与文案不夹字段名;行标识去掉单标识那一段
- 回执骨架 = 计划行数钉进 S3 收步与 S4 哨兵;重做分支不许删计划凑数
- 阶段刻度对齐文档 7 步;起单样例预填满

基座侧:
- 两个后端连同一个库:task-claim 跨进程租约(迁移 0030)+ Postgres LISTEN/NOTIFY
  广播桥;对话区按任务内记忆序号排,不按墙上时钟;并卡「刚说过话」按记忆序列判紧邻
- 多行 write 整批先验后写(dryRun),被拒 = 一行都没落
- 决策卡意见与本单草稿字面比对,归属以观察进轨迹(note-anchors)
- 括号栈补「补闭括号」两形(少一个 } / 行对象没关直接跟下一行)
- 件上加行:空着的自增主键直进续号,不从别的行借号;未落下的行点名回给前端
- 人答复落库那一刻就把 runtime 翻 running(task.wake),两块同帧变脸
- 试跑单可落真库(land_master,迁移 0031,默认开);数据域按活动分组;
  JSON 列不再是 [object Object];按钮键感 + 在办进度条 + 防抖
- 报价链 A1–A7 从系统卸载(只卸不删源码),种子只装 E04/I01

契约变更(api-report 已刷新):FieldDeclaration.distinct / refersTo,
objectDeclaration.file,landMaster,unkeyed,assetFiles,ledgerByActivityRoute。
1700 测 / typecheck / lint / depcruise / api:check 绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEJX8Z7CHejvwmDQyPQuBX
2026-08-25 01:23:30 +08:00

747 lines
28 KiB
TypeScript

/**
* DI 装配 —— `app/` 是**唯一组装层**(编码规范 §5.2)。
* 只有这里能同时认识多个模块:new 出 adapter、注入 port、把用例接起来。
*
* 跨层协作一律经端口注入:`ledger` 要发数据事件 → 注入 `event` 的出口;
* `event` 的三级消费下游(trigger L7 / message L2)在其**上**,同样经端口注入。
*/
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import { evaluateCondition } from '@sfeel';
import type { Db } from '@shared/db.ts';
import type { TaskId } from '@contracts';
import type { EventService, TriggerMatcher } from '@modules/event';
import { createEventService, createEventStore } from '@modules/event';
import type { LedgerService } from '@modules/ledger';
import type { LoopEngineService } from '@modules/loop-engine';
import { createDraftStore, createLedgerService, createLedgerStore, DRAFT_DDL } from '@modules/ledger';
import type { OrgService, InFlightWorkProbe } from '@modules/org';
import {
createGrantRepository,
createOrgService,
createPositionRepository,
createSessionRepository,
createStaffRepository,
createWorkGroupRepository,
hashPassword,
newSessionToken,
verifyPassword,
} from '@modules/org';
import type { LlmService, ModelGateway } from '@modules/llm';
import {
createLlmInstanceRepository,
createLlmService,
createModelGateway,
createProviderInvoker,
createReplayStore,
} from '@modules/llm';
import type { MemoryService } from '@modules/memory';
import { createActivityMemoryStore, createMemoryService, createTaskMemoryStore } from '@modules/memory';
import type { ToolboxService, ToolHandler, ToolInvoker } from '@modules/toolbox';
import {
createSchemaValidator,
createToolHandoffStore,
createToolInvoker,
createToolRegistry,
createToolReplayStore,
createToolRuntime,
createToolboxService,
} 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 { createSkillEngineService } from '@modules/skill-engine';
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 type { MinuteService } from '@modules/minute';
import type { ReportService } from '@modules/report';
import { FIRST_PARTY_TOOLS } from './tools/first-party.ts';
import { httpToolHandler } from './tools/http-tool.ts';
import { createMastraSpanFactory } from './mastra-spans.ts';
import { createTraceService, createTraceStore } from '@modules/trace';
import type { TraceService } from '@modules/trace';
import { wireEngines } from './wiring/engines.ts';
import { wireDeskElements } from './wiring/desk-elements.ts';
import type { ChannelService } from '@modules/channel';
import {
createChannelRepository,
createChannelService,
createFileAttachmentArchive,
createSimulatedMailTransport,
} from '@modules/channel';
import type { AppConfig } from './config.ts';
import { createIdGenerator, systemClock } from './runtime.ts';
import { createSecretBox, loadSecretKey } from './secret-box.ts';
import { createBroadcaster } from './broadcast.ts';
import { noteAnchors, noteAnchorTitle } from './note-anchors.ts';
import { bridgeBroadcaster } from './broadcast-bridge.ts';
import type { Broadcaster } from './broadcast.ts';
export type Container = {
readonly config: AppConfig;
/** 投递回执广播 —— 前端 SSE 的服务端一侧;不承担投递语义 */
readonly broadcaster: Broadcaster;
readonly pool: Pool;
readonly db: Db;
readonly org: OrgService;
readonly ledger: LedgerService;
readonly event: EventService;
readonly llm: LlmService;
readonly modelGateway: ModelGateway;
readonly memory: MemoryService;
readonly toolbox: ToolboxService;
readonly toolInvoker: ToolInvoker;
readonly message: MessageService;
readonly channel: ChannelService;
readonly humanTool: HumanToolService;
readonly interaction: InteractionService;
readonly registry: ActivityRegistryService;
readonly release: ReleaseService;
readonly task: TaskService;
/** 案例引擎 —— 工作台按它算阶段进度;推进照旧走 task,不从这里走 */
readonly loop: LoopEngineService;
/** 视图闸 —— 人在件上改完数据,按同一张模版用新数据重渲一帧(`views.repush`) */
readonly views: { repush(taskId: TaskId): Promise<boolean> };
/** 此刻这一单的判据 —— 按当下数据现算(发卡那一刻算死的话,人补一格判据不会动) */
readonly checks: { of(taskId: TaskId): Promise<ReadonlyArray<{ key: string; label: string; level: string; pass: boolean; settled?: boolean; rationale?: string }>> };
readonly process: ProcessService;
readonly trigger: TriggerService;
readonly minute: MinuteService;
readonly report: ReportService;
/** 全链观测:工作流 → 节点 → 步骤 / 工具 / 模型 / 人卡 */
readonly trace: TraceService;
close(): Promise<void>;
};
/**
* 在途工作探针 —— 停用前置检查要问的是 task(L6)与 human-tool(L3),二者层级在 org 之上,
* 故以**延迟绑定**的端口接入:org 先构造,真身就位后回填(§5.2 跨层协作经端口)。
*/
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;
},
};
}
/**
* 触发器判定的**延迟绑定**:`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)。
* 表达式写坏了不该让事件流停摆:求值异常按「不命中」处置并留痕。
*/
const sfeelEvaluator = (expression: string, scope: Readonly<Record<string, unknown>>): boolean => {
try {
return evaluateCondition(expression, scope);
} catch (error) {
process.stderr.write(
`[listener] 条件求值失败,按不命中处置:${expression} —— ${error instanceof Error ? error.message : String(error)}\n`,
);
return false;
}
};
/** 第一方工具实现 —— 由 app 注册进 ToolRuntime;活动只见调用契约 */
export const firstPartyToolHandlers = new Map<string, ToolHandler>(FIRST_PARTY_TOOLS);
export async function createContainer(config: AppConfig): Promise<Container> {
/*
* 广播器**先建** —— 装配期就要往它上面接线。
*
* 它从前是在 `return` 里现造的,于是装配期谁也拿不到它:
* 对话流推送、视图推送、发消息这三条**全都没有广播**,
* 前端只好靠人点刷新(用户实测报的:任务列表 / 消息气泡 / 工作区 / 对话区都不自己动)。
*/
const broadcaster = createBroadcaster();
const pool = new Pool({ connectionString: config.databaseUrl, max: 10 });
/*
* 空闲连接被服务端掐断(重启、`pg_terminate_backend`、网络抖动)会以 'error' 事件抛到池上;
* **没有处理器就是未捕获异常,整个进程随之退出** —— 一次运维操作能把 API 带崩。
* 池会自行剔除坏连接并在下次取用时重连,这里只负责让它别掀桌子。
*/
pool.on('error', (error: Error) => {
process.stderr.write(`[pg] 空闲连接异常(已剔除,下次取用自动重连):${error.message}\n`);
});
const db = drizzle(pool);
/*
* **广播接到库上** —— 多个后端连同一个库时,别的进程动了同一张单,
* 这边的浏览器也得知道(不然就停在「AI 推进中」直到人手动刷新,2026-08-24 用户实测)。
* 单进程部署时它只是空转一条 LISTEN 连接,没有副作用。
*/
const bridge = bridgeBroadcaster(pool, broadcaster);
const ids = createIdGenerator();
const secrets = createSecretBox(loadSecretKey());
// 草稿层的建表: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),
sessions: createSessionRepository(db),
workGroups: createWorkGroupRepository(db),
// 散列算法在 adapter,usecase 只认这三个动作(ADR-010)
passwords: { hash: hashPassword, verify: verifyPassword, newToken: newSessionToken },
inFlight: inFlightProbe,
clock: systemClock,
seedPassword: config.seedPassword,
ids,
});
// message 是 event 的 `MessageFallback` 实现面 —— event(L1)不反向依赖它,由此处注入
const message = createMessageService({
repo: createMessageRepository(db),
org: {
staffOfPosition: async (positionId) => {
const { staff } = await org.listStaff();
return staff.filter((s) => s.positionId === positionId).map((s) => ({ id: s.id }));
},
governanceStaff: async () => {
const { staff } = await org.listStaff();
const governing = await Promise.all(
staff.map(async (s) => ({
s,
ok: await org.authorize(s.id, { kind: 'governance', function: 'task' }, 'govern'),
})),
);
return governing.filter((g) => g.ok).map((g) => ({ id: g.s.id }));
},
},
clock: systemClock,
ids,
});
const triggerMatcher = createDeferredTriggerMatcher();
const event = createEventService({
store: createEventStore(db, ids),
clock: systemClock,
ids,
evaluate: sfeelEvaluator,
triggers: triggerMatcher,
messages: message,
});
const ledger = createLedgerService({
store: createLedgerStore(pool),
drafts: createDraftStore(pool),
events: { publish: async (envelope) => void (await event.publish(envelope)) },
clock: systemClock,
ids,
/*
* 试跑判定 —— 设计台有**两条**试跑路径,认漏一条就等于没挡:
* 单活动试跑:起的是不挂工作流的单,标 `task.origin = 'testbed'`
* 流程包试跑:节点任务挂在工作流上,标在**流**身上 `workflow.source->>'ref'`
* 一条 SQL 两条都认。认不出就当生产单(宁可少挡也不误挡:误挡会让真单丢主数据)。
*/
simulation: {
isSimulated: async (taskId) => {
const res = await pool.query<{ simulated: boolean }>(
/*
* **勾了「落真库」的试跑单不算试跑** —— 这道判断只服务一件事:
* 放行合并时主数据要不要挡下来(见 `ledger-service`)。
* 2026-08-24 用户要的正是这个:演示时要看见 `customer.contact` 的亲密度真的变了,
* 而不是单据里写着「3 → 10」、主数据还是 3。默认仍然挡 —— 破例得有人按一下。
*/
`select exists (
select 1 from sys.task t
left join sys.workflow w on w.id = t.workflow_id
where t.id = $1
and (t.origin = 'testbed' or w.source->>'ref' = 'testbed')
and coalesce(t.land_master, false) = false
) as simulated`,
[taskId],
);
return res.rows[0]?.simulated === true;
},
},
});
const instances = createLlmInstanceRepository(db);
const provider = createProviderInvoker();
const llm = createLlmService({ instances, provider, secrets, clock: systemClock, ids });
const modelGateway = createModelGateway({
instances,
provider,
replay: createReplayStore(db),
secrets,
clock: systemClock,
mode: 'live',
});
const memory = createMemoryService({
tasks: createTaskMemoryStore(db),
activities: createActivityMemoryStore(db),
model: {
// 系统用途模型:叙述性小结 / 汇报生成 / 蒸馏钩子 —— 计量入系统账,不占活动预算
generate: async (input) => {
const result = await modelGateway.generate({
model: input.model,
prompt: input.prompt,
...(input.outputSchema === undefined ? {} : { outputSchema: input.outputSchema }),
label: 'system-use',
});
return { text: result.text, ...(result.object === undefined ? {} : { object: result.object }) };
},
systemModelName: async () => (await llm.systemUseBinding())?.name,
},
clock: systemClock,
ids,
});
const toolRegistry = createToolRegistry(db);
// 导入的自定义工具走通用 HTTP 实现:契约申报 endpoint,运行时只把那一个连接目标交出去
const toolRuntime = createToolRuntime(firstPartyToolHandlers, httpToolHandler);
const schemas = createSchemaValidator();
const toolbox = createToolboxService({
registry: toolRegistry,
runtime: toolRuntime,
schemas,
secrets,
clock: systemClock,
});
/** 工具转人台(§6.2):系统里没有这件工具时,请人代办 —— 人答回的值就是这次调用的出参 */
const toolHandoff = createToolHandoffStore(db);
const toolInvoker = createToolInvoker({
registry: toolRegistry,
runtime: toolRuntime,
replay: createToolReplayStore(db),
schemas,
secrets,
clock: systemClock,
mode: 'live',
});
const channel = createChannelService({
channels: createChannelRepository(db),
events: {
ingestInbound: (input) => event.ingestInbound(input),
recordOutbound: (ref) => event.recordOutbound(ref),
},
ledger: { systemWrite: (input) => ledger.systemWrite(input) },
mail: createSimulatedMailTransport(db, ids),
archive: createFileAttachmentArchive(config.attachmentRoot),
clock: systemClock,
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: [] },
})),
// 对话流的 AI 发言与系统事件由此投影而来 —— 与工作日志同源
history: (taskId) => memory.history(taskId),
},
audience: { canSee: async () => true },
clock: systemClock,
ids,
});
/*
* **对话流与视图推送接进广播** —— `interaction.subscribe` 从前一个订阅者都没有:
* AI 说的每一句、推的每一帧画面都广播给了空气,前端只好靠人点刷新。
*
* 这里只做转接,不带业务载荷:推送说「这个任务动了」,画面照旧由服务端投影重建
* (§9.2 前端零过程状态)。收件人是谁不在信封里,所以按任务广播 ——
* 前端按任务键失效,负责人那一屏自然重取。
*/
interaction.onPush((envelope) => {
// invalidate 信封自己说目标(task / workflow / desk);其余信封都以任务为轴
if (envelope.kind === 'invalidate') {
broadcaster.publish({
kind: envelope.target === 'workflow' ? 'workflow' : 'task',
id: envelope.id,
reason: 'invalidate',
});
return;
}
broadcaster.publish({ kind: 'task', id: envelope.taskId, reason: envelope.kind });
});
const humanTool = createHumanToolService({
cards: createCardStore(db),
outbox: {
/*
* 跨人卡以系统消息送达对方 —— 送到了就**广播给那个人**:
* 他的消息气泡该立刻多一条,而不是等他自己点刷新。
* 发消息不经事件投递(那条路才有广播),所以这里得自己说一声。
*/
sendElicitation: async (input) => {
const sent = await message.sendElicitation(input);
broadcaster.publish({ kind: 'staff', id: input.staffId, reason: 'elicitation' });
return sent;
},
},
memory: {
append: async (input) => {
const stamp = { contractVersion: '1', businessModelVersion: '1', skillVersions: [] };
await memory.append({ ...input, stamp });
/*
* **驳回意见点了谁,基座当场比对好摆进轨迹**(2026-08-24 柴浩实测:意见写
* 「测试111建议动作改成拜访CTO」,模型把改动落到了 Itelma 那一行)。
* 归属是纯字符串包含 —— 引擎有现成事实的,不让模型从一句没标点的话里自己抠。
* 观察挂了不拦回传:它是给下一拍的路标,不是这一笔的门。
*/
if (input.kind === 'card-reply') {
const detail = (input.detail ?? {}) as { note?: unknown; value?: unknown };
const custom = typeof detail.value === 'string' && detail.value.startsWith('custom:') ? detail.value.slice('custom:'.length) : '';
const note = typeof detail.note === 'string' && detail.note.trim() !== '' ? detail.note : custom;
if (note.trim() !== '') {
try {
const hits = noteAnchors(note, await ledger.taskDrafts(input.taskId));
if (hits.length > 0) {
await memory.append({ taskId: input.taskId, kind: 'observation', title: noteAnchorTitle(hits), stamp });
}
} catch {
/* 比对失败只是少一条路标 */
}
}
}
},
},
events: {
publishHumanEvent: async (input) => {
/*
* 转人代办的回传**就是工具出参**:先落转人台,再抛事件。
*
* 顺序不能反 —— 事件会唤起下一拍,那一拍 invoke 必须立刻取到答案;
* 先抛后写就会空跑一拍(拍里还是「等人」,而人其实已经答了)。
* 卡不是转人卡时 `answer` 查无此行,静默无事发生。
*/
const values = input.payload['values'];
if (typeof values === 'object' && values !== null && !Array.isArray(values)) {
await toolHandoff.answer(input.cardId, values as Record<string, unknown>);
}
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',
});
/*
* skill-engine(L4)—— 技能是活动内部的构件格式,由两个范式引擎装配。
* 目录与白名单都取自**活动清单**:技能提及的工具不在本活动白名单者不可调,
* 装配前查扩权 —— 载方法,不载权力(AM 08)。
*/
const skills = createSkillEngineService({
catalog: {
skillsOf: async (activityId) => [...(await registry.manifestOf(activityId)).assets.skills],
allowlistOf: async (activityId) =>
(await registry.manifestOf(activityId)).declarations.tools.map((t) => t.name),
},
tools: {
invoke: (input) => toolInvoker.invoke(input),
},
memory: {
append: async (input) =>
void (await memory.append({
...input,
stamp: { contractVersion: '1', businessModelVersion: '1', skillVersions: [] },
})),
},
clock: systemClock,
});
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: []`,是这条链上的最后一处丢值:
* 契约声明了、submit 传了、release 转了,到这儿被一句空数组抹平,
* 验收报告于是只能看不能改(第 12 章③)。
*/
fields: (input.fields ?? []).map((f) => ({ ...f })) as never,
/*
* 正文块照传 —— 与 `fields` 同一个坑:这个显式对象是这条链的最后一关,
* 少写一行,上游传了一路的东西就在这里静默落地(卡面 blocks 恒空,踩过两次)。
*/
...(input.blocks === undefined || input.blocks.length === 0
? {}
: { blocks: input.blocks.map((b) => ({ ...b })) as never }),
},
);
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:范式引擎、任务、流程层 ─────────────────────────────── */
/*
* 全链观测:一条工作流一条 trace。span 由 Mastra 的 Observability 组件产出
* (我们的执行不经 Mastra 图,所以手动打点),一开就落库 —— 测试台要「点一下就看得见」。
*/
const trace = createTraceService({
store: createTraceStore(db),
clock: systemClock,
spans: createMastraSpanFactory(),
});
const engines = wireEngines({
db,
trace,
clock: systemClock,
ids,
event,
ledger,
memory,
toolbox,
toolInvoker,
toolHandoff,
humanTool,
interaction,
message,
org,
llm,
modelGateway,
registry,
release,
skills,
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,
}),
});
// L8:工作台后两个要素 —— 只读 memory 与 task 的投影,一行都不另记
const deskElements = wireDeskElements({
db,
memory,
task: engines.task,
org,
process: engines.process,
ids,
clock: systemClock,
});
return {
config,
broadcaster,
pool,
db,
org,
ledger,
event,
llm,
modelGateway,
memory,
toolbox,
toolInvoker,
message,
channel,
humanTool,
interaction,
registry,
release,
task: engines.task,
loop: engines.loop,
/** 件是投影 —— 人在件上改完数据,得能按同一张模版用新数据重渲一帧(见 `views.repush`) */
views: engines.views,
checks: engines.checks,
process: engines.process,
trigger: engines.trigger,
minute: deskElements.minute,
report: deskElements.report,
trace,
async close() {
bridge.stop();
await pool.end();
},
};
}