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:
@@ -7,6 +7,7 @@ import type {
|
||||
ChatCardReply,
|
||||
ExecutionWorkOrder,
|
||||
StaffId,
|
||||
TaskId,
|
||||
WorkOrder,
|
||||
WorkOrderReply,
|
||||
} from '@contracts';
|
||||
@@ -38,9 +39,35 @@ export type HumanToolServiceDeps = {
|
||||
readonly ids: IdGenerator;
|
||||
};
|
||||
|
||||
/**
|
||||
* 录入卡上那一格叫什么 —— 键是 `段号.列名`(一张卡可以落好几张表)。
|
||||
* 按段号找回它属于哪一段;认不出的原样留着(宁可笼统,不可编)。
|
||||
*/
|
||||
function entryLabel(card: Extract<ChatCard, { k: 'entry' }>, key: string): string {
|
||||
const at = /^(\d+)\.(.+)$/.exec(key);
|
||||
const groups = [{ title: '', fields: card.fields }, ...(card.also ?? [])];
|
||||
if (at === null) return card.fields.find((f) => f.key === key)?.label ?? key;
|
||||
const group = groups[Number(at[1])];
|
||||
const name = at[2] ?? key;
|
||||
const label = group?.fields.find((f) => f.key === name)?.label ?? name;
|
||||
return group === undefined || group.title === '' ? label : `${group.title}·${label}`;
|
||||
}
|
||||
|
||||
export function createHumanToolService(deps: HumanToolServiceDeps): HumanToolService {
|
||||
/** 调人预算(打扰上限)由基座在**环外**强制 —— 活动不得自查自守(AM 10.1) */
|
||||
async function assertBudget(context: IssueContext): Promise<void> {
|
||||
/**
|
||||
* 调人预算(打扰上限)由基座在**环外**强制 —— 活动不得自查自守(AM 10.1)。
|
||||
*
|
||||
* **验收放行那一张不计预算**(2026-07-31 加)。
|
||||
*
|
||||
* 预算管的是「打扰」——Agent 自己选择去问人的那些次数:补全卡、决策卡、执行工单。
|
||||
* 而放行不是它选的:铁律③终局唯放行,这一单**必须**由人签字才能收口。
|
||||
* 把它和提问算在同一个额度里,后果是活动可以合规地把自己饿死 ——
|
||||
* 实测 A4:预算 3 次,两张决策卡加一张补全卡用完,活全干完了,
|
||||
* 提交放行被「调人预算触顶 3/3」拒回,再提再拒,连拒五拍熔断。
|
||||
* 该拦的是「又去问人」,不是「办完了要交差」。
|
||||
*/
|
||||
async function assertBudget(context: IssueContext, mode?: WorkOrder['mode']): Promise<void> {
|
||||
if (mode === 'acceptance') return;
|
||||
const used = await deps.cards.humanCallCount(context.taskId);
|
||||
if (used >= context.humanCallCap) {
|
||||
throw new HumanCallBudgetExceededError(context.taskId, used, context.humanCallCap);
|
||||
@@ -61,7 +88,12 @@ export function createHumanToolService(deps: HumanToolServiceDeps): HumanToolSer
|
||||
taskId: context.taskId,
|
||||
kind: 'card-sent',
|
||||
title,
|
||||
detail: { kind, id, arrival, recipient },
|
||||
/*
|
||||
* `payload` 里那一个键(对话卡的 `k` / 工单的 `mode`)要一并入痕 ——
|
||||
* 少了它,日志只知道「发了一张卡」,说不出发的是补全卡还是录入卡,
|
||||
* 四字动作那一格就只能写「发出卡片」这种等于没说的词。
|
||||
*/
|
||||
detail: { kind, id, arrival, recipient, ...payload },
|
||||
});
|
||||
if (arrival === 'task-chat') return { arrival };
|
||||
// 跨人卡以系统消息送达对方,就地作答 —— 任务要素永远只装「我负责的任务」
|
||||
@@ -79,7 +111,7 @@ export function createHumanToolService(deps: HumanToolServiceDeps): HumanToolSer
|
||||
async function issueOrder(context: IssueContext, order: WorkOrder): Promise<IssuedWorkOrder> {
|
||||
const violations = checkWorkOrder(order);
|
||||
if (violations.length > 0) throw new BareCallError(violations);
|
||||
await assertBudget(context);
|
||||
await assertBudget(context, order.mode);
|
||||
|
||||
const recipient = context.recipientId ?? context.ownerId;
|
||||
await deps.cards.saveWorkOrder(context.taskId, order, recipient);
|
||||
@@ -171,8 +203,8 @@ export function createHumanToolService(deps: HumanToolServiceDeps): HumanToolSer
|
||||
await deps.memory.append({
|
||||
taskId: found.taskId,
|
||||
kind: reply.mode === 'acceptance' ? 'release-verdict' : 'card-reply',
|
||||
title: reply.mode === 'acceptance' ? `放行裁决:${reply.verdict}` : `执行工单回传:${found.order.title}`,
|
||||
detail: { by, values, ...(reply.mode === 'acceptance' ? { comment: reply.comment } : {}) },
|
||||
title: reply.mode === 'acceptance' ? `裁决:${reply.verdict}` : found.order.title,
|
||||
detail: { by, values, mode: reply.mode, ...(reply.mode === 'acceptance' ? { comment: reply.comment } : {}) },
|
||||
});
|
||||
// 回传经统一入口以人事件抛回任务
|
||||
await deps.events.publishHumanEvent({
|
||||
@@ -186,15 +218,28 @@ export function createHumanToolService(deps: HumanToolServiceDeps): HumanToolSer
|
||||
|
||||
return {
|
||||
order,
|
||||
taskId: found.taskId,
|
||||
producesCompletion: producesCompletion(reply),
|
||||
...(reply.mode === 'acceptance' && reply.verdict !== 'pass'
|
||||
? { rejection: { kind: reply.verdict, comment: reply.comment ?? '' } }
|
||||
: {}),
|
||||
selectedPorts: reply.mode === 'acceptance' ? reply.selectedPorts : {},
|
||||
/*
|
||||
* 出口:**算出来的优先**。
|
||||
*
|
||||
* 契约给每个出口写了 `when`,放行那一刻这一口就已经由基座按序求值定下了
|
||||
* (`routedPort`),人回传里带不带都不作数 —— 去向是台账那几个数的函数,
|
||||
* 不是一道让人点的选择题。算不出来的组才退回 `selectedPorts`。
|
||||
*/
|
||||
selectedPorts:
|
||||
reply.mode !== 'acceptance'
|
||||
? {}
|
||||
: (order as AcceptanceWorkOrder).routedPort !== undefined
|
||||
? { [(order as AcceptanceWorkOrder).routedPort!.group]: (order as AcceptanceWorkOrder).routedPort!.name }
|
||||
: reply.selectedPorts,
|
||||
};
|
||||
},
|
||||
|
||||
async replyChatCard(reply: ChatCardReply, by: StaffId): Promise<ChatCard> {
|
||||
async replyChatCard(reply: ChatCardReply, by: StaffId): Promise<{ card: ChatCard; taskId: TaskId }> {
|
||||
const found = await deps.cards.getChatCard(reply.cardId);
|
||||
if (found === undefined) throw new ReplyRejectedError([{ code: 'not-found', message: '卡不存在' }]);
|
||||
|
||||
@@ -205,14 +250,45 @@ export function createHumanToolService(deps: HumanToolServiceDeps): HumanToolSer
|
||||
const updated = applyChatReply(found.card, reply, by, at);
|
||||
await deps.cards.updateChatCard(updated);
|
||||
|
||||
/*
|
||||
* 标题写**内容**,不写「entry 卡回传」这种行话 —— 日志里动作已有自己一格
|
||||
* (四字章:员工确认 / 员工补全 …),标题再复述一遍卡的类型就是白占地方。
|
||||
* 录入卡尤其要说清落到哪张表:人回看时问的是「我那一下写了什么」。
|
||||
*/
|
||||
/*
|
||||
* **日志里不摆台账地址与列名**(2026-08-02 用户指出:
|
||||
* 「柴浩:写入 customer.profile / customer_code=chchch;name=青岛海洋电子有限公司」
|
||||
* —— 这为啥没换成纯描述)。
|
||||
*
|
||||
* `customer.profile` 与 `customer_code` 是系统内的名字;人回看时问的是
|
||||
* **「我那一下办了什么」**,答案该是「建了客户档案 · 青岛海洋电子有限公司」。
|
||||
* 卡上本来就有人话:`ask` 是问他的那句,`fields[].label` 是每一格的中文名 ——
|
||||
* 用它们,不用地址与列名。
|
||||
*/
|
||||
const title = found.card.ask.split('\n')[0] ?? found.card.ask;
|
||||
await deps.memory.append({
|
||||
taskId: found.taskId,
|
||||
kind: 'card-reply',
|
||||
title: `${found.card.k} 卡回传`,
|
||||
detail: { by, ...reply },
|
||||
title,
|
||||
detail: {
|
||||
by,
|
||||
...reply,
|
||||
...(reply.k === 'entry' && found.card.k === 'entry'
|
||||
? {
|
||||
/*
|
||||
* 列名换成卡上那个中文 label;认不出的原样留着(宁可笼统,不可编)。
|
||||
* 值的键是 `段号.列名`(一张卡可以落好几张表)—— 按段号找回它属于哪一段,
|
||||
* 不然「联系人姓名」会显示成公司档那个同名的「公司名」。
|
||||
*/
|
||||
summary: Object.entries(reply.values)
|
||||
.map(([k, v]) => `${found.card.k === 'entry' ? entryLabel(found.card, k) : k}:${v}`)
|
||||
.join(' · '),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
await deps.events.publishHumanEvent({ taskId: found.taskId, cardId: reply.cardId, payload: { ...reply } });
|
||||
return updated;
|
||||
return { card: updated, taskId: found.taskId };
|
||||
},
|
||||
|
||||
/** 驳回重答:新卡实例(附驳回理由),原卡闭合 —— 每次请求与回答都是独立消息,只增不改 */
|
||||
@@ -239,6 +315,26 @@ export function createHumanToolService(deps: HumanToolServiceDeps): HumanToolSer
|
||||
return { card: fresh, arrival: arrivalOf(found.recipientId, found.recipientId) } satisfies IssuedChatCard;
|
||||
},
|
||||
|
||||
async attachRoutedPort(workOrderId, routed) {
|
||||
await deps.cards.patchWorkOrder(workOrderId, {
|
||||
routedPort: {
|
||||
group: routed.group,
|
||||
name: routed.name,
|
||||
...(routed.label === undefined ? {} : { label: routed.label }),
|
||||
because: routed.because,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async attachPortChoice(workOrderId, choice) {
|
||||
await deps.cards.patchWorkOrder(workOrderId, {
|
||||
portChoice: {
|
||||
group: choice.group,
|
||||
ports: choice.ports.map((p) => ({ name: p.name, ...(p.label === undefined ? {} : { label: p.label }) })),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async openWorkOrders(taskId) {
|
||||
return deps.cards.listWorkOrders(taskId, true);
|
||||
},
|
||||
@@ -270,6 +366,9 @@ function applyChatReply(card: ChatCard, reply: ChatCardReply, by: StaffId, at: s
|
||||
done: { at, by, values: reply.values, ...(reply.reason === undefined ? {} : { reason: reply.reason }) },
|
||||
};
|
||||
}
|
||||
if (card.k === 'entry' && reply.k === 'entry') {
|
||||
return { ...card, done: { at, by, values: reply.values } };
|
||||
}
|
||||
if (card.k === 'rating' && reply.k === 'rating') {
|
||||
return { ...card, done: { at, by, score: reply.score, ...(reply.note === undefined ? {} : { note: reply.note }) } };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user