forked from ChaiHao/BPOS-MVP
归档过时文档,立仓库根 CLAUDE.md 记最新状态
**归档四份**(docs/archive/,每份头部带归档说明与现行去向,另配 archive/README.md 索引):
· MVP规格包-README.md(原 docs/README.md)—— 从零实现的启动包,MVP 已建成,
启动提示词与里程碑纪律不再是现行指引;代码注释「README《归一化对照表》」等引用
的各节原文保留,查历史语境仍来这里;
· PROGRESS.md —— M0-M6 全部完成后停止维护,「下一步」多数已做完或被后续决策取代;
· plan-2026-07-27-{mvp-foundation,three-surfaces}.md —— 两份已执行完毕的实施计划
(三面形态此后又有演进:运营撤数据区、触发器与大模型独立成区)。
语义源三份 html(操作系统/活动模型/MVP示例)不归档 —— 仍是 OS/AM 条款的出处,
只是 flow 相关章节读作历史记述。
**新立 CLAUDE.md**(仓库根,每会话装载):现状快照(三面一体 · 编排层测死 · 范式退役
一律案例模型 · 无模型密钥时三套 e2e 跳过 · 下一件大事 = 按活动开发规范第 12 章销账)、
文档权威地图(活文档/语义源/前端权威/归档)、八条铁规矩(五闸以退出码验 · 契约四步 ·
§12.5 禁止清单 · mock 只在外界 · 密钥只从环境 · 试跑用 AI 账号测完清数据 ·
安装恒新增改必升版 · 界面不撒谎)、常用命令与结构速查。
五闸绿。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
> **⚠️ 已归档(2026-07-30)** —— 这是 2026-07-26 交给编码 Agent 的**从零实现启动包**,
|
||||
> MVP 已建成,启动提示词与里程碑纪律不再是现行指引;「归一化对照表」里的 flow/loop 两分
|
||||
> 已随 2026-07-29 范式退役成为历史记述。代码注释里引用的「README《归一化对照表》
|
||||
> 《已裁定的落地决策》《种子数据》《MVP 完成判据》《前端 UI · 原型 1:1 平移》」各节
|
||||
> 仍指本文,查历史语境时来这里。现行状态看仓库根 `CLAUDE.md`;现行规范看
|
||||
> `docs/编码规范.md` / `docs/产品设计.md` / `docs/活动开发规范.md`。
|
||||
|
||||
# JCAI 任务操作系统 · MVP 规格包
|
||||
|
||||
这个目录是「企业级 AI Native 任务操作系统」的**完整规格包**:两卷语义规范 + 一份工程结构契约 +
|
||||
一份业务验收样例。交给编码 Agent,即可从零实现 MVP。
|
||||
|
||||
> 一句话系统:组织的工作被定义为可编排的工作流,工作流由活动编排而成;一个活动是一个完成一项
|
||||
> 任务的 Agent 机制,像小程序一样在系统外开发、安装即用;人只在责任点与放行点上出现——
|
||||
> 确定交系统,临场交 AI,落地交人。
|
||||
|
||||
---
|
||||
|
||||
## 启动提示词(编码仓库的 Agent 从这里启动)
|
||||
|
||||
```text
|
||||
你是本仓库的开发 Agent,任务:从零实现「JCAI 任务操作系统」的 MVP。
|
||||
|
||||
【第 0 步 · 建立权威——写任何代码之前必须完成】
|
||||
1. 读 docs/编码规范.md 的前言与 §0–§5 —— 先知道代码长什么样、放在哪、谁能依赖谁;
|
||||
2. 通读 docs/操作系统.html(V2.15,系统侧)—— 五层总图 / 工作台五要素与治理台九功能 /
|
||||
流程层(流程包·工作流·触发器·审批点·网关·边映射)/ 引擎(流程引擎 + 活动基座六件)/
|
||||
人类工具 / 统一事件标准 / 调用层与工具箱 / 记忆与度量 / 运行拓扑与写三分 / 基座与治理 /
|
||||
系统不变量——最后的不变量清单要能默写要点;
|
||||
3. 通读 docs/活动模型.html(V2.0,活动侧)—— 契约七件 / loop(CMMN+DMN·语义引擎×Agent 回路·
|
||||
双向驱动)/ flow(步序声明·四种步型)/ 人类工具的调用 / 交互内容 / 技能 / 记忆与迭代 /
|
||||
Harness 与验收分级 L1–L5 / 活动包与开发 / 合规判据(通用 17 + loop 8 + flow 6);
|
||||
4. 回头读完 docs/编码规范.md 全文(§6–§17);
|
||||
5. 读 docs/MVP示例工作流.html —— MVP 要跑通的那单生意(统一报价),
|
||||
按本 README 的《归一化对照表》把业务素材映射成系统概念。
|
||||
|
||||
【权威分工——冲突时的裁决顺序】
|
||||
- 语义(系统做什么、什么语义):以两卷规范为准;
|
||||
- 结构(代码长什么样、模块边界、依赖方向):以 编码规范.md 为准;
|
||||
- 业务内容(MVP 做哪单生意、字段与规则):以 MVP示例工作流.html 为准;
|
||||
- 前端 UI:**不是参照,是 1:1 平移**。两台高保真原型的前端代码就是 UI 的实现规格,
|
||||
原型仓库在本机(用户已授予读取权限):`~/coding/JCAI_PRD/workbench`(员工面工作台)与
|
||||
`~/coding/JCAI_PRD/governance`(管理面治理台)。按本 README《前端 UI · 原型 1:1 平移》
|
||||
一节的清单与规则执行——CSS 与 DOM 结构原样搬、组件按编码规范 §9 重排、mock 数据层换成
|
||||
真实 API;**只平移不 import**,编码仓与原型仓零依赖;
|
||||
- 发现两类权威打架:停下来报告,不得自行仲裁(编码规范·前言)。
|
||||
- 谨记双层边界:范式(loop / flow / skill)是后端实现逻辑的模型,只存在于引擎层;
|
||||
交互模型(工单两模式 + 对话卡族)是前端呈现协议——两层不冲突,前端永远感知不到范式
|
||||
(编码规范 §9 注记)。UI 文案用两卷术语(标准活动 / 案例活动 / 审批点),
|
||||
SOP / CMMN / BPMN 等字样只出现在代码与内部文档,不出现在界面上。
|
||||
|
||||
【实施纪律】
|
||||
- 严格按 编码规范.md §15 的里程碑序 M0→M6 推进;每个里程碑先立骨架
|
||||
(index / contract / port / CONTRACT.md + 空测试)再填实现;
|
||||
- 机器门禁(tsc / eslint / depcruise / 契约快照 / test)从 M0 起全绿,红了先修门禁再前进;
|
||||
- 遵守 §12 AI 协作协议(多 Agent 并行 · 主 Agent 编排制):**你即主 Agent(唯一集成人)**,
|
||||
按 §12.0 授权矩阵组织子 Agent 并行承包——分支即工单、合入唯经你(§12.7)、契约面由你单写;
|
||||
§12.5 禁止清单按矩阵授权,三类保留项(破坏性契约 / 新依赖 / 改门禁规则)永远上升用户;
|
||||
- 本 docs/ 即规格包原件,整包置于仓库 docs/ 下;另建 docs/debt.md 与 docs/adr/
|
||||
(ADR 编号从 005 续,001–004 已在编码规范 §14);
|
||||
- 四个落地裁量点已由用户裁定(见本 README《已裁定的落地决策》),照此实现、不必再问;
|
||||
实现中出现**新的**裁量点仍照前言规矩停下报告。
|
||||
|
||||
【MVP 完成判据——统一报价全链路验收场景】
|
||||
以下场景在真实运行的系统上从头到尾走通(mock 只允许出现在外界:邮件通道与外部系统可用
|
||||
模拟通道 / 录制回放替身,系统内部机制不得 mock):
|
||||
1. 治理台:录入五名员工与岗位、绑定 LLM 实例(任一 OpenAI 兼容端点)、
|
||||
编排「统一报价」流程包 v1 并发布(边映射闭合校验通过)、
|
||||
配置「客户RFQ询价」事件触发器与邮件通道实例;
|
||||
2. 一封样例 RFQ 邮件入站 → 归属解析 → 触发器命中起单:生成一条工作流,
|
||||
A1 任务落到 BD 冯兵的工作台任务清单;
|
||||
3. A1 受理归一(flow):步序推进,LLM 步抽取归一 → 台账「统一受理记录」(任务级草稿层),
|
||||
缺关键字段走补全卡,逐条打标走 DMN(三分类规则 P-03),放行通过 → 草稿合并正式版、
|
||||
数据事件发射,G2 网关按 Type 分流;
|
||||
4. B 类条目 → A3 渠道寻源(loop,林佩萱):CMMN 装载、Agent 回路推进、发询后登记
|
||||
数据监听 + 时限;供应商回价(模拟入站)→ 数据事件续跑;寻源比价表交付放行;
|
||||
5. C 类条目 → A2 替代选型(loop,王梓名):含一次跨人卡(向 BD 补全),
|
||||
替代对标表放行交回;
|
||||
6. A4 成本定价(flow):取价基准 → DMN 定价(利润模型 P-01)→ 底价自检(底价门禁 P-02)
|
||||
→ 报价草案;
|
||||
7. AP 定价审批(审批点):审批单卡到 PM 丁子乔工位——通过放行进 A6;
|
||||
驳回按声明走向回 A4 重开新实例;
|
||||
8. A6 报价议价(flow):生成正式报价单(写类动作登记为动作意向)→ 落地预检 →
|
||||
放行通过即落地(台账回写 + 出站发送)→ 输出口发射;建 D+3 跟进监听;
|
||||
9. 客户拒绝(模拟)→ A7 情报归档:历史价格情报回写、落地率统计落账,工作流「已完成」;
|
||||
10. 全程可审计:任务记忆只追加、信封盖戳、归侧标注;工作台五要素齐活
|
||||
(任务 / 审批 / 消息 / 纪要 / 汇报聚合自任务日志);治理台·任务管理可在途监控、
|
||||
叫停并恢复一单任务;终态封卷触发蒸馏钩子产出活动记忆候选条目(可为最小实现)。
|
||||
|
||||
【种子数据】
|
||||
- 员工五人:丁子乔(PM·项目经理)· 冯兵(BD·商务)· 王梓名(FAE·现场应用)·
|
||||
林佩萱(采购/寻源)· 李元初(OP·运营);登录者 = 冯兵;
|
||||
- 单次部署 ≤50 人、单组织、不做多租户——一切机制复杂度以此为预算。
|
||||
|
||||
现在从【第 0 步】开始。读完四份文档后,先提交 M0 的变更申请,不要直接写代码。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件清单与权威分工
|
||||
|
||||
| 文件 | 版本 | 权威范围 |
|
||||
| --- | --- | --- |
|
||||
| `操作系统.html` | V2.15 · 2026-07-24 | **系统侧语义权威**:五层、交互两面、流程层、引擎与活动基座六件、人类工具、统一事件标准、调用层、记忆与度量、运行拓扑、边界四象限、基座与治理、系统不变量 |
|
||||
| `活动模型.html` | V2.0 · 2026-07-25 | **活动侧语义权威**:契约七件、loop(CMMN+DMN)、flow(步序声明)、人类工具的调用、交互内容、技能、记忆与迭代、Harness 与 L1–L5、活动包与开发、合规判据 17+8+6 |
|
||||
| `编码规范.md` | V0.1 · 2026-07-26 | **工程结构权威**:技术选型定案、monorepo 唯一形态、模块层级 L1–L8 与依赖门禁、Mastra 映射、持久层、前端 features、AI 协作协议、里程碑 M0–M6。服从两卷,只管「代码长什么样」 |
|
||||
| `MVP示例工作流.html` | 2026-07-26 导出 | **业务验收样例**:统一报价流程全套素材——7 节点(SOP×5 + Case×2)、3 网关、13 张流转单据、16 张静态库表、5 条规则判定表 + 经验。是原始业务素材,需按下表归一化 |
|
||||
|
||||
两卷互为关联卷(系统供给 vs 活动定义是镜像同表);编码规范前言载有仲裁规则:
|
||||
**语义冲突以两卷为准,结构冲突以编码规范为准,两类权威打架停下来报告**。
|
||||
|
||||
## 归一化对照表(MVP 素材 → 系统概念)
|
||||
|
||||
样例是业务分析会话的原始导出,术语与两卷未对齐;实现时按此表映射:
|
||||
|
||||
> **2026-07-29 范式退役**:用户定「活动不再分 flow 和 loop,内部实现都是 Loop」——
|
||||
> `flow-engine` 模块整个删除,`paradigm` 字段从清单与两张表退场,活动的业务模型只剩案例模型。
|
||||
> 下表提到「标准活动(flow)」的行为**历史记述**;今天 A1/A4/A6/A7 也已改写成案例模型。
|
||||
> 后果见 docs/debt.md。
|
||||
|
||||
| 样例里的物件 | 归一化为 |
|
||||
| --- | --- |
|
||||
| 流程「统一报价」 | **流程包** v1:治理台编排五要素、边映射闭合、发布;运行即工作流 |
|
||||
| 触发器「客户RFQ询价」 | **事件触发器**:通道入站命中规则 → 字段映射 → 经调用接口起单 |
|
||||
| SOP 节点(A1/A4/A6/A7) | **标准活动(flow)**:任务定义卡→契约七件;作业指导书 S1…Sn→步序声明(四种步型);步内判断(如 A1 的 D1 完整度)→条件谓词或人类工具步;SLA→Harness 时限与预算 |
|
||||
| Case 节点(A2/A3) | **案例活动(loop)**:案例图→CMMN XML;案例说明书→同版本叙事面;`!必做`→entryCriterion 常项、`看情况`→discretionary、`#可反复`→repetitionRule、`▶需拍板`→manualActivation + 放行;`⏱时限/回价到期/✋客户改需求`→时间/数据/人监听(时限必配) |
|
||||
| A5 定价审批 | **审批点**(流程层内置人裁节点,功能属性四件);G3「审批结果」网关并入审批点的驳回走向(回 A4 重开新实例)。见裁量点 ① |
|
||||
| 规则五件(P-01 利润模型 / P-02 底价门禁 / P-03 三分类 / P-04 有效期 / 受理规范) | 判定表部分→ **DMN 决策表**(确定性求值、命中行留痕);「经验」段落→**技能 / 活动记忆种子**(载方法不载权力) |
|
||||
| 动态数据(流转单据 13 张) | **台账对象**(活动自持域,schema 声明自生长)+ **输出口载荷 schema**(流转的事实经边映射拼接) |
|
||||
| 静态底座(16 张库表) | **共享域对象 / 主数据**(客户资料库、PN物料主库、供应商渠道目录等);写授权按域.对象在契约申报 |
|
||||
| 「转订单履约(ORM)」 | 流程包**显式终点**的输出声明;MVP 以回执与日志代真实 ORM 对接 |
|
||||
| 角色 BD / PM / PRM / FAE | **岗位**;PRM=采购 → 林佩萱,FAE → 王梓名,负责人四策略解析起单 |
|
||||
|
||||
## 已裁定的落地决策(2026-07-26 用户拍板,照此实现)
|
||||
|
||||
1. **A5 定价审批 = 审批点**(流程层内置人裁节点,功能属性四件):样例里的「策略决策」落审批
|
||||
意见与批注;G3「审批结果」网关并入审批点的驳回走向(驳回 → 回 A4 重开新实例)。
|
||||
2. **G2 料号分流 = 并行网关**:三道并行、每边载荷按 Type 过滤条目子集;某类条目为空的分支
|
||||
按「容忍」处置,A4 汇合处的必填映射不得依赖可空分支。
|
||||
3. **A4 成本定价 = flow**(从样例):取价→查表定价→自检的判断全部装进 DMN 决策表确定性
|
||||
求值;实现中一旦出现「由 AI 决定怎么办」,按活动模型 2.1 的上升规则改 loop 或上升为
|
||||
流程层编排。
|
||||
4. **议价回环「有异议 → 返回调整 → A4」= 流程层回边**:人裁决后重开新任务实例
|
||||
(同节点、新任务 ID、留痕关联),不复活旧任务。
|
||||
|
||||
## 前端 UI · 原型 1:1 平移(强制)
|
||||
|
||||
`apps/web` 的界面**不重新设计**:两台原型的前端代码就是实现规格,逐屏平移,
|
||||
验收标准 = 与原型逐屏对照、视觉不可辨差异。平移清单:
|
||||
|
||||
**员工面(workbench,浅色三栏工作面)** —— 源根 `~/coding/JCAI_PRD/workbench/src/app/(proto)/`
|
||||
|
||||
| 源文件 | 是什么 |
|
||||
| --- | --- |
|
||||
| `page.tsx` | 外壳(左轨五要素 + 冯兵徽标)+ 任务四区骨架(任务列表 / 标题区 / 工作区 / 对话区、收展把手) |
|
||||
| `_components/ViewRegion.tsx` | 标题区 + 工作区四页签:待处理(HTML 工单翻页,验收=可修订报告 / 执行=指导书+空表单)、工作成果、任务详情(KPI 带 + 流程链 + 模型扇形图 + 上下文档案)、工作日志 |
|
||||
| `_components/TaskChat.tsx` | 对话区(任务助理):气泡二元制、对话卡族(决策/补全/评价/验收单)、composer 锁、跨栏定位 |
|
||||
| `_components/ModuleWorkspace.tsx` | 审批 / 消息 / 纪要 / 汇报四要素(各配助理对话壳) |
|
||||
| `_components/`(其余) | FileViewer / Ico / ChatAvatar / ScrollArea / Markdown / SettingsButton |
|
||||
| `app.css` + `proto.vendor.css` | **整文件搬**:设计令牌(`.proto-root .app-shell`)+ 全部 `jc-*` 组件样式 + vendor 皮肤(布局骨架,类名被它占用,`jc-` 前缀因此必须保留) |
|
||||
| `_data/model.ts` | 交互模型类型——`@contracts/card.ts` 的参照原型(正式版以 @contracts 为准) |
|
||||
| `public/samples/` | 样例 PDF 报价单等资产(演示用,可选) |
|
||||
|
||||
**管理面(governance,深轨浅面控制台)** —— 源根 `~/coding/JCAI_PRD/governance/src/app/`
|
||||
|
||||
| 源文件 | 是什么 |
|
||||
| --- | --- |
|
||||
| `_components/Governance.tsx` | 九区全体:区头 + 区内页签 + 整页下钻 + 就地确认带(AskBand·影响预览三必答)+ 事件流 + 数据区记录管理器 |
|
||||
| `_flow/`(六文件) | 编排画布:diagram-js 布局引擎(蛇形折行)+ 画笔(三色 skin + 琥珀审批点)+ 编辑器核 |
|
||||
| `theme.css` + `console.css` | 设计令牌 + 治理台组件层(`g-*` + 色调字典 `t-*`),整文件搬 |
|
||||
| `_components/Ico.tsx` · `_data/gov-data.ts` | 图标;mock 数据(字段口径与文案的参照,替换为真实 API) |
|
||||
|
||||
**平移规则**:
|
||||
|
||||
1. **DOM 结构与类名原样保留**(`jc-*` / `g-*` 前缀、vendor 皮肤一并搬);CSS 整文件搬,不重写不「优化」;
|
||||
2. **代码组织按编码规范 §9 重排**(拆进 features、路由改 TanStack Router、数据改 typed-fetch + SSE)——重排只动模块边界与数据来源,**渲染输出保持 1:1**;
|
||||
3. 原型是 Next.js、正式系统是 Vite SPA:去掉 `"use client"` 等 Next 专有件即可,组件均为纯 React 19;
|
||||
4. 原型内嵌的 mock 数据与内存状态(`_data/*`、doneMap 等)全部换成服务端投影;「刷新即重置」的原型缺陷不得带入——正式系统零过程状态、刷新从投影重建(§9.2);
|
||||
5. 平移中发现原型与两卷/编码规范冲突(如原型 `/api/chat` 直连 DeepSeek),以规范为准并报告,不照搬。
|
||||
|
||||
## MVP 之外(明确不做)
|
||||
|
||||
多租户 / 移动端 / 真实 ERP·CRM 对接(用调用契约 + 录制回放替身)/ 活动市场与多组织分发 /
|
||||
两卷中标注「可选」的高级项(如叙述性小结可后置)。治理台九功能在 MVP 里以支撑验收场景为度:
|
||||
每区可用、不追求满配。
|
||||
|
||||
---
|
||||
|
||||
*规格包整理于 2026-07-26。四份文件的任何改动都应回到源头(PRD 工作区)同步,勿在编码仓单方改规格。*
|
||||
@@ -0,0 +1,163 @@
|
||||
> **⚠️ 已归档(2026-07-30)** —— M0–M6 里程碑全部完成后本表停止维护;
|
||||
> 「下一步」「已知待办」两节是当时的快照,多数已做完或已被后续决策取代。
|
||||
> 现行状态看仓库根 `CLAUDE.md`;在办的账看 `docs/debt.md` 与 `docs/活动开发规范.md` 第 12 章。
|
||||
|
||||
# 实施进度(主 Agent 维护)
|
||||
|
||||
> 里程碑序见 编码规范 §15。每个里程碑先立骨架(index / contract / port / CONTRACT.md + 空测试)
|
||||
> 再填实现;门禁从 M0 起全绿,红了先修门禁再前进。
|
||||
|
||||
## 状态总览
|
||||
|
||||
| 里程碑 | 内容 | 状态 |
|
||||
|---|---|---|
|
||||
| M0 | 工作区骨架 + 五道门禁 + 环境自举 | ✅ 完成 |
|
||||
| M1 | `@contracts` 首版、`event`、`ledger`、`org`、`llm` | ✅ 完成 |
|
||||
| M2 | `memory`、`toolbox`、`channel`、`message` | ✅ 完成 |
|
||||
| M3 | `interaction`、`human-tool`、`release`、`activity-registry`、`@sfeel` | ✅ 完成 |
|
||||
| M4 | `skill-engine`、`loop-engine`、~~`flow-engine`~~(2026-07-29 退役)、`task` | ✅ 完成(模块与 domain;app 装配随 M5) |
|
||||
| M5 | `process`、`trigger`、`@activity-sdk` + 六个活动包 | ✅ 完成 |
|
||||
| M6 | 前端:工作台五要素 → 治理台九功能;`minute`、`report` | 🟡 五要素九功能均已平移并接真实 API;`minute`/`report` 已落;编排画布只读态已接,手绘编辑器待接 |
|
||||
|
||||
**MVP 十条判据:① ② ③ ⑥ ⑦ ⑧ ⑨ ⑩ 已在真实系统上端到端跑通;④ ⑤ 的 loop 机制(CMMN 装载 / Enabled 集 / 断点 / 调人)跑通,余下部分受限于 LLM 端点。**
|
||||
|
||||
**当前:429 个测试通过(含对真实 PG 的集成测试),门禁五道全绿。20 个后端模块 + 5 个包 + 6 个活动包 + 14 个前端 feature。**
|
||||
|
||||
**LLM:DeepSeek provider 已接,两档实例 `deepseek-v4-flash` / `deepseek-v4-pro` +
|
||||
用途名 `quote-extractor`(→ flash)。密钥只从环境读、落库即密封。**
|
||||
|
||||
### MVP 完成判据的跑通进度(README《MVP 完成判据》)
|
||||
|
||||
| # | 场景 | 状态 |
|
||||
|---|---|---|
|
||||
| 1 | 治理台:员工岗位 / LLM 实例 / 流程包发布 / 触发器 / 邮件通道 | ✅ `pnpm db:seed` 一次做完 |
|
||||
| 2 | RFQ 邮件入站 → 归属解析 → 触发器命中起单 → A1 落到冯兵工作台 | ✅ 已跑通 |
|
||||
| 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
|
||||
pnpm gates # gen:tiers → typecheck → lint → depcruise → api:check → test(五道门禁)
|
||||
pnpm db:up # docker compose 起 PG16(四 schema 由 db/init 建)
|
||||
pnpm db:generate # drizzle-kit 生成 sys schema 迁移
|
||||
pnpm db:migrate # 应用迁移
|
||||
pnpm db:seed # 播种五岗位五员工(登录者 = 冯兵)
|
||||
pnpm dev:api # tsup watch + node dist/main.js(:8787)
|
||||
pnpm dev:web # vite(:5173,/api 反代到 8787)
|
||||
```
|
||||
|
||||
契约变更后:`pnpm api:check --update && git add api-report`(§12.4)。
|
||||
|
||||
## 已完成的落点
|
||||
|
||||
**M0** — pnpm workspaces / tsconfig 双工程 / eslint flat / dependency-cruiser
|
||||
(基线八条 + JCAI 增补,tier-order 由 `tools/module-tiers.json` 生成)/ 契约快照只留契约面 /
|
||||
docker-compose PG16 + 四 schema。
|
||||
|
||||
**M1** — `@contracts` 九文件首版;四个 L1 模块各带 index / contract / port / CONTRACT.md /
|
||||
domain 零 mock 测试;app 层 Hono 服务、鉴权中间件、DI 装配、drizzle 迁移、AES-256-GCM 凭证密封。
|
||||
已验证:种子播种、岗位授权(BD 被拒 / PM 放行)、LLM 四元绑定密钥不外泄、
|
||||
ledger 对真实 PG 的 schema 自生长与写三分。
|
||||
|
||||
**M2** — 四个 L2 模块:`memory`(任务记忆四性质 / 活动记忆四层 / 任务日志两级投影 / 度量)、
|
||||
`toolbox`(注册两审 / 效应等级 / 白名单 / `ToolInvoker` 第二条缝)、
|
||||
`channel`(入站归一 → 系统写 → 数据事件;出站与关联记录)、`message`(通知 / 跨任务征答 / 死信 / 无主兜底)。
|
||||
app 侧接上派发器(advisory lock 串行 + 消费幂等)与 `/api/ingest` 模拟通道。
|
||||
已验证:一封无主 RFQ 邮件入站 → 归属解析 no-match → 事件落库 → 兜底落到 OP 岗位李元初的消息要素。
|
||||
|
||||
**M3** — `@sfeel` 全套(lexer / parser / evaluate / static-check);
|
||||
`human-tool` 三规矩机检 + 工单两模式 + 对话卡族 + 到达面两路 + 驳回重答;
|
||||
`release` 落地预检五项 + 幂等落地 + 落地收敛才发射输出口;
|
||||
`interaction` 控制权按态转交 + 视图净化门 + SSE 推送;
|
||||
`activity-registry` 安装两审 + 悬空监听拒绝 + 零环境能力扫描 + 资产与包分离版本化 + 进化管线状态机。
|
||||
|
||||
**M4** — `skill-engine`(载方法不载权力 / 结晶片段经 ToolInvoker 留痕 / 装配入信封);
|
||||
`flow-engine`(四种步型 / 条件跳步 / 声明给定的失败路径 / 异步步即监听 / 步序位置持久);
|
||||
`loop-engine`(**自研 CMMN 子集解析器**,表外构件直接报错;两副标准生命周期与迁移拓扑;
|
||||
Enabled ≠ Active;前提求值与语义性前提交裁量;DMN 确定性求值与命中行留痕;DRD 缺口演绎;
|
||||
双向驱动与交集导航,断点即人环;三型行动);
|
||||
`task`(终态五名裁定 / Harness 预算与停滞熔断 / 控制三语义 / 改派 / 断点续跑三份持久状态 /
|
||||
封卷触发蒸馏)。
|
||||
|
||||
**M5** — `process`:闭合校验(七类违规)+ 四策略解析 + 状态聚合 + 确定性流转
|
||||
(选边 / 装配 / AND 回齐 / 失败走向四选一 / 审批驳回重开新实例)+ 三表持久;
|
||||
`trigger`:三类触发器 + 起单幂等 + 数据日历型逐行扫描;
|
||||
`@activity-sdk`:基座六件接口 + `defineActivity()` + 本地开发宿主(mock 工具表 / 模拟工位 / 录制);
|
||||
`activities/quote-intake`:统一报价 A1 的首个活动包 —— 契约七件 + 步序声明(五步)+
|
||||
DMN 三分类规则 P-03 + 台账声明 + 样例集。depcruise 已实测拦下活动包越界 import。
|
||||
|
||||
## 下一步(按序)
|
||||
|
||||
1. **app 装配收尾(打通端到端的关键一步)**:把 M4/M5 的九个模块接入 `container.ts` ——
|
||||
`loop-engine` 需要 `internal/adapter/agent-runner.ts` 的 Mastra Agent(§6.1);
|
||||
派发器注册任务与触发器两个消费者;`TriggerMatcher` / `InFlightWorkProbe` 占位换真身;
|
||||
pg-boss 承接周期扫描与监听时限。
|
||||
2. **活动装载通道**:`activity-registry` 装载 .JS 产物并把基座六件句柄注入活动
|
||||
(tsup 单文件 ESM 编译预设,§8.2 / ADR-004)。
|
||||
3. **余下活动包**:A4 成本定价(flow · DMN P-01/P-02)、A6 报价议价(flow)、
|
||||
A7 情报归档(flow)、A3 渠道寻源(loop · CMMN)、A2 替代选型(loop · CMMN)。
|
||||
5. **M6 · 前端(进行中)**:任务要素已平移并接真实 API(样式整文件搬、DOM 与类名原样、
|
||||
`@ui` 的沙箱 iframe 视图容器)。余下:
|
||||
- 工作台:审批 / 消息(已有列表,待接就地作答)/ 纪要 / 汇报四要素 + SSE 订阅层
|
||||
- 治理台九功能:`Governance.tsx`(2415 行)+ `_flow/` 编排画布六文件(diagram-js)
|
||||
- `minute`、`report` 两个后端模块(L8)
|
||||
6. **验收**:README《MVP 完成判据》十条场景端到端跑通。
|
||||
|
||||
## 待接的占位(详见 docs/debt.md)
|
||||
|
||||
- `container.ts` 两个占位端口:`InFlightWorkProbe`(M4)、`TriggerMatcher`(M5)
|
||||
- 派发器的消费者表为空(M4 任务 / M5 触发器)
|
||||
- pg-boss 已装未用(M5 定时作业)
|
||||
- M4 四模块尚未接入 `container.ts`(需要 Mastra Agent adapter 与流程层起单,随 M5)
|
||||
|
||||
## 已建 ADR
|
||||
|
||||
编码规范 §14 已占 001–006,新 ADR 自 **007** 续(README 写「从 005 续」是笔误):
|
||||
|
||||
- ADR-007 工具链与门禁的落地形态
|
||||
- ADR-008 投递队列:`sys.event_delivery` 是队列本体,pg-boss 只管定时
|
||||
- ADR-009 凭证加密:AES-256-GCM + `JCAI_SECRET_KEY`
|
||||
|
||||
## 已知待办(按优先级)
|
||||
|
||||
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、活动启停、记忆修剪等写动作
|
||||
路由已在,前端确认带已备,逐个接上即可。
|
||||
5. **`ch-mail` 遗留通道**:库里有一条早期迭代留下的通道配置(只含 `quoteNo` 提取规则),
|
||||
与种子的 `ch-rfq-mail` 并存;确认无引用后清理。
|
||||
@@ -0,0 +1,12 @@
|
||||
# docs/archive —— 已归档文档
|
||||
|
||||
完成使命或被后续决策取代的文档收在这里,**只读不再维护**;每份文件头部有归档说明与现行去向。
|
||||
|
||||
| 文件 | 曾是什么 | 归档原因 |
|
||||
|---|---|---|
|
||||
| `MVP规格包-README.md` | 从零实现 MVP 的启动包(启动提示词 / 归一化对照表 / 已裁定决策 / 平移规则) | MVP 已建成;flow/loop 两分随范式退役成历史。代码注释「README《…》」引用的各节仍在此文 |
|
||||
| `PROGRESS.md` | M0–M6 里程碑进度表 | 全部完成,停止维护 |
|
||||
| `plan-2026-07-27-mvp-foundation.md` | MVP 底座实施计划 | 已执行完毕 |
|
||||
| `plan-2026-07-27-three-surfaces.md` | 三面拆分实施计划 | 已执行完毕,形态此后又有演进 |
|
||||
|
||||
现行文档地图见仓库根 `CLAUDE.md`。
|
||||
@@ -0,0 +1,977 @@
|
||||
> **⚠️ 已归档(2026-07-30)** —— MVP 底座计划,已执行完毕。现行状态看仓库根 `CLAUDE.md`。
|
||||
|
||||
# MVP 底座(四域数据 · 1:1 流程包 · 三类工具 · 卡片设计台)实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 按 `docs/MVP示例工作流.html` 一比一落一个流程包实例(统一报价:6 活动槽位 + 1 审批点 + 3 网关 + 双终点),数据按 客户/情报/供应商/项目 四域重划,工具箱呈现 功能/LLM/人类 三类已有工具与 自定义导入/MCP 接入,设计台四库全部卡片化;活动只落**槽位契约**,不刻画实现。
|
||||
|
||||
**Architecture:** 种子重写为三件套(`seed-mvp-data` 四域对象声明 / `seed-mvp-slots` 六槽位清单经安装通道装入 / `seed-mvp-flow` 流程包 v1 + 手绘图坐标 1:1 + 触发器与通道),声明物化自动建四域十八对象;活动与工具靠**声明名**解耦(`declarations.tools` 只写名字,白名单在治理台)。前端:`domains.ts` 换四域短名,设计台四页换 `g-tile`/卡片网格,工具箱一页四段。
|
||||
|
||||
**Tech Stack:** 既有栈零新增 —— zod 契约 + 安装通道(`registry.install` 顺带声明物化)、diagram-js 手绘图存证(`pkg.diagram`)、React + `g-*` 皮肤(console.css 已有类 + 既有「三面补充」节)。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 五门全绿才算一个任务完成:`pnpm typecheck && pnpm lint && pnpm depcruise && pnpm api:check && pnpm test`(契约变更:同步 CONTRACT.md → `pnpm api:check --update` → `git add api-report`)。
|
||||
- §12.5 禁用:`any`、`as unknown as`、`@ts-ignore`、domain 层 IO、全局可变状态、删测试、改门禁配置。
|
||||
- **mock 只允许出现在外界**:槽位活动**不得**内置假执行(不发既定载荷、不装作跑过);未实现就是未实现,起单后任务停在那里是诚实状态。
|
||||
- **流程包只落一个实例**:种子只产出 `unified-quote` v1(published),不再产出多版本;验收含「库里恰一行」。
|
||||
- **活动与工具完全解耦**:槽位清单 `declarations.tools` 只引用工具**名**;工具注册、连接值、白名单全在治理侧(seed 的 toolbox 段),槽位不含任何工具实现或 import。
|
||||
- 活动细节不刻画:槽位的 `assets.businessModel` 只携 MVP 文档里的步骤/里程碑**骨架**(标题级),入口产物为注释性占位 JS(不可执行语义,运行时如实报「未实现」)。
|
||||
- 密钥只从环境读(`JCAI_DEEPSEEK_API_KEY`);种子口令走 `JCAI_SEED_PASSWORD`。
|
||||
- 提交信息中文一句话,结尾两行:
|
||||
`Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>` 与 `Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m`。
|
||||
- 服务:API `:8787`、Web `:5273`;账号 `Chaihao` / `ch19900614`;curl 先 `POST /api/login` 拿 cookie。
|
||||
- 本计划会**整库重建**(`docker compose down -v`)—— 现有运行数据(在途单/消息)全部清掉,这是四域重划的必然代价,执行前无需再确认(用户已在需求 2 中决定重划)。
|
||||
|
||||
---
|
||||
|
||||
## 版图(先读懂再动手)
|
||||
|
||||
### 四域十九对象(需求 2 的落法)
|
||||
|
||||
划分原则:**流转单据(过程)归项目域;沉淀资产按内容归 客户/情报/供应商**。
|
||||
定价审批表不入台账:MVP 标其载体为「审批流」—— 它就是审批点的审批单记录,落在审批机制里(sys 侧),不物化为数据对象。
|
||||
|
||||
| 域 | 键 | 短名 | 对象(object 键 · 中文名) |
|
||||
|---|---|---|---|
|
||||
| 项目 | `project` | 项目 | `rfq_file` RFQ/BOM文件 · `intake_record` 统一受理记录 · `pn_class` 料号分类表 · `alt_request` 替代评估请求 · `inquiry_request` 询价请求 · `supplier_floor` 供应商底价表 · `alt_benchmark` 替代对标表 · `sourcing_compare` 寻源比价表 · `quote_draft` 报价草案 · `formal_quote` 正式报价单 · `followup` 跟进提醒 · `landing_stats` 报价落地率统计 |
|
||||
| 客户信息 | `customer` | 客户 | `profile` 客户资料库 |
|
||||
| 市场与销售情报 | `intel` | 情报 | `history_price` 历史价格情报 · `chip_spec` 芯片规格书 · `public_select` 公网选型库 |
|
||||
| 供应商信息 | `supplier` | 供应商 | `channel_dir` 供应商渠道目录 · `dead_stock` 呆滞库存表 |
|
||||
|
||||
字段全表(name:type,`*`=主键;type ∈ text/number/boolean/date/timestamp/json,label 用 MVP 中文名):
|
||||
|
||||
```
|
||||
project.rfq_file RFQ/BOM文件 *rfq_ref:text *pn:text qty_eau:text brand:text analog_flag:boolean aec_cert:text target_price:number lead_time:text
|
||||
project.intake_record 统一受理记录 *customer_code:text *pn:text brand:text eau:text type_tag:text target_price:number status:text
|
||||
project.pn_class 料号分类表 *pn:text type_tag:text basis:text history_hit:boolean review_note:text
|
||||
project.alt_request 替代评估请求 *material:text chip_type:text spec_params:text scenario:text requester:text
|
||||
project.inquiry_request 询价请求 *pn:text brand:text era:text project_stage:text accept_alt:boolean unit_usage:text tp:number scenario:text
|
||||
project.supplier_floor 供应商底价表 *pn:text *supplier:text floor_price:number moq:text lead_time:text
|
||||
project.alt_benchmark 替代对标表 *original_pn:text *candidate_pn:text alt_grade:text param_diff:text diff_risk:text mod_eval:text verify_advice:text
|
||||
project.sourcing_compare 寻源比价表 *pn:text *supplier:text quote_price:number moq:text lead_time:text channel_grade:text condition:text verdict:text
|
||||
project.quote_draft 报价草案 *pn:text proposed_price:number margin:number deviation:number
|
||||
project.formal_quote 正式报价单 *pn:text unit_price_cny:number lead_time:text spq:text moq:text trade_terms:text valid_until:date
|
||||
project.followup 跟进提醒 *quote_no:text customer_code:text d3:date d7:date reply_status:text
|
||||
project.landing_stats 报价落地率统计 *period:text landing_rate:number type_a_hit:number d3_reply_rate:number return_rate:number
|
||||
customer.profile 客户资料库 *customer_code:text grade:text tags:text payment_terms:text industry:text risk_level:text annual_purchase:number
|
||||
intel.history_price 历史价格情报 *pn:text *quoted_at:timestamp brand:text unit_price_cny:number lead_time:text moq:text valid_until:date
|
||||
intel.chip_spec 芯片规格书 *pn:text key_params:text package:text version_date:date
|
||||
intel.public_select 公网选型库 *alt_relation:text param_compare:text spec_ref:text lifecycle:text price_ref:text
|
||||
supplier.channel_dir 供应商渠道目录 *supplier:text channel_type:text brand_lines:text contact:text
|
||||
supplier.dead_stock 呆滞库存表 *pn:text stock_qty:number stock_age:text cost_price:number
|
||||
```
|
||||
|
||||
### 六个活动槽位 + 一个审批点(契约面;MVP §1.1–1.7 一比一)
|
||||
|
||||
| 槽位 id | 节点 | 名 | 范式 | 岗位 | 入(契约 input) | 出口(output.mandatory) | 声明的域对象(reads→writes) |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `rfq-intake` | A1 | 客户RFQ受理 | flow | BD | subject/body/customerCode/rfqRef | `intake`:统一受理记录(records)+ `needAlternative:boolean` + `needSourcing:boolean` | 读 intel.history_price · customer.profile → 写 project.rfq_file · intake_record · pn_class · alt_request · inquiry_request |
|
||||
| `alt-selection` | A2 | 替代选型 | loop | FAE | 替代评估请求(material/chip_type/spec_params/scenario)+ needSourcing 透传 | `benchmark`:替代对标表 + `needSourcing:boolean`(原样带出) | 读 intel.chip_spec · intel.public_select → 写 project.alt_benchmark |
|
||||
| `sourcing-inquiry` | A3 | 寻源询价 | loop | PRM | 询价请求(pn/brand/era/…) | `compare`:寻源比价表 + 供应商底价表 | 读 supplier.channel_dir · supplier.dead_stock → 写 project.sourcing_compare · project.supplier_floor |
|
||||
| `cost-pricing` | A4 | 成本定价 | flow | BD | 受理记录 + 底价/对标(可空) | `draft`:报价草案 | 读 intel.history_price · customer.profile → 写 project.quote_draft |
|
||||
| `quote-negotiation` | A6 | 报价议价 | loop | BD | 报价草案(已批准)+ 定价审批表 | `formal`:正式报价单 + 跟进提醒 + `outcome:"won"\|"revise"\|"lost"` | 写 project.formal_quote · project.followup |
|
||||
| `intel-archive` | A7 | 情报归档 | flow | BD | 议价结果与单据 | `stats`:落地率统计 | 写 intel.history_price · customer.profile · project.landing_stats |
|
||||
|
||||
> **A5 定价审批 = 内置审批点**(教义:审批点与网关是系统内置要素,零开发、不入库;审批不是任务)。
|
||||
> 审批点自带 通过/驳回 两出口:通过走 A5→A6 边;驳回走 `reject: { kind:'reopen', nodeId:'A4', mapping }`
|
||||
> 声明(v1 旧包的 AP 节点即此范式,mapping 照抄其式样)。**G3「审批结果」被审批点吸收** ——
|
||||
> 判据就是审批结果本身,不另立网关;图上以琥珀审批框 + 通过边(ok)+ 底部「退回调整→A4」卡点呈现,
|
||||
> 与 MVP 图差一颗菱形,语义同构(此取舍已与用户确认:A5 必须是审批点)。
|
||||
|
||||
### 流程包 1:1(节点 · 坐标取自 MVP SVG,已取整到 10px 珊格)
|
||||
|
||||
语义节点 13 + 图上卡点 2(卡点只入 `diagram`,不入语义):
|
||||
|
||||
| id | kind | label | x,y(w×h) | 备注 |
|
||||
|---|---|---|---|---|
|
||||
| start | start | 客户RFQ询价 | 30,62(36×36) | |
|
||||
| A1 | activity | 客户RFQ受理 | 150,60(100×40) | sub:BD |
|
||||
| G1 | gateway(exclusive/fork) | 是否需要选型 | 293,58(44×44) | basis:受理判定 needAlternative |
|
||||
| A2 | activity | 替代选型 | 390,60(100×40) | sub:FAE |
|
||||
| G2 | gateway(exclusive/fork) | 是否需要寻源 | 553,58(44×44) | basis:受理判定 needSourcing |
|
||||
| A3 | activity | 寻源询价 | 660,60(100×40) | sub:PRM |
|
||||
| A4 | activity | 成本定价 | 810,60(100×40) | sub:BD |
|
||||
| A5 | approval | 定价审批 | 810,260(100×40) | sub:PM · 审批点(琥珀) |
|
||||
| A6 | activity | 报价议价 | 370,260(100×40) | sub:BD |
|
||||
| G4 | gateway(exclusive/fork) | 议价结果 | 183,258(44×44) | basis:A6 outcome(G3 已被审批点吸收,G4 保留) |
|
||||
| A7 | activity | 情报归档 | 150,170(100×40) | sub:BD |
|
||||
| end_won | end | 转订单履约 | 30,262(36×36) | 图上 end(绿) |
|
||||
| end_arch | end | 归档闭环 | 30,172(36×36) | 图上 end-bad(红) |
|
||||
| (callout) | callout | 退回调整→A4 | 590,370(100×30) | 仅 diagram |
|
||||
| (callout) | callout | 返回调整→A4 | 150,370(100×30) | 仅 diagram |
|
||||
|
||||
边 15(label/tone 按图;条件按上表出参;mapping 见 Task 4 代码)。
|
||||
A5 的**驳回不是边**:走审批点 reject 声明(reopen A4),图上由「退回调整→A4」卡点呈现:
|
||||
|
||||
```
|
||||
start→A1 (起单五映射:customerCode/rfqRef/subject/body/quotedAt)
|
||||
A1→G1
|
||||
G1→A2 label 需要选型 cond: needAlternative = true
|
||||
G1→G2 label 不需要选型 cond: needAlternative = false (走图顶部旁路)
|
||||
A2→G2
|
||||
G2→A3 label 需要寻源 cond: needSourcing = true
|
||||
G2→A4 label 不需要寻源 cond: needSourcing = false
|
||||
A3→A4
|
||||
A4→A5 label 报价草案提审
|
||||
A5→A6 label 通过 tone ok (审批点通过出口;驳回走 reject 声明,非边)
|
||||
A6→G4
|
||||
G4→end_won label 成交 tone ok cond: outcome = "won"
|
||||
G4→A4 label 有异议 tone bad cond: outcome = "revise" reopen: true
|
||||
G4→A7 label 拒绝/超时 tone bad cond: outcome = "lost"
|
||||
A7→end_arch label 归档闭环
|
||||
```
|
||||
|
||||
> 网关条件的求值作用域是 `{...initiation, ...vars, ...payload}`(process-service.ts:184),
|
||||
> payload 是**上一发射节点**的出口载荷 —— 所以 G2 在「A2 路径」上读的是 A2 的载荷,
|
||||
> A2 契约必须把 `needSourcing` 原样带出(上表已列)。G1 旁路(G1→G2)时载荷穿网关不换,
|
||||
> G2 读到的仍是 A1 的载荷,两条入路都取到 needSourcing。
|
||||
|
||||
### 工具箱四段(需求 4)
|
||||
|
||||
| 段 | 内容 | 来源 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 功能工具 | `crm.history-price-read`(读)· `mail.send`(写) | toolbox 目录(种子注册,两审) | 实装,白名单按槽位授 |
|
||||
| LLM 工具 | deepseek-v4-flash / deepseek-v4-pro / quote-extractor | llm 模块实例(`listLlmInstancesRoute`) | 实装(调模型经 ModelGateway 一缝) |
|
||||
| 人类工具 | 执行工单 · 验收工单 · 决策卡 · 补全卡 · 评价卡 | human-tool 模块(card.ts 五形态) | 实装(调人经工单/卡族),目录静态呈现 |
|
||||
| 接入 | 导入自定义工具(表单→`toolbox.register` 两审入目录)· 连接 MCP(说明卡) | 新路由 `registerToolRoute`;MCP 无运行时 | 导入实装;MCP 仅呈现+记 debt |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 四域短名与业务序(前端命名层)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/shared/gov/domains.ts`
|
||||
- Modify: `apps/web/src/shared/gov/__tests__/domains.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `DOMAIN_SHORT` 含 `customer:'客户' intel:'情报' supplier:'供应商' project:'项目'`;`DOMAIN_ORDER = ['project','customer','intel','supplier']`(项目是每天看的,排第一);`domainShort`/`sortDomains` 签名不变。
|
||||
|
||||
- [ ] **Step 1: 改测试(先红)**
|
||||
|
||||
`domains.test.ts` 整段替换两个用例的期望:
|
||||
|
||||
```ts
|
||||
it('已知域给 2-3 字人读名', () => {
|
||||
expect(domainShort('project')).toBe('项目');
|
||||
expect(domainShort('customer')).toBe('客户');
|
||||
expect(domainShort('intel')).toBe('情报');
|
||||
expect(domainShort('supplier')).toBe('供应商');
|
||||
});
|
||||
```
|
||||
|
||||
`sortDomains` 用例期望改为:
|
||||
|
||||
```ts
|
||||
expect(sortDomains(['supplier', 'intel', 'zzz_custom', 'project'])).toEqual([
|
||||
'project',
|
||||
'intel',
|
||||
'supplier',
|
||||
'zzz_custom',
|
||||
]);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `pnpm exec vitest run apps/web/src/shared/gov 2>&1 | tail -5`
|
||||
Expected: FAIL(旧字典是 quote_* 键)。
|
||||
|
||||
- [ ] **Step 3: 改实现**
|
||||
|
||||
`domains.ts` 的字典与序换成:
|
||||
|
||||
```ts
|
||||
export const DOMAIN_SHORT: Readonly<Record<string, string>> = {
|
||||
project: '项目',
|
||||
customer: '客户',
|
||||
intel: '情报',
|
||||
supplier: '供应商',
|
||||
};
|
||||
|
||||
/** 业务序:项目(每天看的)→ 客户 → 情报 → 供应商;字典外的域殿后 */
|
||||
const DOMAIN_ORDER = ['project', 'customer', 'intel', 'supplier'];
|
||||
```
|
||||
|
||||
(`domainShort` 回落逻辑与 `sortDomains` 本体不动。)
|
||||
|
||||
- [ ] **Step 4: 测试过 + 提交**
|
||||
|
||||
Run: `pnpm exec vitest run apps/web/src/shared/gov 2>&1 | tail -4` → PASS。
|
||||
|
||||
```bash
|
||||
git add apps/web/src/shared/gov/domains.ts apps/web/src/shared/gov/__tests__/domains.test.ts
|
||||
git commit -m "MVP底座①:数据四域命名 —— 项目/客户/情报/供应商"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 四域对象声明(seed 数据件)
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/src/app/seed-mvp-data.ts`
|
||||
- Test: `apps/api/src/app/__tests__/seed-mvp-data.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `objectDeclarationSchema`(@contracts:domain/object/label/fields[{name,type,label,primaryKey}])。
|
||||
- Produces:
|
||||
```ts
|
||||
export const MVP_OBJECTS: readonly ObjectDeclaration[]; // 19 个,即版图字段全表
|
||||
export function declarationsFor(objects: readonly `${string}.${string}`[]): ObjectDeclaration[];
|
||||
```
|
||||
`declarationsFor(['project.intake_record', ...])` 供槽位清单按名取声明子集。
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
|
||||
```ts
|
||||
/**
|
||||
* 四域十九对象 —— 声明即物化的输入。守三件事:数量与域别齐、主键齐、按名可取。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MVP_OBJECTS, declarationsFor } from '../seed-mvp-data.ts';
|
||||
|
||||
describe('MVP_OBJECTS', () => {
|
||||
it('四域十八对象,不多不少 —— 定价审批表载体是审批流,不入台账', () => {
|
||||
expect(MVP_OBJECTS).toHaveLength(18);
|
||||
const byDomain = Object.groupBy(MVP_OBJECTS, (o) => o.domain);
|
||||
expect(byDomain['project']).toHaveLength(12);
|
||||
expect(byDomain['customer']).toHaveLength(1);
|
||||
expect(byDomain['intel']).toHaveLength(3);
|
||||
expect(byDomain['supplier']).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('每个对象至少一个主键字段 —— 只增不破坏靠主键立身', () => {
|
||||
for (const o of MVP_OBJECTS) {
|
||||
expect(o.fields.some((f) => f.primaryKey), `${o.domain}.${o.object}`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('declarationsFor 按名取子集,名不存在即抛', () => {
|
||||
expect(declarationsFor(['project.intake_record', 'intel.history_price'])).toHaveLength(2);
|
||||
expect(() => declarationsFor(['project.nope'])).toThrow('未知对象');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `pnpm exec vitest run apps/api/src/app/__tests__/seed-mvp-data.test.ts 2>&1 | tail -4` → FAIL。
|
||||
|
||||
- [ ] **Step 3: 写实现**
|
||||
|
||||
按「版图 · 字段全表」逐对象转录(`objectDeclarationSchema.parse` 包住,label 用中文名)。骨架 + 一个完整示例,其余 18 个照表机械展开:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* 四域十八对象(docs/MVP示例工作流.html · 动态数据 + 静态底座;定价审批表落审批机制,不入台账)。
|
||||
* 划分原则:流转单据归 project;沉淀资产按内容归 customer / intel / supplier。
|
||||
* 声明只写活动**要用**的字段;物化走安装通道的声明合并(最大化合并、只增不破坏)。
|
||||
*/
|
||||
import { objectDeclarationSchema } from '@contracts';
|
||||
import type { ObjectDeclaration } from '@contracts';
|
||||
|
||||
const f = (name: string, type: 'text' | 'number' | 'boolean' | 'date' | 'timestamp', label: string, primaryKey = false) =>
|
||||
({ name, type, label, primaryKey, masked: false });
|
||||
|
||||
export const MVP_OBJECTS: readonly ObjectDeclaration[] = [
|
||||
objectDeclarationSchema.parse({
|
||||
domain: 'project',
|
||||
object: 'intake_record',
|
||||
label: '统一受理记录',
|
||||
fields: [
|
||||
f('customer_code', 'text', '客户码', true),
|
||||
f('pn', 'text', 'PN', true),
|
||||
f('brand', 'text', '厂牌'),
|
||||
f('eau', 'text', 'EAU'),
|
||||
f('type_tag', 'text', 'Type标'),
|
||||
f('target_price', 'number', '目标价'),
|
||||
f('status', 'text', '状态'),
|
||||
],
|
||||
}),
|
||||
// …其余 18 个:严格照「版图 · 字段全表」,*号列 primaryKey=true
|
||||
];
|
||||
|
||||
export function declarationsFor(objects: readonly `${string}.${string}`[]): ObjectDeclaration[] {
|
||||
return objects.map((key) => {
|
||||
const [domain, object] = key.split('.');
|
||||
const hit = MVP_OBJECTS.find((o) => o.domain === domain && o.object === object);
|
||||
if (hit === undefined) throw new Error(`未知对象:${key} —— 先在 MVP_OBJECTS 登记`);
|
||||
return hit;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 测试过 + 提交**
|
||||
|
||||
Run: `pnpm exec vitest run apps/api/src/app/__tests__ 2>&1 | tail -4` → PASS;`pnpm typecheck` 归零。
|
||||
|
||||
```bash
|
||||
git add apps/api/src/app/seed-mvp-data.ts apps/api/src/app/__tests__/seed-mvp-data.test.ts
|
||||
git commit -m "MVP底座②:四域十八对象声明 —— 流转归项目,沉淀归客户/情报/供应商"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 六槽位清单 + 经安装通道装入
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/src/app/seed-mvp-slots.ts`
|
||||
- Test: `apps/api/src/app/__tests__/seed-mvp-slots.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `activityManifestSchema`(identity/contract/declarations/assets/samples/entry)、`declarationsFor`(Task 2)。
|
||||
- Produces:
|
||||
```ts
|
||||
export const MVP_SLOTS: readonly ActivityManifest[]; // 6 个,id 见版图槽位表(A5 是审批点,不在其列)
|
||||
export async function installSlots(container: Container): Promise<void>; // 逐个 registry.install
|
||||
export const SLOT_ARTIFACT: string; // 占位入口 JS(注释体,不可执行语义)
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
|
||||
```ts
|
||||
/**
|
||||
* 六个活动槽位 —— 契约就绪、实现留白。守:范式/岗位/出口与 MVP 一致;
|
||||
* 声明只有名(工具按名引用、域对象按名取)—— 活动与工具的解耦就靠这一条。
|
||||
* A5 定价审批是**内置审批点**,零开发、不入库 —— 不在槽位之列。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MVP_SLOTS, SLOT_ARTIFACT } from '../seed-mvp-slots.ts';
|
||||
|
||||
describe('MVP_SLOTS', () => {
|
||||
it('六槽位,id 与范式如 MVP(A5 审批点内置,不入库)', () => {
|
||||
const byId = new Map(MVP_SLOTS.map((m) => [m.identity.activityId, m.identity.paradigm]));
|
||||
expect(MVP_SLOTS).toHaveLength(6);
|
||||
expect(byId.get('rfq-intake')).toBe('flow');
|
||||
expect(byId.get('alt-selection')).toBe('loop');
|
||||
expect(byId.get('sourcing-inquiry')).toBe('loop');
|
||||
expect(byId.get('cost-pricing')).toBe('flow');
|
||||
expect(byId.has('pricing-approval')).toBe(false);
|
||||
expect(byId.get('quote-negotiation')).toBe('loop');
|
||||
expect(byId.get('intel-archive')).toBe('flow');
|
||||
});
|
||||
|
||||
it('A1 出口带两枚去向旗;A2 透传 needSourcing —— 网关条件的数据来源', () => {
|
||||
const a1 = MVP_SLOTS.find((m) => m.identity.activityId === 'rfq-intake')!;
|
||||
const props = (a1.contract.output.mandatory[0]!.payload as { properties?: Record<string, unknown> }).properties ?? {};
|
||||
expect(Object.keys(props)).toEqual(expect.arrayContaining(['needAlternative', 'needSourcing']));
|
||||
const a2 = MVP_SLOTS.find((m) => m.identity.activityId === 'alt-selection')!;
|
||||
const p2 = (a2.contract.output.mandatory[0]!.payload as { properties?: Record<string, unknown> }).properties ?? {};
|
||||
expect(Object.keys(p2)).toContain('needSourcing');
|
||||
});
|
||||
|
||||
it('槽位不携实现:入口是注释占位,工具声明只有名', () => {
|
||||
expect(SLOT_ARTIFACT).toContain('槽位');
|
||||
for (const m of MVP_SLOTS) {
|
||||
for (const t of m.declarations.tools) expect(typeof t.name).toBe('string');
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败** → FAIL(文件不存在)。
|
||||
|
||||
- [ ] **Step 3: 写实现**
|
||||
|
||||
要点(完整写 A1,其余 6 个按版图槽位表机械展开):
|
||||
|
||||
```ts
|
||||
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');
|
||||
|
||||
/** flow 槽位的业务模型骨架:MVP 作业指导书的步骤标题,一步一条 human 执行工单 —— 未实现期人工可兜 */
|
||||
const flowSkeleton = (steps: readonly string[]) => ({
|
||||
paradigm: 'flow' as const,
|
||||
steps: steps.map((label, i) => ({
|
||||
id: `S${i + 1}`,
|
||||
label,
|
||||
kind: 'human' as const,
|
||||
workOrder: 'execution' as const,
|
||||
card: {},
|
||||
deadline: 'P1D',
|
||||
})),
|
||||
parallelGroups: [],
|
||||
});
|
||||
|
||||
/** loop 槽位:CMMN 骨架(MVP 案例图的项),叙事 = 案例卡原文一句话 */
|
||||
const loopSkeleton = (items: readonly string[], narrative: string) => ({
|
||||
paradigm: 'loop' as const,
|
||||
cmmnXml: `<case><casePlanModel autoComplete="true">${items.map((it) => `<task name="${it}"/>`).join('')}</casePlanModel></case>`,
|
||||
narrative,
|
||||
dmn: { drd: [], tables: [] },
|
||||
});
|
||||
|
||||
const identity = (activityId: string, name: string, paradigm: 'flow' | 'loop') => ({
|
||||
activityId, name, packageVersion: '0.1.0', category: '统一报价', paradigm,
|
||||
sdkVersion: '1.0.0', checksum,
|
||||
});
|
||||
|
||||
const harness = { memoryWindow: 20, stallAfter: 'PT30M', nudge: [] };
|
||||
|
||||
export const MVP_SLOTS: readonly ActivityManifest[] = [
|
||||
activityManifestSchema.parse({
|
||||
identity: identity('rfq-intake', '客户RFQ受理', 'flow'),
|
||||
contract: {
|
||||
activityId: 'rfq-intake', version: '0.1.0',
|
||||
// contractInputSchema:data 是 JSON schema;message 允许但不得携带扩大授权的效力
|
||||
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: [] },
|
||||
requiredDeliverables: [], acceptanceCriteria: [], prohibitions: [],
|
||||
authorization: { discretion: [], preauthorizedActions: [], humanCallBudget: 3 },
|
||||
resources: {
|
||||
tools: [{ name: 'crm.history-price-read' }], skills: [], models: [], roles: ['BD'],
|
||||
budget: { steps: 40, tokens: 200_000, activeMinutes: 30, humanCalls: 3 },
|
||||
},
|
||||
},
|
||||
declarations: {
|
||||
models: [], listeners: [], harness,
|
||||
tools: [{ name: 'crm.history-price-read' }],
|
||||
ledger: declarationsFor([
|
||||
'project.rfq_file', 'project.intake_record', 'project.pn_class',
|
||||
'project.alt_request', 'project.inquiry_request',
|
||||
'intel.history_price', 'customer.profile',
|
||||
]),
|
||||
},
|
||||
assets: {
|
||||
businessModel: flowSkeleton(['接收登记', '追问补全信息', '逐条解析归一', '查历史价', '逐条定状况标', '定去向与派单']),
|
||||
skills: [], viewTemplates: [], cardSchemas: {}, distillHooks: [],
|
||||
},
|
||||
samples: [{ name: '契约样例:受理一单', input: { customerCode: 'C001', rfqRef: 'RFQ-001' },
|
||||
expect: { ports: ['intake'] } }],
|
||||
entry: { file: 'index.js', checksum },
|
||||
}),
|
||||
// …A2/A3/A4/A6/A7 同构:范式/岗位见版图表;out 口 payload 至少含表中出参;
|
||||
// A6 含 outcome:{enum:['won','revise','lost']};
|
||||
// businessModel:A4 flowSkeleton(['取价基准','逐条定价']);
|
||||
// A7 flowSkeleton(['结果登记','画像更新','落地率统计']);
|
||||
// A2 loopSkeleton(['替代对标表','初筛清单'], 'MVP §1.5:齐了即收 —— 必做项完成且无进行中项即结案');
|
||||
// A3 loopSkeleton(['寻源比价表','供应商回价单'], 'MVP §1.7:齐了即收');
|
||||
// A6 loopSkeleton(['正式报价单'], 'MVP §1.4:上接审批通过,办结经议价结果定去向');
|
||||
];
|
||||
|
||||
export async function installSlots(container: Container): Promise<void> {
|
||||
for (const manifest of MVP_SLOTS) {
|
||||
const verdict = await container.registry.install(manifest, SLOT_ARTIFACT);
|
||||
process.stdout.write(` 槽位 ${manifest.identity.name}(${manifest.identity.paradigm})静态审 ${verdict.staticFindings.length} 条\n`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意两处形状要以真实 schema 为准(写前 grep 校对,发现不合就以 schema 为准调用例):
|
||||
`contractInputSchema` / `outputPortsSchema`(contract7.ts)与 `authorization`/`resources` 的必填名;
|
||||
静态审若因「工具契约缺失」报 error,把该工具名先注册(Task 5 先行注册两件功能工具即可覆盖)。
|
||||
|
||||
- [ ] **Step 4: 测试过 + typecheck + 提交**
|
||||
|
||||
```bash
|
||||
pnpm exec vitest run apps/api/src/app/__tests__ && pnpm typecheck
|
||||
git add apps/api/src/app/seed-mvp-slots.ts apps/api/src/app/__tests__/seed-mvp-slots.test.ts
|
||||
git commit -m "MVP底座③:六活动槽位清单 —— 契约就绪,实现留白,工具按名解耦"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 流程包 1:1 + 触发器/通道 + 种子切换 + 整库重建
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/api/src/app/seed-mvp-flow.ts`
|
||||
- Create: `apps/api/src/app/seed-mvp.ts`(编排 ①LLM ②工具 ③槽位 ④流程包+触发器+通道)
|
||||
- Modify: `apps/api/src/app/seed.ts`(改调 `seedMvp`)
|
||||
- Delete: `apps/api/src/app/seed-quote.ts`
|
||||
- Test: `apps/api/src/app/__tests__/seed-mvp-flow.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `processPackageSchema`(nodes/edges/diagram)、`triggerConfigSchema`、Task 3 的槽位 id。
|
||||
- Produces: `export const MVP_PACKAGE: ProcessPackage`(id `unified-quote`,version 1,含 `diagram`);`export async function seedMvpFlow(container): Promise<void>`。
|
||||
|
||||
- [ ] **Step 1: 写失败测试(拓扑即契约)**
|
||||
|
||||
```ts
|
||||
/**
|
||||
* 流程包 1:1 —— 守三件事:拓扑与 MVP 图一致;网关条件读得到出参;闭合校验可发布。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MVP_PACKAGE } from '../seed-mvp-flow.ts';
|
||||
|
||||
describe('MVP_PACKAGE', () => {
|
||||
it('13 语义节点 · 15 边 · 三网关一审批点双终点', () => {
|
||||
expect(MVP_PACKAGE.nodes).toHaveLength(13);
|
||||
expect(MVP_PACKAGE.edges).toHaveLength(15);
|
||||
expect(MVP_PACKAGE.nodes.filter((n) => n.kind === 'end')).toHaveLength(2);
|
||||
expect(MVP_PACKAGE.nodes.filter((n) => n.kind === 'gateway')).toHaveLength(3);
|
||||
expect(MVP_PACKAGE.nodes.filter((n) => n.kind === 'approval')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('A5 是审批点:PM 审,驳回 reopen 回 A4 —— 驳回不是边', () => {
|
||||
const a5 = MVP_PACKAGE.nodes.find((n) => n.id === 'A5');
|
||||
expect(a5?.kind).toBe('approval');
|
||||
if (a5?.kind !== 'approval') return;
|
||||
expect(a5.approver.position).toBe('PM');
|
||||
expect(a5.reject.kind).toBe('reopen');
|
||||
if (a5.reject.kind === 'reopen') expect(a5.reject.nodeId).toBe('A4');
|
||||
// 唯一的 reopen 边是议价「有异议」回 A4
|
||||
expect(MVP_PACKAGE.edges.filter((e) => e.reopen).map((e) => `${e.from}→${e.to}`)).toEqual(['G4→A4']);
|
||||
});
|
||||
|
||||
it('网关条件按出参写:G1/G2 读旗,G4 读 outcome', () => {
|
||||
const cond = (from: string, to: string) => MVP_PACKAGE.edges.find((e) => e.from === from && e.to === to)?.condition;
|
||||
expect(cond('G1', 'A2')).toBe('needAlternative = true');
|
||||
expect(cond('G2', 'A4')).toBe('needSourcing = false');
|
||||
expect(cond('G4', 'A7')).toBe('outcome = "lost"');
|
||||
});
|
||||
|
||||
it('手绘图存证在包上 —— 呈现 1:1 的真相', () => {
|
||||
expect(MVP_PACKAGE.diagram).toBeDefined();
|
||||
const nodes = (MVP_PACKAGE.diagram as { nodes: { id: string; kind: string }[] }).nodes;
|
||||
expect(nodes.filter((n) => n.kind === 'callout')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 确认失败** → FAIL。
|
||||
|
||||
- [ ] **Step 3: 写 `seed-mvp-flow.ts`**
|
||||
|
||||
按「版图 · 节点表 + 边表」逐行转录。骨架:
|
||||
|
||||
```ts
|
||||
export const MVP_PACKAGE = 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' }, body: { type: 'string' }, quotedAt: { type: 'string' } } },
|
||||
output: { type: 'object', properties: { landingRate: { type: 'number' } } } },
|
||||
nodes: [ /* 节点表 13 行:activity 带 owner{position,strategy:'sole'} failure{kind:'terminate'};
|
||||
A5 审批点照 v1 的 AP 节点范式:
|
||||
{ id:'A5', kind:'approval', label:'定价审批',
|
||||
approver:{ position:'PM', strategy:'sole' },
|
||||
reject:{ kind:'reopen', nodeId:'A4', mapping:[…照 v1 e-g4-a4 式样,从 intake/draft 口取] },
|
||||
nudge:{ steps:[{ after:'PT20M', action:'nudge' }], deadline:'PT30M' },
|
||||
layout:{ row:1, col:6, x:810, y:260 } }
|
||||
gateway 带 gateway:'exclusive' mode:'fork' basis;layout{row,col,x,y} 用坐标表 x,y */ ],
|
||||
edges: [ /* 边表 15 行:label/tone/condition/reopen 逐字照表;mapping:
|
||||
start→A1 五条 initiation 映射(customerCode/rfqRef/subject/body/quotedAt)
|
||||
G1→A2 { target:'material',source:{from:'output-port',port:'intake',path:'pn'} } 等按 A2 入参
|
||||
G2→A3 按 A3 入参自 intake 口取 pn/brand
|
||||
G2→A4 / A3→A4 / A2→G2:A4 入参自 intake 口 + 上游口(supplier_floor/benchmark 可空)
|
||||
A4→A5 { target:'draft',source:{from:'output-port',port:'draft',path:'rows'} }
|
||||
A5→A6 通过(审批点唯一出边):draft 口取报价草案,照 v1 e-ap-a6 式样
|
||||
G4→A4(reopen) 参照旧 v1 的 reopen 映射式样(定稿于库中 v1:e-g4-a4)
|
||||
G4→A7 outcome/quote_no/customer_code 自 formal 口
|
||||
末端两边 mapping: [] */ ],
|
||||
variables: [],
|
||||
diagram: { /* FlowGraph:节点表全部 15 项(13 语义 + 2 callout)带 x/y/w/h;
|
||||
edges 带 label/tone 与折点(A4→A5 走右侧 910,80→990,80→990,280→910,280;
|
||||
G1→G2 顶部旁路经 y≈20;G4→A4 回边走底部 y≈370 过「返回调整→A4」卡点;
|
||||
「退回调整→A4」卡点(590,370)是 A5 驳回声明的呈现,不带边) */ },
|
||||
});
|
||||
```
|
||||
|
||||
`seed-mvp.ts` 统筹(顺序:LLM → 工具+白名单 → 槽位 → 流程包发布 → 触发器/通道),其中
|
||||
LLM 段与通道段**整段沿用** `seed-quote.ts` 现文(deepseek 三实例、`ch-rfq-mail`/`ch-rfq-review`、
|
||||
`trg-rfq`(event)与 `trg-rfq-manual`(registration)),白名单换新槽位 id:
|
||||
|
||||
```ts
|
||||
await container.toolbox.setAllowlist('rfq-intake', ['crm.history-price-read']);
|
||||
await container.toolbox.setAllowlist('cost-pricing', ['crm.history-price-read']);
|
||||
await container.toolbox.setAllowlist('quote-negotiation', ['crm.history-price-read', 'mail.send']);
|
||||
await container.toolbox.setAllowlist('sourcing-inquiry', ['crm.history-price-read', 'mail.send']);
|
||||
await container.toolbox.setAllowlist('alt-selection', ['crm.history-price-read']);
|
||||
await container.toolbox.setAllowlist('intel-archive', []);
|
||||
```
|
||||
|
||||
功能工具只留 `HISTORY_TOOL` 与 `MAIL_TOOL` 两件(quote-* 私有纯工具随旧活动退场 —— 纯工具属活动实现,槽位期没有实现就没有纯工具)。`seed.ts` 把 `seedQuote` 换成 `seedMvp`;删除 `seed-quote.ts`。
|
||||
|
||||
- [ ] **Step 4: 整库重建 + 种子**
|
||||
|
||||
```bash
|
||||
pnpm db:down && docker compose down -v 2>/dev/null; pnpm db:up && sleep 4 && pnpm db:migrate && pnpm db:seed
|
||||
```
|
||||
Expected 种子日志:LLM 三实例;两件功能工具;七槽位装入(静态审无 error);
|
||||
`流程包「统一报价」v1 已发布 —— 13 节点`;触发器两条 + 通道两条。
|
||||
|
||||
- [ ] **Step 5: 单实例与物化验收(curl + SQL)**
|
||||
|
||||
```bash
|
||||
docker exec jcai-pg psql -U jcai -d jcai -tAc "select id, version, state from sys.process_package;"
|
||||
# 期望:恰一行 unified-quote|1|published
|
||||
docker exec jcai-pg psql -U jcai -d jcai -tAc "select schemaname, count(*) from pg_tables where schemaname in ('project','customer','intel','supplier') group by 1 order by 1;"
|
||||
# 期望:customer 1 · intel 3 · project 12 · supplier 2
|
||||
```
|
||||
再起服务用 Chaihao 登录 → `GET /api/gov/process` 恰一包;`/design` 流程包页恰一张卡,
|
||||
点开画布 1:1(双终点、四网关、两卡点、边标签齐)。
|
||||
|
||||
- [ ] **Step 6: 五门 + 提交**
|
||||
|
||||
api:check 若报 seed 相关 d.ts 变化照常 `--update`。
|
||||
|
||||
```bash
|
||||
git add -A && git commit -m "MVP底座④:统一报价 1:1 流程包单实例 —— 四域物化,种子整体切换"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 自定义工具导入路由(后端)
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/contracts/src/api/governance.ts`
|
||||
- Modify: `apps/api/src/app/routes/governance-ops.ts`
|
||||
- Modify: `api-report/`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `container.toolbox.register(input): RegistrationVerdict`(两审:静态 + 样例)。
|
||||
- Produces:
|
||||
```ts
|
||||
export const registerToolRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/tools',
|
||||
body: z.object({
|
||||
contract: z.object({
|
||||
name: z.string().min(1), version: z.string().min(1), description: z.string().min(1),
|
||||
input: z.record(z.string(), z.unknown()), output: z.record(z.string(), z.unknown()),
|
||||
effect: z.enum(['read', 'write']),
|
||||
errors: z.array(z.object({ code: z.string(), retryable: z.boolean(), message: z.string() })).default([]),
|
||||
idempotent: z.boolean().default(true),
|
||||
timeoutMs: z.number().int().default(10_000), retries: z.number().int().default(1),
|
||||
connectionKeys: z.array(z.string()).default([]),
|
||||
}),
|
||||
/** 连接值只写不读 —— 键声明在契约,值密封在治理台(OS 7.3) */
|
||||
connectionValues: z.record(z.string(), z.string()).optional(),
|
||||
samples: z.array(z.object({ input: z.record(z.string(), z.unknown()) })).min(1),
|
||||
}),
|
||||
output: z.object({ accepted: z.boolean(), findings: z.array(z.object({ severity: z.string(), message: z.string() })) }),
|
||||
} as const satisfies RouteContract;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 契约 + handler**
|
||||
|
||||
handler 挨着 `listToolsRoute`,守卫 `guard('tool','govern')`(文件内工厂现成):
|
||||
|
||||
```ts
|
||||
/** 导入自定义工具 —— 与第一方同一条门:两审(静态 + 样例)过了才入目录 */
|
||||
app.post(registerToolRoute.path, async (c) => {
|
||||
const denied = await governTool(c.get('account'));
|
||||
if (denied) return c.json(denied, denyStatus(denied));
|
||||
const parsed = registerToolRoute.body.safeParse(await c.req.json());
|
||||
if (!parsed.success) {
|
||||
const body: ApiError = { code: 'validation-failed', message: '工具契约不合 schema', detail: parsed.error.issues };
|
||||
return c.json(body, 400);
|
||||
}
|
||||
const verdict = await container.toolbox.register({
|
||||
contract: parsed.data.contract,
|
||||
samples: parsed.data.samples.map((s) => ({ input: s.input, expectShape: { type: 'object' } })),
|
||||
...(parsed.data.connectionValues === undefined ? {} : { connectionValues: parsed.data.connectionValues }),
|
||||
});
|
||||
return c.json({
|
||||
accepted: verdict.accepted,
|
||||
findings: verdict.staticFindings.map((finding) => ({ severity: finding.severity, message: finding.message })),
|
||||
});
|
||||
});
|
||||
```
|
||||
(`governTool` 若不存在,按文件内 `guard('tool', 'govern')` 补一行;
|
||||
`toolbox.register` 的入参/回参字段名以 `apps/api/src/modules/toolbox/contract.ts` 实际为准,写前 grep。)
|
||||
|
||||
- [ ] **Step 2: curl 验证**
|
||||
|
||||
```bash
|
||||
curl -s -b $CLAUDE_JOB_DIR/tmp/ch.cookie -X POST http://localhost:8787/api/gov/tools \
|
||||
-H 'content-type: application/json' -d '{
|
||||
"contract": { "name": "erp.stock-read", "version": "1.0.0",
|
||||
"description": "读取 ERP 现货库存:PN → 库存量与库龄", "effect": "read",
|
||||
"input": { "type": "object", "required": ["pn"], "properties": { "pn": { "type": "string" } } },
|
||||
"output": { "type": "object", "properties": { "qty": { "type": "number" } } },
|
||||
"connectionKeys": ["endpoint"] },
|
||||
"connectionValues": { "endpoint": "replay://erp" },
|
||||
"samples": [{ "input": { "pn": "LM358DR" } }] }'
|
||||
```
|
||||
Expected: `{"accepted":true,...}`;随后 `GET /api/gov/tools` 目录多一件;坏 schema 400;无权 403。
|
||||
|
||||
- [ ] **Step 3: api:check + 提交**
|
||||
|
||||
```bash
|
||||
pnpm api:check --update && git add api-report
|
||||
git add packages/contracts apps/api/src/app/routes/governance-ops.ts
|
||||
git commit -m "MVP底座⑤:自定义工具导入路由 —— 与第一方同门两审"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 工具箱一页四段(卡片化 + LLM/人类/接入呈现)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/features/gov-tool/internal/panel.tsx`(整页重写)
|
||||
- Modify: `apps/web/src/shared/gov/gov-route.ts`(tools TABS:`catalog 能力目录` 保持单签,页内分段)
|
||||
- Modify: `docs/debt.md`(MCP 连接:仅呈现)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useGovTools`(功能工具)、`useGovLlm`(LLM 实例)、`registerToolRoute`(Task 5)。
|
||||
- Produces: `ToolsHome({ tab, nav }: HomeProps)` 四段卡片;`ToolPage` 详情保持。
|
||||
|
||||
- [ ] **Step 1: 重写 ToolsHome**
|
||||
|
||||
四段,全部用既有类(`g-shelf`/`g-tile` 网格 + `Sec` 分段 + `Prop/Field` 表单):
|
||||
|
||||
```tsx
|
||||
export function ToolsHome({ nav }: HomeProps) {
|
||||
const tools = useGovTools();
|
||||
const llm = useGovLlm();
|
||||
return (
|
||||
<QueryGate query={tools}>
|
||||
<>
|
||||
<Sec ico="wrench" title="功能工具" extra="读类自由调用 · 写类登记意向,放行后落地">
|
||||
<div className="g-shelf">
|
||||
{(tools.data?.tools ?? []).map((t) => (
|
||||
<button key={t.name} className={'g-tile t-' + (t.effect === 'write' ? 'warn' : 'ok')}
|
||||
onClick={() => nav.push({ k: 'tool', id: t.name })}>
|
||||
<span className="g-tile-h">
|
||||
<span className="g-tile-i"><Ico name="wrench" s={16} /></span>
|
||||
<span className="g-tile-t"><b>{t.name}</b><em>v{t.version}</em></span>
|
||||
<St tone={t.effect === 'write' ? 'warn' : 'ok'} ico={t.effect === 'write' ? 'pen-line' : 'circle-check'}>
|
||||
{t.effect === 'write' ? '写' : '读'}
|
||||
</St>
|
||||
</span>
|
||||
<span className="g-tile-m"><Chip tone="mute">{t.description.slice(0, 40)}</Chip></span>
|
||||
<span className="g-tile-f">
|
||||
<Chip ico="key">{t.connectionKeys.length === 0 ? '无连接' : `连接 ${t.connectionKeys.join('/')}`}</Chip>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Sec>
|
||||
|
||||
<Sec ico="cpu" title="LLM 工具" extra="调模型经模型网关一缝;活动只见逻辑名,不触厂商与密钥">
|
||||
<div className="g-shelf">
|
||||
{(llm.data?.instances ?? []).map((m) => (
|
||||
<button key={m.id} className={'g-tile t-' + (m.enabled ? 'ok' : 'mute')}
|
||||
onClick={() => nav.jump('system', 'llm', { k: 'llm', id: m.id })}>
|
||||
<span className="g-tile-h">
|
||||
<span className="g-tile-i"><Ico name="cpu" s={16} /></span>
|
||||
<span className="g-tile-t"><b>{m.name}</b><em>{m.provider} · {m.model}</em></span>
|
||||
<St tone={m.apiKeyBound ? 'ok' : 'danger'} ico={m.apiKeyBound ? 'circle-check' : 'alert-circle'}>
|
||||
{m.apiKeyBound ? '已绑密钥' : '未绑密钥'}
|
||||
</St>
|
||||
</span>
|
||||
<span className="g-tile-f">
|
||||
{m.systemUse && <Chip tone="brand">系统用途</Chip>}
|
||||
<span className="r">{m.enabled ? '启用中' : '已停用'}</span>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Sec>
|
||||
|
||||
<Sec ico="hand" title="人类工具" extra="调人经工单两模式与对话卡族;打扰上限在契约授权边界">
|
||||
<div className="g-shelf">
|
||||
{HUMAN_TOOLS.map((h) => (
|
||||
<div key={h.name} className="g-tile t-collab">
|
||||
<span className="g-tile-h">
|
||||
<span className="g-tile-i"><Ico name={h.ico} s={16} /></span>
|
||||
<span className="g-tile-t"><b>{h.name}</b><em>{h.mode}</em></span>
|
||||
<St tone="ok" ico="circle-check">内置</St>
|
||||
</span>
|
||||
<span className="g-tile-m"><Chip tone="mute">{h.desc}</Chip></span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Sec>
|
||||
|
||||
<Sec ico="plus" title="接入" extra="自定义工具走两审同门;MCP 工具经 ToolInvoker 一缝接入">
|
||||
<ImportToolCard />
|
||||
<McpCard />
|
||||
</Sec>
|
||||
</>
|
||||
</QueryGate>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`HUMAN_TOOLS` 静态目录(五形态,语义出自 `packages/contracts/src/card.ts`):
|
||||
|
||||
```tsx
|
||||
const HUMAN_TOOLS = [
|
||||
{ name: '执行工单', mode: 'work-order · execution', ico: 'clipboard-check', desc: '作业指导书 + 空表单,请人把一步做完;落地失败卡是它的系统预置形态' },
|
||||
{ name: '验收工单', mode: 'work-order · acceptance', ico: 'shield-check', desc: '请人验收产出:通过 / 驳回重做 / 驳回迭代' },
|
||||
{ name: '决策卡', mode: 'chat-card · decision', ico: 'git-branch', desc: '分支条件选择表,含自定义收尾项 —— 决策是对话区的选择题' },
|
||||
{ name: '补全卡', mode: 'chat-card · elicitation', ico: 'pen-line', desc: '就地要几个小信息;「没法填」旁路与提交两条路必走其一' },
|
||||
{ name: '评价卡', mode: 'chat-card · rating', ico: 'star', desc: '任务评价:评分 1–5 + 改进建议,回流度量与蒸馏' },
|
||||
] as const;
|
||||
```
|
||||
|
||||
`ImportToolCard`:Card + Prop/Field 表单(名称/说明/效应 Seg/入参 schema textarea(JSON)/连接键 ListEdit/连接值(逐键 input)/样例入参 textarea)→ `AskBand` 确认(影响:两审过了即入目录、可入白名单;审不过给 findings)→ `call(registerToolRoute)` → 失效 `['gov','tools']`,失败把 findings 列成 `Note tone="warn"`。
|
||||
`McpCard`:呈现卡(St mute「未连接」),正文一句话:「连接 MCP 服务器后,其工具经两审入目录、按白名单授给活动 —— 接入走 ToolInvoker 一缝,底座就绪后开放。」按钮 disabled。
|
||||
|
||||
- [ ] **Step 2: debt 登记**
|
||||
|
||||
`docs/debt.md` 追加:「工具箱 · MCP 连接(2026-07-27):仅呈现卡。真接入需 MCP 客户端 + 工具目录同步(经 ToolInvoker 一缝、两审入目录),当前无运行时 —— 卡上如实标注未连接,连接按钮不可点。」
|
||||
|
||||
- [ ] **Step 3: 浏览器验收 + 五门 + 提交**
|
||||
|
||||
`/design` → 工具箱:四段齐;导入 `erp.stock-read`(若 Task 5 已注册过,换名 `erp.po-read`)走完确认带 → 功能工具段多一张卡。五门全绿。
|
||||
|
||||
```bash
|
||||
git add apps/web/src/features/gov-tool apps/web/src/shared/gov/gov-route.ts docs/debt.md
|
||||
git commit -m "MVP底座⑥:工具箱四段卡片 —— 功能/LLM/人类/接入,导入走两审同门"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 活动库卡片化(六槽位卡)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/features/gov-activity/internal/panel.tsx`(LibraryHome 列表→卡片网格)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useGovActivities`(activityId/name/category/paradigm/enabled/packageVersions/assetVersions)。
|
||||
- Produces: `LibraryHome` 卡网格;`ActPage` 详情保持。
|
||||
|
||||
- [ ] **Step 1: LibraryHome 重写为 `g-shelf` 卡网格**
|
||||
|
||||
每活动一张 `g-tile`:范式章(标准活动=ok / 案例活动=collab)+ 岗位/分类 chips + 版本徽 + **槽位状态**——
|
||||
`packageVersions` 里最新版即契约版;实现是否留白,以「活动分类 `统一报价` + 当前无资产增量」呈现为
|
||||
`St tone="warn" ico="square-pen"` 「契约就绪 · 待实现」(判定:`assetVersions.length === 0` 或全部 `effective === false`;
|
||||
若字段实际形状不同,以 `GovActivitiesOutput` 为准 grep 后调整):
|
||||
|
||||
```tsx
|
||||
<button key={a.activityId} className={'g-tile t-' + (a.paradigm === 'flow' ? 'ok' : 'warn')}
|
||||
onClick={() => nav.push({ k: 'act', id: a.activityId })}>
|
||||
<span className="g-tile-h">
|
||||
<span className="g-tile-i"><Ico name={a.paradigm === 'flow' ? 'list-checks' : 'rotate-ccw'} s={16} /></span>
|
||||
<span className="g-tile-t"><b>{a.name}</b><em>{a.activityId}</em></span>
|
||||
<Chip tone={a.paradigm === 'flow' ? 'ok' : 'warn'}>{ACTIVITY_TYPE_LABEL[a.paradigm]}</Chip>
|
||||
</span>
|
||||
<span className="g-tile-m">
|
||||
<Chip ico="boxes">{a.category}</Chip>
|
||||
<Chip ico="git-commit">包 v{a.packageVersions[a.packageVersions.length - 1]?.version ?? '—'}</Chip>
|
||||
</span>
|
||||
<span className="g-tile-f">
|
||||
<St tone="warn" ico="square-pen">契约就绪 · 待实现</St>
|
||||
<span className="r">{a.enabled ? '已上架' : '停用'}</span>
|
||||
</span>
|
||||
</button>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验收 + 提交**
|
||||
|
||||
`/design` → 活动库:六张槽位卡(A1/A2/A3/A4/A6/A7,范式两色、全部「契约就绪 · 待实现」;A5 是审批点,不在库中);点卡进详情不回退。五门全绿。
|
||||
|
||||
```bash
|
||||
git add apps/web/src/features/gov-activity
|
||||
git commit -m "MVP底座⑦:活动库卡片化 —— 六槽位卡,契约就绪待实现"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 数据域卡片化(四域大卡)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/features/gov-data/internal/panel.tsx`(DataHome 域分组列表→四域大卡)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useGovObjects` / `useGovLedger`(列/主键)、`domainShort`/`sortDomains`(Task 1)。
|
||||
- Produces: `DataHome` 四域卡;`LedgersHome`/`ObjPage` 不动(运营台列表为主,按需求 1 保留)。
|
||||
|
||||
- [ ] **Step 1: DataHome 重写**
|
||||
|
||||
一域一张大卡(`Card`),卡头 = 短名 + 域键 mono + 对象数;卡体 = 对象 chips 网格(`g-entries` 或 chips 流式),
|
||||
点对象 chip → `nav.push({ k: 'obj', id })`(设计侧详情仍只见结构):
|
||||
|
||||
```tsx
|
||||
export function DataHome({ nav }: HomeProps) {
|
||||
const q = useGovObjects();
|
||||
const objects = q.data?.objects ?? [];
|
||||
const domains = sortDomains([...new Set(objects.map((o) => o.domain))]);
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
<>
|
||||
<Note>数据按经营对象四分:<b>项目</b>(每一单的流转单据)· <b>客户</b> · <b>情报</b> · <b>供应商</b>(沉淀资产)。这里改结构;行数据在运营中心 · 台账看。</Note>
|
||||
{domains.map((d) => (
|
||||
<Card key={d} ico="database" title={domainShort(d)}
|
||||
extra={<span className="g-mono">{d} · {objects.filter((o) => o.domain === d).length} 对象</span>}>
|
||||
<div className="g-entries">
|
||||
{objects.filter((o) => o.domain === d).map((o) => (
|
||||
<Entry key={o.object} ico="table" title={o.object}
|
||||
desc={`${d}.${o.object}`}
|
||||
onClick={() => nav.push({ k: 'obj', id: `${d}.${o.object}` })} />
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
{objects.length === 0 && <Clear>还没有物化的对象 —— 槽位安装时按声明物化,种子跑完就出现在这里。</Clear>}
|
||||
</>
|
||||
</QueryGate>
|
||||
);
|
||||
}
|
||||
```
|
||||
(`Entry` 的 desc 想带列数/主键需逐对象再查 —— 设计首屏不发 19 个查询,列数留给点开后的 ObjPage。)
|
||||
|
||||
- [ ] **Step 2: 验收 + 提交**
|
||||
|
||||
`/design` → 数据域:四张大卡(项目 12 · 客户 1 · 情报 3 · 供应商 2);运营台台账页签同步变四域短名(项目/客户/情报/供应商 —— Task 1 字典已生效)。五门全绿。
|
||||
|
||||
```bash
|
||||
git add apps/web/src/features/gov-data
|
||||
git commit -m "MVP底座⑧:数据域四域大卡 —— 项目/客户/情报/供应商"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: 收口 —— 全链走查 · 文档对账 · 五门
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/产品设计.md`(附录追记:四域划分决策 + 设计台卡片/运营台列表原则)
|
||||
- Modify: `docs/debt.md`(对账)
|
||||
|
||||
- [ ] **Step 1: 走查清单(浏览器,Chaihao)**
|
||||
|
||||
1. `/design` 蓝图:链上四数 = 流程包 1 · 活动 6 · 工具 ≥3(2 内置 + 导入的)· 数据域 4;待补清单为空(全闭合、全安装、有调起);
|
||||
2. 流程包卡 → 画布对照 `docs/MVP示例工作流.html` FIG(双终点/三网关/琥珀审批点/两卡点/边标签;G3 被审批点吸收是唯一预期差异);属性卡点 A5 见审批人 PM 与驳回去向 A4;
|
||||
3. 活动库七卡 / 工具箱四段 / 数据域四卡;
|
||||
4. `/ops` 台账页签 = 项目/客户/情报/供应商,各域对象行数 0(新库);
|
||||
5. 起单(登记触发)→ 工作流建单、A1 任务出现;任务如实报未实现(不装作跑过)—— 截图留档;
|
||||
6. 调起规则页两条(事件 + 登记)。
|
||||
|
||||
- [ ] **Step 2: 文档**
|
||||
|
||||
`产品设计.md` 附录补一行决策:「数据四域(2026-07-27):项目=流转单据,客户/情报/供应商=沉淀资产;设计台卡片式、运营台列表式为交互基调」。`debt.md` 确认含:MCP 仅呈现、槽位未实现(逐活动开发清单 A1→A7)。
|
||||
|
||||
- [ ] **Step 3: 五门 + 提交**
|
||||
|
||||
```bash
|
||||
pnpm gen:tiers && pnpm typecheck && pnpm lint && pnpm depcruise && pnpm api:check && pnpm test
|
||||
git add docs && git commit -m "MVP底座⑨:收口 —— 走查通过,四域决策入档"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自审记录
|
||||
|
||||
- **规格覆盖**:需求 1→Task 6/7/8(+流程包已卡片);需求 2→Task 1/2/4(四域物化与呈现);需求 3→Task 3/4(槽位契约 + 1:1 拓扑与手绘图;A5 审批点、G3 吸收为唯一预期差异);需求 4→Task 5/6(功能/LLM/人类三段 + 导入实装 + MCP 呈现,MCP 无运行时为**已声明偏差**记 debt);需求 5→Global Constraints(工具按名声明、槽位无实现、占位入口不可执行语义)+ Task 3。
|
||||
- **占位扫描**:重复结构以「版图数据表 + 单个完整样例」承载,表即内容;两处「以实际 schema 为准 grep 后调整」是形状核对动作,不是留白。
|
||||
- **类型一致性**:槽位 id 六处(Task 3 清单 / Task 4 白名单与节点 activityId / Task 7 呈现)统一用 `rfq-intake / alt-selection / sourcing-inquiry / cost-pricing / quote-negotiation / intel-archive`(A5 审批点无 activityId);`declarationsFor` 签名 Task 2 定义、Task 3 消费;`registerToolRoute` Task 5 定义、Task 6 消费。
|
||||
- **形状已核**:`contractInputSchema={data,messageAllowed}`、`outputPortSchema={name,payload,description?}`、`authorization={discretion,preauthorizedActions,humanCallBudget}`、`resources={tools,skills,models,roles,budget{steps,tokens,activeMinutes,humanCalls}}` —— Task 3 样例即按此写,A2–A7 展开照抄此形。Node 24,`Object.groupBy` 可用。
|
||||
- **已知取舍(执行者与评审须知)**:① **A5 = 内置审批点**(用户确认):驳回走 reject 声明 reopen A4,G3「审批结果」被审批点吸收 —— 图与 MVP 差一颗菱形,语义同构;定价审批表落审批机制不入台账(对象 18);② 槽位 flow 骨架的步骤是 human 执行工单标题级占位,入口 JS 直接抛「未实现」—— 运行不会假装成功;③ 整库重建,旧 quote_* 域与多版本包全部退场,`activities/quote-*` 目录留仓作参考实现、不再安装。
|
||||
@@ -0,0 +1,822 @@
|
||||
> **⚠️ 已归档(2026-07-30)** —— 三面拆分计划,已执行完毕(拆分后又有演进:
|
||||
> 运营中心撤「数据」区成六区、触发器与大模型独立成区)。现行形态看 `docs/产品设计.md`。
|
||||
|
||||
# 三面一体(设计中心 / 运营中心 / 工作台)实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 按 `docs/产品设计.md` 把治理中心拆成设计中心(`/design`)与运营中心(`/ops`)两个台面,实现到**编排层**为止 —— 壳、两张首页、触发器归位、台账动态位、工作组管理;活动内部(活动开发、契约深化)不在本计划内。
|
||||
|
||||
**Architecture:** 纯前端信息架构重排 + 三处小后端增量(触发器 upsert 路由、度量路由补齐八指标、工作组读写路由 —— 全部是既有模块用例的 HTTP 面,零模块语义改动)。壳层由一份 surface 配置驱动:`surfaces.ts` 定静态区表与权限亮灯,`gov-shell` 参数化成两壳,台账导航项按已发布数据域运行时生成。
|
||||
|
||||
**Tech Stack:** React 19 + Vite + TanStack Query(前端零过程状态,服务端投影);Hono + zod 契约(`packages/contracts/src/api/governance.ts`);vitest(纯逻辑单测);皮肤复用 `console.css` 的 `g-*` 类,**不新造类名**。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 五门必须全绿才算一个任务完成:`pnpm typecheck && pnpm lint && pnpm depcruise && pnpm api:check && pnpm test`(api:check 报契约变更时:同步 CONTRACT.md → `pnpm api:check --update` → `git add api-report`)。
|
||||
- 编码规范 §12.5 禁用:`any`、`as unknown as`、`@ts-ignore`、domain 层 IO、全局可变状态、删测试、改门禁配置。
|
||||
- `exactOptionalPropertyTypes: true`:显式赋 `undefined` 的可选属性要写成 `field?: T | undefined`。
|
||||
- ESM 相对导入必须带扩展名(`./x.ts` / `./x.tsx`);web 别名:`@contracts` `@web-shared` `@features` `@ui`。
|
||||
- 不新增任何 npm 依赖。
|
||||
- 治理台界面只用 `console.css` 已有的 `g-*` 类(自造类名 = 没样式);共享件从 `@web-shared/gov/primitives.tsx` 取(Chip/St/Note/Sec/Kpi/Card/Rows/NList/NRow/Entry/PageHead/Todo/Clear/QueryGate/Prop/Field/Seg/ListEdit/Acts)。
|
||||
- 空态文案用**用户视角**(「还没有…,会有什么」),不写「还没接」。
|
||||
- 授权模型不动:九治理功能 × 读/写/治;台面入口按「持有该面任一功能的任意授权」亮灯(动作蕴含 govern ⊃ write ⊃ read,有任意 grant 即至少可读)。
|
||||
- 提交信息:中文一句话,风格同 `git log`(如「三面拆分:…」),结尾加
|
||||
`Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>` 与 `Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m`。
|
||||
- 服务地址:API `:8787`(`pnpm --filter @app/api dev`),Web `:5273`(`cd apps/web && npx vite --port 5273 --strictPort`;5173 被别的项目占用)。测试账号 `Chaihao` / `ch19900614`。
|
||||
- 浏览器验证一律先 `POST /api/login` 拿 cookie(HttpOnly 会话;curl 用 `-c/-b /tmp/ch.cookie`)。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 基线入库
|
||||
|
||||
工作区里有约 90 个未提交文件(鉴权、工作组、工作台修复、编排工作区、产品设计文档等)。**先整体入库一次**,否则后续每个任务的提交都会混入无关变更,评审无从做起。
|
||||
|
||||
**Files:**
|
||||
- Modify: 无(纯 git 操作)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: 干净的工作区 —— 后续任务的 `git add` 只含本任务文件。
|
||||
|
||||
- [ ] **Step 1: 确认五门全绿(基线必须是好的)**
|
||||
|
||||
Run: `pnpm typecheck && pnpm lint && pnpm depcruise && pnpm test`
|
||||
Expected: 全部通过(api:check 已在上一轮 `--update` 并 `git add`,若再报差异则先 `pnpm api:check --update && git add api-report`)。
|
||||
|
||||
- [ ] **Step 2: 整体提交**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "基线:鉴权会话+工作组+工作台三区修复+编排工作区+产品设计文档
|
||||
|
||||
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||||
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 确认干净**
|
||||
|
||||
Run: `git status --porcelain | wc -l`
|
||||
Expected: `0`
|
||||
|
||||
---
|
||||
|
||||
### Task 2: surface 配置与纯逻辑(surfaces.ts)
|
||||
|
||||
三面骨架的唯一事实源:区归属、权限亮灯、旧链接改写 —— 全部纯函数,先测后写。
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/web/src/shared/gov/surfaces.ts`
|
||||
- Create: `apps/web/src/shared/gov/__tests__/surfaces.test.ts`
|
||||
- Modify: `apps/web/src/shared/gov/gov-route.ts`(GovZone 增减 + TABS/ZM 相应条目)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `gov-route.ts` 的 `GovZone`、`ZONES`、`TABS`、`ZM`;`@contracts` 的 `Session`、`GovernanceFunction`。
|
||||
- Produces(后续任务按名引用):
|
||||
```ts
|
||||
export type Surface = 'design' | 'ops';
|
||||
export const SURFACE_META: Readonly<Record<Surface, { path: string; brand: string; searchHint: string }>>;
|
||||
export const SURFACE_ZONES: Readonly<Record<Surface, readonly GovZone[]>>;
|
||||
export const SURFACE_FNS: Readonly<Record<Surface, readonly GovernanceFunction[]>>;
|
||||
export function canSeeSurface(session: Session | undefined, surface: Surface): boolean;
|
||||
export function surfaceOfZone(zone: GovZone): Surface;
|
||||
/** 旧 #/gov/<zone>/… 深链 → 新台面完整 URL(pathname+hash);非 gov 哈希返回 undefined */
|
||||
export function legacyGovTarget(hash: string): string | undefined;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 改 `gov-route.ts` 的区表(surfaces 的类型依赖它,先动它)**
|
||||
|
||||
在 `gov-route.ts`:
|
||||
1. `GOV_ZONES` 改为:
|
||||
```ts
|
||||
export const GOV_ZONES = [
|
||||
'blueprint', 'workflows', 'library', 'tools', 'data', // 设计中心
|
||||
'cockpit', 'tasks', 'people', 'ledgers', 'knowledge', 'system', // 运营中心
|
||||
] as const;
|
||||
```
|
||||
(删 `'triggers'` 与 `'memory'` —— 触发器定义并入 workflows,处置并入 tasks;度量升驾驶舱,活动记忆归知识库。)
|
||||
2. `ZONES` 数组同步(label/icon):`blueprint 蓝图/route`、`cockpit 驾驶舱/gauge`、`ledgers 台账/database`、`knowledge 知识库/layers`;其余沿用;删 triggers/memory 两行。
|
||||
3. `ZM` 增四条、删两条(desc 从 `docs/产品设计.md` 二、三节的定位文字取;`ledgers` 的 desc:「行数据 · 版本 · 引用 —— 域的 schema 在设计中心改,这里做运营动作」)。
|
||||
4. `TABS` 增删:
|
||||
```ts
|
||||
blueprint: [{ key: 'map', label: '企业蓝图', ico: 'route' }],
|
||||
cockpit: [{ key: 'dash', label: '驾驶舱', ico: 'gauge' }],
|
||||
knowledge: [{ key: 'layers', label: '活动记忆', ico: 'layers' }],
|
||||
ledgers: [], // 动态:页签即数据域,由壳层运行时生成
|
||||
workflows: [ …原三条…, { key: 'rules', label: '调起规则', ico: 'zap' } ],
|
||||
tasks: [ …原四条…, { key: 'trg', label: '起单开关', ico: 'zap' } ],
|
||||
```
|
||||
(`TABS` 类型放宽为 `ReadonlyArray<…>` 可空数组即可,`readPlace` 对空 tabs 的兜底在 Task 4 处理。)
|
||||
|
||||
- [ ] **Step 2: 写失败测试**
|
||||
|
||||
`apps/web/src/shared/gov/__tests__/surfaces.test.ts`:
|
||||
```ts
|
||||
/**
|
||||
* 三面骨架的唯一事实源 —— 区归属、亮灯、旧链接改写。
|
||||
* 这里守的是「配置自洽」:每个区恰属一面;亮灯只看该面功能;旧深链一条不死。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Session } from '@contracts';
|
||||
import { sessionSchema } from '@contracts';
|
||||
import { GOV_ZONES } from '../gov-route.ts';
|
||||
import { SURFACE_ZONES, canSeeSurface, legacyGovTarget } from '../surfaces.ts';
|
||||
|
||||
/** 经 schema 构造合法会话 —— §12.5 禁 `as unknown as`,测试同样遵守 */
|
||||
const sessionWith = (fns: string[]): Session =>
|
||||
sessionSchema.parse({
|
||||
staff: { id: 's-1', account: 'a', name: 'n', positionId: 'p-1', state: 'active' },
|
||||
position: { id: 'p-1', code: 'PM', name: '项目经理' },
|
||||
grants: fns.map((fn) => ({
|
||||
positionId: 'p-1',
|
||||
object: { kind: 'governance', function: fn },
|
||||
action: 'read',
|
||||
})),
|
||||
});
|
||||
|
||||
describe('SURFACE_ZONES', () => {
|
||||
it('每个区恰属一面,不重不漏', () => {
|
||||
const all = [...SURFACE_ZONES.design, ...SURFACE_ZONES.ops];
|
||||
expect([...all].sort()).toEqual([...GOV_ZONES].sort());
|
||||
expect(new Set(all).size).toBe(all.length);
|
||||
});
|
||||
it('设计面没有任何运行态区', () => {
|
||||
for (const z of ['tasks', 'cockpit', 'ledgers'] as const) {
|
||||
expect(SURFACE_ZONES.design).not.toContain(z);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('canSeeSurface —— 入口按读权亮灯', () => {
|
||||
it('持有该面任一功能的授权即亮', () => {
|
||||
expect(canSeeSurface(sessionWith(['process']), 'design')).toBe(true);
|
||||
expect(canSeeSurface(sessionWith(['task']), 'ops')).toBe(true);
|
||||
});
|
||||
it('只有另一面的授权不亮', () => {
|
||||
expect(canSeeSurface(sessionWith(['task']), 'design')).toBe(false);
|
||||
});
|
||||
it('trigger 与 data 两面都算(定义归设计、处置与行数据归运营)', () => {
|
||||
expect(canSeeSurface(sessionWith(['trigger']), 'design')).toBe(true);
|
||||
expect(canSeeSurface(sessionWith(['trigger']), 'ops')).toBe(true);
|
||||
expect(canSeeSurface(sessionWith(['data']), 'ops')).toBe(true);
|
||||
});
|
||||
it('无会话不亮', () => {
|
||||
expect(canSeeSurface(undefined, 'design')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacyGovTarget —— 旧 #/gov 深链一条不死', () => {
|
||||
it('设计态区改写到 /design', () => {
|
||||
expect(legacyGovTarget('#/gov/workflows/pkgs/pkg:unified-quote%404')).toBe(
|
||||
'/design#/design/workflows/pkgs/pkg:unified-quote%404',
|
||||
);
|
||||
});
|
||||
it('运行态区改写到 /ops', () => {
|
||||
expect(legacyGovTarget('#/gov/tasks/board')).toBe('/ops#/ops/tasks/board');
|
||||
});
|
||||
it('已删的区落到对应新家:triggers→设计·workflows/rules,memory→运营·cockpit', () => {
|
||||
expect(legacyGovTarget('#/gov/triggers/rules')).toBe('/design#/design/workflows/rules');
|
||||
expect(legacyGovTarget('#/gov/memory/metrics')).toBe('/ops#/ops/cockpit/dash');
|
||||
expect(legacyGovTarget('#/gov/memory/layers')).toBe('/ops#/ops/knowledge/layers');
|
||||
});
|
||||
it('裸 #/gov 落运营首页;非 gov 哈希不管', () => {
|
||||
expect(legacyGovTarget('#/gov')).toBe('/ops#/ops/cockpit/dash');
|
||||
expect(legacyGovTarget('#/whatever')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
- [ ] **Step 3: 跑测试确认失败**
|
||||
|
||||
Run: `pnpm exec vitest run apps/web/src/shared/gov 2>&1 | tail -5`
|
||||
Expected: FAIL(`surfaces.ts` 不存在 / gov-route 缺新区)。
|
||||
|
||||
- [ ] **Step 4: 写实现 `surfaces.ts`**
|
||||
|
||||
```ts
|
||||
/**
|
||||
* 三面骨架的唯一事实源(docs/产品设计.md §1/§6)。
|
||||
* 区归属、权限亮灯、旧链接改写都从这里出 —— 壳层只消费,不各写各的。
|
||||
*/
|
||||
import type { Session } from '@contracts';
|
||||
import type { GovZone } from './gov-route.ts';
|
||||
|
||||
export type Surface = 'design' | 'ops';
|
||||
|
||||
export const SURFACE_META: Readonly<Record<Surface, { path: string; brand: string; searchHint: string }>> = {
|
||||
design: { path: '/design', brand: '设计中心', searchHint: '检索定义:流程包 / 活动 / 工具 / 数据域…' },
|
||||
ops: { path: '/ops', brand: '运营中心', searchHint: '检索运行:任务 / 工作流 / 员工 / 台账…' },
|
||||
};
|
||||
|
||||
export const SURFACE_ZONES: Readonly<Record<Surface, readonly GovZone[]>> = {
|
||||
design: ['blueprint', 'workflows', 'library', 'tools', 'data'],
|
||||
ops: ['cockpit', 'tasks', 'people', 'ledgers', 'knowledge', 'system'],
|
||||
};
|
||||
|
||||
/**
|
||||
* 亮灯功能集:trigger 与 data 两面都算 ——
|
||||
* 触发器「定义归设计、处置归运营」;数据「schema 归设计、行数据归运营」。
|
||||
*/
|
||||
export const SURFACE_FNS: Readonly<Record<Surface, readonly string[]>> = {
|
||||
design: ['process', 'activity', 'tool', 'data', 'trigger'],
|
||||
ops: ['task', 'staff', 'memory', 'system', 'trigger', 'data'],
|
||||
};
|
||||
|
||||
export function canSeeSurface(session: Session | undefined, surface: Surface): boolean {
|
||||
if (session === undefined) return false;
|
||||
// 动作蕴含 govern ⊃ write ⊃ read:持有任意动作的 grant 即至少可读,入口即亮
|
||||
return session.grants.some(
|
||||
(g) => g.object.kind === 'governance' && (SURFACE_FNS[surface] as readonly string[]).includes(g.object.function),
|
||||
);
|
||||
}
|
||||
|
||||
export function surfaceOfZone(zone: GovZone): Surface {
|
||||
return (SURFACE_ZONES.design as readonly string[]).includes(zone) ? 'design' : 'ops';
|
||||
}
|
||||
|
||||
/** 已删区的搬迁表:旧深链按内容归新家 */
|
||||
const LEGACY_ZONE: Readonly<Record<string, { surface: Surface; zone: string; tab?: string }>> = {
|
||||
triggers: { surface: 'design', zone: 'workflows', tab: 'rules' },
|
||||
memory: { surface: 'ops', zone: 'cockpit', tab: 'dash' },
|
||||
};
|
||||
|
||||
export function legacyGovTarget(hash: string): string | undefined {
|
||||
const parts = hash.replace(/^#\/?/, '').split('/').filter((s) => s !== '');
|
||||
if (parts[0] !== 'gov') return undefined;
|
||||
const zone = parts[1];
|
||||
if (zone === undefined) return '/ops#/ops/cockpit/dash';
|
||||
|
||||
const moved = LEGACY_ZONE[zone];
|
||||
if (moved !== undefined) {
|
||||
// memory 的两个旧页签各归其位
|
||||
const tab = zone === 'memory' && parts[2] === 'layers' ? undefined : moved.tab;
|
||||
const target = zone === 'memory' && parts[2] === 'layers' ? { surface: 'ops' as const, zone: 'knowledge', tab: 'layers' } : { ...moved, tab };
|
||||
return `${SURFACE_META[target.surface].path}#/${target.surface}/${target.zone}/${target.tab ?? ''}`.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
const surface = surfaceOfZone(zone as GovZone);
|
||||
const rest = parts.slice(1).join('/');
|
||||
return `${SURFACE_META[surface].path}#/${surface}/${rest}`;
|
||||
}
|
||||
```
|
||||
(实现时按测试逐条对齐 —— 特别是 `#/gov/memory/layers` 与 `#/gov/triggers/rules` 两条的期望输出。)
|
||||
|
||||
- [ ] **Step 5: 跑测试确认通过**
|
||||
|
||||
Run: `pnpm exec vitest run apps/web/src/shared/gov 2>&1 | tail -5`
|
||||
Expected: PASS(全部用例)。
|
||||
随后 `pnpm typecheck` —— gov-route 改动会把还引用 `triggers`/`memory` 区的旧代码打红(gov-shell / PkgPage 的 `nav.jump('triggers', …)` 等)。**本任务只修类型层面**:PkgPage 里 `nav.jump('triggers', 'rules', …)` 改为 `nav.jump('workflows', 'rules', …)`;gov-shell 的 TriggersHome/MemoryHome 引用暂时改挂到新区键(`knowledge`→MemoryHome、`cockpit`→MemoryHome,Task 5/8 再真正拆)。tsc 必须归零。
|
||||
|
||||
- [ ] **Step 6: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/web/src/shared/gov/surfaces.ts apps/web/src/shared/gov/__tests__/surfaces.test.ts \
|
||||
apps/web/src/shared/gov/gov-route.ts apps/web/src/features/gov-process/internal/panel.tsx \
|
||||
apps/web/src/app/gov-shell.tsx
|
||||
git commit -m "三面拆分①:surface 配置与区表 —— 归属/亮灯/旧链改写皆纯函数先测后写"
|
||||
```
|
||||
(此后每个提交都带 Global Constraints 里的两行 footer,下略。)
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 网页标签三章 + 入口路由(/design、/ops、/gov 跳转)
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/app/tab-identity.ts`
|
||||
- Modify: `apps/web/src/app/main.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `legacyGovTarget`、`SURFACE_META`(Task 2)。
|
||||
- Produces: `applyTabIdentity(surface: 'desk' | Surface): void`;`main.tsx` 导出的常量 `surface: 'desk' | Surface`(Gate 组件与 Task 4 的壳选择用)。
|
||||
|
||||
- [ ] **Step 1: `tab-identity.ts` 扩为三章**
|
||||
|
||||
签名 `applyTabIdentity(surface: 'desk' | 'design' | 'ops')`。新增两枚 SVG(与现有 `DESK_ICON`/`GOV_ICON` 同构;`GOV_ICON` 弃用删除):
|
||||
```ts
|
||||
/** 设计中心:深靛底 + 路网(定义企业的流转) */
|
||||
const DESIGN_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="8" fill="#232b6e"/>
|
||||
<g transform="translate(4,4)" fill="none" stroke="#9db1ff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="6" cy="19" r="3"/><circle cx="18" cy="5" r="3"/>
|
||||
<path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15" stroke="#fff"/>
|
||||
</g>
|
||||
</svg>`;
|
||||
/** 运营中心:深色底 + 仪表(看企业今天跑得怎样) */
|
||||
const OPS_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="8" fill="#141b2e"/>
|
||||
<g transform="translate(4,4)" fill="none" stroke="#7fe0c3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m12 14 4-4" stroke="#fff"/>
|
||||
<path d="M3.34 19a10 10 0 1 1 17.32 0"/>
|
||||
</g>
|
||||
</svg>`;
|
||||
```
|
||||
标题:`JCAI 工作台` / `JCAI 设计中心` / `JCAI 运营中心`。
|
||||
|
||||
- [ ] **Step 2: `main.tsx` 入口判台**
|
||||
|
||||
替换现有 `isGovernance` 段:
|
||||
```ts
|
||||
import { legacyGovTarget, SURFACE_META } from '@web-shared/gov/surfaces.ts';
|
||||
import type { Surface } from '@web-shared/gov/surfaces.ts';
|
||||
|
||||
/** 旧 /gov 链接一条不死:带着深链改写到新台面(replace,不留历史) */
|
||||
const legacy = window.location.pathname.startsWith('/gov')
|
||||
? (legacyGovTarget(window.location.hash) ?? '/ops#/ops/cockpit/dash')
|
||||
: undefined;
|
||||
if (legacy !== undefined) window.location.replace(legacy);
|
||||
|
||||
const surface: 'desk' | Surface = window.location.pathname.startsWith(SURFACE_META.design.path)
|
||||
? 'design'
|
||||
: window.location.pathname.startsWith(SURFACE_META.ops.path)
|
||||
? 'ops'
|
||||
: 'desk';
|
||||
applyTabIdentity(surface);
|
||||
```
|
||||
样式装载:`surface === 'desk'` 装 `proto.vendor.css + app.css`,否则装 `theme.css + console.css`(两台治理面同一套皮肤)。`Gate` 里 `isGovernance` 相应改为 `surface !== 'desk'`,`GovShell` 暂以 `<GovShell />` 原样渲染(Task 4 传 surface)。
|
||||
|
||||
- [ ] **Step 3: 验证**
|
||||
|
||||
```bash
|
||||
lsof -ti:8787 >/dev/null || (nohup pnpm --filter @app/api dev >/tmp/api.log 2>&1 &)
|
||||
lsof -ti:5273 >/dev/null || (cd apps/web && nohup npx vite --port 5273 --strictPort >/tmp/web.log 2>&1 &)
|
||||
sleep 8
|
||||
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:5273/design # 200
|
||||
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:5273/ops # 200
|
||||
```
|
||||
浏览器(agent-browser)验证:开 `/design` 标签题为「JCAI 设计中心」、`/ops` 为「JCAI 运营中心」;开 `http://localhost:5273/gov#/gov/workflows/pkgs` 应落到 `/design#/design/workflows/pkgs`。
|
||||
Run: `pnpm typecheck && pnpm lint`,Expected: 通过。
|
||||
|
||||
- [ ] **Step 4: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/web/src/app/tab-identity.ts apps/web/src/app/main.tsx
|
||||
git commit -m "三面拆分②:三章标签与入口路由,/gov 深链整链改写"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 壳参数化 —— 两套左轨、动态台账项、顶栏互跳亮灯
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/app/gov-shell.tsx`
|
||||
- Modify: `apps/web/src/app/main.tsx`(把 `surface` 传给壳)
|
||||
- Modify: `apps/web/src/app/app-shell.tsx`(工作台轨底加两面入口,按亮灯)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `SURFACE_META / SURFACE_ZONES / canSeeSurface`(Task 2);`useGovObjects`(域清单);`sessionRoute` + `queryKeys.session`(亮灯与轨底身份)。
|
||||
- Produces: `GovShell({ surface }: { surface: Surface })`;hash 格式 `#/<surface>/<zone>/<tab>/<k>:<id>…`;`Place.tab` 对 `ledgers` 区 = 域名。
|
||||
|
||||
- [ ] **Step 1: `readPlace`/`writePlace` 带 surface**
|
||||
|
||||
```ts
|
||||
function readPlace(surface: Surface): Place {
|
||||
const raw = window.location.hash.replace(/^#\/?/, '');
|
||||
const parts = raw.split('/').filter((s) => s !== '');
|
||||
// parts[0] 是台面名;区限定在本台面的区表内,越界落回本台首区
|
||||
const zones = SURFACE_ZONES[surface];
|
||||
const zone = (zones.includes(parts[1] as GovZone) ? parts[1] : zones[0]) as GovZone;
|
||||
const tabs = TABS[zone];
|
||||
// ledgers 的页签是数据域(动态):不校验,原样收;静态区仍校验并兜底到首签
|
||||
const tab = zone === 'ledgers' ? (parts[2] ?? '') : (tabs.find((t) => t.key === parts[2])?.key ?? tabs[0]?.key ?? '');
|
||||
…stack 解析不变…
|
||||
}
|
||||
const writePlace = (surface: Surface, p: Place): string =>
|
||||
['#', surface, p.zone, p.tab, ...p.stack.map((r) => `${r.k}:${encodeURIComponent(r.id)}`)].join('/');
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 壳按 surface 渲染**
|
||||
|
||||
`GovShell({ surface })`:
|
||||
- 左轨:`SURFACE_ZONES[surface]` 过滤 `ZONES`;品牌字 `SURFACE_META[surface].brand`;轨章图标 design=`route`、ops=`gauge`。
|
||||
- **ledgers 动态项**(仅 ops):`useGovObjects()` 取 `objects`,`domains = [...new Set(objects.map(o => o.domain))]`;左轨在 `ledgers` 位置渲染一枚分组小标题「台账」+ 每域一个按钮(`goZone` 改为 `go({zone:'ledgers', tab: domain, stack: []})`,高亮条件 `zone==='ledgers' && tab===domain`)。域为空时渲染一条灰字「随数据域发布自动生成」。
|
||||
- 顶栏:`工作台` 链接保留;新增另一面按钮(design 壳给「运营中心」→ `/ops`,ops 壳给「设计中心」→ `/design`),以 `canSeeSurface(session, …)` 决定渲染;`告警` 按钮仅 ops 壳渲染。session 取法:`useQuery({ queryKey: queryKeys.session, queryFn: () => call<Session>(sessionRoute) })`。
|
||||
- 轨底身份:删除 `account === 'fengbing'` 硬编码(现存 bug),改用上面的 session(`session.data?.staff.name`)。
|
||||
- 面包屑首项:`治理台` 改为 `SURFACE_META[surface].brand`。
|
||||
- `ZoneHome` 映射:`blueprint: BlueprintHome`(Task 6 前先渲染 `<Note>企业蓝图建设中</Note>` 的内联占位组件)、`cockpit/knowledge: MemoryHome`(Task 5 拆)、`ledgers: LedgersHome`(Task 10 前内联占位)、其余不变;`triggers` 分支删除。
|
||||
- `Detail`:不动(所有 `Route.k` 仍有效;`trigger` 详情页仍可从两面进)。
|
||||
|
||||
- [ ] **Step 3: 工作台入口亮灯**
|
||||
|
||||
`app-shell.tsx` 轨底(`jc-rail-foot` 内、身份钮上方)加:
|
||||
```tsx
|
||||
{canSeeSurface(session.data, 'design') && (
|
||||
<a className="jc-nav-user" href="/design" title="设计中心"><span className="jc-nav-ava">设</span><span className="jc-nav-name">设计</span></a>
|
||||
)}
|
||||
{canSeeSurface(session.data, 'ops') && (
|
||||
<a className="jc-nav-user" href="/ops" title="运营中心"><span className="jc-nav-ava">运</span><span className="jc-nav-name">运营</span></a>
|
||||
)}
|
||||
```
|
||||
(复用 `jc-nav-user` 皮肤,不造新类。)
|
||||
|
||||
- [ ] **Step 4: 验证**
|
||||
|
||||
浏览器:
|
||||
1. `/ops` 登录柴浩(TEST 岗九功能全 govern)→ 左轨 = 驾驶舱/任务/员工/台账(报价域、共享域)/知识库/系统;顶栏见「设计中心」「工作台」。
|
||||
2. `/design` → 左轨 = 蓝图/流程包/活动库/工具箱/数据域;无告警钮。
|
||||
3. 点台账「报价域」→ hash 变 `#/ops/ledgers/quote_intake…`(域名),再点「共享域」切换。
|
||||
4. 工作台 `/` 轨底出现 设计/运营 两枚入口。
|
||||
Run: `pnpm typecheck && pnpm lint && pnpm exec vitest run apps/web`,Expected: 通过。
|
||||
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add apps/web/src/app/gov-shell.tsx apps/web/src/app/main.tsx apps/web/src/app/app-shell.tsx
|
||||
git commit -m "三面拆分③:壳参数化两台,台账随域动态生成,顶栏互跳按权亮灯"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 驾驶舱(运营首页)+ 度量路由补齐八指标
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/contracts/src/api/governance.ts`(`govMetricsRoute` 输出补三字段)
|
||||
- Modify: `apps/api/src/app/routes/governance-ops.ts`(metrics handler 补映射)
|
||||
- Modify: `apps/web/src/shared/gov/gov-api.ts`(`usePkgReports` 自 gov-process 面板上移并更名 `useGovPkgReports` —— 驾驶舱的未闭合草稿数用它;实现体见 Task 6 Step 1 的代码块,**在本任务完成上移**,Task 6 只消费)
|
||||
- Modify: `apps/web/src/features/gov-process/internal/panel.tsx`(改引上移后的 hook,删本地副本)
|
||||
- Modify: `apps/web/src/features/gov-memory/internal/panel.tsx`(拆出 CockpitHome / KnowledgeHome)
|
||||
- Modify: `apps/web/src/features/gov-memory/index.ts`(出口)
|
||||
- Modify: `apps/web/src/app/gov-shell.tsx`(cockpit/knowledge 挂新组件)
|
||||
- Modify: `api-report/`(api:check --update)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `container.memory.metrics`(已计算八指标,现路由只暴露六)、`useGovTasks`(异常工作流数)、`useGovPackages` + `validateProcessRoute`(未闭合草稿数)。
|
||||
- Produces: `govMetricsRoute.output` 增 `breakpoint: z.number()`, `unitCost: z.object({ tokens: z.number(), minutes: z.number(), humanCalls: z.number() })`, `terminalMix: z.record(z.string(), z.number())`;组件 `CockpitHome({ tab, nav }: HomeProps)`、`KnowledgeHome({ tab, nav }: HomeProps)`。
|
||||
|
||||
- [ ] **Step 1: 契约与 handler**
|
||||
|
||||
`govMetricsRoute.output` 补三字段(全部 memory 模块已算,handler 里从 `metrics` 结果透传;对照 `packages/contracts/src/api/workbench.ts` 的 `reportMetricsSchema` 字段名保持一致)。跑 `pnpm api:check` 确认只报此处差异 → 同步 `apps/api/src/modules/memory/CONTRACT.md` 影响半径一句 → `pnpm api:check --update && git add api-report`。
|
||||
|
||||
- [ ] **Step 2: 拆 gov-memory 面板**
|
||||
|
||||
`CockpitHome` = 原 metrics 页签内容 + 顶部加一排 KPI:
|
||||
```tsx
|
||||
<div className="g-kpis">
|
||||
<Kpi ico="alert-triangle" tone="danger" label="异常工作流" value={abnormal} sub="失败走向未消化,待人工处置" />
|
||||
<Kpi ico="git-merge" tone="warn" label="未闭合草稿" value={draftBad} sub="边不闭合不得发布" />
|
||||
<Kpi ico="activity" tone="brand" label="在途任务" value={running} sub="运行中 + 等待中" />
|
||||
</div>
|
||||
```
|
||||
`abnormal` 自 `useGovTasks().data.workflows.filter(w=>w.state==='abnormal').length`;`draftBad` 自 `useGovPackages` + `useGovPkgReports`(本任务上移到 gov-api.ts);`running` 自 tasks。八指标卡沿用原 metrics 渲染,补 `断点率/单位成本/终态分布` 三卡(数据来自 Step 1 扩的路由)。点「异常工作流」卡 → `nav.jump('tasks','todo')`。
|
||||
`KnowledgeHome` = 原 layers 页签内容原样搬入。原 `MemoryHome` 删除。
|
||||
**死信卡不做**(无查询数据源),在 Step 4 记入 debt。
|
||||
|
||||
- [ ] **Step 3: 验证**
|
||||
|
||||
curl:`curl -s -b /tmp/ch.cookie http://localhost:8787/api/gov/metrics | python3 -m json.tool` → 九个键齐(六旧 + 三新)。
|
||||
浏览器:`/ops` 开屏即驾驶舱(KPI 排 + 八指标);知识库页签只剩活动记忆。
|
||||
Run: 五门,Expected: 全绿。
|
||||
|
||||
- [ ] **Step 4: 记欠账 + 提交**
|
||||
|
||||
`docs/debt.md` 追加:「驾驶舱·死信卡:死信目前只送治理者消息要素,无计数查询面 —— 驾驶舱首版不含死信卡,补上需给 system_message 一个按 kind 计数的治理路由」。
|
||||
```bash
|
||||
git add packages/contracts apps/api/src/app/routes/governance-ops.ts apps/api/src/modules/memory/CONTRACT.md \
|
||||
api-report apps/web/src/features/gov-memory apps/web/src/app/gov-shell.tsx docs/debt.md
|
||||
git commit -m "三面拆分④:驾驶舱首页 —— 度量路由补齐八指标,异常/草稿/在途三卡"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 企业蓝图(设计首页)
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/web/src/features/gov-blueprint/index.ts`
|
||||
- Create: `apps/web/src/features/gov-blueprint/internal/panel.tsx`
|
||||
- Modify: `apps/web/src/app/gov-shell.tsx`(blueprint 挂 BlueprintHome)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useGovPackages / useGovActivities / useGovTools / useGovObjects / useGovTriggers` + `useGovPkgReports(packages)`。
|
||||
- Produces: `useGovPkgReports(packages: readonly ProcessPackage[]): UseQueryResult<Map<string, ClosureReport>>`;`BlueprintHome({ tab, nav }: HomeProps)`。
|
||||
|
||||
- [ ] **Step 1: 确认 hook 已上移(Task 5 完成);若按乱序执行先补上移**
|
||||
|
||||
`gov-api.ts` 的 `useGovPkgReports`(实现体即原 gov-process 面板里的 `usePkgReports`,原地删除并改引用):
|
||||
```ts
|
||||
export const useGovPkgReports = (packages: readonly ProcessPackage[]): UseQueryResult<Map<string, ClosureReport>> =>
|
||||
useQuery({
|
||||
queryKey: ['gov', 'process-reports', packages.map((p) => `${p.id}@${p.version}`).join(',')],
|
||||
enabled: packages.length > 0,
|
||||
queryFn: async () =>
|
||||
new Map(
|
||||
await Promise.all(
|
||||
packages.map(
|
||||
async (p) => [`${p.id}@${p.version}`, await call<ClosureReport>(validateProcessRoute, { body: { pkg: p } })] as const,
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: BlueprintHome**
|
||||
|
||||
因果链一屏(全部用既有 `g-*` 件):
|
||||
```tsx
|
||||
export function BlueprintHome({ nav }: HomeProps) {
|
||||
const pkgs = useGovPackages(); const acts = useGovActivities();
|
||||
const tools = useGovTools(); const objs = useGovObjects(); const trgs = useGovTriggers();
|
||||
const reports = useGovPkgReports(pkgs.data?.packages ?? []);
|
||||
const drafts = (pkgs.data?.packages ?? []).filter((p) => p.state !== 'published');
|
||||
const draftBad = drafts.filter((p) => reports.data?.get(`${p.id}@${p.version}`)?.publishable === false);
|
||||
const violations = [...(reports.data?.values() ?? [])].flatMap((r) => r.violations);
|
||||
const domains = [...new Set((objs.data?.objects ?? []).map((o) => o.domain))];
|
||||
return (
|
||||
<>
|
||||
<Note>战略 → 方法 → 能力 → 对象:流程包决定企业以哪些流转运转,活动库决定每件事怎么干,工具箱决定系统能对世界做什么,数据域决定企业运营什么。设计中心没有任何运行态数字。</Note>
|
||||
<div className="g-kpis">
|
||||
<Kpi ico="workflow" tone="brand" label="流程包" value={pkgs.data?.packages.length ?? 0} sub={`草稿 ${drafts.length} · 未闭合 ${draftBad.length}`} />
|
||||
<Kpi ico="boxes" tone="collab" label="活动" value={acts.data?.activities.length ?? 0} sub="一个活动 = 一个任务的定义" />
|
||||
<Kpi ico="wrench" tone="auto" label="工具" value={tools.data?.tools.length ?? 0} sub="读类自由调用 · 写类登记意向" />
|
||||
<Kpi ico="database" tone="ok" label="数据域" value={domains.length} sub="发布即在运营中心长出台账位" />
|
||||
</div>
|
||||
<Sec ico="flag" title="待补清单" extra="闭合校验与契约缺口聚在这里,点进即改">
|
||||
{violations.length === 0 ? (
|
||||
<Clear>无待补 —— 全部流程包闭合校验通过。</Clear>
|
||||
) : (
|
||||
<div className="g-todo">
|
||||
{draftBad.map((p) => (
|
||||
<Todo key={`${p.id}@${p.version}`} tone="warn" ico="git-merge"
|
||||
title={`${p.name} v${p.version} · 闭合校验未过`}
|
||||
desc={(reports.data?.get(`${p.id}@${p.version}`)?.violations ?? []).map((v) => v.message).join(';')}
|
||||
acts={[{ label: '去编排', ico: 'route', onClick: () => nav.push({ k: 'pkg', id: `${p.id}@${p.version}` }) }]} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Sec>
|
||||
<Sec ico="zap" title="调起规则" extra={`${(trgs.data?.triggers ?? []).length} 条 —— 定义随流程包,处置在运营中心`}>
|
||||
<div className="g-entries">
|
||||
<Entry ico="zap" title="查看全部调起规则" desc="周期 / 事件 / 登记三类 · 条件用 S-FEEL 子集" onClick={() => nav.jump('workflows', 'rules')} />
|
||||
</div>
|
||||
</Sec>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
(工具/活动的 output 字段名以 `GovToolsOutput`/`GovActivitiesOutput` 实际为准 —— 写前 `grep listToolsRoute -A10` 校对。)
|
||||
|
||||
- [ ] **Step 3: 验证 + 提交**
|
||||
|
||||
浏览器:`/design` 开屏即蓝图(四卡 + 待补清单;v5 草稿若仍未发布应出现在待补或草稿计数里)。五门全绿。
|
||||
```bash
|
||||
git add apps/web/src/shared/gov/gov-api.ts apps/web/src/features/gov-blueprint apps/web/src/features/gov-process/internal/panel.tsx apps/web/src/app/gov-shell.tsx
|
||||
git commit -m "三面拆分⑤:企业蓝图首页 —— 四库因果链与待补清单"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 触发器 upsert 路由(后端)
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/contracts/src/api/governance.ts`
|
||||
- Modify: `apps/api/src/app/routes/governance-ops.ts`
|
||||
- Modify: `api-report/`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `container.trigger.upsert(config: TriggerConfig)`(模块出口已有,播种在用)。
|
||||
- Produces:
|
||||
```ts
|
||||
export const upsertTriggerRoute = {
|
||||
method: 'PUT',
|
||||
path: '/api/gov/triggers',
|
||||
body: z.object({ trigger: triggerConfigSchema }),
|
||||
output: z.object({ trigger: triggerConfigSchema }),
|
||||
} as const satisfies RouteContract;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: 契约 + handler**
|
||||
|
||||
governance-ops.ts(紧挨 `toggleTriggerRoute`,复用 `governTrigger` 守卫):
|
||||
```ts
|
||||
/** 调起规则的定义面(设计中心):新建 / 改规则 / 改装配 —— 启停仍走 toggle */
|
||||
app.put(upsertTriggerRoute.path, async (c) => {
|
||||
const denied = await governTrigger(c.get('account'));
|
||||
if (denied) return c.json(denied, denyStatus(denied));
|
||||
const parsed = upsertTriggerRoute.body.safeParse(await c.req.json());
|
||||
if (!parsed.success) {
|
||||
const body: ApiError = { code: 'validation-failed', message: '触发器不合 schema', detail: parsed.error.issues };
|
||||
return c.json(body, 400);
|
||||
}
|
||||
return c.json({ trigger: await container.trigger.upsert(parsed.data.trigger) });
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: curl 验证**
|
||||
|
||||
```bash
|
||||
curl -s -b /tmp/ch.cookie -X PUT http://localhost:8787/api/gov/triggers \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"trigger":{"id":"trg-rfq-manual","name":"人工登记起单(统一报价)","enabled":true,"processPackageId":"unified-quote","mapping":[],"spec":{"kind":"registration"}}}'
|
||||
```
|
||||
Expected: 200 + trigger 回显;错 schema 给 400;无权给 403。
|
||||
|
||||
- [ ] **Step 3: api:check + 提交**
|
||||
|
||||
CONTRACT.md:trigger 模块面未变(路由在 app 层),无需动;`pnpm api:check --update && git add api-report`。五门全绿。
|
||||
```bash
|
||||
git add packages/contracts apps/api/src/app/routes/governance-ops.ts api-report
|
||||
git commit -m "三面拆分⑥:触发器 upsert 路由 —— 定义面的 HTTP 口"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 调起规则(设计侧)—— workflows/rules 页签 + 包详情可编辑
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/features/gov-trigger/internal/panel.tsx`(TriggersHome 改造为 `TriggerRulesHome` 定义面:列表 + 编辑表单;`TriggerPage` 保留)
|
||||
- Modify: `apps/web/src/features/gov-trigger/index.ts`
|
||||
- Modify: `apps/web/src/features/gov-process/internal/panel.tsx`(WorkflowsHome 增 `tab === 'rules'` 分支渲染 `TriggerRulesHome`;PkgPage「调起它的触发器」段加「编辑」进入行内表单)
|
||||
- Modify: `apps/web/src/app/gov-shell.tsx`(TriggersHome 引用清理)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `upsertTriggerRoute`(Task 7)、`useGovTriggers`、`toggleTriggerRoute`(启停不在此面,只读展示状态)。
|
||||
- Produces: `TriggerRulesHome({ nav }: { nav: Nav })` —— 供 workflows 区 rules 页签与蓝图入口复用。
|
||||
|
||||
- [ ] **Step 1: TriggerRulesHome(定义面)**
|
||||
|
||||
列表(NList/NRow):名称 / 三类章 / 规则原文 / 目标包;行点开 `nav.push({k:'trigger', id})`。行尾「编辑」展开行内表单(Prop/Field 版式):
|
||||
- `name` 文本;`processPackageId` 下拉(useGovPackages 的 id 去重);
|
||||
- `spec` 按 kind 三分支:registration 无参;event 给 `eventType`(data/time/human/control 下拉)+ `rule` textarea;periodic-calendar 给 `calendar` 文本;periodic-data 给 domain/object/dateField/offset/filter 五格(domain/object 下拉自 useGovObjects);
|
||||
- `mapping` 用 `ListEdit` 编辑 `target=source.path` 形式的行,提交时解析为 `{target, source:{from:'initiation', path}}`(定义面先支持起单载荷映射这一种 —— 其余 source 类型显示只读,注明「在流程包边映射里维护」);
|
||||
- 保存:`call(upsertTriggerRoute, { body: { trigger } })` → 失效 `['gov','triggers']`;`AskBand` 确认带(影响:此后命中规则即按新定义起单;可再改;即时生效)。
|
||||
**启停开关不出现在此面**(那是运营的事,Task 9)。
|
||||
|
||||
- [ ] **Step 2: 接线**
|
||||
|
||||
WorkflowsHome:`if (tab === 'rules') return <TriggerRulesHome nav={nav} />;`。
|
||||
PkgPage「调起它的触发器」段:每行加「编辑规则」按钮 → `nav.jump('workflows', 'rules')`(编辑集中在 rules 页签,包详情不重复摆表单);无绑定时的 Note 文案追加「在调起规则页签新建」。
|
||||
|
||||
- [ ] **Step 3: 验证 + 提交**
|
||||
|
||||
浏览器:`/design` → 流程包 → 调起规则:改 `trg-rfq-manual` 的 name 存盘,刷新仍在;`curl …/api/gov/triggers` 回显新名。工作台起单验证不受影响(`POST /api/triggers/trg-rfq-manual/register` 仍 200)。五门全绿。
|
||||
```bash
|
||||
git add apps/web/src/features/gov-trigger apps/web/src/features/gov-process/internal/panel.tsx apps/web/src/app/gov-shell.tsx
|
||||
git commit -m "三面拆分⑦:调起规则并入流程包区 —— 触发器定义可编辑"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: 起单开关(运营侧)—— tasks/trg 页签
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/features/gov-trigger/internal/panel.tsx`(新增 `TriggerOpsTab`:启停 + 状态)
|
||||
- Modify: `apps/web/src/features/gov-trigger/index.ts`
|
||||
- Modify: `apps/web/src/features/gov-task/internal/panel.tsx`(TasksHome 增 `tab === 'trg'` 分支)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useGovTriggers`、`toggleTriggerRoute`(既有:`POST /api/gov/triggers/:id/toggle`,body `{enabled}` —— 写前 grep 确认参数形状)。
|
||||
- Produces: `TriggerOpsTab({ nav }: { nav: Nav })`。
|
||||
|
||||
- [ ] **Step 1: TriggerOpsTab**
|
||||
|
||||
NList 每触发器一行:三类章 / 规则原文 / 目标包 / `St` 状态章;行尾「启用/停用」按钮走 `AskBand` 确认(影响文案照原型:「触发器不入流程包结构…此后不再起新单;已起的照常跑完」),`useMutation` → toggle → 失效 `['gov','triggers']`。行点开仍 `nav.push({k:'trigger', id})`。「编辑规则」入口按 `canSeeSurface(session,'design')` 亮灯,点了整页跳 `/design#/design/workflows/rules`。
|
||||
|
||||
- [ ] **Step 2: 验证 + 提交**
|
||||
|
||||
浏览器:`/ops` → 任务 → 起单开关:停用 `trg-rfq-manual` → 工作台登记起单应被拒(`fireByRegistration` 回 `skipped:'disabled'`);再启用恢复。五门全绿。
|
||||
```bash
|
||||
git add apps/web/src/features/gov-trigger apps/web/src/features/gov-task/internal/panel.tsx
|
||||
git commit -m "三面拆分⑧:起单开关进运营·任务区 —— 定义与处置各归其位"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: 台账动态位 + 数据域定义面瘦身
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/web/src/features/gov-data/internal/panel.tsx`(拆三件:`DataHome` 定义面、`LedgersHome` 运营面、`ObjPage` 按来源分呈现)
|
||||
- Modify: `apps/web/src/features/gov-data/index.ts`
|
||||
- Modify: `apps/web/src/app/gov-shell.tsx`(ledgers 挂 `LedgersHome`,替换 Task 4 的占位)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useGovObjects`(域/对象清单)、`useGovLedger(domain, object)`(列 / 主键 / 行 / 总数)。
|
||||
- Produces: `LedgersHome({ domain, nav }: { domain: string; nav: Nav })` —— 壳层以 `tab` 为域名传入;`DataHome` 保持 `HomeProps` 签名。
|
||||
|
||||
- [ ] **Step 1: LedgersHome(运营面)**
|
||||
|
||||
按传入 `domain` 过滤对象,每对象一张 `NRow`(名称 / 行数徽 —— 行数取 `useGovLedger` 的 `total`,懒加载:仅当前域的对象查询);点行 `nav.push({k:'obj', id: `${domain}.${object}`})` 进 `ObjPage` 看行数据(现 ObjPage 已是行浏览,复用)。空域文案:「这个域还没有对象 —— 对象随活动安装物化」。
|
||||
|
||||
- [ ] **Step 2: DataHome 瘦身(定义面)**
|
||||
|
||||
设计侧只留定义信息:域分组 → 对象卡(列结构 `columns`、主键、被哪些活动声明 —— 声明反查暂无查询面,此卡先列 `columns/primaryKey`,行数与行浏览入口**移除**);卡底加一行灰字「行数据在运营中心 · 台账」。`ObjPage` 从设计侧不再可达(设计侧对象卡不下钻行数据)。
|
||||
|
||||
- [ ] **Step 3: 验证 + 提交**
|
||||
|
||||
浏览器:`/ops` 台账·报价域 → 对象行数与行浏览正常;`/design` 数据域 → 只见结构,无行数据入口。五门全绿。
|
||||
```bash
|
||||
git add apps/web/src/features/gov-data apps/web/src/app/gov-shell.tsx
|
||||
git commit -m "三面拆分⑨:台账动态位入运营,数据域瘦身为纯定义面"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 11: 工作组管理(员工区新页签 + 读写路由)
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/contracts/src/api/governance.ts`(两条路由)
|
||||
- Modify: `apps/api/src/app/routes/governance-ops.ts`(handler,app 层组合 org 出口)
|
||||
- Modify: `apps/web/src/features/gov-staff/internal/panel.tsx`(PeopleHome 增 `tab === 'groups'`)
|
||||
- Modify: `apps/web/src/shared/gov/gov-route.ts`(people TABS 增 `{ key: 'groups', label: '工作组', ico: 'users' }`)
|
||||
- Modify: `api-report/`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `container.org.listStaff() / workGroupOf(staffId) / saveWorkGroup({id?, name, ownerId, members})`(均在 org 出口);`workGroupSchema`(@contracts)。
|
||||
- Produces:
|
||||
```ts
|
||||
export const listWorkGroupsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/work-groups',
|
||||
output: z.object({ groups: z.array(workGroupSchema.extend({ ownerName: z.string() })) }),
|
||||
} as const satisfies RouteContract;
|
||||
export const saveWorkGroupRoute = {
|
||||
method: 'PUT',
|
||||
path: '/api/gov/work-groups',
|
||||
body: z.object({ ownerId: staffIdSchema, name: z.string().min(1), members: z.record(z.string(), staffIdSchema) }),
|
||||
output: z.object({ group: workGroupSchema }),
|
||||
} as const satisfies RouteContract;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: handler(守卫 `guard('staff', 'read'/'govern')`)**
|
||||
|
||||
```ts
|
||||
app.get(listWorkGroupsRoute.path, async (c) => {
|
||||
const denied = await readStaff(c.get('account'));
|
||||
if (denied) return c.json(denied, denyStatus(denied));
|
||||
const { staff } = await container.org.listStaff();
|
||||
const groups = [];
|
||||
for (const s of staff) {
|
||||
const g = await container.org.workGroupOf(s.id);
|
||||
if (g !== undefined) groups.push({ ...g, ownerName: s.name });
|
||||
}
|
||||
return c.json({ groups });
|
||||
});
|
||||
|
||||
app.put(saveWorkGroupRoute.path, async (c) => {
|
||||
const denied = await governStaff(c.get('account'));
|
||||
if (denied) return c.json(denied, denyStatus(denied));
|
||||
const parsed = saveWorkGroupRoute.body.safeParse(await c.req.json());
|
||||
if (!parsed.success) {
|
||||
const body: ApiError = { code: 'validation-failed', message: '工作组不合 schema', detail: parsed.error.issues };
|
||||
return c.json(body, 400);
|
||||
}
|
||||
// 一人一组:已有组即更新(id 复用),没有即新建 —— 与 org.workGroupOf 的取组口径一致
|
||||
const existing = await container.org.workGroupOf(parsed.data.ownerId);
|
||||
const group = await container.org.saveWorkGroup({
|
||||
...(existing === undefined ? {} : { id: existing.id }),
|
||||
name: parsed.data.name,
|
||||
ownerId: parsed.data.ownerId,
|
||||
members: parsed.data.members,
|
||||
});
|
||||
return c.json({ group });
|
||||
});
|
||||
```
|
||||
(`readStaff`/`governStaff` 守卫若不存在,按文件内既有 `guard('staff', …)` 工厂补两行。)
|
||||
|
||||
- [ ] **Step 2: 界面(people/groups 页签)**
|
||||
|
||||
列表:每组一张 `Card`(组名 · 组长 ownerName · 成员表 `Rows`:岗位 code → 员工名,员工名经 `useGovStaff` 的 id→name 映射)。编辑:选组长(下拉全员)→ 逐岗位下拉选员工(岗位集 = `useGovStaff().positions` 的 code;可留空 = 该岗位不绑定)→ `AskBand` 确认(影响:「仅新起的工作流;在途不受影响 —— 绑定是起单时的快照」)→ PUT → 失效 `['gov','work-groups']` 键(新增 hook `useGovWorkGroups` 于 gov-api.ts)。空态:「还没有工作组 —— 建一个,起单时整条流可以按组绑定到人」。
|
||||
|
||||
- [ ] **Step 3: 验证 + 提交**
|
||||
|
||||
curl:GET 应见播种的 `wg-solo-ai`/`wg-solo-chaihao`;PUT 改柴浩组名后 GET 回显。浏览器:`/ops` 员工 → 工作组页签可看可改;改完用柴浩起一单,`sys.workflow.work_group` 快照应含新成员表。`pnpm api:check --update && git add api-report`(org 模块 CONTRACT.md 影响半径补一句:app 层新增工作组读写路由)。五门全绿。
|
||||
```bash
|
||||
git add packages/contracts apps/api/src/app/routes/governance-ops.ts apps/api/src/modules/org/CONTRACT.md \
|
||||
api-report apps/web/src/features/gov-staff apps/web/src/shared/gov/gov-route.ts apps/web/src/shared/gov/gov-api.ts
|
||||
git commit -m "三面拆分⑩:工作组管理落员工区 —— ADR-011 欠账清账"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 12: 收口 —— 走查、门禁、文档对账
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/debt.md`(核销已清项:工作组界面;新增项见各任务)
|
||||
- Modify: `docs/产品设计.md`(如实现与设计有出入,改设计或记偏差 —— 文档与实现不许各说各话)
|
||||
|
||||
**Interfaces:** 无新接口。
|
||||
|
||||
- [ ] **Step 1: 全链走查(浏览器,柴浩账号)**
|
||||
|
||||
1. `/` 工作台:轨底两枚入口 → 进 `/ops`;
|
||||
2. `/ops` 驾驶舱开屏 → 任务/员工/台账×2/知识库/系统逐区开一遍,无空白页、无 `(无)`;
|
||||
3. 起单开关停/启验证一轮;
|
||||
4. `/design` 蓝图开屏 → 流程包(编排工作区仍可用:v5 草稿点开、属性卡、编辑芯片)→ 调起规则改名 → 活动库/工具箱/数据域逐区开;
|
||||
5. 旧链接 `/gov#/gov/workflows/pkgs`、`/gov#/gov/memory/layers` 各自落对;
|
||||
6. 工作台起一单 → `/ops` 驾驶舱在途 +1。
|
||||
|
||||
- [ ] **Step 2: 五门 + 文档**
|
||||
|
||||
Run: `pnpm gen:tiers && pnpm typecheck && pnpm lint && pnpm depcruise && pnpm api:check && pnpm test`
|
||||
Expected: 全绿(api:check 无未入库差异)。
|
||||
`docs/debt.md`:划掉「工作组无治理台界面」;确认「驾驶舱死信卡」「声明反查无查询面」两条在列。
|
||||
|
||||
- [ ] **Step 3: 提交**
|
||||
|
||||
```bash
|
||||
git add docs
|
||||
git commit -m "三面拆分⑪:收口 —— 走查通过,债务台账对齐"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自审记录(写完计划后跑过)
|
||||
|
||||
- **规格覆盖**:产品设计.md §5 前端改动清单五项 → Task 3/4(壳与标签)、5/6(两首页)、7/8/9(触发器归位)、10(台账+数据域)、11(工作组);§6 权限亮灯 → Task 2/4;§1 `/gov` 跳转 → Task 3。驾驶舱「死信卡」为已声明偏差(Task 5 Step 4 记 debt)。
|
||||
- **占位扫描**:无 TBD;两处「写前 grep 校对」是对既有代码形状的核对动作,不是留白。
|
||||
- **类型一致性**:`canSeeSurface(session, surface)` 三处消费(Task 4 壳、Task 4 工作台、Task 9 编辑入口)同签名;`useGovPkgReports` 的**上移动作归 Task 5**,Task 6 只消费 —— 前向依赖已消除。
|
||||
Reference in New Issue
Block a user