diff --git a/.data/attachments/inbound/rfq-20260728F@x.com.eml b/.data/attachments/inbound/rfq-20260728F@x.com.eml new file mode 100644 index 0000000..33fcc9b --- /dev/null +++ b/.data/attachments/inbound/rfq-20260728F@x.com.eml @@ -0,0 +1,12 @@ +{ + "messageId": "rfq-20260728F@x.com", + "from": "buyer@x.com", + "to": [ + "rfq@we.com" + ], + "cc": [], + "subject": "【询价】客户 C700 RFQ-20260728F 求报价", + "body": "客户编码 C700,询价单号 RFQ-��\nPN-FINAL-01 TI 年用量 9000 目标价 6.00 交期 10w", + "receivedAt": "2026-07-27T20:00:00.000Z", + "attachments": [] +} \ No newline at end of file diff --git a/.data/attachments/inbound/rfq-20260728G@x.com.eml b/.data/attachments/inbound/rfq-20260728G@x.com.eml new file mode 100644 index 0000000..e79ffef --- /dev/null +++ b/.data/attachments/inbound/rfq-20260728G@x.com.eml @@ -0,0 +1,12 @@ +{ + "messageId": "rfq-20260728G@x.com", + "from": "buyer@x.com", + "to": [ + "rfq@we.com" + ], + "cc": [], + "subject": "【询价】客户 C800 RFQ-20260728G 求报价", + "body": "客户编码 C800,询价单号 RFQ-��\nPN-DIST-01 TI 年用量 9000 目标价 6.00 交期 10w", + "receivedAt": "2026-07-27T19:00:00.000Z", + "attachments": [] +} \ No newline at end of file diff --git a/api-report/apps/api/src/modules/flow-engine/contract.d.ts b/api-report/apps/api/src/modules/flow-engine/contract.d.ts index 9b120f3..817e80b 100644 --- a/api-report/apps/api/src/modules/flow-engine/contract.d.ts +++ b/api-report/apps/api/src/modules/flow-engine/contract.d.ts @@ -23,6 +23,8 @@ export type AdvanceOutcome = { }; /** finished 时:末步验收工单的裁决决定终态 */ readonly released?: boolean; + /** 选择组的出口选择:恰发一口,随放行一并确认 */ + readonly selectedPorts?: Record; readonly blockedReason?: string; }; export type FlowEngineService = { diff --git a/api-report/apps/api/src/modules/task/port.d.ts b/api-report/apps/api/src/modules/task/port.d.ts index 27b4136..c435340 100644 --- a/api-report/apps/api/src/modules/task/port.d.ts +++ b/api-report/apps/api/src/modules/task/port.d.ts @@ -52,6 +52,8 @@ export type EngineOutcome = { readonly reason?: string; /** 谱走完时:放行是否通过 —— **完成只出自放行通过**,引擎不自行宣布完成 */ readonly released?: boolean; + /** 选择组的出口选择:恰发一口,随放行一并确认 */ + readonly selectedPorts?: Record; }; /** * 落地收敛 —— app 注入 `release`(L4)。 @@ -62,6 +64,8 @@ export type LandingGateway = { taskId: TaskId; ownerId: StaffId; humanCallCap: number; + /** 选择组的出口选择 —— 恰发一口,由放行时的确认给定 */ + selectedPorts: Readonly>; }): Promise<{ converged: boolean; emittedPorts: readonly string[]; diff --git a/apps/api/src/modules/memory/__tests__/task-memory.test.ts b/apps/api/src/modules/memory/__tests__/task-memory.test.ts index 3e4b34f..5ccb550 100644 --- a/apps/api/src/modules/memory/__tests__/task-memory.test.ts +++ b/apps/api/src/modules/memory/__tests__/task-memory.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'; import { assertAppendOnly, deriveSide, + deterministicDistill, distillable, ENTRY_KINDS, redactForReader, @@ -204,3 +205,27 @@ describe('归侧:治理干预', () => { expect(lines.map((l) => l.sideLabel)).toEqual(['人裁决', '机器产出']); }); }); + +describe('确定性蒸馏 —— 零 LLM 也留得下事实', () => { + it('终态、人兜底、驳回各归纳一条,且都带证据链', () => { + const drafts = deterministicDistill([ + entry('terminal', { seq: 1, title: '终态:released' }), + entry('card-sent', { seq: 2, title: '步「S3 归一」重试仍败,转人兜底' }), + entry('release-verdict', { seq: 3, title: '放行裁决:redo' }), + ]); + expect(drafts.map((d) => d.layer)).toEqual(['fact', 'fact', 'fact']); + expect(drafts.every((d) => d.entryIds.length > 0)).toBe(true); + expect(drafts[1]?.statement).toContain('转人兜底'); + }); + + it('不可学习 / 已脱敏的条目不入蒸馏', () => { + const drafts = deterministicDistill([ + entry('terminal', { seq: 1, governance: { masked: false, learnable: false } }), + ]); + expect(drafts).toEqual([]); + }); + + it('无可蒸馏来源时返回空,不硬造条目', () => { + expect(deterministicDistill([])).toEqual([]); + }); +}); diff --git a/apps/api/src/modules/memory/internal/domain/task-memory.ts b/apps/api/src/modules/memory/internal/domain/task-memory.ts index c7f1f5a..9b3f52d 100644 --- a/apps/api/src/modules/memory/internal/domain/task-memory.ts +++ b/apps/api/src/modules/memory/internal/domain/task-memory.ts @@ -116,3 +116,53 @@ export function redactForReader(entry: MemoryEntry, canSeeMasked: boolean): Memo if (!entry.governance.masked || canSeeMasked) return entry; return { ...entry, title: '【已脱敏】', ...(entry.detail === undefined ? {} : { detail: {} }) }; } + +/** + * 确定性蒸馏骨架 —— **零 LLM**,从结构化日志直接归纳的事实层条目。 + * + * 「结构化部分零 LLM,叙述是视图不是事实」(OS 8.1):这一单**确实发生了什么** + * 不需要模型来讲 —— 终态、被纠正的次数、人兜底了几步,都写在归侧条目里。 + * 模型可用时在此之上再补模式层与裁决层的归纳;不可用时也不至于什么都留不下。 + * + * 每条都带证据链(引用的条目 ID),与 LLM 产出的条目走同一道门。 + */ +export function deterministicDistill(entries: readonly MemoryEntry[]): ReadonlyArray<{ + layer: 'fact'; + statement: string; + entryIds: readonly string[]; +}> { + const source = distillable(entries); + if (source.length === 0) return []; + + const out: Array<{ layer: 'fact'; statement: string; entryIds: readonly string[] }> = []; + + const terminal = source.filter((e) => e.kind === 'terminal'); + if (terminal.length > 0) { + out.push({ + layer: 'fact', + statement: `本单以「${terminal[terminal.length - 1]!.title}」收场`, + entryIds: terminal.map((e) => e.id), + }); + } + + // 人兜底的步 = 机器没接住的地方,是资产迭代的首要线索 + const fallbacks = source.filter((e) => e.kind === 'card-sent' && e.title.includes('转人兜底')); + if (fallbacks.length > 0) { + out.push({ + layer: 'fact', + statement: `有 ${fallbacks.length} 步由机器转人兜底:${fallbacks.map((e) => e.title).join(';')}`, + entryIds: fallbacks.map((e) => e.id), + }); + } + + const rejections = source.filter((e) => e.kind === 'release-verdict' && !e.title.includes('pass')); + if (rejections.length > 0) { + out.push({ + layer: 'fact', + statement: `放行被驳回 ${rejections.length} 次`, + entryIds: rejections.map((e) => e.id), + }); + } + + return out; +} diff --git a/apps/api/src/modules/memory/internal/usecase/memory-service.ts b/apps/api/src/modules/memory/internal/usecase/memory-service.ts index 3966317..f1f377d 100644 --- a/apps/api/src/modules/memory/internal/usecase/memory-service.ts +++ b/apps/api/src/modules/memory/internal/usecase/memory-service.ts @@ -8,7 +8,14 @@ import type { ActivityMemoryEntry, MemoryLayer } from '../domain/activity-memory import { assertThreePart, MEMORY_LAYERS, selectForLoading } from '../domain/activity-memory.ts'; import { computeMetrics, structuredLog } from '../domain/task-log.ts'; import type { MemoryEntry } from '../domain/task-memory.ts'; -import { assertAppendOnly, deriveSide, distillable, redactForReader, windowOf } from '../domain/task-memory.ts'; +import { + assertAppendOnly, + deriveSide, + deterministicDistill, + distillable, + redactForReader, + windowOf, +} from '../domain/task-memory.ts'; export type MemoryServiceDeps = { readonly tasks: TaskMemoryStore; @@ -114,23 +121,36 @@ export function createMemoryService(deps: MemoryServiceDeps): MemoryService { /** 写入唯蒸馏:从任务记忆归纳,条目必挂证据链 */ async distill(input: DistillInput): Promise { - const model = await deps.model.systemModelName(); - if (model === undefined) return []; - const source = distillable(await deps.tasks.list(input.taskId)); if (source.length === 0) return []; - const { object } = await deps.model.generate({ - model, - prompt: - `${input.prompt}\n\n` + - '从以下任务记忆里归纳活动记忆条目。每条必须给出 layer(fact/pattern/verdict/exception)、' + - 'statement(一般化陈述)与 entryIds(引用的条目 ID,不得为空)。只输出 JSON。\n\n' + - source.map((e) => `${e.id} [${e.kind}] ${e.title}`).join('\n'), - outputSchema: DISTILL_SCHEMA, - }); + /* + * 先落**确定性骨架**(零 LLM):这一单确实发生了什么,写在归侧条目里, + * 不需要模型来讲。模型可用时再在其上补模式层 / 裁决层的归纳; + * 模型不可用(端点不通、未绑定)也不至于什么都留不下 —— + * 「结构化部分零 LLM,叙述是视图不是事实」。 + */ + const drafts: Array<{ layer: string; statement: string; entryIds: string[] }> = deterministicDistill( + source, + ).map((d) => ({ layer: d.layer, statement: d.statement, entryIds: [...d.entryIds] })); - const drafts = extractDrafts(object); + const model = await deps.model.systemModelName(); + if (model !== undefined) { + try { + const { object } = await deps.model.generate({ + model, + prompt: + `${input.prompt}\n\n` + + '从以下任务记忆里归纳活动记忆条目。每条必须给出 layer(fact/pattern/verdict/exception)、' + + 'statement(一般化陈述)与 entryIds(引用的条目 ID,不得为空)。只输出 JSON。\n\n' + + source.map((e) => `${e.id} [${e.kind}] ${e.title}`).join('\n'), + outputSchema: DISTILL_SCHEMA, + }); + drafts.push(...extractDrafts(object)); + } catch { + // 模型这一轮归纳不出来:确定性骨架已经落下,不因此丢掉整次蒸馏 + } + } const known = new Set(source.map((e) => e.id)); const now = deps.clock.now().toISOString(); diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 6957dc1..1781c14 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -15,7 +15,9 @@ | M5 | `process`、`trigger`、`@activity-sdk` + 六个活动包 | ✅ 完成 | | M6 | 前端:工作台五要素 → 治理台九功能;`minute`、`report` | 🟡 五要素九功能均已平移并接真实 API;`minute`/`report` 已落;编排画布只读态已接,手绘编辑器待接 | -**当前:393 个测试通过(含 1 组对真实 PG 的集成测试),门禁五道全绿。20 个后端模块 + 5 个包 + 6 个活动包 + 14 个前端 feature。** +**MVP 十条判据:① ② ③ ⑥ ⑦ ⑧ ⑨ ⑩ 已在真实系统上端到端跑通;④ ⑤ 的 loop 机制(CMMN 装载 / Enabled 集 / 断点 / 调人)跑通,余下部分受限于 LLM 端点。** + +**当前:402 个测试通过(含对真实 PG 的集成测试),门禁五道全绿。20 个后端模块 + 5 个包 + 6 个活动包 + 14 个前端 feature。** ### MVP 完成判据的跑通进度(README《MVP 完成判据》) @@ -24,8 +26,13 @@ | 1 | 治理台:员工岗位 / LLM 实例 / 流程包发布 / 触发器 / 邮件通道 | ✅ `pnpm db:seed` 一次做完 | | 2 | RFQ 邮件入站 → 归属解析 → 触发器命中起单 → A1 落到冯兵工作台 | ✅ 已跑通 | | 3 | A1 受理归一(flow):步序推进 → LLM 抽取 → 台账草稿 → 补全卡 → DMN 打标 → 放行落地 | ✅ 全链路跑通至 `released`:D1 条件跳步、S4 工具、S5 DMN 三分类(LM358DR → Type A)、放行后草稿合并正式版 2 行、输出口 `intake` 发射。**LLM 步因占位 key 走声明给定的人兜底**,接真 key 即走机器路径 | -| 4–9 | A3 / A2 / A4 / AP / A6 / A7 | 🟡 六个活动包已装入且静态审全过、流程包全图已发布;A1 释放后 G2 分流的下游节点逐条待跑 | -| 10 | 全程可审计 · 工作台五要素 · 治理台在途监控与叫停恢复 | 🟡 五要素齐活(任务 / 审批 / 消息 / 纪要 / 汇报),纪要「决议 → 登记起单」已端到端跑通;治理台在途监控 + 叫停 + 恢复已跑通且留痕归人裁决侧;终态封卷蒸馏钩子已挂,候选条目待验 | +| 4 | B 类 → A3 渠道寻源(loop) | 🟡 CMMN 装载(21 计划项)、Enabled 集计算、断点判定、Agent 回路调人 —— 全部跑通;**发询 → 数据监听 → 供应商回价续跑需真实 LLM**(Agent 每一轮决断都要模型) | +| 5 | C 类 → A2 替代选型(loop) | 🟡 同上:CMMN 装载(19 计划项)+ 回路推进跑通;跨人卡与交付放行需真实 LLM | +| 6 | A4 成本定价(flow) | ✅ 取价基准 → DMN 定价(利润模型 P-01)→ 算偏离率 → DMN 底价自检(底价门禁 P-02)→ 条件跳步 → 报价草案验收放行 → released | +| 7 | AP 定价审批(审批点) | ✅ 卡到 PM 丁子乔工位,依据快照齐;**驳回必附意见**(空意见被拒)、**驳回 → 回 A4 重开新实例**(A4 #2,旧实例不复活)、**通过 → 进 A6** | +| 8 | A6 报价议价(flow) | ✅ 正式报价单生成 → 写类动作**登记为动作意向**(不碰世界)→ 放行即落地(台账回写 1 行)→ 落地收敛 → 输出口发射 | +| 9 | 客户拒绝 → A7 情报归档 | ✅ 出口选 lost → G4 按 `outcome = "lost"` 判定 → A7 起单 → 历史价格情报回写(拒绝原因)、客户画像、落地率统计落账 → **end 节点 released,工作流「已完成」** | +| 10 | 全程可审计 · 工作台五要素 · 治理台在途监控与叫停恢复 | ✅ 任务记忆只追加、信封盖戳、归侧标注(实测 A7:机器产出 13 / 人裁决 3 / 世界等待 2 / 人产出 1);五要素齐活,纪要「决议 → 登记起单」端到端跑通;治理台在途监控 + 叫停 + 恢复跑通且留痕归人裁决侧;**终态封卷蒸馏钩子产出活动记忆候选条目**(确定性骨架,零 LLM,条目带证据链) | ## 常用命令 @@ -113,14 +120,13 @@ DMN 三分类规则 P-03 + 台账声明 + 样例集。depcruise 已实测拦下 ## 已知待办(按优先级) -1. **判据 ④–⑨ 逐节点端到端**:A1 已能到 `released` 且节点实例正确落 `terminal`, - 但 **G2 并行网关未把下游 A3 / A2 起出来** —— 工作流停在 A1 之后。 - 已排除:投递无失败、无异常日志、`selectEdges` 对并行分叉返回全部出边、 - `dispatchOf` 对活动节点返回 start-task。下一步从 `advanceFrom` 的实际入参查起 - (`onTaskTerminal` 里 `Object.values(ports)[0]` 目前是 `{}`, - `outputPortsOf` 按输出口声明的属性名取值、与台账列名对不上,值得先看这里)。 -2. **判据 ③ 的机器路径**:`JCAI_LLM_API_KEY` 现为占位符,LLM 步一律走声明给定的人兜底。 - 接任一 OpenAI 兼容端点即可验证机器路径;两条缝的 record/replay 也需先有一次 live 录制。 +1. **A4 的 P-01 因子**:客户等级与数量档目前在边映射上取默认档(常量), + 物料类型取的是 A1 真实打的标。接 CRM 后改为读 `shared.customer` 与按 EAU 分档 —— + P-01 决策表本就有兜底行,默认档不会让求值失真,但也谈不上真实。 +2. **LLM 端点**(唯一的外部依赖):`JCAI_LLM_API_KEY` 现为占位符。 + 影响面已收敛清楚:flow 的 LLM 步一律走**声明给定的人兜底**(全链因此跑通); + loop 的 Agent 每一轮决断都要模型,故 A3 / A2 在装载与第一轮之后必须转人。 + 接任一 OpenAI 兼容端点即可;两条缝的 record/replay 也需先有一次 live 录制。 3. **编排画布的手绘编辑器**:只读态(`FlowDiagram`)已接真实流程包; 原型的 `FlowEditor` / `flow-editor-core` / `manual-flow` 尚未平移。 4. **治理台写面**:员工增删、岗位授权编辑、LLM 实例 upsert、活动启停、记忆修剪等写动作