人手改的那一手真正落得下去;界面不撒谎那几处补齐

手改是**兜 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:
2026-08-02 13:41:44 +08:00
co-authored by Claude Opus 5
parent 94f1c7a4ef
commit 259192f736
228 changed files with 27850 additions and 2847 deletions
@@ -15,19 +15,62 @@ export type LogLine = {
readonly at: string;
readonly side: AttributionSide;
readonly sideLabel: string;
/**
* **这一条是谁干的** —— 日志只认三档(2026-08-02 用户定):
* `owner` 负责人(绿)· `agent` 智能体(蓝)· `system` 系统(灰)。
*
* 归侧 `side` 有五种(machine / human / human-verdict / world-wait / trigger),
* 那是**度量**的分法(自主率要把「等外面」与「机器干的」分开);
* 而人扫这一屏时只问一件事:**这一步是我办的、AI 办的,还是流程自己走到的**。
* 拿五种去上色,屏上就是五种深浅,而那五种对读的人不构成区别。
*/
readonly who: 'owner' | 'agent' | 'system';
/**
* **类型章:四字短语**,与对话区同词同粒度(2026-07-31 用户定)。
*
* 从前这一格摆的是归侧(机器产出 / 系统等待 …)—— 那是**度量的口径**,
* 答的是「这一步该算谁的工时」;人翻日志想知道的是「这一步是什么动作」。
* 两者不是一回事:同一个「系统等待」下面既有派工单也有发录入卡。
* 归侧仍在(`side` / `sideLabel`),度量与配色照旧靠它,只是不再占着这一格。
*/
readonly action: string;
/** 主语:任务助手 / 系统;人做的那些留空,由上层照 `actorId` 换成名字 */
readonly actor: string;
/** 人做的那一条是谁做的 —— 域里不认识人名(纯函数零 IO),只交出 id */
readonly actorId: string | undefined;
/** 内容本身,**不含主语** —— 主语由上层拼在前头(名字要查库才知道) */
readonly title: string;
readonly summary: string | undefined;
readonly eventRef: string | undefined;
readonly position: string | undefined;
};
/** 骨架:只有这些种类进结构化日志(OS 8.1 列举) */
/**
* 骨架:哪些种类进结构化日志(OS 8.1 列举)。
*
* **工作日志是给人看的**(2026-07-31 用户定)——「装配技能 xxx@2.0.0」
* 「装载案例模型(6 个计划项)」这类是引擎自己的机械动作,人看了不知道该拿它怎么办;
* 工具调用的过程同理:**人要的是产出结果,不是调用细节**。
* 所以骨架之外还有一道 `machinery()` 过滤(见下),两道一起才干净。
*
* `observation` 是**新进来的**:系统事件(一封信触发了这条流、邮件确认、
* 人把来件标了已读)本来一条都不在日志里 —— 而那恰恰是人最想回看的
* 「这单怎么起来的、谁在什么时候看过」。
*
* `action` **整类退出**(2026-07-31 第二轮,用户定)。它只有三个来源,全是机制:
* 调工具、写草稿、技能结晶。「写入草稿:quote_intake.record 3 行(3 行有变化)」
* 对人说不出任何事 —— 写了什么在工作成果与台账里看得见,「几行有变化」是引擎的账。
* 从前只滤掉带 `tool` 的那一半,留着写草稿的另一半,理由是「它本身就是产出」;
* 可产出是**台账里那几行**,不是「我写了一笔」这个动作。
* 「为什么不行」不在这一类里:行动被拒、提交被拒、换画面失败都记在 `result`,照进。
*/
const SKELETON: ReadonlySet<EntryKind> = new Set<EntryKind>([
'trigger',
'observation',
'engine-transition',
'action',
'result',
'card-sent',
'card-reply',
'narration',
'ledger-write',
'action-intent',
'release-verdict',
@@ -36,32 +79,268 @@ const SKELETON: ReadonlySet<EntryKind> = new Set<EntryKind>([
'control',
]);
/**
* 内容取原文 —— **不加前缀**。
*
* 从前每种各挂一个前缀(`发出:` `回传:` `写入:` `终态:` …),那是在用前缀
* 兼职「这是什么动作」。现在动作有了自己的一格(`action`),前缀就成了重复:
* 「[数据录入] 发出:这是位新客户…」——「发出」两个字白占地方。
* 主语也不在这儿拼:名字要查库,域里查不了(纯函数零 IO)。
*/
const TEMPLATE: Readonly<Record<EntryKind, (e: MemoryEntry) => string>> = {
observation: (e) => `观察:${e.title}`,
thought: (e) => `思考:${e.title}`,
action: (e) => `调用:${e.title}`,
result: (e) => `返回:${e.title}`,
'engine-transition': (e) => `迁移:${e.title}`,
evaluation: (e) => `求值:${e.title}`,
'card-sent': (e) => `发出:${e.title}`,
'card-reply': (e) => `回传:${e.title}`,
'ledger-write': (e) => `写入:${e.title}`,
'action-intent': (e) => `动作意向:${e.title}`,
'release-verdict': (e) => `放行裁决:${e.title}`,
terminal: (e) => `终态:${e.title}`,
'human-input': (e) => `留言:${e.title}`,
control: (e) => `治理:${e.title}`,
observation: (e) => e.title,
thought: (e) => e.title,
action: (e) => actionEntryOf(e).body,
result: (e) => e.title,
'engine-transition': (e) => plain(transitionOf(e.title).body),
evaluation: (e) => e.title,
'card-sent': (e) => e.title,
'card-reply': (e) => e.title,
'ledger-write': (e) => plain(e.title),
'action-intent': (e) => e.title,
'release-verdict': (e) => plain(e.title),
terminal: (e) => plain(e.title),
'human-input': (e) => e.title,
narration: (e) => e.title,
control: (e) => e.title,
trigger: (e) => e.title,
};
/**
* 四字动作 —— **对话区说什么词,这里就是什么词**。
*
* 对齐的出处是 `chat-bubbles.tsx` 的三张表:AI 的类型章(`TAG_UI`)、
* 系统气泡的章(`SYS_TAG`)、各卡卡头的类型名。同一件事在两屏用两个说法,
* 人得在脑子里做一次翻译 —— 而这本来是同一件事。
*/
const CARD_ACTION: Readonly<Record<string, string>> = {
elicitation: '请你补全',
decision: '需要你定',
entry: '数据录入',
rating: '任务评价',
execution: '派出工单',
acceptance: '请你验收',
};
/** 人回传:一律「员工 X X」句式 —— 点确认那一下就是**员工确认**(用户定) */
const REPLY_ACTION: Readonly<Record<string, string>> = {
entry: '员工确认',
elicitation: '员工补全',
decision: '员工决策',
rating: '员工评价',
execution: '员工办结',
acceptance: '放行裁决',
};
/** 叙述的三种签,与对话区 `TAG_UI` / `SAY_UI` 同词 */
const NARRATION_ACTION: Readonly<Record<string, string>> = {
conclusion: '阶段结论',
guidance: '交代进展',
criterion: '请你验收',
};
/**
* **日志里不许出现系统内的英文名**(2026-08-02 用户定)。
*
* 工作日志是给人看的。而引擎留痕时顺手把内部标识写了进去:
* `manualStart:S3_confirm`、`终态:released`、`发射输出口:intake、to-alternative`——
* 那是给排障的人看的机器话,摆在人的日志上等于没写。
* 这里逐条换成人话;认不出的原样留着(宁可笼统,不可编)。
*/
const PLAIN: ReadonlyArray<{ readonly re: RegExp; readonly to: string }> = [
/*
* **计划项号对人是天书**:`manualStart:S3_confirm`、`开始办:T_release` ——
* 那一格里唯一的信息是「引擎开始办某一步了」,而哪一步下一行的「完成一项」会说清。
* 与其摆一个 id,不如只说这件事本身。
*/
// 老留痕里还有 `manualStart:S3_confirm` 这种(新的已经写名字了)—— 认不出名字就只说这件事
{ re: /^manualStart[::]\s*[A-Za-z0-9_]+$/, to: '开始办这一步' },
{ re: /^manualStart[::]\s*/, to: '开始办:' },
// 去向那一条:口名与判据表达式都是给排障看的;人要知道的是「转到哪儿」
{ re: /^去向已算出[::].*→\s*([a-z-]+).*$/, to: '去向已算出,随放行转入下游' },
// 终态有两种写法:带「终态:」前缀的,与裸状态名 —— 两种都得认,漏一种就露一个英文
{ re: /^(终态[::]\s*)?released$/, to: '这一单已放行完成' },
{ re: /^(终态[::]\s*)?blocked$/, to: '这一单卡住了,没能落地' },
{ re: /^(终态[::]\s*)?rejected$/, to: '这一单被退回了' },
{ re: /^(终态[::]\s*)?exhausted$/, to: '这一单预算用尽,停下了' },
{ re: /^裁决[::]\s*pass$/, to: '验收通过' },
{ re: /^裁决[::]\s*redo$/, to: '验收退回重做' },
{ re: /^放行写合并\s*/, to: '成果落账,共 ' },
{ re: /^人工修订[::]\s*/, to: '你改的那一笔已落账:' },
];
/** 把一行里的系统名换成人话 —— 认不出就原样留着 */
function plain(title: string): string {
for (const p of PLAIN) if (p.re.test(title)) return title.replace(p.re, p.to);
return title;
}
/**
* 引擎流转的措辞由引擎自己拼(`起单:xxx` / `完成:xxx` …)。
* 认出前缀是为了两件事:动作那一格说得具体,以及**把前缀从内容里摘掉** ——
* 章上已经写着「任务起单」,内容再来一遍「起单:」就是同一句话说两遍。
* 认不出的一律「状态推进」,内容原样留着:宁可笼统,不可编。
*/
const TRANSITION: ReadonlyArray<{ readonly prefix: string; readonly action: string }> = [
{ prefix: '起单:', action: '任务起单' },
{ prefix: '完成:', action: '完成一项' },
{ prefix: '自动收步:', action: '自动收步' },
{ prefix: '立监听:', action: '立起监听' },
{ prefix: '驳回重入:', action: '驳回重入' },
];
function transitionOf(title: string): { action: string; body: string } {
for (const t of TRANSITION) {
if (title.startsWith(t.prefix)) return { action: t.action, body: title.slice(t.prefix.length) };
}
return { action: '状态推进', body: title };
}
/** action 条目有两种:写草稿与调工具 —— 靠 detail 上的 `ledger` 记号分,不靠措辞 */
function actionEntryOf(e: MemoryEntry): { action: string; body: string } {
if (e.detail?.['ledger'] === undefined) return { action: '调用工具', body: e.title };
const p = '写草稿:';
return { action: '写入草稿', body: e.title.startsWith(p) ? e.title.slice(p.length) : e.title };
}
function actionOf(e: MemoryEntry): string {
const d = e.detail ?? {};
const cardKey = String(d['k'] ?? d['mode'] ?? '');
switch (e.kind) {
case 'trigger':
return '触发事件';
case 'observation':
return '系统事件';
case 'engine-transition':
return transitionOf(e.title).action;
case 'action':
return actionEntryOf(e).action;
case 'narration':
return NARRATION_ACTION[String(d['tag'] ?? '')] ?? '阶段结论';
case 'card-sent':
return CARD_ACTION[cardKey] ?? '发出卡片';
case 'card-reply':
return REPLY_ACTION[cardKey] ?? '员工回传';
// 人在界面上的动作:点确认 = 员工确认,看来件 = 员工阅件
case 'human-input':
return d['act'] === 'read-comm' ? '员工阅件' : '员工确认';
case 'ledger-write':
return '写入台账';
case 'result':
return '工具产出';
case 'action-intent':
return '动作意向';
case 'release-verdict':
return '放行裁决';
case 'terminal':
return '任务收尾';
case 'control':
return '治理干预';
case 'thought':
return '模型思考';
case 'evaluation':
return '判据求值';
}
}
/**
* 主语 —— 日志每一行都要有主语,不然是一串没头脑的流水账。
*
* 三种:**任务助手**(AI 干的)、**系统**(基座干的:起单、事件到达、收尾)、
* 以及**具体某个人**。人名要查库,域里查不了,所以这里只交出 `actorId`,
* 由上层换成名字;换不到就退回「员工」,不编名字。
*/
function actorOf(e: MemoryEntry): { actor: string; actorId: string | undefined; who: 'owner' | 'agent' | 'system' } {
const by = e.detail?.['by'];
const id = typeof by === 'string' && by !== '' ? by : undefined;
switch (e.kind) {
case 'trigger':
case 'observation':
case 'terminal':
return { actor: '系统', actorId: undefined, who: 'system' };
/*
* **AI 办完的事要记在 AI 名下**(2026-08-02 用户:日志「要能看出人和 AI 分别干了啥」)。
*
* 引擎流转从前一律署「系统」,可这一类里混着两种事:
* · 「完成一项」「开始办」——**AI 报的**(它 finish / start,引擎照办);
* · 「起单」「自动收步」「驳回重入」—— 引擎自己做主的。
* 全署系统,人就看不出这一单里 AI 到底干了什么、自己干了什么 ——
* 而那正是他翻这一屏想知道的事。留痕上带 `by: 'agent'` 的从这里认。
*/
case 'engine-transition':
return e.detail?.['by'] === 'agent' || /^(完成|开始办)/.test(e.title)
? { actor: '任务助手', actorId: undefined, who: 'agent' }
: { actor: '系统', actorId: undefined, who: 'system' };
case 'card-reply':
case 'human-input':
case 'release-verdict':
case 'control':
return { actor: '', actorId: id, who: 'owner' };
/*
* 台账写入有两种,主语不同:**放行合并**是基座在人签完字之后做的一次落账
* (记在 AI 名下会让人以为是它自作主张写了正式表);**人工修订**是人自己改的
* (留痕带上了他的 id)。认 id,不认措辞。
*/
case 'ledger-write':
return id === undefined ? { actor: '系统', actorId: undefined, who: 'system' } : { actor: '', actorId: id, who: 'owner' };
default:
return { actor: '任务助手', actorId: undefined, who: 'agent' };
}
}
/** 结构化日志:零 LLM,可重算,每条自带谁 · 何时 · 依据 · 留痕指针 */
/**
* **机械动作** —— 进得了骨架,但人看了没有用处的那些。
*
* 判据是「这一条能不能让人做出判断」:
* · 装配技能 / 装载案例模型 —— 引擎每单必做,一模一样,说不出这一单的任何事;
* · `internal` 标的观察(已在进行中 / 同拍余下作废)—— 引擎给自己看的踪迹;
* · 人事件的到达 —— 一串卡号,而这件事上一行已经用人话说过了。
*
* (动作那一整类 `action` 不在这儿滤 —— 它压根不进骨架,见上面 SKELETON。)
*
* 不用「白名单标题」那一路:标题措辞随时会改,改一次过滤就失灵而没人发现。
* 这里认的是 kind + detail 上的结构化标记,措辞怎么变都不影响。
*/
/**
* **同一件事的复述** —— 人签完字那一刻,基座与引擎各留了一条痕,说的是同一件事。
*
* 实测一单的日志尾巴是这样的(用户 2026-08-02 提的「日志是给人看的」):
* · 成果落账,共 10 行(试跑:另有 1 行只留在活动缓存) ← 放行写合并,**带得住实情**
* · 成果已落账,交给下游 ← 发射输出口,同一件事
* · 成果落账,共 10 行,已交下游 ← 引擎流转,还是同一件事
* · 这一单已放行完成 ← 终态
* 四行里有三行在说「落账了」。留痕那一侧三条都该在(排障要分得清是谁做的),
* 但**日志这一侧只该说一遍**:留合并那一条(它是唯一说得出「几行没进正式表」的),
* 另外两条的信息它全包含。
*/
const RESTATED: ReadonlyArray<RegExp> = [/^落地收敛[::]\s*合并/, /^落地收敛,发射输出口/];
function machinery(e: MemoryEntry): boolean {
if (e.detail?.['internal'] === true) return true;
if (RESTATED.some((re) => re.test(e.title))) return true;
if (e.kind === 'thought') return true;
if (e.kind === 'engine-transition' && e.title.startsWith('装载案例模型')) return true;
/*
* **人事件的到达**:对人它只是「事件到达(human):d455688a-…」一串卡号,
* 而这件事上一行已经用人话说过了(「员工确认:写入 customer.profile」)。
* 同一次点击记两行,后一行还是 UUID —— 对话区早就滤掉了它,日志该同一口径。
* 数据/时间事件的到达留着:那些是外面来的,人确实要知道什么时候到的。
*/
if (e.kind === 'observation' && e.detail?.['kind'] === 'human') return true;
return false;
}
export function structuredLog(entries: readonly MemoryEntry[]): LogLine[] {
return entries
.filter((e) => SKELETON.has(e.kind))
.filter((e) => SKELETON.has(e.kind) && !machinery(e))
.map((e) => ({
id: e.id,
at: e.at,
side: e.side,
sideLabel: ATTRIBUTION_SIDE_LABEL[e.side],
action: actionOf(e),
...actorOf(e),
title: TEMPLATE[e.kind](e),
summary: typeof e.detail?.['summary'] === 'string' ? e.detail['summary'] : undefined,
eventRef: e.eventRef,
@@ -76,6 +355,7 @@ export function splitBySide(lines: readonly LogLine[]): Record<AttributionSide,
human: [],
'human-verdict': [],
'world-wait': [],
trigger: [],
};
for (const line of lines) out[line.side].push(line);
return out;
@@ -84,7 +364,7 @@ export function splitBySide(lines: readonly LogLine[]): Record<AttributionSide,
/* ── 度量八指标(OS 8.3):只读投影,零埋点,统计即查询 ─────────────── */
export type Metrics = {
/** 自主率 = 机器产出 ÷(机器产出 + 人产出);裁决与等待不进分母 */
/** 自主率 = 机器产出 ÷(机器产出 + 员工动作);裁决与等待不进分母 */
readonly autonomy: number;
/** 纠正率 = 被纠正批次 ÷ 提交批次 */
readonly correction: number;