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:
@@ -6,6 +6,7 @@ import { z } from 'zod';
|
||||
import type { RouteContract } from './http.ts';
|
||||
import { disableCheckSchema, grantSchema, positionSchema, staffSchema, workGroupSchema } from '../org.ts';
|
||||
import { positionIdSchema, staffIdSchema, taskIdSchema } from '../id.ts';
|
||||
import { ledgerObjectKindSchema } from '../ledger.ts';
|
||||
import { approvalTemplateSchema, closureReportSchema, processPackageSchema, triggerConfigSchema } from '../process-package.ts';
|
||||
import { controlSemanticSchema, taskRuntimeStateSchema, terminalStateSchema, workflowStateSchema } from '../terminal.ts';
|
||||
import { activityManifestSchema } from '../manifest.ts';
|
||||
@@ -230,7 +231,10 @@ export const govTaskLogRoute = {
|
||||
path: '/api/gov/tasks/:taskId/log',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
output: z.object({
|
||||
log: z.array(z.object({ id: z.string(), at: z.string(), sideLabel: z.string(), title: z.string() })),
|
||||
// 与工作台同一份日志:章是四字动作,内容带主语(见 workbench 的 log 项)
|
||||
log: z.array(
|
||||
z.object({ id: z.string(), at: z.string(), sideLabel: z.string(), action: z.string(), title: z.string() }),
|
||||
),
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
@@ -511,7 +515,23 @@ export const listActivitiesRoute = {
|
||||
*/
|
||||
contract: z.object({
|
||||
requiredInputs: z.array(z.string()),
|
||||
inputFields: z.array(z.object({ name: z.string(), type: z.string(), required: z.boolean() })),
|
||||
inputFields: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
required: z.boolean(),
|
||||
/** 人读名(清单里的 title)—— 表单上摆中文,不摆字段名 */
|
||||
label: z.string().optional(),
|
||||
/** `file` = 这一格收**真文件**(界面给文件选择器);缺席 = 普通文本格 */
|
||||
format: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
/**
|
||||
* 包自带的样例集 —— 试跑一个活动时**照它预填**。
|
||||
* 「首装审以静态审 + 样例集替代回放审」的那份样例,本来就是这个活动
|
||||
* 「一单长什么样」的权威说法;让人对着空表单自己编,编出来的多半跑不通。
|
||||
*/
|
||||
samples: z.array(z.object({ name: z.string(), input: z.record(z.string(), z.unknown()) })).default([]),
|
||||
outputPorts: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
@@ -559,7 +579,24 @@ export const listLedgerObjectsRoute = {
|
||||
path: '/api/gov/data/objects',
|
||||
/** `label` 是人读名(落在表注释上);缺省即没登记,呈现退回对象键 */
|
||||
output: z.object({
|
||||
objects: z.array(z.object({ domain: z.string(), object: z.string(), label: z.string().optional() })),
|
||||
objects: z.array(
|
||||
z.object({
|
||||
domain: z.string(),
|
||||
object: z.string(),
|
||||
label: z.string().optional(),
|
||||
/**
|
||||
* 这张表的**性质**(单据 / 主数据 / 派生)—— 取自装在库里的活动清单声明。
|
||||
*
|
||||
* 物化出来的物理表本身不知道自己是什么:表里只有列和主键。
|
||||
* 而「该不该管它、谁有权改它」全看性质,所以这一维必须摆到治理台上 ——
|
||||
* 从前没有它,一屏 19 张表看着一模一样,人分不出哪几张是要治理的主数据。
|
||||
* 缺省 = 库里有这张表,却没有哪个在装的活动声明它(多半是退役活动留下的壳)。
|
||||
*/
|
||||
kind: ledgerObjectKindSchema.optional(),
|
||||
/** 单据的单标识列名 —— 呈现上摆出来,人一眼看得出「这张表按什么分单」 */
|
||||
caseKey: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
@@ -713,6 +750,31 @@ export const testRunRoute = {
|
||||
output: z.object({ workflowId: z.string(), state: workflowStateSchema }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 仿真测试台 · 试跑**一个活动** —— 与试跑流程包并列的第二种试法。
|
||||
*
|
||||
* 为什么要单有一条:开发一个活动时,关心的是「这个活动自己跑不跑得动」——
|
||||
* 案例模型推不推得开、判据取不取得齐、台账写不写得对、验收卡长什么样。
|
||||
* 走流程包试,前面几个节点的噪音全得先趟一遍,而且每改一次活动就要重跑整条流。
|
||||
*
|
||||
* 起出来的是**真任务**(不挂工作流),照旧落到被指派人的工作台、照旧过 Harness 与放行门 ——
|
||||
* 试跑不等于假跑:假跑试不出任何东西。区别只在它带 `origin='testbed'`,可以连根拔掉。
|
||||
*/
|
||||
export const testRunActivityRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/testbed/run-activity',
|
||||
body: z.object({
|
||||
activityId: z.string().min(1),
|
||||
/** 起单入参 —— 按该活动契约的 `input.data` 填;可用包自带样例预填 */
|
||||
data: z.record(z.string(), z.unknown()).default({}),
|
||||
/** 委托消息(可空)—— 契约允许时才有意义 */
|
||||
message: z.string().default(''),
|
||||
/** 派给谁;留空 = 操作者自己(试跑不该派到别人工作台上) */
|
||||
ownerId: z.string().default(''),
|
||||
}),
|
||||
output: z.object({ taskId: z.string(), title: z.string() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 停止仿真 —— **把这一单连根拔掉**:工作流、它的活动任务、工单、实例、trace 全部销毁。
|
||||
* 仿真不是生产:试跑留下的东西不该在库里、在别人工作台上过夜。
|
||||
@@ -720,15 +782,207 @@ export const testRunRoute = {
|
||||
export const testStopRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/testbed/stop',
|
||||
body: z.object({ workflowId: z.string() }),
|
||||
/** 两种试法各有其拔法:流程包给 workflowId,单活动给 taskId;都不给 = 拔掉所有试跑残留 */
|
||||
body: z.object({ workflowId: z.string().default(''), taskId: z.string().default('') }),
|
||||
output: z.object({ destroyed: z.boolean(), tasks: z.number().int() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 当前在跑的仿真(测试台一次只跑一条)—— 刷新页面也认得回来 */
|
||||
/**
|
||||
* 当前这一台仪器上的试跑单(测试台一次只跑一条)—— 刷新页面也认得回来。
|
||||
*
|
||||
* **返回还在跑的那一单,以及刚跑完的那一单** —— 两者要分得开:
|
||||
* `live` 为真才锁旋钮(在跑的不许换台);为假表示它已收尾,观测面照旧摆着结果给人看,
|
||||
* 但可以再起一单了。从前不分,已终结的单照样被当成「在跑」——
|
||||
* 跑完之后旋钮永久锁住,人只能刷新整页才起得了下一单(实测)。
|
||||
*/
|
||||
export const testCurrentRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/testbed/current',
|
||||
output: z.object({ workflowId: z.string().optional(), subject: z.string().optional() }),
|
||||
output: z.object({
|
||||
workflowId: z.string().optional(),
|
||||
subject: z.string().optional(),
|
||||
/** 单活动试跑:没有工作流,认的是任务 */
|
||||
taskId: z.string().optional(),
|
||||
activityId: z.string().optional(),
|
||||
/** 还在跑(未终结)—— 只有它为真才算「占着这台仪器」 */
|
||||
live: z.boolean().default(false),
|
||||
/** 已收尾时的终态名(released / blocked / terminated …),给观测面收灯用 */
|
||||
terminal: z.string().optional(),
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 单活动试跑的**观测面** —— 一屏看穿这一单:
|
||||
* 轨迹(每一拍干了什么)· 台账草稿(成果落到哪儿了)· 卡(问了谁什么)· 预算读数。
|
||||
*
|
||||
* 流程包那侧看的是 trace 树(节点 → 步骤);活动这侧要看的是**活动内部**,
|
||||
* 拿 trace 树看不出「第三拍为什么被拒」。
|
||||
*/
|
||||
export const testActivityStateRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/testbed/activity/:taskId',
|
||||
params: z.object({ taskId: z.string() }),
|
||||
output: z.object({
|
||||
taskId: z.string(),
|
||||
activityId: z.string(),
|
||||
title: z.string(),
|
||||
runtime: taskRuntimeStateSchema,
|
||||
terminal: terminalStateSchema.optional(),
|
||||
reason: z.string().optional(),
|
||||
used: z.object({ steps: z.number().int(), tokens: z.number().int(), humanCalls: z.number().int() }),
|
||||
caps: z.object({ steps: z.number().int(), tokens: z.number().int(), humanCalls: z.number().int() }),
|
||||
/** 每一拍的轨迹:干了什么、拿回了什么、被拒的理由 */
|
||||
trail: z.array(
|
||||
z.object({
|
||||
at: z.string(),
|
||||
kind: z.string(),
|
||||
title: z.string(),
|
||||
detail: z.record(z.string(), z.unknown()).optional(),
|
||||
}),
|
||||
),
|
||||
/** 本单写下的台账草稿 —— 成果就是这些行 */
|
||||
drafts: z.array(
|
||||
z.object({
|
||||
address: z.string(),
|
||||
key: z.record(z.string(), z.unknown()),
|
||||
patch: z.record(z.string(), z.unknown()),
|
||||
}),
|
||||
),
|
||||
/** 待答与已答的卡 */
|
||||
cards: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
mode: z.enum(['execution', 'acceptance']),
|
||||
title: z.string(),
|
||||
done: z.boolean(),
|
||||
result: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/* ── 试跑日志:运行数据不过夜,观测记录留下来 ─────────────────────────── */
|
||||
|
||||
/* ── 上传件:附件要能真正进活动,不能只剩一个文件名 ────────────────── */
|
||||
|
||||
/**
|
||||
* 上传一个文件 —— 走 base64 而不是 multipart。
|
||||
*
|
||||
* 这一层所有路由都由同一套 zod 契约与 typed-fetch 收着,multipart 得在它旁边另开一条
|
||||
* 不受契约管的路;而这里要传的是 BOM、规格书这类几十 KB 的件,base64 那点膨胀
|
||||
* 换来「照旧一条契约管到底」是划算的。真大的件不该走界面上传,该走通道。
|
||||
*/
|
||||
export const uploadFileRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/upload',
|
||||
body: z.object({
|
||||
name: z.string().min(1),
|
||||
contentType: z.string().default(''),
|
||||
/** 文件字节的 base64 —— 上限 8MB(超了当场拒,不静默截断) */
|
||||
dataBase64: z.string().min(1),
|
||||
}),
|
||||
output: z.object({
|
||||
ref: z.string(),
|
||||
name: z.string(),
|
||||
contentType: z.string(),
|
||||
size: z.number().int(),
|
||||
/** 读出来的文本;缺席 = 这类文件读不出文本(理由在 note 里) */
|
||||
text: z.string().optional(),
|
||||
note: z.string(),
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
export type UploadedFile = z.infer<typeof uploadFileRoute.output>;
|
||||
|
||||
/** 取回原件 —— 存的是真字节,人点得开才算真留住了 */
|
||||
export const downloadFileRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/upload/:id',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({ name: z.string(), contentType: z.string(), dataBase64: z.string() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 一次试跑的存档 —— **销毁那一刻拍的快照**。
|
||||
*
|
||||
* 「停止仿真 = 连根拔掉」这条不变(试跑起的是真任务,不该在别人工作台上过夜);
|
||||
* 拔掉之前把这一单的观测面存下来。开发活动是反复试的活,「上一版跑了几拍、
|
||||
* 卡在哪一步、台账写成什么样」正是要拿来比对的东西 —— 拔干净等于每改一次
|
||||
* 就把上一次的账一并烧了。
|
||||
*/
|
||||
export const testLogRowSchema = z.object({
|
||||
id: z.string(),
|
||||
kind: z.enum(['activity', 'package']),
|
||||
subject: z.string(),
|
||||
/** 试的是谁:activityId 或 packageId */
|
||||
target: z.string(),
|
||||
taskId: z.string().optional(),
|
||||
workflowId: z.string().optional(),
|
||||
/** 终态名;缺省 = 人中途停的 */
|
||||
terminal: z.string().optional(),
|
||||
reason: z.string().optional(),
|
||||
used: z.object({ steps: z.number().int(), tokens: z.number().int(), humanCalls: z.number().int() }).partial(),
|
||||
caps: z.object({ steps: z.number().int(), tokens: z.number().int(), humanCalls: z.number().int() }).partial(),
|
||||
/** 列表页只要计数,详情才给正文 —— 一次试跑的轨迹有上百条 */
|
||||
trailCount: z.number().int(),
|
||||
draftCount: z.number().int(),
|
||||
cardCount: z.number().int(),
|
||||
startedAt: z.string().optional(),
|
||||
archivedAt: z.string(),
|
||||
});
|
||||
export type TestLogRow = z.infer<typeof testLogRowSchema>;
|
||||
|
||||
export const listTestLogsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/testbed/logs',
|
||||
output: z.object({ runs: z.array(testLogRowSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 一次试跑的正文 —— 逐拍轨迹 / 台账草稿 / 发过的卡 */
|
||||
export const testLogDetailRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/testbed/logs/:id',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: testLogRowSchema.extend({
|
||||
trail: z.array(
|
||||
z.object({
|
||||
at: z.string(),
|
||||
kind: z.string(),
|
||||
title: z.string(),
|
||||
detail: z.record(z.string(), z.unknown()).optional(),
|
||||
}),
|
||||
),
|
||||
drafts: z.array(
|
||||
z.object({
|
||||
address: z.string(),
|
||||
key: z.record(z.string(), z.unknown()),
|
||||
patch: z.record(z.string(), z.unknown()),
|
||||
}),
|
||||
),
|
||||
cards: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
mode: z.string(),
|
||||
title: z.string(),
|
||||
done: z.boolean(),
|
||||
result: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 删一条 / 清空 —— 日志是给人比对用的,留多久由人说了算 */
|
||||
export const deleteTestLogRoute = {
|
||||
method: 'DELETE',
|
||||
path: '/api/gov/testbed/logs/:id',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({ deleted: z.number().int() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const clearTestLogsRoute = {
|
||||
method: 'DELETE',
|
||||
path: '/api/gov/testbed/logs',
|
||||
output: z.object({ deleted: z.number().int() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 全链 Trace:一条工作流的 span 树(节点 → 步骤 / 工具 / 模型 / 人卡) */
|
||||
|
||||
@@ -53,9 +53,21 @@ export const taskCardSchema = z.object({
|
||||
title: z.string(),
|
||||
runtime: taskRuntimeStateSchema,
|
||||
terminal: terminalStateSchema.optional(),
|
||||
/** 它在工作流上的序位 */
|
||||
/** 它在工作流上的序位 —— **与下面的阶段进度不是一回事**:这是「第几个活动」 */
|
||||
position: z.number().int().min(1),
|
||||
total: z.number().int().min(1),
|
||||
/**
|
||||
* 活动**自己**的阶段进度 —— 进度条读的是它。
|
||||
*
|
||||
* 从前进度条读的是上面那对序位:A1 在七节点流里是 1/7,一开跑就画成 14%,
|
||||
* 跑到收口还是 14%(节点没换,序位当然不动);单活动试跑更荒唐 —— 恒 1/1,
|
||||
* 起单那一刻就满格。序位答的是「这单在整条流的哪一环」,不是「这单办到哪了」。
|
||||
*
|
||||
* 活动没声明阶段就没有这个键:界面照此不画进度条,不编一个出来。
|
||||
*/
|
||||
phase: z
|
||||
.object({ index: z.number().int().min(1), total: z.number().int().min(1), name: z.string() })
|
||||
.optional(),
|
||||
workflowSubject: z.string(),
|
||||
workflowState: workflowStateSchema,
|
||||
startedAt: z.string(),
|
||||
@@ -111,6 +123,39 @@ export const taskDetailSchema = z.object({
|
||||
file: fileDocSchema,
|
||||
}),
|
||||
),
|
||||
/**
|
||||
* 沟通记录 —— 跟客户/供应商往来的**原件**,按单归档。
|
||||
* 进来的那一封是开案的那份料(活动的入参就是它);出去的是放行后系统真发的件。
|
||||
*/
|
||||
comms: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
at: z.string(),
|
||||
direction: z.enum(['in', 'out']),
|
||||
channel: z.string(),
|
||||
party: z.string(),
|
||||
subject: z.string(),
|
||||
body: z.string(),
|
||||
attachments: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
size: z.number().int().optional(),
|
||||
ref: z.string().optional(),
|
||||
/** 基座读出来的说明:读出多少字 / 为什么读不出 —— 界面照抄,不改写 */
|
||||
note: z.string().optional(),
|
||||
readable: z.boolean().optional(),
|
||||
}),
|
||||
),
|
||||
note: z.string(),
|
||||
/**
|
||||
* **未读**(2026-07-31)——「看没看过」由人按按钮说了算,不由打开页签自动清。
|
||||
* 自动清等于替人宣布「你看过了」,而他可能只是路过那个页签。
|
||||
*/
|
||||
unread: z.boolean().default(false),
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
workOrders: z.array(workOrderSchema),
|
||||
chat: z.array(chatMessageSchema),
|
||||
view: z.array(viewBlockSchema),
|
||||
@@ -122,6 +167,17 @@ export const taskDetailSchema = z.object({
|
||||
at: z.string(),
|
||||
side: attributionSideSchema,
|
||||
sideLabel: z.string(),
|
||||
/**
|
||||
* 这一条**是谁干的** —— 日志只认三档:`owner` 负责人 · `agent` 智能体 · `system` 系统。
|
||||
*
|
||||
* 与 `side`(五种归侧)分工不同:那一维是**度量**用的(自主率要把「等外面」
|
||||
* 与「机器干的」分开);这一维是**呈现**用的 —— 人扫这一屏只问
|
||||
* 「这一步是我办的、AI 办的,还是流程自己走到的」,三档各一个颜色。
|
||||
*/
|
||||
who: z.enum(['owner', 'agent', 'system']),
|
||||
/** 类型章:**四字短语**,与对话区同词(触发事件 / 阶段结论 / 数据录入 / 员工确认 …) */
|
||||
action: z.string(),
|
||||
/** 内容,**带主语**:`任务助手:…` / `柴浩:…` —— 日志不写没头脑的流水账 */
|
||||
title: z.string(),
|
||||
summary: z.string().optional(),
|
||||
}),
|
||||
@@ -155,6 +211,74 @@ export const replyChatCardRoute = {
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 标记沟通记录已读 / 未读 —— **气泡计数跟着这一下走**。
|
||||
* 不做「打开即已读」:那是替人宣布他看过了。已读还能改回未读(看了一半想留个记号)。
|
||||
*/
|
||||
export const markCommReadRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/comms/:commId/read',
|
||||
params: z.object({ commId: z.string() }),
|
||||
body: z.object({ read: z.boolean() }),
|
||||
output: z.object({ ok: z.boolean(), unread: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* **就地改完按下「保存」** —— 把件上那张表写回**本单草稿层**,工单不办结。
|
||||
*
|
||||
* 为什么非要立刻落库、而不是攒到提交那一下:件是**服务端投影**。
|
||||
* 人改了几格,只读那份跟着变了(那是他自己敲的),而由数据算出来的东西
|
||||
* ——类徽 A/B/C、分布条、结论横幅那条去向——全是渲染那一刻的旧值。
|
||||
* 于是屏上一半新一半旧:**人把三条料全改成 A,底下的去向还写着「去选型」**
|
||||
* (用户实测)。那不是没更新,那是界面在撒谎。
|
||||
*
|
||||
* 落库之后前端重取任务投影,服务端拿新数据重渲染整份件 —— 算出来的那些跟着一起变,
|
||||
* 去向也由同一次谓词求值重算。**放行门的位置没变**:写的仍是草稿层,
|
||||
* 合并进正式版依旧只发生在人签字那一刻。
|
||||
*
|
||||
* ── 按**地址**收,不按「当下那张卡的第几块」收(2026-08-02 用户实测逼出来)──────
|
||||
*
|
||||
* 载荷从前是 `{workOrderId, values:{'table:0': …}}`:列名由**那张工单的 `blocks[0]`**
|
||||
* 给出,于是「这份件能不能改」被绑在了「当下开着的那张卡长什么样」上。
|
||||
* 实测的下场:一单在放行门连拒五拍熔断,引擎发的是一张**没有 blocks 的熔断卡**;
|
||||
* 而屏上摆着的正是受理记录那份件,闸也在、编辑也进得去 —— 人把那条料的年用量补上、
|
||||
* 按了保存,前端找不到匹配的块,**一格都没往下送**。他要补的恰恰就是把这一单
|
||||
* 卡在那儿的那个必填格(判据 `quote_intake_record_missing = 0`),
|
||||
* 而屏上什么反应都没有 —— 界面提供了一个落不了地的编辑口。
|
||||
*
|
||||
* 现在收的是件自己说的话:每张可改的表挂着 `data-edit="域.对象"`,格子挂着
|
||||
* `data-col="台账列名"` —— 地址与列名**本来就在件里**,不必再绕一趟工单。
|
||||
* 授权面改由**活动清单的台账声明**把关(不在声明里的地址一概不写),
|
||||
* 比「当下那张卡碰巧带了哪一块」既严格又稳定。
|
||||
*
|
||||
* **改、增、删是同一件事的三种样子**(2026-08-02 用户:「编辑列表应该还能增删行」):
|
||||
* 人手改本来就是**兜 AI 的错**的那一手 —— 而错不只有「这一格填错了」,
|
||||
* 还有「这一行根本不该在」(客户点名本轮跳过的料被写了进来)与「漏了一行」。
|
||||
* 只给改格子,前两种就没有出口:那一单会一路走到放行门被判据拦住,
|
||||
* 而人明明一眼就看出问题在哪。
|
||||
*/
|
||||
export const saveTableRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/tasks/:taskId/table',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
body: z.object({
|
||||
/** `{ '域.对象': [ { 台账列名: 值 } ] }` —— 主键列用来认行,不参与改行;台账里没有的行就是**新增** */
|
||||
tables: z.record(z.string(), z.array(z.record(z.string(), z.string()))),
|
||||
/**
|
||||
* **人在件上删掉的那几行** —— 摆主键,一行一条。
|
||||
*
|
||||
* 为什么删要单独说,而不是「`tables` 里没有的就当删了」:那种口径把**一次不完整的
|
||||
* 回传**变成一次删表 —— 件上摆的是不是整张表,服务端看不见(模版绑哪条路径是活动的事)。
|
||||
* 说出来的删才删:回传缺了什么一律当「这次没提到」,不当「要撤掉」。
|
||||
*
|
||||
* 撤的只是**本单草稿层**那一笔(`draftDrop`),正式版一个字都动不了、不抛事件 ——
|
||||
* 与 Agent 的 `drop` 同一条路子。放行门的位置没变。
|
||||
*/
|
||||
drops: z.record(z.string(), z.array(z.record(z.string(), z.string()))).optional(),
|
||||
}),
|
||||
output: z.object({ ok: z.boolean(), rows: z.number().int().min(0) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 负责人的自由输入 —— 人事件的第二形态 */
|
||||
export const sayRoute = {
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user