数据工单有了自己的面孔;件上四处样式对齐;缺库不再问人;5173 反代改回本机

用户在工作台上连报六条(2026-08-27):
- 对话区把数据工单摆成「执行工单」→ 契约 ExecutionWorkOrder.variant:'data'(引擎侧与执行工单无异,
  只换面孔),归一层从卡上带下来,web 新增 DataForm(同一套办法、另一副面孔:色/图标/「数据工单」/「确认写入」),
  办结卡与治理台人类工具目录同步;活动的两张数据工单卡骨架标 variant:"data"。
- 表显示不全不能右滑 → 目标件把 overflow-x:auto 写在 .tbl-wrap,平台包装是 .jc-tbl-wrap,谁都没管到;补一条。
- 表头只留中文 → 三张数据工单表去英文列名与 .thd 小字。
- 编辑态内部料号「只读字 + 输入框」并排 → 目标件 #tbl1 .jc-vcell{display:inline}(id 特异度)盖过
  table.on 的隐藏规则,表 1 每格其实都并排,PN 那格恰好有字;用 id 特异度覆盖回去。
- input 与 select 没对齐 → 徽标从前包 .ro 编辑态留着、压在下拉框头上把那格顶低 13px,改成随只读格一起藏;
  input/select 钉同一高度。
- 第一步「派数据工单还是放弃候选」不该问人 → 决策卡退役(覆盖规格「候选 ≥2 可放弃」那一条,用户定),
  查无记录一律自动派;剧本/说明书/测试/e2e 同步(e2e 新增 expectAsks.max 断言「不该问的没问」)。
顺带:5173 的 /api 反代默认指向一条公网隧道(150.158.83.131:46100 → nps → npc → 本机 :8787),
浏览器每次请求先出公网再绕回来;默认改回本机,要远端的显式给 API_PROXY_TARGET。

静态渲染实证:900px 宽的框里横向能滑 200px;编辑态同一行 3 input + 1 select 全部 32px 同一 y;
只读 PN 格 display:none。活动包 1.4.0;2097 测 / typecheck / lint / depcruise 绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
This commit is contained in:
xcs
2026-08-27 12:36:13 +08:00
co-authored by Claude Fable 5
parent 3171d8a864
commit 51c87af583
15 changed files with 197 additions and 116 deletions
@@ -185,15 +185,13 @@ describe('案例模型与清单的对齐', () => {
for (const name of mentioned) expect(ITEMS, `说明书提到但不存在的计划项: ${name}`).toContain(name);
});
it('两种决策卡骨架都在说明书里:候选放弃/创建(create/abandon)与审核(approve/reject/custom:)', () => {
it('说明书里只剩审核那一种决策卡(approve/reject/custom:);候选放弃/创建那张已退役 —— 缺库一律自动派数据工单(2026-08-27 用户定)', () => {
const n = manifest.assets.businessModel.narrative;
expect(n).toContain('"kind":"decide","itemId":"S0d_p08_master"');
expect(n).toContain('"value":"create"');
expect(n).toContain('"value":"abandon"');
expect(n).toContain('"kind":"decide","itemId":"S4_review"');
expect(n).toContain('"approve"');
expect(n).toContain('"reject"');
expect(n).toContain('"custom:"');
expect(n).not.toContain('"value":"create"');
expect(n).not.toContain('"value":"abandon"');
expect(n).toContain('"value":"approve"');
expect(n).toContain('"value":"reject"');
expect(n).toContain('custom:');
});
it('T_release 剧本里 judge criterion 的原文逐字对得上 acceptanceCriteria 的 label', () => {
@@ -139,7 +139,9 @@ describe('doneWhen —— 开局不许真,干完必须真', () => {
describe('说明书与剧本的骨架', () => {
it('五张卡的骨架都住在说明书里(压拍之后只有说明书还在)', () => {
expect(NARRATIVE).toContain('"kind":"entry","itemId":"S0_bind"');
expect(NARRATIVE).toContain('"kind":"decide","itemId":"S0d_p08_master"');
expect(NARRATIVE).not.toContain('"kind":"decide","itemId":"S0d_p08_master"');
// 两张数据工单都得标 variant:"data" —— 对话区凭它换成「数据工单」那副面孔
expect(NARRATIVE.match(/"variant":"data"/g)?.length).toBe(2);
expect(NARRATIVE).toContain('"title":"数据工单 · 芯片主数据创建(P-08)');
expect(NARRATIVE).toContain('"title":"数据工单 · 原厂技术资料创建(P-10)');
expect(NARRATIVE).toContain('"title":"评估输入与资料补齐确认');
@@ -166,22 +168,21 @@ describe('说明书与剧本的骨架', () => {
expect(secondPass).toMatch(/收不了步|收不掉/);
});
it('S0 剧本写明都在库时数据工单两项本就不会进「当下可为」;S0d_p08_master 剧本按候选总数判断该不该问人(=1 必建、≥2 才问、原 PN 硬阻塞)', () => {
it('S0 剧本写明都在库时数据工单两项本就不会进「当下可为」;S0d_p08_master 剧本写明 found 的一颗都不建', () => {
const s0 = ASSESSMENT_STEPS.find((s) => s.item === 'S0_bind')?.guide ?? '';
expect(s0).toContain('当下可为');
expect(s0).toContain('不要对计划项用');
expect(s0).toMatch(/都在库/);
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(s0d).toContain('硬阻塞');
expect(s0d).toContain('候选总数 = 1');
expect(s0d).toContain('absent');
expect(s0d).toMatch(/found" 的一行都不建/);
});
it('S0d_p08_master 剧本按候选总数(不是「不在库的颗数」)判断要不要问人 —— 候选总数 ≥2 时逐颗不在库的都要问(2026-08-27 实测逼出的措辞修正)', () => {
it('S0d_p08_master:查无记录一律派数据工单,不问人 —— 决策卡退役(2026-08-27 用户定:「只要有缺失就要自动派数据工单不需要人选择」)', () => {
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(s0d).toContain('候选总数');
expect(s0d).toContain('不在库');
expect(s0d).toMatch(/候选总数\s*≥\s*2/);
// 反面提示:「不在库的只有一颗」不该被读成跳过决策卡的理由
expect(s0d).toContain('不是跳过决策卡的理由');
expect(s0d).toMatch(/一律建,不问人/);
expect(s0d).toMatch(/exists_status = "found" 的一行都不建/);
expect(s0d).not.toMatch(/发一张决策卡问人/);
expect(s0d).not.toMatch(/候选总数\s*≥\s*2/);
});
it('s_bound 哨兵真的检查了「有料号不在库」这件事(都在库时门不该开;退回旧写法会红)', () => {
@@ -208,11 +209,6 @@ describe('说明书与剧本的骨架', () => {
expect(p10?.doneWhen).toBeUndefined();
});
it('候选放弃到只剩最后一颗时不许再放弃(至少留一颗,防止全放弃死锁)', () => {
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(s0d).toContain('至少留一颗候选');
});
it('P-08 交回之后先 finish 再发 P-10 卡,顺序写死进两段 guide(2026-08-27 实测:漏收步会让工作区画面停在 P-08 那两张表,P-10 卡发出后人却看不见资料表、提交闸拦死)', () => {
const s0d = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(s0d).toContain('finish S0d_p08_master');
@@ -239,8 +235,8 @@ describe('说明书与剧本的骨架', () => {
it('数据工单的名单只认 absent,「建哪几颗」与「要不要问人」分开写(2026-08-27 实测:原 PN 缺库那单,模型把在库的候选派成了数据工单,缺的那颗没人建)', () => {
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(g).toMatch(/问题一 · 建哪几颗/);
expect(g).toMatch(/问题二 · 建之前要不要先问人/);
expect(g).toMatch(/【名单】/);
expect(g).toMatch(/一律建,不问人/);
expect(g).toMatch(/exists_status = "found" 的一行都不建/);
});
@@ -300,8 +296,8 @@ describe('说明书与剧本的骨架', () => {
it('第二颗缺库的候选也要先问(每一颗都有被放弃的权利)', () => {
const g = ASSESSMENT_STEPS.find((s) => s.item === 'S0d_p08_master')?.guide ?? '';
expect(g).toMatch(/这一颗也要先发一张决策卡问人/);
expect(g).toMatch(/第一颗问过了不代表第二颗也问过/);
expect(g).toMatch(/不问人,直接建/);
expect(g).not.toMatch(/这一颗也要先发一张决策卡问人/);
});
it('created 认的是「卡交回来了」,不是「表里有行」—— 写骨架 ≠ 建好了(2026-08-27 e2e:模型铺完骨架就标 created、直接 finish,撞「从没给它发过卡」连拒五拍)', () => {
@@ -311,6 +307,23 @@ describe('说明书与剧本的骨架', () => {
expect(g).not.toMatch(/只有它在 \\`parts\.profile\\` 里真有一行/);
});
it('件上三处样式毛病钉死(2026-08-27 用户:表显示不全不能右滑 / 编辑态内部料号只读字与输入框并排 / input 与 select 没对齐)', () => {
const html = manifest.assets.viewTemplates.find((v) => v.name === 'p08-master-create')?.html ?? '';
expect(html).toContain('.jc-tbl-wrap { overflow-x: auto;');
expect(html).toContain('#tbl1.on td > .jc-vcell:not(.ro) { display: none; }');
expect(html).toContain('input.jc-cell, select.jc-cell { height: 32px;');
// 徽标不再包 .ro(编辑态要随只读格一起藏,只留下拉框)
expect(html).not.toContain('<span class="jc-vcell ro"><span class="stx"');
});
it('数据工单三张表的表头只留中文:没有英文列名、没有 .thd 小字(2026-08-27 用户定)', () => {
for (const name of ['p08-master-create', 'p10-document-create'] as const) {
const html = manifest.assets.viewTemplates.find((v) => v.name === name)?.html ?? '';
expect(html, name).not.toContain('class="thd"');
for (const th of html.match(/<th[^>]*>[^<]*<\/th>/g) ?? []) expect(th, name).not.toMatch(/[a-z]_[a-z]/);
}
});
it('五段进度,每段名 ≤10 字;除放行外每项恰属一段', () => {
expect(ASSESSMENT_PHASES).toHaveLength(5);
for (const p of ASSESSMENT_PHASES) expect(p.name.length, p.name).toBeLessThanOrEqual(10);
@@ -196,8 +196,8 @@ export const NARRATIVE = `§0 开案:**判断候选料号在技术上能不能
**存在性检查与数据工单**:启动时逐个料号在 \`parts.profile\` 里查(原 PN + 每个候选)。查无记录 = 「完全没有」,
要人从零建整条主数据(表 1 主记录 + 表 2 标准化参数子表 \`parts.parameter\`)—— 发**数据工单**(一张单:P-08 一张、P-10 一张);
PN 在库但个别字段缺 = 「有而不全」,归执行工单 01 补。**该不该问人,按候选总数判断,不是按不在库的颗数判断**:
原 PN 缺 = 硬阻塞必须建、不问;候选总数 = 1 时那一颗必须建、不问;候选总数 ≥ 2 时,每一颗不在库的候选都要先问人放弃还是创建。
PN 在库但个别字段缺 = 「有而不全」,归执行工单 01 补。**查无记录的一律派数据工单,不问人**(2026-08-27 用户定:
「只要有缺失就要自动派数据工单,不需要人选择」)—— 原 PN 缺是硬阻塞、候选缺同样直接建,**没有「放弃这颗候选」这个岔路口**。
**库里都有的话这两项的入场前提本就不成立**(此单不走这条),不会出现在你的「当下可为」里,别发空单。
**六步走**:绑定与存在性核对 → (等待数据)→ 人补评估输入 → 标准化对照表 → 逐项评估 → 人审结论(可循环)→ 拟落账 → 人放行。
@@ -219,20 +219,14 @@ PN 在库但个别字段缺 = 「有而不全」,归执行工单 01 补。**该
把九个计划项的全名摆到人脸上,他一个字也用不上(2026-08-27 实测)。
人确认那一下基座已把这一行落进草稿层,**你不许再 write 一遍 customer.profile**。
② 候选不在库、且候选总数 ≥2 时,一颗一张决策卡(按候选数组顺序,一次只发一张;候选总数 = 1 时不问、直接建):
{"kind":"decide","itemId":"S0d_p08_master",
"ask":"候选 <internal_pn> 在芯片主数据里查无记录。是从零建它的主数据(派数据工单),还是本轮放弃这颗候选?",
"options":[{"value":"create","label":"创建主数据(派数据工单)","recommended":true},
{"value":"abandon","label":"放弃这颗候选"}],"allowCustom":true}
\`value\` 只有 \`"create"\` / \`"abandon"\` / \`"custom:"\` 开头三种:\`"custom:"\` 开头按放弃处理、\`note\` 就是放弃原因;
点了 \`"abandon"\` 没写原因,\`abandon_reason\` 写「负责人本轮放弃(未注明原因)」。
② (已退役)候选缺库不再问人 —— 2026-08-27 用户定「只要有缺失就自动派数据工单」;所有查无记录的料号直接进③那张卡。
③ 数据工单 · P-08(所有要建的料号一起,一张单;零输入,填报全在工作区的件上)——
**发这张卡之前,\`parts.profile\` 与 \`parts.parameter\` 必须先有骨架行**(名单里每颗料各一行 + 它的参数各一行)。
不管这一拍是不是顺手把上一步收掉的(压拍常发生,那时这一步的剧本还没摊开),写骨架与发卡都得在同一串里,顺序不能反 ——
件上一行都没有的话,人打开是一张空表、什么也补不了,而这一单的主数据从头到尾不会存在
(实测走到 S2 才被引用检查拦下,那时两张数据工单都已经「办完」了)
{"kind":"human","itemId":"S0d_p08_master","workOrder":"execution","card":{
{"kind":"human","itemId":"S0d_p08_master","workOrder":"execution","card":{"variant":"data",
"title":"数据工单 · 芯片主数据创建(P-08):<料号列表,顿号分隔>",
"ask":"这些料号在芯片主数据里查无记录,请在工作区的件上把整条记录建齐(一颗一行,可加行)。",
"steps":["表 1 每一行:品牌、规格描述、封装、生命周期状态为必填,缺一格确认不了",
@@ -251,7 +245,7 @@ PN 在库但个别字段缺 = 「有而不全」,归执行工单 01 补。**该
**与③同一条:发这张卡之前 \`parts.document\` 必须先有骨架行**(名单里每颗料至少一行,
key 是 {internal_pn, doc_seq:"01"});一行都没有的话人打开是空表、什么也补不了,
提交闸会如实拦住(「这一单还一行都没有」),这一单就卡在这儿了(2026-08-27 实测)。
{"kind":"human","itemId":"S0d_p10_document","workOrder":"execution","card":{
{"kind":"human","itemId":"S0d_p10_document","workOrder":"execution","card":{"variant":"data",
"title":"数据工单 · 原厂技术资料创建(P-10):<料号列表>",
"ask":"请把这些料号的 datasheet / 承诺函等资料记录补进原厂技术资料库(一份资料一行,可加行)。",
"steps":["资料类型、标题、版本、文件链接、资料状态为必填;证据定位写页码或章节,差异评估的证据引用会指向它",
@@ -357,7 +351,7 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不
},
{
item: 'S0d_p08_master',
intent: '料号不在库:派 P-08 数据工单让人从零建芯片主数据 + 标准化参数子表;候选总数 ≥2 才逐颗问放弃还是创建',
intent: '料号不在库:派 P-08 数据工单让人从零建芯片主数据 + 标准化参数子表 —— 查无记录一律派,不问人',
writes: ['parts.profile', 'parts.parameter', 'parts.document', `${D}.pn_check`],
reads: [`${D}.pn_check`, 'candidateInternalPns', 'originalInternalPn', 'datasheets', 'parts.profile', 'parts.parameter', 'parts.document'],
view: 'p08-master-create',
@@ -365,25 +359,11 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不
/* 触达人,不给 doneWhen;人工激活:发卡就算启动 */
guide: `这一项只在 \`${D}.pn_check\` 里有 absent 行时走 —— 都在库时入场前提本就不成立,这一项与下一项 S0d_p10_document 都不会进你的「当下可为」。**先分岔,再动作**:
【先数两个数,再分岔】(先看轨迹:这一项里已经发过哪些卡、人答了什么,【当前这一步】下面那行引擎事实说得清楚)
· **候选总数**:入参 \`candidateInternalPns\` 的长度(\`${D}.pn_check\` 里 role = "candidate" 的行数也是它,不随本单进展变化);
· **不在库的候选数**:\`${D}.pn_check\` 里 role = "candidate" 且 exists_status = "absent" 的行数(这个数会随着人放弃/创建而变小)。
**这里要分开答两个问题,别混成一个**:
**问题一 · 建哪几颗?** 名单 = \`${D}.pn_check\` 里 **exists_status = "absent" 的每一行**,不管它是原 PN 还是候选。
【名单】= \`${D}.pn_check\` 里 **exists_status = "absent" 的每一行**,不管它是原 PN 还是候选 —— **一律建,不问人**
(2026-08-27 用户定:只要有缺失就自动派数据工单;从前那张「派数据工单还是放弃这颗候选」的决策卡已退役,别再发)。
**exists_status = "found" 的一行都不建** —— 那几颗的主数据本来就在库里,给它们派数据工单等于让人把已有的东西再填一遍,
而真正缺的那颗还是没人建、单子照样卡死。名单为空(一行 absent 都没有)这一项根本不会出现在你的「当下可为」里。
**问题二 · 建之前要不要先问人?** 只有 **absent 的候选**才谈得上问,**看的是候选总数,不是不在库的颗数**
——**「不在库的只有一颗」不是跳过决策卡的理由**:
· 原 PN absent → **硬阻塞**,不问人,直接进名单(它是评估基准,没有它整单无从谈起);
· **候选总数 = 1** → 那一颗候选**如果是 absent** 就不许放弃、不问人,直接进名单(候选是 found 的话它压根不在名单里,没有可问的);
· **候选总数 ≥ 2** → **每一颗 absent 的候选都要先发一张决策卡问人**(一次一张,按候选数组顺序;哪怕这一单不在库的候选只有一颗,
只要候选总数 ≥ 2 就要问)照说明书②;人选 "abandon"/"custom:" 的,把那一行 \`${D}.pn_check\` 改成
exists_status = "abandoned" + abandon_reason(同 PK 覆盖写),这颗不再评估;选 "create" 的进「建」的名单。
**至少留一颗候选**:放弃到只剩最后一颗还是 absent 的候选时,那一颗按「候选总数 = 1」办 —— 不再发决策卡问,直接进「建」的名单
(候选全被放弃,存在性核对永远凑不齐、单子会死锁,这一条就是防它)。
**别把同一颗候选问两遍**:决策卡答过就按答案办。
**名单里的料号全放进同一张 P-08 卡**(标题列全料号,件上一颗一行,参数各归各的料号)—— 别一颗一张。
【「建」的名单定了 → 写骨架行,发 P-08 数据工单(一张单,件上两张表)】
1. 给名单里每颗料写一行 \`parts.profile\` 骨架(表 1):key {internal_pn},patch 只带你确有依据的列 —— 有 \`datasheets[].text\` 就从里面读
@@ -414,11 +394,8 @@ current_stage = "assessing"。application_scenario / bom_position **这一步不
门上还有一条逐颗对账(建好几颗就得有几行主数据、几行资料),标了却没建,这一步照样过不去。
【P-08 那张确认回来了】**先看名单里还有没有 absent 的料号**(\`${D}.pn_check\` 现读):
· **还有** → **这一项千万别 finish**。先回到上面【问题二】那一档:候选总数 ≥2 的话,
**这一颗也要先发一张决策卡问人**(照说明书②,人选 create 才建 —— 每一颗 absent 的候选都有被放弃的权利,
第一颗问过了不代表第二颗也问过);人选 create 之后再把它的骨架写好(profile + parameter,有 datasheet 就预填),
发一张 P-08 卡(标题带那一颗的料号)—— 同一个计划项可以先后发好几张卡,一颗一张也行。
原 PN 与「候选总数 = 1」那两档不问,直接建。
· **还有** → **这一项千万别 finish**。把还 absent 的那几颗的骨架写好(profile + parameter,有 datasheet 就预填),
再发一张 P-08 卡(标题列上它们的料号)—— 不问人,直接建;同一个计划项可以先后发好几张卡。
**收掉了就回不去了**:这一项一旦 Completed,再想给它挂第二张卡会被拒「这一步已经办完收掉了」,
而那颗料从此没人建,后面写替代关系时被引用检查逐颗拦下(2026-08-27 实测,一单连拒四次)。
· **一颗都不剩了** → 才走下面这两步。
@@ -89,7 +89,7 @@ export const manifest: ActivityManifest = {
identity: {
activityId: ACTIVITY_ID,
name: 'ACT-WORK-05 · 差异评估',
packageVersion: '1.3.6',
packageVersion: '1.4.0',
category: '需求分析与方案匹配',
sdkVersion: '0.1.0-m5',
checksum: 'dev-checksum-part-substitution-assessment',
@@ -29,6 +29,14 @@ import { D } from '../constants.ts';
* 绑定层公共开关 —— 五张模版共用,与目标件自己的样式表分开存(那份要保持逐字节可比)
* ==================================================================================== */
const BIND_COMMON = `
/* ── 2026-08-27 用户四连指出的件上毛病,病根三处都在「目标件的样式与平台的编辑态没对上」 ── */
/* ① 表宽超出就得能横着滑(E04/I01 08-23 同款):目标件把 overflow-x:auto 写在 .tbl-wrap 上,而平台包装是 .jc-tbl-wrap,谁都没管到 */
.jc-tbl-wrap { overflow-x: auto; overflow-y: hidden; }
/* ② 编辑态只留输入格:目标件 #tbl1 .jc-vcell{display:inline}(id 特异度)盖过了 table.on td>.jc-vcell{display:none},
于是表 1 每格都是「只读字 + 输入框」并排 —— 内部料号那格恰好有字,人看见的就是它 */
#tbl1.on td > .jc-vcell:not(.ro) { display: none; }
/* ③ 一行里的 input 与 select 同高同线:select 天生比 input 矮 4px,编辑态里一排格子高低不齐 */
input.jc-cell, select.jc-cell { height: 32px; box-sizing: border-box; padding-top: 0; padding-bottom: 0; vertical-align: middle; }
/* 只在「待处理」这一态露面;归档 / 验收只读态即便 HTML 里还带着 data-edit 也不摆闸和加行钮
(目标件是静态 demo,不区分这两态;html.editable 是平台在 iframe 根节点上真开的关) */
html:not(.editable) .jc-editgate, html:not(.editable) .jc-addrow { display: none !important; }
@@ -305,10 +313,14 @@ const cell = (col: string, cls = ''): string => `<span class="jc-vcell${cls ===
const ro = (col: string, cls = ''): string => `<span class="jc-vcell ro${cls === '' ? '' : ` ${cls}`}" data-slot="${col}" data-blank></span>`;
const select = (col: string, options: ReadonlyArray<readonly [string, string]>, required = false): string =>
`<select class="jc-cell" data-col="${col}"${required ? ' required' : ''}><option value=""></option>${options.map(([v, l]) => `<option value="${v}">${l}</option>`).join('')}</select>`;
/** 枚举徽标(可改表里用,旁边挨着 `.jc-cell` 孪生件时要包一层 `.ro` 才不被编辑态藏起来) */
const badge = (klass: string, col: string, wrapRo = false): string => {
/**
* 枚举徽标。可改表里挨着 `<select class="jc-cell">` 孪生件时包一层 `.jc-vcell`(不带 `.ro`):
* 看的时候摆徽标,**编辑态随只读格一起藏起来,只留下拉框** —— 从前包的是 `.ro`(编辑态留着),
* 徽标压在下拉框头上把那一格顶低 13px,一排格子高低不齐(2026-08-27 用户指出)。
*/
const badge = (klass: string, col: string, wrapTwin = false): string => {
const inner = `<span class="${klass}" data-class-of="${col}"></span>`;
return wrapRo ? `<span class="jc-vcell ro">${inner}</span>` : inner;
return wrapTwin ? `<span class="jc-vcell">${inner}</span>` : inner;
};
/* ==================== 局面 0 · 数据工单 P-08(搬自「数据工单-01」)==================== */
@@ -316,17 +328,17 @@ const badge = (klass: string, col: string, wrapRo = false): string => {
const P08_TABLE = `<div class="jc-tbl-wrap"><table id="tbl1">
<thead><tr>
<th style="width:38px">#</th>
<th class="req" data-h="内部料号">内部料号 internal_pn<span class="thd">全链路统一基准,不可自由文本</span></th>
<th class="req" data-h="品牌名称">品牌 brand_name<span class="thd">与制造商、供应商分开</span></th>
<th data-h="制造商名称">制造商 manufacturer_name<span class="thd">与品牌不一致时分开记录</span></th>
<th class="req" data-h="规格描述">规格描述 specification_description<span class="thd">按原厂资料口径</span></th>
<th class="req" data-h="封装">封装 package<span class="thd">影响硬约束比对</span></th>
<th data-h="工艺">工艺 process_technology<span class="thd">硬约束比较项</span></th>
<th class="req" data-h="生命周期状态">生命周期 lifecycle_status<span class="thd">Active/NRND/EOL/停产/风险</span></th>
<th data-h="EOL/PCN状态">EOL/PCN eol_pcn_status<span class="thd">通知/影响/生效时间</span></th>
<th data-h="Datasheet引用">Datasheet 引用 datasheet_ref</th>
<th data-h="Datasheet版本">Datasheet 版本 datasheet_version<span class="thd">与引用成对</span></th>
<th data-h="适配应用场景">适配场景 application_scenarios<span class="thd">判断实际适用性</span></th>
<th class="req" data-h="内部料号">内部料号</th>
<th class="req" data-h="品牌名称">品牌</th>
<th data-h="制造商名称">制造商</th>
<th class="req" data-h="规格描述">规格描述</th>
<th class="req" data-h="封装">封装</th>
<th data-h="工艺">工艺</th>
<th class="req" data-h="生命周期状态">生命周期</th>
<th data-h="EOL/PCN状态">EOL/PCN</th>
<th data-h="Datasheet引用">Datasheet 引用</th>
<th data-h="Datasheet版本">Datasheet 版本</th>
<th data-h="适配应用场景">适配场景</th>
</tr></thead>
<tbody data-rows="mine:parts.profile" data-edit="parts.profile"><tr>
<td class="rn"><span class="jc-rn-num"></span><button class="jc-rowdel" type="button" title="删除此行" data-edit-del="parts.profile">×</button></td>
@@ -354,7 +366,7 @@ const P08_TABLE = `<div class="jc-tbl-wrap"><table id="tbl1">
const P08_PARAMS_TABLE = `<div class="jc-tbl-wrap"><table id="tbl2">
<thead><tr>
<th style="width:38px">#</th>
<th class="req" data-h="所属料号">所属料号 internal_pn<span class="thd">归属表 1 哪条记录</span></th>
<th class="req" data-h="所属料号">所属料号</th>
<th class="req" data-h="参数名">参数名</th>
<th class="req" data-h="值">值</th>
<th data-h="单位">单位</th>
@@ -408,15 +420,15 @@ export const P08_MASTER_CREATE = doc({
const P10_TABLE = `<div class="jc-tbl-wrap"><table>
<thead><tr>
<th style="width:38px">#</th>
<th class="req" data-h="关联料号">关联料号 internal_pn<span class="thd">对应 P-08 那条记录</span></th>
<th class="req" data-h="资料序号">资料序号 doc_seq<span class="thd">同一料号下 01/02… 自己续</span></th>
<th class="req" data-h="资料类型">资料类型 document_type</th>
<th data-h="品牌名称">品牌 brand_name</th>
<th class="req" data-h="资料标题">资料标题 document_title</th>
<th class="req" data-h="资料版本">资料版本 document_version</th>
<th class="req" data-h="文件链接">文件链接 document_ref</th>
<th data-h="证据定位">证据定位 citation_locator<span class="thd">页码/章节/表格</span></th>
<th class="req" data-h="资料状态">资料状态 document_status</th>
<th class="req" data-h="关联料号">关联料号</th>
<th class="req" data-h="资料序号">资料序号</th>
<th class="req" data-h="资料类型">资料类型</th>
<th data-h="品牌名称">品牌</th>
<th class="req" data-h="资料标题">资料标题</th>
<th class="req" data-h="资料版本">资料版本</th>
<th class="req" data-h="文件链接">文件链接</th>
<th data-h="证据定位">证据定位</th>
<th class="req" data-h="资料状态">资料状态</th>
</tr></thead>
<tbody data-rows="mine:parts.document" data-edit="parts.document"><tr>
<td class="rn"><span class="jc-rn-num"></span><button class="jc-rowdel" type="button" title="删除此行" data-edit-del="parts.document">×</button></td>
+6
View File
@@ -612,6 +612,9 @@ export declare const executionWorkOrderSchema: z.ZodObject<{
preset: z.ZodOptional<z.ZodEnum<{
"landing-failure": "landing-failure";
}>>;
variant: z.ZodOptional<z.ZodEnum<{
data: "data";
}>>;
ask: z.ZodString;
standard: z.ZodOptional<z.ZodString>;
steps: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -1226,6 +1229,9 @@ export declare const workOrderSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
preset: z.ZodOptional<z.ZodEnum<{
"landing-failure": "landing-failure";
}>>;
variant: z.ZodOptional<z.ZodEnum<{
data: "data";
}>>;
ask: z.ZodString;
standard: z.ZodOptional<z.ZodString>;
steps: z.ZodOptional<z.ZodArray<z.ZodString>>;
+3
View File
@@ -1694,6 +1694,8 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
const gateLedgers = Array.isArray(input.card['gateLedgers'])
? input.card['gateLedgers'].map((x) => String(x).trim()).filter((x) => /^[a-z0-9_]+\.[a-z0-9_]+$/i.test(x))
: [];
/* 数据工单 = 执行工单的变体(契约 `variant`):活动在卡上写 `"variant":"data"`,对话区据此换一副面孔 */
const variant = input.card['variant'] === 'data' ? ('data' as const) : undefined;
const issued = await deps.humanTool.issueExecution(context, {
title,
basis: basis.length > 0 ? basis : [{ k: '来源', v: '案例模型当下局面' }],
@@ -1703,6 +1705,7 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
...(standard === '' ? {} : { standard }),
...(blocks === undefined ? {} : { blocks: blocks as ExecutionWorkOrderDraft['blocks'] }),
...(gateLedgers.length === 0 ? {} : { gateLedgers }),
...(variant === undefined ? {} : { variant }),
});
// 打扰率的计量对象:每发一张卡就是调了一次人 —— 不计就等于没有上限
await taskStore.addConsumption(input.taskId, { humanCalls: 1 });
@@ -32,3 +32,4 @@
- **调人预算环外强制**:超过契约声明的打扰上限即 `HumanCallBudgetExceededError`,
活动代码无从自查自守
- **落地失败卡是执行工单的 preset**,不构成第五种卡
- **数据工单是执行工单的 `variant:'data'`**,同样不构成新的 mode:发卡 / 闸 / 回传全走执行工单那条路,只是对话区换一副面孔(2026-08-27)
+2
View File
@@ -44,6 +44,7 @@
--st-execute: #e07a00; --st-execute-soft: #fff4e5;
--st-decision: #7f3bf5; --st-decision-soft: #f5f0fe;
--st-collab: #0aa294; --st-collab-soft: #e4f8f5;
--st-data: #1f6fe0; --st-data-soft: #eaf2fd;
--st-wait: #6a7590; --st-wait-soft: #eef0f5;
/* —— 字体 / 投影 / 动效 —— */
--mono: ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono", Menlo, Consolas, monospace;
@@ -2387,6 +2388,7 @@
.proto-root .jc-fc { --fc: var(--brand); --fc-soft: var(--brand-wash); width: 100%; max-width: 100%; background: #fff; border: 1px solid color-mix(in srgb, var(--fc) 22%, var(--line-soft)); border-radius: 13px; overflow: hidden; box-shadow: 0 1px 2px rgba(31,35,41,.04), 0 4px 14px rgba(31,35,41,.06); }
.proto-root .jc-fc.t-exe { --fc: var(--st-execute); --fc-soft: var(--st-execute-soft); }
.proto-root .jc-fc.t-acc { --fc: var(--st-collab); --fc-soft: var(--st-collab-soft); }
.proto-root .jc-fc.t-data { --fc: var(--st-data); --fc-soft: var(--st-data-soft); }
.proto-root .jc-fc.t-dec { --fc: var(--st-decision); --fc-soft: var(--st-decision-soft); }
.proto-root .jc-fc.t-fill { --fc: var(--brand); --fc-soft: var(--brand-wash); }
/* 题头:色染带 + 计数章 */
@@ -357,6 +357,15 @@ function Msg({
onLocate={onLocate}
{...(orderValues === undefined ? {} : { orderValues })}
/>
) : order.mode === 'execution' && order.variant === 'data' && !settled ? (
/* 数据工单 = 执行工单的变体(契约 `variant`):同一套办法,另一副面孔 */
<DataForm
order={order}
onReplyOrder={onReplyOrder}
onLocate={onLocate}
onField={onField}
{...(orderValues === undefined ? {} : { orderValues })}
/>
) : order.mode === 'execution' && !settled ? (
/* 没办结的执行单 = 一张能就地办的卡:指导书 + 依据 + 表单 + 提交 */
<ExecForm
@@ -582,11 +591,12 @@ function WoCard({ order, onLocate }: { order: WorkOrder; onLocate: (id: string)
const result = order.done?.result;
const bad = result !== undefined && isRejection(result);
const exe = order.mode === 'execution';
const data = order.mode === 'execution' && order.variant === 'data';
return (
<div className={'jc-fc ' + (exe ? 't-exe' : 't-acc')}>
<div className={'jc-fc ' + (data ? 't-data' : exe ? 't-exe' : 't-acc')}>
<div className="jc-fc-h">
<Ico name={exe ? 'file-text' : 'shield-check'} s={13} />
<b>工单 · {exe ? '执行' : '验收'}</b>
<Ico name={data ? 'database' : exe ? 'file-text' : 'shield-check'} s={13} />
<b>工单 · {data ? '数据' : exe ? '执行' : '验收'}</b>
<em className={result === undefined ? '' : bad ? 'bad' : 'ok'}>
{result === undefined ? '待处理' : bad ? '已退回' : '已办结'}
</em>
@@ -777,19 +787,54 @@ function Guide({ steps, standard }: { steps?: readonly string[] | undefined; sta
* 表格那一份值不在这儿收 —— 人是在工作区的件上改的,由外层缓冲带过来
* (与验收裁决同一条路子:编辑与提交分在两个区,缓冲得由壳层持有)。
*/
function ExecForm({
order,
onReplyOrder,
onLocate,
orderValues,
onField,
}: {
/** 执行工单与数据工单是同一套办法、两副面孔 —— 面孔在这儿定,办法只写一份 */
type FormFace = {
readonly klass: 't-exe' | 't-data';
readonly headIcon: 'clipboard-check' | 'database';
readonly head: string;
readonly docIcon: 'file-text' | 'table';
readonly docTitle: string;
readonly docHint: string;
readonly submit: string;
};
const EXEC_FACE: FormFace = {
klass: 't-exe',
headIcon: 'clipboard-check',
head: '工单 · 执行',
docIcon: 'file-text',
docTitle: '执行工单',
docHint: '整份清单在工作区,每一格都能就地改;改完回这里提交',
submit: '提交',
};
/**
* 数据工单(2026-08-27 用户:「工单类型新加入了一类数据工单…照着这个大概的样式为数据工单单独做一个组件」):
* 让人**从零建一张主数据表** —— 件上的表就是要建的记录,填齐、保存,回这里确认即入库,活动从「等待数据」唤醒。
* 引擎那一侧与执行工单无异(契约 `ExecutionWorkOrder.variant = 'data'`),所以只换面孔:色、图标、四字、按钮字。
*/
const DATA_FACE: FormFace = {
klass: 't-data',
headIcon: 'database',
head: '工单 · 数据',
docIcon: 'table',
docTitle: '数据工单',
docHint: '件上的表就是要建的记录:每格就地填、保存;填齐后回这里确认,确认即入库',
submit: '确认写入',
};
type ExecFormProps = {
order: Extract<WorkOrder, { mode: 'execution' }>;
onReplyOrder: (reply: WorkOrderReply) => void;
onLocate: (id: string) => void;
orderValues?: Readonly<Record<string, string>> | undefined;
onField: (key: string, value: string) => void;
}) {
};
/** 数据工单卡 —— 执行工单的另一副面孔(见 DATA_FACE) */
function DataForm(props: ExecFormProps) {
return <ExecForm {...props} face={DATA_FACE} />;
}
function ExecForm({ order, onReplyOrder, onLocate, orderValues, onField, face = EXEC_FACE }: ExecFormProps & { face?: FormFace }) {
const busy = useReplyBusy();
const val = (f: FormField) => orderValues?.[f.key] ?? f.ai ?? '';
/*
@@ -805,10 +850,10 @@ function ExecForm({
const gate = order.gate;
const ok = order.fields.every((f) => f.required !== true || val(f).trim() !== '') && gate?.ready !== false;
return (
<div className="jc-fc t-exe">
<div className={'jc-fc ' + face.klass}>
<div className="jc-fc-h">
<Ico name="clipboard-check" s={13} />
<b>工单 · 执行</b>
<Ico name={face.headIcon} s={13} />
<b>{face.head}</b>
{order.steps !== undefined && order.steps.length > 0 && <em>{order.steps.length} 步</em>}
</div>
<div className="jc-fc-b">
@@ -821,11 +866,11 @@ function ExecForm({
*/}
<button className="jc-fc-doc" onClick={() => onLocate(order.id)}>
<span className="jc-fc-doc-ico">
<Ico name="file-text" s={15} />
<Ico name={face.docIcon} s={15} />
</span>
<span className="jc-fc-doc-bd">
<b>执行工单</b>
<em>整份清单在工作区,每一格都能就地改;改完回这里提交</em>
<b>{face.docTitle}</b>
<em>{face.docHint}</em>
</span>
<Ico name="chevron-right" s={14} />
</button>
@@ -878,7 +923,7 @@ function ExecForm({
})
}
>
<Ico name="check" s={13} /> {order.submitLabel ?? '提交'}
<Ico name="check" s={13} /> {order.submitLabel ?? face.submit}
</Btn>
</div>
</div>
@@ -48,6 +48,12 @@ const HUMAN_TOOLS = [
ico: 'clipboard-check',
desc: '作业指导书 + 空表单,请人把一步做完;落地失败卡是它的系统预置形态,不算第五种卡',
},
{
name: '数据工单',
mode: 'work-order · execution · data',
ico: 'database',
desc: '执行工单的变体:件上的表就是要建的主数据记录,人填齐、保存、确认即入库,活动从「等待数据」唤醒',
},
{
name: '验收工单',
mode: 'work-order · acceptance',
+8 -2
View File
@@ -58,8 +58,14 @@ export default defineConfig({
*/
host: true,
proxy: {
// 默认连测试服务器;本地起 API 时用 API_PROXY_TARGET=http://127.0.0.1:8787 覆盖
'/api': { target: process.env.API_PROXY_TARGET ?? 'http://150.158.83.131:46100', changeOrigin: true },
/*
* **默认连本机 API(:8787)**;要连测试服务器就显式给 `API_PROXY_TARGET=http://150.158.83.131:46100`。
* 从前反过来(默认远端、本地要覆盖):实测那条远端是一条**隧道回到本机 :8787**,
* 浏览器的每一次请求都先出公网再绕回来 —— 能用,但慢,且隧道一断本地开发就整个瘫掉,
* 而人只会看到「前端调的后端不太对」(2026-08-27)。CLAUDE.md 写的本地工作流是
* `pnpm dev:api` + `pnpm dev:web`,默认值就该服务于这条路;要远端的人自己说出来。
*/
'/api': { target: process.env.API_PROXY_TARGET ?? 'http://127.0.0.1:8787', changeOrigin: true },
},
},
});
+1 -1
View File
@@ -252,7 +252,7 @@ activities/<id>/src/ ──(pnpm activities:build)──> dist/{manifest.json,in
| `ask` | ~~已退役~~ | 基座原地转成零输入执行工单(字段名并进指导书);别再用 |
| `entry` | `{itemId, ask, ledger, entryKey, fields, also?}` | 数据录入卡,挂起;确认那一下基座直写 |
| `decide` | `{itemId, ask, options(≥2), basis}` | 决策卡,挂起 |
| `human` | `{itemId, workOrder, card{title,ask,steps?,standard?,basis}}` | 执行/验收工单,挂起。**执行卡零输入**(2026-08-02 用户定):不摆 `fields` 不绑 `blocks`,填写全在工作区的件上(editGate + 孪生件),人按「保存」基座直写草稿层;必填由提交闸看住 —— **卡上加 `gateLedgers:["域.对象"]` 告诉闸这一张要拦哪几张表**,不给才回落到必交付物声明 |
| `human` | `{itemId, workOrder, card{title,ask,steps?,standard?,basis}}` | 执行/验收工单,挂起。**执行卡零输入**(2026-08-02 用户定):不摆 `fields` 不绑 `blocks`,填写全在工作区的件上(editGate + 孪生件),人按「保存」基座直写草稿层;必填由提交闸看住 —— **卡上加 `gateLedgers:["域.对象"]` 告诉闸这一张要拦哪几张表**,不给才回落到必交付物声明。**数据工单**(让人从零建一张主数据表)在卡上再加 `variant:"data"` —— 引擎那一侧与执行工单无异,对话区据此换成「数据工单」那副面孔 |
| `listen` | `{itemId, eventType, keyFilter, deadline}` | 立监听,挂起;时限必配 |
| `finish` | `{itemId, rationale}` | 宣告一项完成;非人工激活的项就地过 Active 再收 |
| `submit` | `{rationale, fields?}` | 提交放行 → 预检 → 发验收卡挂起 |
+8
View File
@@ -336,6 +336,14 @@ export const executionWorkOrderSchema = z.object({
mode: z.literal('execution'),
/** 系统预置形态标记:landing-failure = 落地失败卡(OS 7.3) */
preset: z.enum(['landing-failure']).optional(),
/**
* **数据工单**(2026-08-27 用户定:「工单类型新加入了一类数据工单」)——
* 执行工单的一种变体,不是第三种 mode:引擎那一侧(发卡 / 闸 / 回传 / 办结)与执行工单一模一样,
* 区别只在**它要人办的是「把一张主数据表建齐」**(件上的表就是要建的记录,确认即入库),
* 所以对话区要长成自己的样子(色/图标/「数据工单」四字/「确认写入」),不能再顶着「执行工单」的牌子。
* 活动在卡骨架上写 `"variant":"data"`,归一层原样带下来;不写就是普通执行工单。
*/
variant: z.enum(['data']).optional(),
ask: z.string(),
standard: z.string().optional(),
steps: z.array(z.string()).optional(),
+12 -8
View File
@@ -608,9 +608,9 @@ const SUITES = {
cases: [
{
sample: 0,
name: '候选缺库 · XL2596S-5.0 派数据工单(P-08 + P-10)· 审核先点名驳回再通过',
// 决策卡序:候选 ≥2 缺库先问 → create;审核第一张点名意见驳回;第二张通过
decideSeq: ['create', 'custom:使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件', 'approve'],
name: '候选缺库 · XL2596S-5.0 自动派数据工单(P-08 + P-10)· 审核先点名驳回再通过',
// 决策卡序(2026-08-27 起缺库不再问人,数据工单自动派):审核第一张点名意见驳回;第二张通过
decideSeq: ['custom:使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件', 'approve'],
expectDecision: true,
expectPort: 'assessment-released',
stageEdits: {
@@ -706,11 +706,11 @@ const SUITES = {
},
{
sample: 3,
name: '三候选 · 两颗缺库各问一次 · 国产 32V 硬约束不推荐',
decideSeq: ['create', 'create', 'approve'],
name: '三候选 · 两颗缺库一律自动派单 · 国产 32V 硬约束不推荐',
decideSeq: ['approve'],
expectDecision: true,
// 两颗候选缺库 → 按规格各问一次(审核卡也是决策卡,所以要按问法数,不能只看张数)
expectAsks: [{ re: /查无记录/, count: 2 }],
// 缺库不再问人(2026-08-27 用户定):这一例一张「查无记录」决策卡都不该出现
expectAsks: [{ re: /查无记录/, count: 0, max: 0 }],
expectPort: 'assessment-released',
stageEdits: {
'parts.profile': [
@@ -1389,12 +1389,16 @@ async function runCase(suite, kase) {
* 而审核卡也是决策卡 —— `decisions > 0` 于是恒真,该问没问照样绿)。
*/
if (kase.expectAsks !== undefined) {
for (const { re, count } of kase.expectAsks) {
for (const { re, count, max } of kase.expectAsks) {
const got = asked.filter((a) => re.test(a)).length;
if (got < count) {
console.log(` ⚠️ 该问的没问够:${re} 期望 ≥${count} 张,实际 ${got} 张 —— 判失败`);
ok = false;
}
if (max !== undefined && got > max) {
console.log(` ⚠️ 不该问的问了:${re} 最多 ${max} 张,实际 ${got} 张 —— 判失败`);
ok = false;
}
}
}
if (kase.expectDecision && decisions === 0) {