**资料文件全链**(用户:审核资料这一步要真产出 word/excel/md/html,前端能渲、点资料名能开)
接手时这一步一个文件都产不出来 —— S3 剧本白纸黑字写着「file_ref 本 MVP 阶段可留空」。
文件是台账那一行的投影,不另存一份:
· 声明 `file:{titleColumn,bodyColumn,formatColumn}`,它同时是授权面(没声明就点不开)
· 现造真字节 —— docx/xlsx 是自己打的 OOXML 包(zip-write:store+CRC32,零依赖)
· 读回来渲 —— 解的就是造出来那份字节,渲得出来才证明文件是真的
· 件上 `data-file-of` → 工作区二级界面(Word 纸面/Excel 网格/渲好的 md/沙箱 html)+ 下载原件
放行落地前把每一份冻进 `sys.task_asset_file`(迁移 0029):归档件点开的必须是签字那一刻
那一份,而资料落在主数据上、试跑单不落正式表 —— 不存这一份,收工后一个都点不开。
工作成果里「产出的资料」自成一摞。Markdown 富文本渲染顺带销账(仍零依赖)。
**Harness**
· 画面按「这一拍办完之后」在办的那一步挑 —— 从前拿拍首的局面决定拍尾的事:
收掉 S4、发出 S5 的卡,而屏上还摆着 S4 的只读件,人无处可填(墙要配路第四次现形)
· 验收「重做」真能退回重做,且打回后台账没动就不许再提交(软硬两段,不锁死单子)
· 立监听的授权面是清单声明;发卡的授权面是调人预算,不是计划项
· 读工具同参重调有兜底;空转写身后还有行动就不废串;finish 的 doneWhen 按写完后的草稿算
**界面**
· 翻件与翻页各归各位(从前二选一,多卡多页时页翻不动);没得翻写「—」不写 0/0
· 工作成果详情能翻份;表宽超出能横滑(E04/I01 抄版式时把 overflow-x 抄丢了)
· 查看面灰底滚过一屏变白 —— 两条独立的路都治了(沙箱内不同色 / 弹性子项不长个)
· 测试台「取不到」不再当成「还活着」
**活动**
E04 关系覆盖、I01 品牌内容各自补到验收线:判据全读基座数、判据指向的东西摆上卡面、
样例满载且名字 ≤14 字;提交前要裁的判据搬进说明书(模型会压拍,压过之后只有说明书还在)。
验收:e2e i01 4/4、e04 3/3 全部 released,资料在办中渲得出、放行后存了档点得开;
门禁 typecheck/lint/depcruise/1628 测全绿。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEJX8Z7CHejvwmDQyPQuBX
348 lines
15 KiB
TypeScript
348 lines
15 KiB
TypeScript
/**
|
|
* 主数据四域 —— **客户 / 项目 / 供应商 / 市场**(2026-07-31 用户定,照常规 CRM & ERP 的分法)。
|
|
*
|
|
* 为什么主数据要单独立域、由**平台**播种,而不是跟着某个活动走:
|
|
*
|
|
* 台账原来只按「谁写的」分域(`quote_intake.*` / `quote_alternative.*` …)。那是**实现轴**:
|
|
* 六个活动各有一张 verdict,一单的故事散在六处;而真正需要治理的那几张
|
|
* (客户档案、历史价)被挤进一个叫 `shared` 的抽屉 —— 那个名字描述的是机制(跨活动共享),
|
|
* 不是性质(主数据)。用机制命名,抽屉必然变杂物间。
|
|
*
|
|
* 更要紧的是**所有权**:主数据不是任何一个活动的产出。客户档案先于活动存在、
|
|
* 活动退役了它还在;活动只是**读它做判断、提议更新它**。
|
|
* 所以它由平台播种物化,不挂在谁的清单下 —— 活动装了卸了,主数据都在。
|
|
*
|
|
* 与三类性质的关系(见 `ledgerObjectKindSchema`):这里全是 `master`。
|
|
* 单据(`record`)归各活动自己的域;派生指标(`derived`)另说 —— 它本不该手写。
|
|
*
|
|
* **现在是空占位**:结构先立起来,还没有活动往里写。
|
|
* 表建出来但一行数据都没有 —— 治理台「主数据」那一区能看见它们等着被接上。
|
|
*/
|
|
import { objectDeclarationSchema } from '@contracts';
|
|
import type { LedgerFieldType, ObjectDeclaration } from '@contracts';
|
|
|
|
const f = (name: string, type: LedgerFieldType, label: string, primaryKey = false) => ({
|
|
name,
|
|
type,
|
|
label,
|
|
primaryKey,
|
|
masked: false,
|
|
});
|
|
|
|
/** 主数据一律 `kind: 'master'`:按业务实体分行,没有单标识 */
|
|
const master = (domain: string, object: string, label: string, fields: ReturnType<typeof f>[]) =>
|
|
objectDeclarationSchema.parse({ domain, object, label, kind: 'master', fields });
|
|
|
|
export const MASTER_OBJECTS: readonly ObjectDeclaration[] = [
|
|
/* ── 客户:谁在买 ──────────────────────────────────────────────────── */
|
|
master('customer', 'profile', '客户档案', [
|
|
f('customer_code', 'text', '客户编码', true),
|
|
f('name', 'text', '客户名称'),
|
|
f('grade', 'text', '客户等级'),
|
|
f('industry', 'text', '所属行业'),
|
|
f('region', 'text', '区域'),
|
|
f('owner', 'text', '负责 BD'),
|
|
f('payment_terms', 'text', '付款条件'),
|
|
f('credit_limit', 'number', '信用额度'),
|
|
/* 画像标签:价格敏感度、决策周期、常用条款 —— A7 归档时回写的就是这一列 */
|
|
f('profile_tags', 'text', '画像标签'),
|
|
f('status', 'text', '状态'),
|
|
]),
|
|
master('customer', 'contact', '客户联系人', [
|
|
f('customer_code', 'text', '客户编码', true),
|
|
f('contact_code', 'text', '联系人编码', true),
|
|
f('name', 'text', '姓名'),
|
|
f('title', 'text', '职务'),
|
|
/* 邮箱与电话是个人信息 —— 主数据里必须标脱敏,治理面照此裁剪 */
|
|
{ ...f('email', 'text', '邮箱'), masked: true },
|
|
{ ...f('phone', 'text', '电话'), masked: true },
|
|
]),
|
|
master('customer', 'price_terms', '客户价格条款', [
|
|
f('customer_code', 'text', '客户编码', true),
|
|
f('category', 'text', '品类', true),
|
|
f('target_margin', 'number', '目标毛利'),
|
|
f('discount', 'number', '折扣'),
|
|
f('valid_until', 'date', '有效期至'),
|
|
]),
|
|
|
|
/* ── 项目:为什么买 —— 客户的产品项目,反复询价的载体 ───────────────── */
|
|
master('project', 'profile', '项目档案', [
|
|
f('project_code', 'text', '项目编码', true),
|
|
f('customer_code', 'text', '所属客户'),
|
|
f('name', 'text', '项目名称'),
|
|
/* BOM 早期 / BOM 后期 / 量产 —— A3 的询价打法就按这个分 */
|
|
f('stage', 'text', '项目阶段'),
|
|
f('annual_volume', 'integer', '年用量预估'),
|
|
f('start_at', 'date', '立项日期'),
|
|
f('owner', 'text', '负责人'),
|
|
f('status', 'text', '状态'),
|
|
]),
|
|
master('project', 'bom', '项目 BOM', [
|
|
f('project_code', 'text', '项目编码', true),
|
|
f('pn', 'text', '料号', true),
|
|
f('qty_per_unit', 'number', '单机用量'),
|
|
f('eau', 'integer', '年需求量'),
|
|
f('designed_in', 'boolean', '已导入设计'),
|
|
f('status', 'text', '状态'),
|
|
]),
|
|
|
|
/* ── 供应商:从哪买 ───────────────────────────────────────────────── */
|
|
master('supplier', 'profile', '供应商档案', [
|
|
f('supplier_code', 'text', '供应商编码', true),
|
|
f('name', 'text', '供应商名称'),
|
|
/* 原厂 / 授权代理 / 贸易商 —— 「现货场景不得选无库存转单商」判据读的就是它 */
|
|
f('type', 'text', '供应商类型'),
|
|
f('region', 'text', '区域'),
|
|
f('rating', 'text', '评级'),
|
|
f('payment_terms', 'text', '账期'),
|
|
f('status', 'text', '状态'),
|
|
]),
|
|
master('supplier', 'line_card', '代理线卡', [
|
|
f('supplier_code', 'text', '供应商编码', true),
|
|
f('brand', 'text', '品牌', true),
|
|
f('authorized', 'boolean', '是否授权'),
|
|
f('lead_time_typ', 'text', '常规交期'),
|
|
]),
|
|
master('supplier', 'contact', '供应商联系人', [
|
|
f('supplier_code', 'text', '供应商编码', true),
|
|
f('contact_code', 'text', '联系人编码', true),
|
|
f('name', 'text', '姓名'),
|
|
{ ...f('email', 'text', '邮箱'), masked: true },
|
|
{ ...f('phone', 'text', '电话'), masked: true },
|
|
]),
|
|
|
|
/* ── 市场:买什么、行情如何 ──────────────────────────────────────────
|
|
料号主数据放这一域:ERP 里它常自成一域(物料主数据),
|
|
但这门生意关心的是它**在市场上什么状态**(生命周期、有没有货、什么价),
|
|
跟行情摆在一起更顺手。要独立成域也说得通,记在这儿备考。 */
|
|
master('market', 'part', '料号主数据', [
|
|
f('pn', 'text', '料号', true),
|
|
f('brand', 'text', '品牌'),
|
|
f('category', 'text', '品类'),
|
|
f('package', 'text', '封装'),
|
|
/* Active / NRND / EOL —— A1 三分类与 A2 选型都读它 */
|
|
f('lifecycle', 'text', '生命周期'),
|
|
f('rohs', 'boolean', 'RoHS'),
|
|
f('datasheet_url', 'text', '规格书链接'),
|
|
]),
|
|
master('market', 'price_history', '历史价格情报', [
|
|
f('pn', 'text', '料号', true),
|
|
f('quoted_at', 'date', '报价日期', true),
|
|
f('customer_code', 'text', '客户编码'),
|
|
f('unit_price', 'number', '成交/报出单价'),
|
|
f('lead_time', 'text', '交期'),
|
|
f('lost_reason', 'text', '未成交原因'),
|
|
f('competitor_price', 'number', '竞对价位'),
|
|
]),
|
|
master('market', 'quote_index', '市场行情', [
|
|
f('pn', 'text', '料号', true),
|
|
f('observed_at', 'date', '观测日期', true),
|
|
f('source', 'text', '来源'),
|
|
f('price', 'number', '市场价'),
|
|
f('stock', 'integer', '可见库存'),
|
|
f('trend', 'text', '走势'),
|
|
]),
|
|
];
|
|
|
|
/** 四域的呈现次序 —— 谁在买 → 为什么买 → 从哪买 → 买什么 */
|
|
export const MASTER_DOMAINS = ['customer', 'project', 'supplier', 'market'] as const;
|
|
|
|
/**
|
|
* **样例客户** —— 让「老客户」那条路试得出来。
|
|
*
|
|
* 主数据四域建出来是空的,而 A1 的第一个状态有两条路:
|
|
* 认得出客户就直接过、认不出才请人建档。库里一个客户都没有的话,
|
|
* 两条样例跑起来一模一样 —— **第二条路根本试不到**。
|
|
*
|
|
* 所以随主数据一起播一位客户:样例 2 的来信就用他的邮箱发。
|
|
* 只播这一位,且是**幂等的按主键写**:它是让样例跑得起来的最小夹具,
|
|
* 不是往主数据里塞演示数据 —— 真实客户由 A1 建档时一位一位落进来。
|
|
*/
|
|
export const MASTER_SAMPLE_ROWS = [
|
|
/*
|
|
* **Itelma** —— 老客户测试案例的那一位(照 `JCAI_PRD/docs/活动执行页/` 两份目标件)。
|
|
* 俄罗斯车规电子、项目制;客户码 CU-0142 就是需求件上写的「已命中客户资料库」那一条。
|
|
*/
|
|
{
|
|
address: { domain: 'customer', object: 'profile' },
|
|
key: { customer_code: 'CU-0142' },
|
|
values: {
|
|
customer_code: 'CU-0142',
|
|
name: 'Itelma',
|
|
grade: 'A',
|
|
industry: '车规电子',
|
|
region: '俄罗斯',
|
|
payment_terms: 'EXW · CNY 含税',
|
|
profile_tags: '项目制 · 按项目批量询价 · 认证要求严(AEC-Q200)',
|
|
status: 'active',
|
|
/*
|
|
* E04(关系覆盖与关键人经营)字段 —— 2026-08-20 补齐,让「客户已经跑过 E04、
|
|
* 找缺口」这条分支第一次能真被样例跑通(此前这几列全空,每轮都只能从零起草)。
|
|
* decision_chain 只覆盖了 bt-01(采购层),bt-02/bt-03 是刻意留的缺口。
|
|
*/
|
|
cust_type: 'Tier1 OEM',
|
|
coverage_score: 35,
|
|
decision_chain: {
|
|
nodes: [
|
|
{ node: 'bt-01', btLevel: 'bt-01', required: true, contactCode: 'CU-0142-01', covered: true },
|
|
{ node: 'bt-02', btLevel: 'bt-02', required: true, contactCode: '', covered: false },
|
|
{ node: 'bt-03', btLevel: 'bt-03', required: true, contactCode: '', covered: false },
|
|
],
|
|
},
|
|
interaction_timeline: {
|
|
entries: [
|
|
{ date: '2026-06-15', type: '邮件报价跟进', contactCode: 'CU-0142-01', note: 'MD14.1 项目 BOM 报价确认' },
|
|
{ date: '2026-07-20', type: '电话沟通', contactCode: 'CU-0142-01', note: '交期与付款方式确认' },
|
|
],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
address: { domain: 'customer', object: 'contact' },
|
|
key: { customer_code: 'CU-0142', contact_code: 'CU-0142-01' },
|
|
values: {
|
|
customer_code: 'CU-0142',
|
|
contact_code: 'CU-0142-01',
|
|
name: 'Ekaterina Volkova',
|
|
title: '采购部',
|
|
// **认人的钥匙**:老客户样例的来信就从这个邮箱发,A1 第一步照它认得出
|
|
email: 'e.volkova@itelma.ru',
|
|
// E04 字段 —— 对应上面 decision_chain 的 bt-01 节点
|
|
role: '采购',
|
|
decision_node: 'bt-01',
|
|
influence: 'high',
|
|
relationship: 3,
|
|
comm_pref: 'Email',
|
|
key_person: true,
|
|
},
|
|
},
|
|
/*
|
|
* 项目档案 —— 这一单询的是 MD14.1 这个项目的料。
|
|
* 播它是因为需求件上「项目制」那句话得有出处:客户属性说得出「按项目询价」,
|
|
* 库里就该有那个项目,否则那句话是装出来的。
|
|
*/
|
|
{
|
|
address: { domain: 'project', object: 'profile' },
|
|
key: { project_code: 'MD14.1' },
|
|
values: {
|
|
project_code: 'MD14.1',
|
|
customer_code: 'CU-0142',
|
|
name: 'MD14.1 车身控制器',
|
|
stage: 'BOM 后期',
|
|
status: 'active',
|
|
},
|
|
},
|
|
/*
|
|
* **CU-0087** —— E04(关系覆盖与关键人经营)"关键人离职/调岗"样例专用夹具
|
|
* (2026-08-20 补):此前这条样例引用的 CU-0087 在种子数据里根本不存在,
|
|
* 是一处遗留 bug。这里种一位客户 + 两位联系人,decision_chain 的 bt-01
|
|
* (采购层)当下由陈刚覆盖 —— 样例说的正是"这个人要走了,这个节点要重新覆盖"。
|
|
*/
|
|
{
|
|
address: { domain: 'customer', object: 'profile' },
|
|
key: { customer_code: 'CU-0087' },
|
|
values: {
|
|
customer_code: 'CU-0087',
|
|
name: '深圳精密电子有限公司',
|
|
grade: 'B',
|
|
industry: '工业自动化',
|
|
region: '中国·深圳',
|
|
payment_terms: 'T/T 30 天',
|
|
profile_tags: '批量稳定 · 价格敏感',
|
|
status: 'active',
|
|
cust_type: '模组厂',
|
|
coverage_score: 55,
|
|
decision_chain: {
|
|
nodes: [
|
|
{ node: 'bt-01', btLevel: 'bt-01', required: true, contactCode: 'CU-0087-01', covered: true },
|
|
{ node: 'bt-02', btLevel: 'bt-02', required: true, contactCode: 'CU-0087-02', covered: true },
|
|
{ node: 'bt-03', btLevel: 'bt-03', required: false, contactCode: '', covered: false },
|
|
],
|
|
},
|
|
interaction_timeline: {
|
|
entries: [{ date: '2026-05-10', type: '现场拜访', contactCode: 'CU-0087-01', note: '年度框架协议签署' }],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
address: { domain: 'customer', object: 'contact' },
|
|
key: { customer_code: 'CU-0087', contact_code: 'CU-0087-01' },
|
|
values: {
|
|
customer_code: 'CU-0087',
|
|
contact_code: 'CU-0087-01',
|
|
name: 'Chen Gang',
|
|
title: '采购部长',
|
|
// **样例说的就是这个人**:「关键人离职/调岗」引用的正是这一位
|
|
email: 'chen.gang@sz-precision.com',
|
|
role: '采购',
|
|
decision_node: 'bt-01',
|
|
influence: 'high',
|
|
relationship: 4,
|
|
comm_pref: 'Email',
|
|
key_person: true,
|
|
},
|
|
},
|
|
{
|
|
address: { domain: 'customer', object: 'contact' },
|
|
key: { customer_code: 'CU-0087', contact_code: 'CU-0087-02' },
|
|
values: {
|
|
customer_code: 'CU-0087',
|
|
contact_code: 'CU-0087-02',
|
|
name: 'Li Wei',
|
|
title: '技术总监',
|
|
email: 'li.wei@sz-precision.com',
|
|
role: '技术决策人',
|
|
decision_node: 'bt-02',
|
|
influence: 'medium',
|
|
relationship: 3,
|
|
comm_pref: 'Email',
|
|
key_person: false,
|
|
},
|
|
},
|
|
/*
|
|
* I01(品牌形象与销售工具包管理)读的 L-01 宏观市场台账 —— 两条,呼应 I01 的两条
|
|
* 事件驱动样例(「进入俄罗斯市场」「E04 请求定向素材」),让「读市场信号 → 拟内容」
|
|
* 这条分支第一次有真数据可读,不必每轮都读空手。
|
|
*/
|
|
{
|
|
address: { domain: 'market', object: 'signal' },
|
|
key: { signal_id: 'MS-2026-0001' },
|
|
values: {
|
|
signal_id: 'MS-2026-0001',
|
|
signal_type: 'industry_trend',
|
|
title: '俄罗斯车规电子本土化替代加速',
|
|
content: '受供应链管制影响,俄罗斯本土车企与 Tier1 加快引入非欧美芯片供应商,车规级 AEC-Q200 认证成为准入门槛,采购决策周期缩短、更看重供货稳定性承诺。',
|
|
source: '行业报告',
|
|
signal_date: '2026-06-01',
|
|
market_region: '俄罗斯',
|
|
industry: '汽车电子',
|
|
chip_category: 'MCU',
|
|
importance: 4,
|
|
impact_dir: 'opportunity',
|
|
signal_status: 'evaluated',
|
|
version: '2026-Q2',
|
|
review_note: '',
|
|
},
|
|
},
|
|
{
|
|
address: { domain: 'market', object: 'signal' },
|
|
key: { signal_id: 'MS-2026-0002' },
|
|
values: {
|
|
signal_id: 'MS-2026-0002',
|
|
signal_type: 'customer_market',
|
|
title: 'Itelma 系客户新一轮车身控制器项目招标',
|
|
content: 'Itelma 及同类俄罗斯 Tier1 客户密集启动车身控制器新项目招标,价值传递素材需突出认证齐全度与批量交付能力,系统方案白皮书是决策链高层最看重的材料形式。',
|
|
source: '订阅',
|
|
signal_date: '2026-07-10',
|
|
market_region: '俄罗斯',
|
|
industry: '汽车电子',
|
|
chip_category: 'MCU',
|
|
importance: 5,
|
|
impact_dir: 'opportunity',
|
|
signal_status: 'evaluated',
|
|
version: '2026-Q2',
|
|
review_note: '',
|
|
},
|
|
},
|
|
] as const;
|