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

手改是**兜 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
+259 -5
View File
@@ -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 树(节点 → 步骤 / 工具 / 模型 / 人卡) */