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

手改是**兜 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
+5
View File
@@ -23,6 +23,11 @@
port 注入:memory / event / message / interaction / ledger
被依赖: process, org(停用前置检查的在途任务), report, app
近变(2026-07-30):`Task` / `StartTaskInput` 加 `origin?`(`testbed` = 设计台试跑)+
`sys.task.origin` 列。**认得出试跑单才拔得掉** —— 工作流那侧靠
`source->>'ref'='testbed'` 认,任务这侧从前没有对应记号:
「试跑一个活动」起出来的单(不挂工作流)与登记直起单长得一模一样。
不变量(OS 04 / 4.3,AM 10 / 12.1):
- **受理归一**(2026-07-30 宽进,替代旧 AM 12.1「缺项不得启动」):输入可以不定或为空 ——
缺必填不拒单,任务落成 `awaiting-human`,缺的字段做成补全卡(`IntakeDesk` 端口)发给
@@ -76,6 +76,7 @@ function harness(): Harness {
asked.push({ fields: input.fields.map((f) => ({ key: f.key, label: f.label })) });
},
},
comms: { record: async () => undefined },
clock: { now: () => new Date('2026-07-30T00:00:00.000Z') },
ids: { next: () => `t-${(seq += 1)}` },
};
@@ -92,6 +92,7 @@ function harness(land: () => Promise<never> | Promise<{ converged: boolean; emit
notifier: { notify: async () => undefined },
interaction: { setPhase: async () => undefined },
intake: { askCompletion: async () => undefined },
comms: { record: async () => undefined },
clock: { now: () => new Date('2026-07-29T00:00:02.000Z') },
ids: { next: () => 'x' },
};
+3
View File
@@ -52,6 +52,8 @@ export type Task = {
/** 起单来源:工作流节点;登记触发的直起单为 undefined */
readonly workflowId?: string;
readonly nodeId?: string;
/** `testbed` = 设计台试跑 —— 认得出才拔得掉(仿真不该在别人工作台上过夜) */
readonly origin?: string;
readonly ownerId: StaffId;
readonly title: string;
readonly runtime: TaskRuntimeState;
@@ -73,6 +75,7 @@ export type StartTaskInput = {
readonly title: string;
readonly workflowId?: string;
readonly nodeId?: string;
readonly origin?: string;
/** 输入:data(按 schema 校验,缺项不得启动)+ message(委托消息,可为空) */
readonly data: Record<string, unknown>;
readonly message?: string;
@@ -11,6 +11,11 @@ export const taskTable = sys.table(
activityId: text('activity_id').notNull(),
workflowId: text('workflow_id'),
nodeId: text('node_id'),
/**
* 起单来源:`testbed` = 设计台试跑,其余留空。
* 认得出试跑单,才拔得掉 —— 仿真不是生产,它不该在别人工作台上过夜。
*/
origin: text('origin'),
/** 恰有一名负责人 */
ownerId: text('owner_id').notNull(),
title: text('title').notNull(),
@@ -34,6 +39,7 @@ export const taskTable = sys.table(
(t) => [
index('task_owner_idx').on(t.ownerId, t.runtime),
index('task_workflow_idx').on(t.workflowId),
index('task_origin_idx').on(t.origin),
index('task_terminal_idx').on(t.terminal),
],
);
@@ -16,6 +16,7 @@ function toTask(row: Row): Task {
id: row.id as TaskId,
activityId: row.activityId,
...(row.workflowId === null ? {} : { workflowId: row.workflowId }),
...(row.origin === null ? {} : { origin: row.origin }),
...(row.nodeId === null ? {} : { nodeId: row.nodeId }),
ownerId: row.ownerId as StaffId,
title: row.title,
@@ -43,6 +44,7 @@ export function createTaskStore(db: Db): TaskStore {
id: task.id,
activityId: task.activityId,
workflowId: task.workflowId ?? null,
origin: task.origin ?? null,
nodeId: task.nodeId ?? null,
ownerId: task.ownerId,
title: task.title,
@@ -9,6 +9,7 @@ import type { StartTaskInput, Task, TaskService, TickOutcome } from '../../contr
import { TaskAlreadyTerminalError, TaskNotFoundError } from '../../contract.ts';
import type {
ActivityLoader,
CommRecorder,
Clock,
IdGenerator,
IntakeDesk,
@@ -45,6 +46,8 @@ export type TaskServiceDeps = {
* 并武装 human 监听等答复 —— 任务先「等人」,补齐了才开跑。
*/
readonly intake: IntakeDesk;
/** 沟通记录 —— 这一单是被一封信叫起来的话,起单那一刻把来件归档 */
readonly comms: CommRecorder;
readonly clock: Clock;
readonly ids: IdGenerator;
};
@@ -101,6 +104,7 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
activityId: input.activityId,
...(input.workflowId === undefined ? {} : { workflowId: input.workflowId }),
...(input.nodeId === undefined ? {} : { nodeId: input.nodeId }),
...(input.origin === undefined ? {} : { origin: input.origin }),
ownerId: input.ownerId,
title: input.title,
runtime: missing.length > 0 ? 'awaiting-human' : 'running',
@@ -118,6 +122,44 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
// message 计入初始观察、全程留痕;**不得扩大授权边界**
detail: { data: input.data, message: input.message, envelope: loaded.envelope },
});
/*
* **来件先归档,再开跑** —— 顺序有意义:人一点开这一单,沟通记录里就已经躺着
* 那封信;等模型跑几拍之后再补,那几十秒里人看到的是「这单从哪来的?不知道」。
* 声明了才归(`contract.input.inboundComm`);没声明的活动这一步整个不发生。
*/
const inbound = loaded.inboundComm;
if (inbound !== undefined) {
const pick = (field: string | undefined): unknown => (field === undefined ? undefined : input.data[field]);
const attachments = pick(inbound.attachmentsField);
await deps.comms.record({
taskId: task.id,
direction: 'in',
channel: inbound.channel,
party: String(pick(inbound.partyField) ?? ''),
subject: String(pick(inbound.subjectField) ?? task.title),
body: String(pick(inbound.bodyField) ?? ''),
attachments: Array.isArray(attachments) ? attachments : [],
note: '开案来件 —— 这一单就是被它叫起来的',
});
/*
* **对话流的第一句话由系统说**(2026-07-31 用户定)。
*
* 从前人点开这一单,看到的第一句是 AI 的解读 —— 而他还不知道
* 「这单从哪来的、为什么派给我」。那两件事不该由 AI 顺口交代:
* 它们是**系统的事实**(一封信触发了这条流,系统把这一环指给了你),
* AI 说出来反而像它在自我介绍。
*
* 带一个「查看邮件」的去处:原件就在沟通记录里,一步跳过去。
* **点不点都不挡路** —— 这条消息之后 AI 照常开始解读,不等人按。
*/
await deps.memory.append({
taskId: task.id,
// **触发事件**:这一单是被这封信叫起来的 —— 不是机器产出,也不是谁的动作
kind: 'trigger',
title: `一封邮件触发了这条流程,系统指定你负责「${task.title}」。`,
detail: { eventType: 'data', openComms: true },
});
}
await deps.engine.load(task.id, loaded.businessModel);
await deps.interaction.setPhase(task.id, 'active');
if (missing.length > 0) {
@@ -163,6 +205,23 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
return { task: finished, verdict, progressed: false };
}
/*
* **先把「在办事」写进投影,再开跑**(2026-07-31 修)。
*
* 一拍要跑好几秒 —— 模型在算。而这一列从前是**整拍跑完才写**(见下面第 224 行),
* 于是被人唤醒的那一单,这几秒里投影仍停在 `awaiting-event`:
* 人点完数据录入卡,对话区四秒一动不动(实测 4.3 秒),他以为没点上、再点一次。
* 引擎明明在算,投影却说在等外部 —— 界面不撒谎,那这一列就得在开跑前写。
*
* `setPhase` 顺带广播(interaction 的 onPush → broadcaster),浏览器立刻重取。
* 一拍跑完照旧按 outcome 写回真实状态;若这一拍抛了,单停在 running,
* 推进器下一轮会再推它一次 —— 那本来就是该做的事(重试),不是卡死。
*/
if (task.runtime !== 'running') {
await deps.store.update(taskId, { runtime: 'running' });
await deps.interaction.setPhase(taskId, 'active');
}
const startedAt = deps.clock.now().getTime();
const outcome = await deps.engine.advance(taskId);
// 步计量:loop 的一次决策循环 / flow 的一个声明步。
@@ -246,11 +305,20 @@ export function createTaskService(deps: TaskServiceDeps): TaskService {
const task = await require(taskId);
if (task.terminal !== undefined) throw new TaskAlreadyTerminalError(taskId, task.terminal);
/*
* 投递痕只记**这一条**(标题格式与对话流投影的过滤约定一致:human 的不进对话区)。
* 从前 loop 的 onEvent 还会再记一条「事件到达(kind):ref」—— 同一次回传在日志里
* 三行几乎一样(卡回传 + 两层各一条到达),人看着像系统在结巴。
*/
await deps.memory.append({
taskId,
kind: 'observation',
title: `事件到达:${event.kind}/${event.ref}`,
detail: event.payload,
title: `事件到达(${event.kind}):${event.ref}`,
/*
* `kind` 要进 detail:对话区与工作日志都靠它认这是哪一类到达。
* 从前只有 payload,两处只能去抠标题前缀 —— 标题是给人看的、随时会改。
*/
detail: { ...event.payload, kind: event.kind },
});
/*
+30 -2
View File
@@ -25,11 +25,36 @@ export type ActivityLoader = {
envelope: Record<string, unknown>;
/** 输入 data 的 JSON Schema —— 缺项不得启动 */
inputSchema: Record<string, unknown>;
/**
* 这一单是被一封信叫起来的 —— 活动清单声明的**来件字段映射**。
* 声明了,起单那一刻就把来件归进沟通记录(工作台那一屏)。
*/
inboundComm?: {
channel: string;
partyField: string;
subjectField?: string | undefined;
bodyField?: string | undefined;
attachmentsField?: string | undefined;
};
businessModel: unknown;
stallAfter: string;
}>;
};
/** 沟通记录 —— 一单往来的原件存档(工作台「沟通记录」那一屏读它) */
export type CommRecorder = {
record(input: {
taskId: TaskId;
direction: 'in' | 'out';
channel: string;
party: string;
subject: string;
body: string;
attachments: readonly unknown[];
note: string;
}): Promise<void>;
};
export type SchemaValidator = {
validate(schema: Record<string, unknown>, value: unknown): { ok: boolean; errors: string[] };
};
@@ -76,8 +101,11 @@ export type LandingGateway = {
export type MemoryRecorder = {
append(input: {
taskId: TaskId;
/** control = 治理干预(叫停 / 恢复 / 改派):人对过程的裁断,归人裁决侧 */
kind: 'engine-transition' | 'terminal' | 'observation' | 'control';
/**
* control = 治理干预(叫停 / 恢复 / 改派):人对过程的裁断,归人裁决侧。
* trigger = 触发事件:这一单被什么叫起来的 —— 不是机器产出,也不是谁的动作。
*/
kind: 'engine-transition' | 'terminal' | 'observation' | 'control' | 'trigger';
title: string;
detail?: Record<string, unknown>;
}): Promise<void>;