forked from ChaiHao/BPOS-MVP
app 装配收尾:18 个模块接上,MVP 判据 1-2 端到端跑通
装配(app 是唯一组装层): - wiring/engines.ts:skill/flow/loop/task/process/trigger 六个模块经端口接起来; 两个范式引擎按统一 ParadigmEngine 契约暴露给 task —— 对上层不可区分 - agent-runner.ts:loop 的 Agent 回路经 ModelGateway(两缝之一,回放审看得见); 出参过 schema 框住三型行动,不合规退化为「等人」而不是乱动世界 - container:interaction / human-tool / activity-registry / release 全部接上; event 的 TriggerMatcher 与 org 的 InFlightWorkProbe 以延迟绑定解环,端口不变 - workers/consumers.ts:派发器的两个消费者(任务监听推进旧单 / 触发器开新单,独立判定) - tools/first-party.ts:查历史价(录制回放替身)+ 三分类打标(纯工具,DMN 确定性求值) - seed-quote.ts:治理台四件事一次做完 —— 绑 LLM / 注册工具两审 / 经安装通道装活动包 / 发布流程包(闭合校验通过)+ 配触发器与邮件通道 已验证(真机): 1. 治理台配置全过;活动包静态审 0 条提示、流程包闭合校验通过 2. 一封 RFQ 邮件入站 → 归属解析 → 触发器命中 1 → 起一条工作流 → A1 任务按岗位唯一人解析落到 BD 冯兵,runtime=running 修复:zod 判别式重复值(周期触发两型展开为 periodic-calendar / periodic-data, triggerKindOf 对回三类分类学);活动构建脚本移出 activities(它要用 fs, 而活动包零环境能力 —— depcruise 拦下后按边界搬家) 358 个测试通过,门禁五道全绿 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
co-authored by
Claude Opus 5
parent
c6cdb4c310
commit
78dc03bb58
@@ -0,0 +1,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)');
|
||||
}
|
||||
Reference in New Issue
Block a user