diff --git a/.data/attachments/inbound/e2e-20260728E3@x.com.eml b/.data/attachments/inbound/e2e-20260728E3@x.com.eml new file mode 100644 index 0000000..87afea7 --- /dev/null +++ b/.data/attachments/inbound/e2e-20260728E3@x.com.eml @@ -0,0 +1,12 @@ +{ + "messageId": "e2e-20260728E3@x.com", + "from": "buyer@acme.com", + "to": [ + "rfq@we.com" + ], + "cc": [], + "subject": "【询价】客户 C860 RFQ-20260728E3 求报价", + "body": "客户编码 C860,询价单号 RFQ-��\nLM358DR 厂牌 TI 年用量 12000 片 目标价 8.20 元 交期 10 周", + "receivedAt": "2026-07-28T12:00:00.000Z", + "attachments": [] +} \ No newline at end of file diff --git a/.data/attachments/inbound/e2e-20260728E4@x.com.eml b/.data/attachments/inbound/e2e-20260728E4@x.com.eml new file mode 100644 index 0000000..d884cbe --- /dev/null +++ b/.data/attachments/inbound/e2e-20260728E4@x.com.eml @@ -0,0 +1,12 @@ +{ + "messageId": "e2e-20260728E4@x.com", + "from": "buyer@acme.com", + "to": [ + "rfq@we.com" + ], + "cc": [], + "subject": "【询价】客户 C870 RFQ-20260728E4 求报价", + "body": "客户编码 C870,询价单号 RFQ-��\nTL072CDR 厂牌 TI 年用量 12000 片 目标价 8.20 元 交期 10 周", + "receivedAt": "2026-07-28T12:00:00.000Z", + "attachments": [] +} \ No newline at end of file diff --git a/apps/api/src/app/agent-runner.ts b/apps/api/src/app/agent-runner.ts index c52e0e5..dce6192 100644 --- a/apps/api/src/app/agent-runner.ts +++ b/apps/api/src/app/agent-runner.ts @@ -51,6 +51,8 @@ export type AgentRunnerDeps = { readonly gateway: ModelGateway; /** 活动声明的自定义模型名 */ readonly modelOf: (taskId: TaskId) => Promise; + /** token 回报:计入任务预算 —— 不计,预算熔断就永远不会响 */ + readonly onUsage: (taskId: TaskId, tokens: number) => Promise; }; export function createAgentRunner(deps: AgentRunnerDeps): { @@ -95,6 +97,7 @@ export function createAgentRunner(deps: AgentRunnerDeps): { taskId, label: 'loop-turn', }); + await deps.onUsage(taskId, result.usage.inputTokens + result.usage.outputTokens); return normalize(result.object, enabled); } catch (error) { /* diff --git a/apps/api/src/app/wiring/desk-elements.ts b/apps/api/src/app/wiring/desk-elements.ts index a334243..d5c22f1 100644 --- a/apps/api/src/app/wiring/desk-elements.ts +++ b/apps/api/src/app/wiring/desk-elements.ts @@ -45,18 +45,29 @@ export function wireDeskElements(deps: DeskElementDeps): { minute: MinuteService clock: deps.clock, }); - /** 归侧日志行 → 度量事实。判断类与断点由日志标题的确定形态识别,不另设埋点 */ - const toLogFact = (taskId: string, line: { at: string; side: LogFact['side']; title: string }): LogFact => ({ - taskId, - at: line.at, - side: line.side, - title: line.title, - isHumanToolCall: line.title.startsWith('发出:'), - // DMN / 机检的确定性求值在日志里是「求值:」;LLM 的判断落在「调用:」 - isDeterministicJudgement: line.title.startsWith('求值:'), - isJudgement: line.title.startsWith('求值:') || line.title.startsWith('调用:'), - isBreakpoint: line.title.includes('断点') || line.title.includes('交集为空'), - }); + /** + * 归侧日志行 → 度量事实。零埋点:判据全部取自结构化日志本身。 + * + * **判断次数**包含两类:确定性求值(DMN 决策表 / 机检 —— 落在纯工具与工具步) + * 与临场判断(LLM 步、loop 的语义性前提评估)。 + * DMN 覆盖率 = 前者 ÷ 两者之和 ——「临场搬进确定」的进度表。 + * 早先只认标题里的「求值:」,把跑得最多的纯工具步整个漏掉了,覆盖率恒为 0。 + */ + const toLogFact = (taskId: string, line: { at: string; side: LogFact['side']; title: string }): LogFact => { + const deterministic = + line.title.startsWith('求值:') || line.title.includes('(pure-tool)') || line.title.includes('(tool)'); + const situational = line.title.includes('(llm)'); + return { + taskId, + at: line.at, + side: line.side, + title: line.title, + isHumanToolCall: line.title.startsWith('发出:'), + isDeterministicJudgement: deterministic, + isJudgement: deterministic || situational, + isBreakpoint: line.title.includes('断点') || line.title.includes('交集为空'), + }; + }; const report = createReportService({ store: createReportStore(deps.db), diff --git a/apps/api/src/app/wiring/engines.ts b/apps/api/src/app/wiring/engines.ts index 5284f55..e798e89 100644 --- a/apps/api/src/app/wiring/engines.ts +++ b/apps/api/src/app/wiring/engines.ts @@ -270,6 +270,7 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring { criteria: [{ label: '成果齐备', level: 'L5', pass: false }], fields: [], }); + await taskStore.addConsumption(input.taskId, { humanCalls: 1 }); return { id: issued.order.id }; } const fields = Array.isArray(input.card['fields']) @@ -278,9 +279,11 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring { const issued = await deps.humanTool.issueExecution(context, { title, basis: basis.length > 0 ? basis : [{ k: '来源', v: '步序声明' }], - ask: String(input.card['reason'] ?? title), + ask: String(input.card['ask'] ?? input.card['reason'] ?? title), fields, }); + // 打扰率的计量对象:每发一张卡就是调了一次人 —— 不计就等于没有上限 + await taskStore.addConsumption(input.taskId, { humanCalls: 1 }); return { id: issued.order.id }; }, }; @@ -348,6 +351,10 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring { stepNo: input.stepNo, label: 'flow-step', }); + // token 计入任务预算 —— 单位成本与预算熔断都靠它,不计就等于没有预算 + await taskStore.addConsumption(input.taskId, { + tokens: result.usage.inputTokens + result.usage.outputTokens, + }); // 解析与校验归引擎(它才知道这一步声明了什么);这里只如实上交结果与原文 return { ok: true, @@ -373,6 +380,8 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring { data: taskData, agent: createAgentRunner({ gateway: deps.modelGateway, + // loop 的每一回合都要问模型 —— 不计 token,预算熔断就永远不会响 + onUsage: (taskId, tokens) => taskStore.addConsumption(taskId, { tokens }).then(() => undefined), modelOf: async (taskId) => { const { activityId } = await taskContext.of(taskId); const manifest = await deps.registry.manifestOf(activityId); diff --git a/apps/api/src/modules/flow-engine/internal/usecase/flow-runner.ts b/apps/api/src/modules/flow-engine/internal/usecase/flow-runner.ts index 373f526..62eeabd 100644 --- a/apps/api/src/modules/flow-engine/internal/usecase/flow-runner.ts +++ b/apps/api/src/modules/flow-engine/internal/usecase/flow-runner.ts @@ -128,7 +128,8 @@ export function createFlowRunner(deps: FlowRunnerDeps): FlowEngineService { taskId, kind: 'action', title: `步 ${index + 1} · ${step.label}(${step.kind})`, - detail: { step: step.name, attempt, input: payload }, + // 步类型显式入留痕:度量的判据由引擎给出,不靠解析标题后缀 + detail: { step: step.name, stepKind: step.kind, attempt, input: payload }, }); switch (step.kind) { diff --git a/apps/api/src/modules/task/internal/usecase/task-service.ts b/apps/api/src/modules/task/internal/usecase/task-service.ts index 96a8b86..41bb0aa 100644 --- a/apps/api/src/modules/task/internal/usecase/task-service.ts +++ b/apps/api/src/modules/task/internal/usecase/task-service.ts @@ -133,9 +133,14 @@ export function createTaskService(deps: TaskServiceDeps): TaskService { return { task: finished, verdict, progressed: false }; } + const startedAt = deps.clock.now().getTime(); const outcome = await engineOf(task).advance(taskId); - // 步计量:loop 的一次决策循环 / flow 的一个声明步 - const used = await deps.store.addConsumption(taskId, { steps: 1 }); + // 步计量:loop 的一次决策循环 / flow 的一个声明步。 + // 活跃时长按**这一拍真正花掉的时间**计 —— 挂起等人的时间不算活跃,那是世界等待。 + const used = await deps.store.addConsumption(taskId, { + steps: 1, + activeMs: Math.max(0, deps.clock.now().getTime() - startedAt), + }); await deps.store.markProgress(taskId, deps.clock.now()); const runtime = diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 1781c14..3ff74c3 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -17,7 +17,10 @@ **MVP 十条判据:① ② ③ ⑥ ⑦ ⑧ ⑨ ⑩ 已在真实系统上端到端跑通;④ ⑤ 的 loop 机制(CMMN 装载 / Enabled 集 / 断点 / 调人)跑通,余下部分受限于 LLM 端点。** -**当前:402 个测试通过(含对真实 PG 的集成测试),门禁五道全绿。20 个后端模块 + 5 个包 + 6 个活动包 + 14 个前端 feature。** +**当前:429 个测试通过(含对真实 PG 的集成测试),门禁五道全绿。20 个后端模块 + 5 个包 + 6 个活动包 + 14 个前端 feature。** + +**LLM:DeepSeek provider 已接,两档实例 `deepseek-v4-flash` / `deepseek-v4-pro` + +用途名 `quote-extractor`(→ flash)。密钥只从环境读、落库即密封。** ### MVP 完成判据的跑通进度(README《MVP 完成判据》) @@ -25,15 +28,36 @@ |---|---|---| | 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 | B 类 → A3 渠道寻源(loop) | 🟡 CMMN 装载(21 计划项)、Enabled 集计算、断点判定、Agent 回路调人 —— 全部跑通;**发询 → 数据监听 → 供应商回价续跑需真实 LLM**(Agent 每一轮决断都要模型) | -| 5 | C 类 → A2 替代选型(loop) | 🟡 同上:CMMN 装载(19 计划项)+ 回路推进跑通;跨人卡与交付放行需真实 LLM | +| 3 | A1 受理归一(flow):步序推进 → LLM 抽取 → 台账草稿 → 补全卡 → DMN 打标 → 放行落地 | ✅ **机器路径全程跑通,零人工兜底**:模型从询价原文抽出 pn/brand/eau/目标价/交期 → D1 条件跳步 → S4 工具 → S5 DMN 三分类 → 放行 → 草稿合并正式版 → 输出口发射 → `released` | +| 4 | B 类 → A3 渠道寻源(loop) | 🟡 CMMN 装载(21 计划项)、Enabled 集、断点判定、技能装配入信封、Agent 真实决策(接 DeepSeek 后每回合由模型定夺)、案例全景视图 —— 全部跑通;**多轮寻源直至交付放行**受限于回合成本,未跑满 | +| 5 | C 类 → A2 替代选型(loop) | 🟡 同上:CMMN 装载(19 计划项)+ 回路推进 + 技能装配 + 案例全景视图跑通;跨人卡与交付放行未跑满 | | 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,条目带证据链) | +## 活动资产盘点(六个活动) + +| 活动 | 范式 | 视图模版 | 技能 | 结晶片段 | +|---|---|---|---|---| +| A1 RFQ 需求分析 | flow | 事态全景 / 打标判据 / 成果预览 | 读懂一封询价 · 打标前先备齐判据 | 1 | +| A4 成本定价 | flow | 取价基准 / 定价与门禁 / 草案待放行 | 选对价格基准 · 读懂底价门禁的处置 | 1 | +| A6 报价议价 | flow | 正式报价单 / 放行即发出 | 拟一份能发出去的报价单 · 选对议价出口 | 0 | +| A7 情报归档 | flow | 结果登记 / 闭环待放行 | 把拒绝原因写成下一单的判据 | 0 | +| A3 渠道寻源 | loop | 案例全景(阶段 / 可为 / 缺口) | 按场景圈渠道 · 该等还是该问 | 0 | +| A2 替代选型 | loop | 案例全景 | 圈替代候选 · 什么时候必须问客户 | 0 | + +视图外壳与卡片件在 `@sdk`(`viewShell` / `kvCard` / `tableCard` / `stagesCard` / `noteCard`)—— +六个活动共用一套面貌;容器是 `sandbox=""` 的 iframe,样式内联自带。 + +## 端到端跑过的轮次 + +四轮完整链路(入站 → A1 → G2 → A3/A2 → 汇合 → A4 → AP → A6 → G4 → A7/end): +拒绝分支(lost → A7 → 终点)三轮、成交分支(won → 终点)一轮,均收敛为「已完成」。 +度量八指标全部有真实读数(自主率 100% / 有效自主率 95% / DMN 覆盖率 66% / +单位成本 73 token 每放行单 / 终态分布 released 95%)。 + ## 常用命令 ```bash