forked from ChaiHao/BPOS-MVP
人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的: 件上就地改 · 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名, 于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 —— 而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。 · **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。 删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)—— 「没回传就是删了」会把一次不完整的回传变成一次删表。 · **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。 · 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。 判据与门 · 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来, 人填好的目标价它照样判「客户未提供」的毛病由此消掉。 · 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。 · 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开, 于是一单没打标就进了放行门。三个数全由基座现数。 · 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。 **指向两扇打不开的门,比不指还糟。** 界面不撒谎 · 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。 · 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、 往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。 · 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。 · 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。 · 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。 A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。 五道门禁全绿(971 passed)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
This commit is contained in:
@@ -64,6 +64,14 @@ export const formFieldSchema = z.object({
|
||||
hint: z.string().optional(),
|
||||
/** AI 拟值 —— 验收工单落地时已填好,人可改全部字段 */
|
||||
ai: z.string().optional(),
|
||||
/**
|
||||
* 这个值改完**落回哪个台账地址**(`域.对象`)—— 只有验收单的修订字段需要。
|
||||
*
|
||||
* 没有它,人在验收报告上改的值只留在工单留痕里,落地时用的还是 AI 的原值:
|
||||
* 「验收报告可编辑」就成了摆设(第 12 章③)。
|
||||
* 口径:只改**本单在该对象的那一行**;该对象有多行时不猜是哪一行,如实拒改。
|
||||
*/
|
||||
ledger: z.string().optional(),
|
||||
unit: z.string().optional(),
|
||||
required: z.boolean().optional(),
|
||||
cols: z.union([z.literal(1), z.literal(2)]).optional(),
|
||||
@@ -126,6 +134,23 @@ export const viewBlockSchema = z.discriminatedUnion('t', [
|
||||
rows: z.array(z.array(z.string())),
|
||||
editable: z.boolean().optional(),
|
||||
editableCols: z.array(z.number().int()).optional(),
|
||||
/*
|
||||
* ── 绑台账:**行由基座铺,不由活动塞**(2026-08-01)────────────────────
|
||||
*
|
||||
* 给了 `ledger` + `fields`,发卡那一刻基座就从这一单的草稿层把行铺进来
|
||||
* (`rows` 写不写都会被铺出来的行盖掉)。
|
||||
*
|
||||
* 为什么必须这样:人要在卡上改的是**那份清单本身**(PRD 的执行页就是整份 9 行
|
||||
* 逐格可改),而让活动把 9 行塞进回复里有两处死结 ——
|
||||
* · 回复有长度上限,铺长了半途截断,整拍作废(实测);
|
||||
* · 那些行**台账里已经有了**,让模型再抄一遍是拿 token 换一次抄错的机会。
|
||||
*
|
||||
* `fields` 与 `cols` 一一对应:`cols` 是给人看的列名,`fields` 是台账列名。
|
||||
* 人提交之后,基座按同样的对应关系把改动写回草稿层并留痕 —— AI 不经手
|
||||
* (与数据录入卡同一条规矩:该由人亲手落下的,就别在中间加一道传话)。
|
||||
*/
|
||||
ledger: z.string().optional(),
|
||||
fields: z.array(z.string()).optional(),
|
||||
linkDonut: z
|
||||
.object({
|
||||
col: z.number().int(),
|
||||
@@ -301,6 +326,28 @@ export const executionWorkOrderSchema = z.object({
|
||||
blocks: z.array(viewBlockSchema).optional(),
|
||||
fields: z.array(formFieldSchema),
|
||||
submitLabel: z.string().optional(),
|
||||
/**
|
||||
* **必填还空着就按不下提交**(2026-08-02 用户定)—— 由基座按台账声明**现算**。
|
||||
*
|
||||
* 起因是实测的一单:受理需求那张卡上有一条料缺年用量(必填),人照样交了回来;
|
||||
* 这一单于是一路走到放行门,被 L1 机检 `quote_intake_record_missing = 0` 拦住,
|
||||
* 模型连拒五拍熔断 —— **同一件事,在提交那一刻拦下比在第 10 拍拦下便宜得多**:
|
||||
* 那时缺的是哪一格就摆在他眼前;到了放行门,他只看见一条红判据和一个停住的单。
|
||||
*
|
||||
* 「必填」是**台账声明**说了算(`fields[].required`),缺几格由基座数(草稿层现读)——
|
||||
* 不是活动自报的数,也不由前端猜。算不出来(这张卡没绑表)就不给这一格,
|
||||
* 按钮照常能按:**没有依据的时候不拦人**。
|
||||
*/
|
||||
gate: z
|
||||
.object({
|
||||
/** 能交了吗 */
|
||||
ready: z.boolean(),
|
||||
/** 交不了的话,一句话说清差在哪 */
|
||||
why: z.string(),
|
||||
/** 差在哪几行哪一格 —— 摆主键 + 中文列名,人照着就能找到那一行 */
|
||||
blanks: z.array(z.string()).default([]),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
export type ExecutionWorkOrder = z.infer<typeof executionWorkOrderSchema>;
|
||||
|
||||
@@ -308,6 +355,20 @@ export const acceptanceCriterionCheckSchema = z.object({
|
||||
label: z.string(),
|
||||
level: acceptanceLevelSchema,
|
||||
pass: z.boolean(),
|
||||
/**
|
||||
* 裁量项(L3/L4)**裁过没有** —— 机检项与 L5 不带这一维(它们不是裁出来的)。
|
||||
*
|
||||
* 「裁了但裁成不满足」与「压根没裁」是两件完全不同的事:前者是 AI 办过、
|
||||
* 办不到,把判断交给人;后者是活没干完。从前卡上只有一个 `pass`,
|
||||
* 两者长得一模一样(都是红叉「未达标」)—— 人看到一条永远不绿的项,
|
||||
* 既不知道 AI 有没有办过,也不知道它为什么办不到,只能凭感觉签。
|
||||
*/
|
||||
settled: z.boolean().optional(),
|
||||
/**
|
||||
* 裁断的理由 —— **裁成「不满足」的那一条,这句话才是签字的人要看的东西**。
|
||||
* 「AI 说这一条它办不到,理由是……」比一片虚假的绿勾有用得多。
|
||||
*/
|
||||
rationale: z.string().optional(),
|
||||
});
|
||||
export type AcceptanceCriterionCheck = z.infer<typeof acceptanceCriterionCheckSchema>;
|
||||
|
||||
@@ -334,10 +395,21 @@ export const acceptanceWorkOrderSchema = z.object({
|
||||
/** 报告可修订:字段随成果 schema 渲染,AI 拟值已填 */
|
||||
fields: z.array(formFieldSchema).default([]),
|
||||
/**
|
||||
* 选择组(活动契约的 selective 出口):**恰发一口,随放行一并确认**(AM 1.2)。
|
||||
* 人选的那一口就是这次运行的分支结论(如议价的 成交/有异议/拒绝),
|
||||
* 下游异或网关判的正是它 —— 所以放行时必须恰选一个,选口即裁分支。
|
||||
* 没有选择组的活动此字段缺省,验收单照旧只有通过/重做。
|
||||
* 验收页的正文块 —— 与执行工单同一套件(可含**绑台账的可编辑表**)。
|
||||
*
|
||||
* 验收是这一单最重的一道门:人得能在**成果本身**上改,而不是只在几个整单级字段上改。
|
||||
* 绑了台账的表由基座铺行、由基座写回(`blocks[].ledger` + `fields`),AI 不经手 ——
|
||||
* 与数据录入卡同一条规矩:该由人亲手落下的,别在中间加一道传话。
|
||||
*/
|
||||
blocks: z.array(viewBlockSchema).optional(),
|
||||
/**
|
||||
* 选择组(活动契约的 selective 出口)—— **只在算不出去向时才摆给人选**。
|
||||
*
|
||||
* 去向能算就别问:契约里每个出口都写了 `when` 谓词时,基座按序求值(首个命中即出),
|
||||
* 结论随放行一并落定,卡上只如实**告诉**人走哪一口、凭什么(见 `routedPort`)。
|
||||
* 「这一单往哪走」是分类表算出来的事实,不是一道让人凭印象点的选择题 ——
|
||||
* 让人选,等于把一个可回放的判定改成一次不可回放的手感。
|
||||
* 只有当出口没写全谓词(算不出来)时,才退回这张选择题。
|
||||
*/
|
||||
portChoice: z
|
||||
.object({
|
||||
@@ -345,6 +417,42 @@ export const acceptanceWorkOrderSchema = z.object({
|
||||
ports: z.array(z.object({ name: z.string(), label: z.string().optional() })).min(2),
|
||||
})
|
||||
.optional(),
|
||||
/**
|
||||
* **这一签下去,世界上发生了什么**(2026-08-02 用户:「完成验收后反而要把落地结果展示出来」)。
|
||||
*
|
||||
* 只在办结之后才有。放行卡上摆的是**签字前要核的东西**(判据);
|
||||
* 签完之后人关心的换成了另一件事:落了几行、进了哪几张表、转去哪儿了。
|
||||
* 从前这两件事共用一屏判据表 —— 签完再回看,那一摞对勾说的还是签之前的话。
|
||||
*
|
||||
* 数据来自放行那一刻的结构化留痕(逐表计数 + 发出的口),不是把日志句子再解析一遍。
|
||||
*/
|
||||
landing: z
|
||||
.object({
|
||||
/** 合并进正式版的总行数 */
|
||||
rows: z.number().int().min(0),
|
||||
/** 试跑挡下的主数据行数 —— 非 0 就得说,不然「合并 N 行」是假的 */
|
||||
simulated: z.number().int().min(0).default(0),
|
||||
/** 逐表:进了哪张表、几行 */
|
||||
tables: z.array(z.object({ label: z.string(), rows: z.number().int().min(0) })).default([]),
|
||||
/** 转去哪儿了 —— 摆出口的人读名;一个都没有就是这条流到此为止 */
|
||||
ports: z.array(z.string()).default([]),
|
||||
/** 带了哪些字段过去 —— 下游活动读到的就是这几个(摆名字不摆值) */
|
||||
carried: z.array(z.string()).default([]),
|
||||
})
|
||||
.optional(),
|
||||
/**
|
||||
* **算出来的去向** —— 契约的出口谓词按序求值,首个命中即出。
|
||||
* 卡上只告诉人「走哪一口、凭哪一条」,不给他改;放行时按它发口。
|
||||
*/
|
||||
routedPort: z
|
||||
.object({
|
||||
group: z.string(),
|
||||
name: z.string(),
|
||||
label: z.string().optional(),
|
||||
/** 命中的那条谓词原文 —— 人要回核「凭什么走这条」 */
|
||||
because: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
passLabel: z.string().optional(),
|
||||
});
|
||||
export type AcceptanceWorkOrder = z.infer<typeof acceptanceWorkOrderSchema>;
|
||||
@@ -406,7 +514,17 @@ export const elicitationCardSchema = z.object({
|
||||
ask: z.string(),
|
||||
basis: basisSchema.optional(),
|
||||
fields: z.array(
|
||||
z.object({ key: z.string(), label: z.string(), placeholder: z.string().optional() }),
|
||||
z.object({
|
||||
key: z.string(),
|
||||
label: z.string(),
|
||||
placeholder: z.string().optional(),
|
||||
/**
|
||||
* 预填值 —— AI 从来件里读得出的先替人填上(公司名、联系人姓名)。
|
||||
* 人核对一眼、补上填不出的那个即可。**读不出来的别给**:
|
||||
* 填错了人得先擦掉再写,比空着更费事。
|
||||
*/
|
||||
value: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
done: z
|
||||
.object({
|
||||
@@ -420,6 +538,99 @@ export const elicitationCardSchema = z.object({
|
||||
});
|
||||
export type ElicitationCard = z.infer<typeof elicitationCardSchema>;
|
||||
|
||||
|
||||
/**
|
||||
* **数据录入卡** —— 人类工具的第五类(2026-07-31 用户定)。
|
||||
*
|
||||
* 与补全卡的差别是**谁写库**:
|
||||
* · 补全卡:人填的值进轨迹 → **AI 再转写进台账**。中间隔一道传话,
|
||||
* 它可以抄错、可以顺手改一笔,而人不知道自己填的东西最后落成了什么。
|
||||
* · 数据录入卡:**人按下确认那一刻就是写入** —— 卡上写着落哪张表、哪个主键,
|
||||
* 确认即落草稿层。AI 只负责把能填的先填好,填错了人当场改。
|
||||
*
|
||||
* 所以它适合「建档」这类事:客户档案是主数据,一旦建错后面每一单都跟着错;
|
||||
* 那一笔该由人亲手落下,不该经 AI 的手转述一遍。
|
||||
*
|
||||
* 仍守铁律②:确认写的是**草稿层**,放行那一刻才并进正式版 —— 人的确认不越过放行门。
|
||||
*/
|
||||
/** 录入卡的一格 —— 主段与 `also` 各段同一形状 */
|
||||
const entryFieldSchema = z.object({
|
||||
key: z.string(),
|
||||
label: z.string(),
|
||||
/** AI 预填的值 —— 人可改;填不出的留空,界面标「待填写」 */
|
||||
value: z.string().optional(),
|
||||
/** 必填的空着不让确认 —— 缺了这一格这张档就是残的 */
|
||||
required: z.boolean().default(false),
|
||||
placeholder: z.string().optional(),
|
||||
});
|
||||
|
||||
export const dataEntryCardSchema = z.object({
|
||||
...chatBase,
|
||||
k: z.literal('entry'),
|
||||
ask: z.string(),
|
||||
basis: basisSchema.optional(),
|
||||
/** 落哪张表(`域.对象`)—— 确认那一下写的就是它 */
|
||||
ledger: z.string().min(1),
|
||||
/**
|
||||
* 那张表**给人看的名字**(台账登记的表注释,如「客户档案」)。
|
||||
*
|
||||
* 卡面上摆的是它,不是 `customer.profile` —— 界面不摆系统内的英文名
|
||||
* (用户 2026-08-02:「这种卡片里说:确认后写入 xxx数据库」)。
|
||||
* 登记里没有人读名时不给这一项,卡面退回对象地址:宁可笼统,不可编。
|
||||
*/
|
||||
ledgerLabel: z.string().optional(),
|
||||
/** 主键值:确认时按它定位那一行 */
|
||||
key: z.record(z.string(), z.string()),
|
||||
/** 主键那几列的中文列名 —— 卡面摆它,不摆 `customer_code`(界面不摆系统内的英文名) */
|
||||
keyLabels: z.record(z.string(), z.string()).optional(),
|
||||
fields: z.array(entryFieldSchema),
|
||||
/**
|
||||
* **这一段有几列是基座自己生成的** —— 摆一句话告诉人「这个不用你填」。
|
||||
*
|
||||
* 声明里标了 `auto` 的列(如联系人编码)压根不进 `fields`:
|
||||
* 让人填是白占一格他答不上来的表单。但**不能一声不吭地拿掉** ——
|
||||
* 那样他会以为这张档少了一格。
|
||||
*/
|
||||
autoNote: z.string().optional(),
|
||||
/**
|
||||
* **同一次确认还要落的另几张表**(2026-08-02 用户:「新建客户 联系人和公司都需要确认」)。
|
||||
*
|
||||
* 建一位新客户落的是**两条主数据**:公司档(`customer.profile`)与
|
||||
* 联系人档(`customer.contact`)。从前卡上只有公司那一半,联系人由 AI 自己写 ——
|
||||
* 而联系人档里的 `contact_code` 是它**编**出来的,人一眼没看过。
|
||||
* 主数据编错一次,后面每一单都跟着错,这正是数据录入卡存在的理由。
|
||||
*
|
||||
* 分成两张卡也能办,但那是把**一件事**(建档)拆成两次打断;
|
||||
* 一张卡两段,人一次看全、一次确认,基座一次落两行。
|
||||
*
|
||||
* 值按段隔开:`values` 的键是 `段号.列名`(主段是 `0.`)—— 两张表都有 `name` 这一列,
|
||||
* 平铺就会互相盖掉,而盖掉之后没人看得出来。
|
||||
*/
|
||||
also: z
|
||||
.array(
|
||||
z.object({
|
||||
/** 这一段是什么 —— 卡上那行小标题(如「联系人档案」) */
|
||||
title: z.string(),
|
||||
ledger: z.string().min(1),
|
||||
ledgerLabel: z.string().optional(),
|
||||
key: z.record(z.string(), z.string()),
|
||||
keyLabels: z.record(z.string(), z.string()).optional(),
|
||||
fields: z.array(entryFieldSchema),
|
||||
autoNote: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
done: z
|
||||
.object({
|
||||
at: z.iso.datetime(),
|
||||
by: staffIdSchema,
|
||||
/** 键是 `段号.列名`(主段 `0.`)—— 见 `also` 的注释 */
|
||||
values: z.record(z.string(), z.string()),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
export type DataEntryCard = z.infer<typeof dataEntryCardSchema>;
|
||||
|
||||
/** 评价卡:任务评价(评分 1–5 + 改进建议) */
|
||||
export const ratingCardSchema = z.object({
|
||||
...chatBase,
|
||||
@@ -439,6 +650,7 @@ export type RatingCard = z.infer<typeof ratingCardSchema>;
|
||||
|
||||
export const chatCardSchema = z.discriminatedUnion('k', [
|
||||
decisionCardSchema,
|
||||
dataEntryCardSchema,
|
||||
elicitationCardSchema,
|
||||
ratingCardSchema,
|
||||
]);
|
||||
@@ -461,8 +673,14 @@ export const chatMessageSchema = z.discriminatedUnion('k', [
|
||||
k: z.literal('system'),
|
||||
type: z.enum(['data', 'time', 'human', 'control']),
|
||||
text: z.string(),
|
||||
/**
|
||||
* 这条系统消息带一个「查看邮件」的去处 —— 原件在沟通记录里,一步跳过去。
|
||||
* **点不点都不挡路**:开场白之后 AI 照常开跑,不等人按。
|
||||
*/
|
||||
openComms: z.boolean().optional(),
|
||||
}),
|
||||
decisionCardSchema,
|
||||
dataEntryCardSchema,
|
||||
elicitationCardSchema,
|
||||
ratingCardSchema,
|
||||
]);
|
||||
@@ -504,6 +722,16 @@ export const chatCardReplySchema = z.discriminatedUnion('k', [
|
||||
/** 「没法填」旁路 */
|
||||
reason: z.string().optional(),
|
||||
}),
|
||||
/**
|
||||
* 数据录入卡的确认 —— **这一下就是写入**。
|
||||
* 基座按卡上声明的 `ledger` + `key` 把 `values` 落进草稿层,并留一条痕:
|
||||
* 谁、何时、往哪张表写了什么。AI 不经手。
|
||||
*/
|
||||
z.object({
|
||||
k: z.literal('entry'),
|
||||
cardId: cardIdSchema,
|
||||
values: z.record(z.string(), z.string()),
|
||||
}),
|
||||
z.object({
|
||||
k: z.literal('rating'),
|
||||
cardId: cardIdSchema,
|
||||
|
||||
Reference in New Issue
Block a user