Archived
人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 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:
@@ -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' },
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user