Archived
人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的: 件上就地改 · 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名, 于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 —— 而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。 · **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。 删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)—— 「没回传就是删了」会把一次不完整的回传变成一次删表。 · **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。 · 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。 判据与门 · 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来, 人填好的目标价它照样判「客户未提供」的毛病由此消掉。 · 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。 · 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开, 于是一单没打标就进了放行门。三个数全由基座现数。 · 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。 **指向两扇打不开的门,比不指还糟。** 界面不撒谎 · 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。 · 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、 往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。 · 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。 · 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。 · 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。 A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。 五道门禁全绿(971 passed)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
This commit is contained in:
@@ -1,619 +1,49 @@
|
||||
/**
|
||||
* 六个活动槽位(docs/MVP示例工作流.html §1.1–1.7)—— **契约就绪、实现留白**。
|
||||
* 六个活动槽位的**退役名单** —— 槽位时代到此为止(2026-07-31 清账)。
|
||||
*
|
||||
* 槽位 = 活动的契约面:身份 / 七件契约(入参、出口)/ 声明(域对象、工具名)/ 业务模型骨架。
|
||||
* 与工具的解耦就在声明:`declarations.tools` 只写**名**,注册、连接值、白名单全在治理侧。
|
||||
* A5 定价审批是**内置审批点**(审批不是任务,零开发、不入库),不在槽位之列。
|
||||
* 这个文件从前装着六份「契约就绪、实现留白」的占位清单:编排层跑通期,
|
||||
* 靠它们让流走得动。六个真活动包(`activities/quote-*`)全部转正之后,
|
||||
* 占位清单就该走 —— 留着它们不是「多一个备份」,是**三处会骗人的重影**:
|
||||
*
|
||||
* 实现待底座完备后逐个开发:届时以新包版本经安装通道换入,槽位契约不动。
|
||||
* · 活动库与试跑台上并排两个同名活动,选错一个就试了个空壳;
|
||||
* · 它们声明的 `project.*` / `intel.*` / `customer.*` / `supplier.*` 台账,
|
||||
* 与真活动包的 `quote_*.*` 是**同一批业务对象的两套地址** ——
|
||||
* 数据域那一屏上「统一受理记录」「替代对标表」「报价草案」各有两份,
|
||||
* 人分不出该看哪一个(实测 12 对重影,旧的那 19 张表一行数据都没有);
|
||||
* · 工具授权面也各留一份,治理台的授权清单跟着翻倍。
|
||||
*
|
||||
* **2026-07-29:活动不再分范式,一律案例模型**(用户定)。flow 那一路连同 flow-engine
|
||||
* 整个退役,所以占位骨架也从「机器步落台账 + 一张验收单」改成**案例骨架**:
|
||||
* 一个「办理」阶段挂着 SOP 每一件事(全部人工激活)+ 一张收口验收。
|
||||
* 代价写在明处:案例由 Agent 每拍决策,**没有模型可用时占位活动一步也走不动** ——
|
||||
* 这是范式统一的直接后果,不是缺陷;真实现时以新包版本换入真案例模型。
|
||||
* 所以这里只剩一件事:**把它们请出去**。名单里的活动播种时一律卸载;
|
||||
* 卸不掉就如实说(多半是还有在途任务引着它),不掀翻播种。
|
||||
* 物化出来的表不由这里删 —— 台账的规矩是「只增不破坏」,
|
||||
* 清空壳表是一次性的人工决定(2026-07-31 已在开发库执行),不该写进播种。
|
||||
*/
|
||||
import { createHash } from 'node:crypto';
|
||||
import { activityManifestSchema } from '@contracts';
|
||||
import type { ActivityManifest } from '@contracts';
|
||||
import type { Container } from './container.ts';
|
||||
import { declarationsFor } from './seed-mvp-data.ts';
|
||||
|
||||
/**
|
||||
* 槽位占位入口 —— **不是实现**:装载它只会得到「未实现」的诚实报错。
|
||||
* mock 只许在外界:这里绝不发既定载荷装作跑过(README 平移规则)。
|
||||
*/
|
||||
export const SLOT_ARTIFACT = [
|
||||
'// 活动槽位:契约已就绪,实现待底座完备后逐个开发。',
|
||||
"throw new Error('活动尚未实现 —— 这是槽位,契约见治理台活动库');",
|
||||
].join('\n');
|
||||
|
||||
const checksum = createHash('sha256').update(SLOT_ARTIFACT).digest('hex');
|
||||
|
||||
/**
|
||||
* 占位期的业务模型骨架 —— **案例骨架**:SOP 的每件事一个人工任务,末尾一张收口验收。
|
||||
*
|
||||
* 为什么全部 `manualActivationRule`:这些事都触达世界(问客户、查外部、写台账),
|
||||
* AM 4.1 原理一说得明白 —— 触达世界的项 Enabled ≠ Active,要人按一下才动。
|
||||
* 占位期尤其如此:占位不替活动做任何判断,它连「该不该做这件事」都不替人定。
|
||||
*
|
||||
* 收口验收单是活动 release 的唯一出口:没有它,人把前面的事都办完,任务仍不结案。
|
||||
*/
|
||||
const placeholderSkeleton = (sop: readonly string[], name: string) => {
|
||||
const items = sop
|
||||
.map(
|
||||
(label, i) =>
|
||||
` <humanTask id="T${i + 1}" name="${label}">\n` +
|
||||
` <itemControl><manualActivationRule id="m${i + 1}"/></itemControl>\n` +
|
||||
` </humanTask>`,
|
||||
)
|
||||
.join('\n');
|
||||
return {
|
||||
cmmnXml:
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
||||
`<definitions xmlns="http://www.omg.org/spec/CMMN/20151109/MODEL">\n` +
|
||||
` <case id="slot">\n` +
|
||||
` <caseFileModel>\n` +
|
||||
` <caseFileItem id="input" name="调用入参"/>\n` +
|
||||
` <caseFileItem id="draft" name="草稿台账"/>\n` +
|
||||
` </caseFileModel>\n` +
|
||||
` <casePlanModel id="plan" name="${name}" autoComplete="true">\n` +
|
||||
` <sentry id="s_in">\n` +
|
||||
` <onPart id="op_in" caseFileItemRef="input"><standardEvent>create</standardEvent></onPart>\n` +
|
||||
` </sentry>\n` +
|
||||
` <stage id="S1" name="办理" autoComplete="true">\n` +
|
||||
` <entryCriterion id="ec_s1" sentryRef="s_in"/>\n` +
|
||||
items.replace(/^ {6}/gm, ' ') +
|
||||
`\n </stage>\n` +
|
||||
` <milestone id="M_done" name="事已办完"/>\n` +
|
||||
` <humanTask id="T_release" name="收口验收">\n` +
|
||||
` <itemControl><manualActivationRule id="m_rel"/></itemControl>\n` +
|
||||
` </humanTask>\n` +
|
||||
` <timerEventListener id="TL_half" name="时限过半">\n` +
|
||||
` <timerExpression>PT12H</timerExpression>\n` +
|
||||
` </timerEventListener>\n` +
|
||||
` </casePlanModel>\n` +
|
||||
` </case>\n` +
|
||||
`</definitions>`,
|
||||
narrative:
|
||||
`§0 开案:${name}的**契约已就绪、实现留白**。这份说明书是槽位占位,不是业务方法 ——\n` +
|
||||
`它只交代这件事按 MVP 文档分几步办(${sop.join(' → ')}),不替活动做任何判断。\n\n` +
|
||||
`办理:每一件事都要人按一下才动(触达世界的项恒需人工激活)。\n` +
|
||||
`占位期没有自动步:占位若替人落一行台账,下游读到的就是一个编出来的事实。\n\n` +
|
||||
`§Z 结案:事办完、收口验收签字放行即结案。真实现时以新包版本换入真案例模型,\n` +
|
||||
`这份说明书连同 CMMN 一并被替换 —— 契约面不动。`,
|
||||
dmn: { drd: [], tables: [] },
|
||||
};
|
||||
};
|
||||
|
||||
const identity = (activityId: string, name: string) => ({
|
||||
activityId,
|
||||
name,
|
||||
/*
|
||||
* 包版本 —— 改了槽位携带的东西就必须往上走。
|
||||
*
|
||||
* 「安装恒为新增、版本并存」是硬不变量:同 (activityId, version) 再装是 no-op,
|
||||
* 库里的 manifest 一个字都不会动。所以 0.1.0 期间把卡面从空卡改成自述卡,
|
||||
* 重播种会「静默成功」—— 打印「已装入」,库里还是旧的(实测踩到)。
|
||||
*
|
||||
* 0.2.0:步骤卡面自述(标题 / 要你答 / 步序依据)。
|
||||
* 0.3.0:人工调用预算按各槽位自己的步数给 —— 共用 3 会让 6 步的 A1 走到第 3 步就
|
||||
* `exhausted` 终态、结构上永远走不完(实测)。授权面申报值同源。
|
||||
* 0.4.0:flow 骨架末步追加**收口验收**—— 没有它活动永远不 release,
|
||||
* 六步答完就卡在「等人却没东西给人答」(实测)。
|
||||
* 0.5.0:A1/A2 声明「受理结论」对象(两枚去向旗有处落地);A2 入参加 rfqRef。
|
||||
* 0.6.0:占位期骨架改**机器步落台账 + 一张验收单**(用户定:只留验收要人签)。
|
||||
* 六个槽位一律 flow 范式;预算固定 humanCalls: 2。
|
||||
* 0.6.1:修验收步的出参映射 —— 键是**台账列名**(小写蛇形),不是回传字段名。
|
||||
* 0.7.0:A6 的三个去向改**选择组**(契约 0.2.0)—— outcome 由放行选口给出,G4 才判得了。
|
||||
* 0.8.0:对齐 Mastra 数据流 —— A2 的入参 `material` 更名 `pn`(它就是 PN):
|
||||
* 边不再带映射,数据按字段名流动,上游发的名就得是下游收的名。
|
||||
* 0.9.0:范式统一(用户定)—— 业务模型从步序声明改为**案例模型**,
|
||||
* 身份里的 `paradigm` 字段随之消失;占位不再自动落台账。
|
||||
* 在途任务按启动时版本跑完,不受影响。
|
||||
*/
|
||||
packageVersion: '0.9.0',
|
||||
category: '统一报价',
|
||||
sdkVersion: '0.1.0-m5',
|
||||
checksum,
|
||||
});
|
||||
|
||||
const harness = { memoryWindow: 20, stallAfter: 'PT45M', nudge: [] };
|
||||
|
||||
/** 七件契约里逐槽位相同的空段 —— 槽位期没有必交付物/验收细则,实现期再补 */
|
||||
const contractRest = { requiredDeliverables: [], acceptanceCriteria: [], prohibitions: [] };
|
||||
|
||||
/**
|
||||
* 各槽位的步/项清单 —— **预算与骨架共用同一个数组**,数不上不会漂移。
|
||||
* flow 是声明式人类步(一步一张工单);loop 的项由 Agent 临场推进,
|
||||
* 最坏情况一项问一次人,所以也按项数给预算。
|
||||
*/
|
||||
const STEPS = {
|
||||
rfqIntake: ['接收登记', '追问补全信息', '逐条解析归一', '查历史价', '逐条定状况标', '定去向与派单'],
|
||||
altSelection: ['替代对标表', '初筛清单'],
|
||||
sourcingInquiry: ['寻源比价表', '供应商回价单'],
|
||||
costPricing: ['取价基准', '逐条定价'],
|
||||
quoteNegotiation: ['正式报价单'],
|
||||
intelArchive: ['结果登记', '画像更新', '落地率统计'],
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 预算 —— `humanCalls` 必须**够这个槽位实际要问人的次数**。
|
||||
*
|
||||
* 曾经六个槽位共用 `humanCalls: 3`,而那时 A1 有 6 个人类步:走到第 3 步就 `exhausted`
|
||||
* 终态、工作流转 abnormal,活动**结构上永远走不完**(实测:used 3 / cap 3,停在 A1)。
|
||||
* 现在占位期是**机器步 + 一张验收单**,只问人一次,所以固定给 2(1 张验收 + 1 次余量)。
|
||||
* 这条约束本身不随形态变:实现期活动自己声明真实预算时,实际要问人的次数 ≤ humanCalls。
|
||||
*/
|
||||
const budgetFor = () => ({
|
||||
steps: 40,
|
||||
tokens: 200_000,
|
||||
activeMinutes: 60,
|
||||
humanCalls: 2,
|
||||
});
|
||||
|
||||
/**
|
||||
* 授权面申报的人工调用预算 —— 与运行时真正吃的那个数**同源**。
|
||||
* 运行时只认 `resources.budget.humanCalls`(见 `wiring/engines.ts` 的 `caps`);
|
||||
* `authorization.humanCallBudget` 只给治理台显示。两者不一致,治理台就会摆一个假数。
|
||||
*/
|
||||
const authorizationFor = () => ({
|
||||
discretion: [],
|
||||
preauthorizedActions: [],
|
||||
humanCallBudget: budgetFor().humanCalls,
|
||||
});
|
||||
|
||||
export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
/* ── A1 客户RFQ受理(SOP · BD) ──────────────────────────────────── */
|
||||
activityManifestSchema.parse({
|
||||
identity: identity('rfq-intake', '客户RFQ受理'),
|
||||
contract: {
|
||||
activityId: 'rfq-intake',
|
||||
version: '0.1.0',
|
||||
input: {
|
||||
data: {
|
||||
type: 'object',
|
||||
required: ['customerCode', 'rfqRef'],
|
||||
properties: {
|
||||
customerCode: { type: 'string' },
|
||||
rfqRef: { type: 'string' },
|
||||
subject: { type: 'string' },
|
||||
body: { type: 'string' },
|
||||
quotedAt: { type: 'string' },
|
||||
},
|
||||
},
|
||||
messageAllowed: true,
|
||||
},
|
||||
output: {
|
||||
mandatory: [
|
||||
{
|
||||
name: 'intake',
|
||||
description: '统一受理记录(打标)+ 两枚去向旗',
|
||||
payload: {
|
||||
type: 'object',
|
||||
required: ['records', 'needAlternative', 'needSourcing'],
|
||||
properties: {
|
||||
records: { type: 'array' },
|
||||
pn: { type: 'string' },
|
||||
type_tag: { type: 'string' },
|
||||
needAlternative: { type: 'boolean' },
|
||||
needSourcing: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
selective: [],
|
||||
},
|
||||
...contractRest,
|
||||
authorization: authorizationFor(),
|
||||
resources: { tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['BD'], budget: budgetFor() },
|
||||
},
|
||||
declarations: {
|
||||
models: [],
|
||||
listeners: [],
|
||||
harness,
|
||||
tools: [{ name: 'crm.history-price-read' }],
|
||||
ledger: declarationsFor([
|
||||
'project.rfq_file',
|
||||
'project.intake_record',
|
||||
'project.pn_class',
|
||||
// 受理结论:两枚去向旗落在这里(一单一行,G1/G2 读同一个值)
|
||||
'project.intake_verdict',
|
||||
'project.alt_request',
|
||||
'project.inquiry_request',
|
||||
'intel.history_price',
|
||||
'customer.profile',
|
||||
]),
|
||||
},
|
||||
assets: {
|
||||
businessModel: placeholderSkeleton(STEPS.rfqIntake, '客户RFQ受理'),
|
||||
skills: [],
|
||||
viewTemplates: [],
|
||||
cardSchemas: {},
|
||||
distillHooks: [],
|
||||
},
|
||||
samples: [
|
||||
{
|
||||
name: '契约样例:受理一单',
|
||||
input: { customerCode: 'C001', rfqRef: 'RFQ-001' },
|
||||
expect: { ports: ['intake'] },
|
||||
},
|
||||
],
|
||||
entry: { file: 'index.js', checksum },
|
||||
}),
|
||||
|
||||
/* ── A2 替代选型(Case · FAE) ──────────────────────────────────── */
|
||||
activityManifestSchema.parse({
|
||||
identity: identity('alt-selection', '替代选型'),
|
||||
contract: {
|
||||
activityId: 'alt-selection',
|
||||
// 0.3.0:material 更名 pn(对齐 Mastra 名流动 —— 上游 A1 发的就是 pn)
|
||||
version: '0.3.0',
|
||||
input: {
|
||||
data: {
|
||||
type: 'object',
|
||||
required: ['pn', 'rfqRef'],
|
||||
properties: {
|
||||
pn: { type: 'string' },
|
||||
chip_type: { type: 'string' },
|
||||
spec_params: { type: 'string' },
|
||||
scenario: { type: 'string' },
|
||||
needSourcing: { type: 'boolean' },
|
||||
/*
|
||||
* 询价单号。声明它是为了**不偷渡**:边上映射一个契约没声明的字段,
|
||||
* 闭合校验的类型检查会因 `targetType === undefined` 直接跳过 —— 不报错,
|
||||
* 但那是默许不是正当,而且下一个读代码的人无从知道 A2 还收这个字段。
|
||||
*/
|
||||
rfqRef: { type: 'string' },
|
||||
},
|
||||
},
|
||||
messageAllowed: true,
|
||||
},
|
||||
output: {
|
||||
mandatory: [
|
||||
{
|
||||
name: 'benchmark',
|
||||
description: '替代对标表(已签字)+ needSourcing 原样带出(G2 在本路径读它)',
|
||||
payload: {
|
||||
type: 'object',
|
||||
required: ['rows', 'needSourcing'],
|
||||
properties: { rows: { type: 'array' }, needSourcing: { type: 'boolean' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selective: [],
|
||||
},
|
||||
...contractRest,
|
||||
authorization: authorizationFor(),
|
||||
resources: { tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['FAE'], budget: budgetFor() },
|
||||
},
|
||||
declarations: {
|
||||
models: [],
|
||||
listeners: [],
|
||||
harness,
|
||||
tools: [{ name: 'crm.history-price-read' }],
|
||||
// 受理结论同在声明里:A2 要把 needSourcing 回写**同一行**,去向只有一个出处
|
||||
ledger: declarationsFor([
|
||||
'intel.chip_spec',
|
||||
'intel.public_select',
|
||||
'project.alt_benchmark',
|
||||
'project.intake_verdict',
|
||||
]),
|
||||
},
|
||||
assets: {
|
||||
businessModel: placeholderSkeleton(STEPS.altSelection, '替代选型'),
|
||||
skills: [],
|
||||
viewTemplates: [],
|
||||
cardSchemas: {},
|
||||
distillHooks: [],
|
||||
},
|
||||
samples: [
|
||||
{
|
||||
name: '契约样例:一件替代评估',
|
||||
input: { pn: 'LM358DR' },
|
||||
expect: { ports: ['benchmark'] },
|
||||
},
|
||||
],
|
||||
entry: { file: 'index.js', checksum },
|
||||
}),
|
||||
|
||||
/* ── A3 寻源询价(Case · PRM) ──────────────────────────────────── */
|
||||
activityManifestSchema.parse({
|
||||
identity: identity('sourcing-inquiry', '寻源询价'),
|
||||
contract: {
|
||||
activityId: 'sourcing-inquiry',
|
||||
version: '0.1.0',
|
||||
input: {
|
||||
data: {
|
||||
type: 'object',
|
||||
required: ['pn'],
|
||||
properties: {
|
||||
pn: { type: 'string' },
|
||||
brand: { type: 'string' },
|
||||
era: { type: 'string' },
|
||||
project_stage: { type: 'string' },
|
||||
accept_alt: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
messageAllowed: true,
|
||||
},
|
||||
output: {
|
||||
mandatory: [
|
||||
{
|
||||
name: 'compare',
|
||||
description: '寻源比价表 + 供应商底价表',
|
||||
payload: {
|
||||
type: 'object',
|
||||
required: ['rows', 'floors'],
|
||||
properties: { rows: { type: 'array' }, floors: { type: 'array' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selective: [],
|
||||
},
|
||||
...contractRest,
|
||||
authorization: authorizationFor(),
|
||||
resources: {
|
||||
tools: [{ name: 'crm.history-price-read' }, { name: 'mail.send' }],
|
||||
skills: [],
|
||||
models: [],
|
||||
roles: ['PRM'],
|
||||
budget: budgetFor(),
|
||||
},
|
||||
},
|
||||
declarations: {
|
||||
models: [],
|
||||
listeners: [],
|
||||
harness,
|
||||
tools: [{ name: 'crm.history-price-read' }, { name: 'mail.send' }],
|
||||
ledger: declarationsFor([
|
||||
'supplier.channel_dir',
|
||||
'supplier.dead_stock',
|
||||
'project.sourcing_compare',
|
||||
'project.supplier_floor',
|
||||
]),
|
||||
},
|
||||
assets: {
|
||||
businessModel: placeholderSkeleton(STEPS.sourcingInquiry, '渠道寻源'),
|
||||
skills: [],
|
||||
viewTemplates: [],
|
||||
cardSchemas: {},
|
||||
distillHooks: [],
|
||||
},
|
||||
samples: [
|
||||
{
|
||||
name: '契约样例:一单询价',
|
||||
input: { pn: 'LM358DR' },
|
||||
expect: { ports: ['compare'] },
|
||||
},
|
||||
],
|
||||
entry: { file: 'index.js', checksum },
|
||||
}),
|
||||
|
||||
/* ── A4 成本定价(SOP · BD) ────────────────────────────────────── */
|
||||
activityManifestSchema.parse({
|
||||
identity: identity('cost-pricing', '成本定价'),
|
||||
contract: {
|
||||
activityId: 'cost-pricing',
|
||||
version: '0.1.0',
|
||||
input: {
|
||||
data: {
|
||||
type: 'object',
|
||||
required: ['records'],
|
||||
properties: {
|
||||
records: { type: 'array' },
|
||||
floors: { type: 'array' },
|
||||
benchmark: { type: 'array' },
|
||||
customerCode: { type: 'string' },
|
||||
},
|
||||
},
|
||||
messageAllowed: true,
|
||||
},
|
||||
output: {
|
||||
mandatory: [
|
||||
{
|
||||
name: 'draft',
|
||||
description: '报价草案:逐条定价,先过底价门禁再提审',
|
||||
payload: {
|
||||
type: 'object',
|
||||
required: ['rows'],
|
||||
properties: { rows: { type: 'array' }, quote_no: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selective: [],
|
||||
},
|
||||
...contractRest,
|
||||
authorization: authorizationFor(),
|
||||
resources: { tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['BD'], budget: budgetFor() },
|
||||
},
|
||||
declarations: {
|
||||
models: [],
|
||||
listeners: [],
|
||||
harness,
|
||||
tools: [{ name: 'crm.history-price-read' }],
|
||||
ledger: declarationsFor(['intel.history_price', 'customer.profile', 'project.quote_draft']),
|
||||
},
|
||||
assets: {
|
||||
businessModel: placeholderSkeleton(STEPS.costPricing, '成本定价'),
|
||||
skills: [],
|
||||
viewTemplates: [],
|
||||
cardSchemas: {},
|
||||
distillHooks: [],
|
||||
},
|
||||
samples: [
|
||||
{
|
||||
name: '契约样例:一批草案',
|
||||
input: { records: [] },
|
||||
expect: { ports: ['draft'] },
|
||||
},
|
||||
],
|
||||
entry: { file: 'index.js', checksum },
|
||||
}),
|
||||
|
||||
/* ── A6 报价议价(Case · BD) ───────────────────────────────────── */
|
||||
activityManifestSchema.parse({
|
||||
identity: identity('quote-negotiation', '报价议价'),
|
||||
contract: {
|
||||
activityId: 'quote-negotiation',
|
||||
version: '0.2.0',
|
||||
input: {
|
||||
data: {
|
||||
type: 'object',
|
||||
required: ['rows'],
|
||||
properties: { rows: { type: 'array' }, quote_no: { type: 'string' }, customerCode: { type: 'string' } },
|
||||
},
|
||||
messageAllowed: true,
|
||||
},
|
||||
output: {
|
||||
mandatory: [
|
||||
{
|
||||
name: 'formal',
|
||||
description: '正式报价单(已送出)+ 跟进提醒',
|
||||
payload: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
quote_no: { type: 'string' },
|
||||
customer_code: { type: 'string' },
|
||||
rows: { type: 'array' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
/*
|
||||
* 三个去向是**选择组**,不是必交口里的一个字段:人在放行时恰选一口,
|
||||
* 那一口就是这次运行的 outcome(引擎按选口注入),G4 判的正是它。
|
||||
* 从前写成 formal 口的必填 outcome + selective: [] —— 引擎只在选择组
|
||||
* 恰发一口时才注入 outcome,没有选择组就永远没有 outcome,G4 三条边全不成立。
|
||||
*/
|
||||
selective: [
|
||||
{
|
||||
group: '议价结果',
|
||||
ports: [
|
||||
{ name: 'won', description: '成交 —— 转订单履约', payload: { type: 'object' } },
|
||||
{ name: 'revise', description: '有异议 —— 退回重新定价', payload: { type: 'object' } },
|
||||
{ name: 'lost', description: '拒绝或超时 —— 归档闭环', payload: { type: 'object' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
...contractRest,
|
||||
authorization: authorizationFor(),
|
||||
resources: {
|
||||
tools: [{ name: 'crm.history-price-read' }, { name: 'mail.send' }],
|
||||
skills: [],
|
||||
models: [],
|
||||
roles: ['BD'],
|
||||
budget: budgetFor(),
|
||||
},
|
||||
},
|
||||
declarations: {
|
||||
models: [],
|
||||
listeners: [],
|
||||
harness,
|
||||
tools: [{ name: 'crm.history-price-read' }, { name: 'mail.send' }],
|
||||
ledger: declarationsFor(['project.formal_quote', 'project.followup']),
|
||||
},
|
||||
assets: {
|
||||
businessModel: placeholderSkeleton(STEPS.quoteNegotiation, '报价议价'),
|
||||
skills: [],
|
||||
viewTemplates: [],
|
||||
cardSchemas: {},
|
||||
distillHooks: [],
|
||||
},
|
||||
samples: [
|
||||
{
|
||||
name: '契约样例:一单议价',
|
||||
input: { rows: [] },
|
||||
expect: { ports: ['formal'] },
|
||||
},
|
||||
],
|
||||
entry: { file: 'index.js', checksum },
|
||||
}),
|
||||
|
||||
/* ── A7 情报归档(SOP · BD) ────────────────────────────────────── */
|
||||
activityManifestSchema.parse({
|
||||
identity: identity('intel-archive', '情报归档'),
|
||||
contract: {
|
||||
activityId: 'intel-archive',
|
||||
version: '0.1.0',
|
||||
input: {
|
||||
data: {
|
||||
type: 'object',
|
||||
required: ['outcome'],
|
||||
properties: {
|
||||
outcome: { type: 'string' },
|
||||
quote_no: { type: 'string' },
|
||||
customer_code: { type: 'string' },
|
||||
quotedAt: { type: 'string' },
|
||||
},
|
||||
},
|
||||
messageAllowed: true,
|
||||
},
|
||||
output: {
|
||||
mandatory: [
|
||||
{
|
||||
name: 'stats',
|
||||
description: '不成交也留资产:情报、客户画像、落地率',
|
||||
payload: {
|
||||
type: 'object',
|
||||
required: ['landing_rate'],
|
||||
properties: { landing_rate: { type: 'number' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
selective: [],
|
||||
},
|
||||
...contractRest,
|
||||
authorization: authorizationFor(),
|
||||
resources: { tools: [], skills: [], models: [], roles: ['BD'], budget: budgetFor() },
|
||||
},
|
||||
declarations: {
|
||||
models: [],
|
||||
listeners: [],
|
||||
harness,
|
||||
tools: [],
|
||||
ledger: declarationsFor(['intel.history_price', 'customer.profile', 'project.landing_stats']),
|
||||
},
|
||||
assets: {
|
||||
businessModel: placeholderSkeleton(STEPS.intelArchive, '情报归档'),
|
||||
skills: [],
|
||||
viewTemplates: [],
|
||||
cardSchemas: {},
|
||||
distillHooks: [],
|
||||
},
|
||||
samples: [
|
||||
{
|
||||
name: '契约样例:一次归档',
|
||||
input: { outcome: 'lost' },
|
||||
expect: { ports: ['stats'] },
|
||||
},
|
||||
],
|
||||
entry: { file: 'index.js', checksum },
|
||||
}),
|
||||
];
|
||||
/** 已退役的槽位 —— 真活动包顶替它之后,库里那一份也该走 */
|
||||
const RETIRED_SLOTS = [
|
||||
'rfq-intake',
|
||||
'alt-selection',
|
||||
'sourcing-inquiry',
|
||||
'cost-pricing',
|
||||
'quote-negotiation',
|
||||
'intel-archive',
|
||||
] as const;
|
||||
|
||||
export async function installSlots(container: Container): Promise<void> {
|
||||
for (const manifest of MVP_SLOTS) {
|
||||
const { activityId, packageVersion, name } = manifest.identity;
|
||||
|
||||
/*
|
||||
* 先问库里有没有这一版 —— 因为「安装恒为新增、版本并存」:同版再装是 no-op,
|
||||
* manifest 一个字不动。不问就报「已装入」等于**撒谎**:改了槽位内容重播种,
|
||||
* 屏幕说装好了,库里还是旧的,人拿着旧行为查新代码(实测踩到,那次是卡面没换)。
|
||||
*/
|
||||
const already = await container.registry
|
||||
.manifestOf(activityId, packageVersion)
|
||||
.then(() => true)
|
||||
for (const activityId of RETIRED_SLOTS) {
|
||||
const installed = await container.registry
|
||||
.get(activityId)
|
||||
.then((card) => card !== undefined)
|
||||
.catch(() => false);
|
||||
|
||||
const verdict = await container.registry.install(manifest, SLOT_ARTIFACT);
|
||||
const errors = verdict.staticFindings.filter((finding) => finding.severity === 'error');
|
||||
process.stdout.write(
|
||||
` 槽位 ${name}${already ? ` v${packageVersion} 已在库,未改动 —— 要换内容请升包版` : ` v${packageVersion} 已装`}` +
|
||||
` · 静态审 ${verdict.staticFindings.length} 条` +
|
||||
(errors.length > 0 ? ` —— ERROR: ${errors.map((e) => e.message).join('; ')}` : '') +
|
||||
'\n',
|
||||
);
|
||||
if (!installed) continue;
|
||||
try {
|
||||
await container.registry.uninstall(activityId);
|
||||
process.stdout.write(` 槽位 ${activityId} 已退役卸载(真活动包顶替)\n`);
|
||||
} catch (error) {
|
||||
process.stdout.write(
|
||||
` 槽位 ${activityId} 卸不掉:${error instanceof Error ? error.message : String(error)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user