/** * 人类工具的卡协议(编码规范 §8.1 / §9;语义出处 OS 05,AM 06/07)。 * * **双层边界**:范式(loop / flow / skill)是后端实现逻辑的模型,只存在于引擎层; * 本文件是**前端呈现协议**,前端永远感知不到范式。旧文「四卡」的映射: * 执行卡 → 执行工单(work order · execution) * 放行卡 → 验收(工作区可修订报告 + 对话区验收单)(work order · acceptance) * 决策 / 补全 → 对话卡(decision / elicitation) * 落地失败卡 = 执行工单的系统预置形态,不构成第五种卡(OS 7.3)。 * * 形状以 2026-07-26 原型定稿为准(`~/coding/JCAI_PRD/workbench/_data/model.ts`); * 枚举值改用英文标识符 + 本文件的 *_LABEL 字典,界面文案由字典给出、与原型逐字一致。 */ import { z } from 'zod'; import { cardIdSchema, deliverableIdSchema, stageIdSchema, staffIdSchema, workOrderIdSchema, } from './id.ts'; import { acceptanceLevelSchema, rejectionKindSchema } from './terminal.ts'; /* ── 基础原语 ─────────────────────────────────────────────────────────── */ export const TONES = ['ok', 'warn', 'danger', 'brand', 'muted'] as const; export const toneSchema = z.enum(TONES); export type Tone = z.infer; /** 依据快照:调用必附上下文(OS 05)—— 裸调用基座必须拒发 */ export const basisSchema = z.array(z.object({ k: z.string(), v: z.string() })); export type Basis = z.infer; export const formOptionSchema = z.object({ value: z.string(), label: z.string(), negative: z.boolean().optional(), }); export type FormOption = z.infer; export const FORM_FIELD_KINDS = [ 'text', 'textarea', 'radio', 'number', 'select', 'check', 'date', 'money', 'file', ] as const; export const formFieldKindSchema = z.enum(FORM_FIELD_KINDS); export type FormFieldKind = z.infer; /** 表单字段:结构化交互的原子。卡片表单由本 schema 驱动渲染与校验,后端回传校验同 schema(§9.2) */ export const formFieldSchema = z.object({ key: z.string().min(1), label: z.string(), kind: formFieldKindSchema, placeholder: z.string().optional(), options: z.array(formOptionSchema).optional(), /** 字段分组(渲染为表单小节) */ group: z.string().optional(), hint: z.string().optional(), /** AI 拟值 —— 验收工单落地时已填好,人可改全部字段 */ ai: z.string().optional(), unit: z.string().optional(), required: z.boolean().optional(), cols: z.union([z.literal(1), z.literal(2)]).optional(), }); export type FormField = z.infer; /* ── 视图块:视图区的结构化呈现(活动推送,只读) ────────────────────── */ const fileDocSchema = z.discriminatedUnion('kind', [ z.object({ kind: z.literal('markdown'), name: z.string(), md: z.string() }), z.object({ kind: z.literal('excel'), name: z.string(), sheet: z.string(), rows: z.array(z.array(z.string())), }), z.object({ kind: z.literal('word'), name: z.string(), blocks: z.array( z.union([ z.object({ t: z.enum(['h1', 'h2', 'p']), text: z.string() }), z.object({ t: z.literal('ul'), items: z.array(z.string()) }), ]), ), }), z.object({ kind: z.literal('pdf'), name: z.string(), src: z.string() }), ]); export type FileDoc = z.infer; export { fileDocSchema }; export const viewBlockSchema = z.discriminatedUnion('t', [ z.object({ t: z.literal('callout'), tone: toneSchema, text: z.string() }), z.object({ t: z.literal('kv'), title: z.string().optional(), items: z.array( z.object({ k: z.string(), v: z.string(), /** 绑定某工作事项的回填字段:未答显示占位,作答后实时落定 */ fromItem: z.object({ item: z.string(), field: z.string() }).optional(), }), ), }), z.object({ t: z.literal('badges'), title: z.string().optional(), items: z.array(z.object({ label: z.string(), on: z.boolean() })), }), z.object({ t: z.literal('table'), title: z.string().optional(), cols: z.array(z.string()), rows: z.array(z.array(z.string())), editable: z.boolean().optional(), editableCols: z.array(z.number().int()).optional(), linkDonut: z .object({ col: z.number().int(), title: z.string().optional(), unit: z.string().optional(), map: z.array(z.object({ match: z.string(), label: z.string(), tone: toneSchema })), }) .optional(), }), z.object({ t: z.literal('bars'), title: z.string().optional(), max: z.number().optional(), redline: z.number().optional(), items: z.array( z.object({ label: z.string(), value: z.number(), text: z.string().optional(), tone: toneSchema.optional(), }), ), }), z.object({ t: z.literal('donut'), title: z.string().optional(), unit: z.string().optional(), items: z.array(z.object({ label: z.string(), value: z.number(), tone: toneSchema })), }), z.object({ t: z.literal('mail'), dir: z.enum(['in', 'out']), statusLabel: z.string().optional(), editable: z.boolean().optional(), from: z.string(), fromAddr: z.string().optional(), to: z.string(), cc: z.string().optional(), subject: z.string(), time: z.string(), body: z.string(), attachments: z .array(z.object({ name: z.string(), meta: z.string().optional(), file: fileDocSchema.optional() })) .optional(), }), z.object({ t: z.literal('cards'), title: z.string().optional(), items: z.array( z.object({ tag: z.string(), icon: z.string().optional(), tone: toneSchema.optional(), title: z.string(), code: z.string().optional(), swap: z.object({ from: z.string(), to: z.string(), hint: z.string().optional() }).optional(), big: z.string(), bigLabel: z.string().optional(), meta: z.array(z.object({ k: z.string(), v: z.string() })).optional(), note: z.string().optional(), from: z.string().optional(), time: z.string().optional(), }), ), }), z.object({ t: z.literal('waiting'), label: z.string(), hint: z.string().optional() }), z.object({ t: z.literal('files'), title: z.string().optional(), items: z.array(z.object({ label: z.string(), desc: z.string(), file: fileDocSchema })), }), ]); export type ViewBlock = z.infer; /* ── 阶段与过程成果 ───────────────────────────────────────────────────── */ /** * 阶段:交互层的统一进展刻度。 * 引擎层的来源随范式而异(loop = CMMN Stage / 里程碑,flow = 步序位置),前端不感知。 */ export const stageSchema = z.object({ id: stageIdSchema, label: z.string(), round: z.number().int().min(1).optional(), }); export type Stage = z.infer; export const DELIVERABLE_STATES = ['draft', 'pending-acceptance', 'released', 'rejected'] as const; export const deliverableStateSchema = z.enum(DELIVERABLE_STATES); export type DeliverableState = z.infer; export const DELIVERABLE_STATE_LABEL: Readonly> = { draft: '草稿', 'pending-acceptance': '待验收', released: '已放行', rejected: '已驳回', }; /** 过程成果:每个成果都要经验收放行才落地(放行门,OS 09) */ export const deliverableSchema = z.object({ id: deliverableIdSchema, stage: stageIdSchema, round: z.number().int().min(1).optional(), title: z.string(), desc: z.string().optional(), blocks: z.array(viewBlockSchema), state: deliverableStateSchema, at: z.string().optional(), }); export type Deliverable = z.infer; /* ── 工单两模式(执行 / 验收) ────────────────────────────────────────── */ export const WORK_ORDER_MODES = ['execution', 'acceptance'] as const; export const workOrderModeSchema = z.enum(WORK_ORDER_MODES); export type WorkOrderMode = z.infer; export const WORK_ORDER_MODE_LABEL: Readonly> = { execution: '执行', acceptance: '验收', }; /** 完成回传:谁、何时、答了什么(回传即留痕) */ export const workDoneSchema = z.object({ at: z.iso.datetime(), by: staffIdSchema, result: z.string(), values: z.record(z.string(), z.string()).optional(), }); export type WorkDone = z.infer; const workOrderBase = { id: workOrderIdSchema, at: z.iso.datetime(), title: z.string(), /** 调用必附上下文 —— basis 为空的工单基座必须拒发 */ basis: basisSchema, stage: stageIdSchema.optional(), done: workDoneSchema.optional(), }; /** 执行工单:作业指导书 + 空表单(补全并入此类);落地失败卡是它的系统预置形态 */ export const executionWorkOrderSchema = z.object({ ...workOrderBase, mode: z.literal('execution'), /** 系统预置形态标记:landing-failure = 落地失败卡(OS 7.3) */ preset: z.enum(['landing-failure']).optional(), ask: z.string(), standard: z.string().optional(), steps: z.array(z.string()).optional(), /** 报告正文取自哪份过程成果 —— 人要在数据上作判断,数据就得摆在表单旁边 */ basedOn: deliverableIdSchema.optional(), blocks: z.array(viewBlockSchema).optional(), fields: z.array(formFieldSchema), submitLabel: z.string().optional(), }); export type ExecutionWorkOrder = z.infer; export const acceptanceCriterionCheckSchema = z.object({ label: z.string(), level: acceptanceLevelSchema, pass: z.boolean(), }); export type AcceptanceCriterionCheck = z.infer; /** 待落地动作意向:预检全过才上卡,卡面呈现的即已验证的动作清单(OS 7.3) */ export const actionIntentViewSchema = z.object({ label: z.string(), effect: z.enum(['read', 'write']), detail: z.string(), }); export type ActionIntentView = z.infer; export const ACTION_EFFECT_LABEL: Readonly> = { read: '读', write: '写', }; /** 验收工单:AI 拟好的可修订报告 + 判据逐条核对 + 放行三选一 */ export const acceptanceWorkOrderSchema = z.object({ ...workOrderBase, mode: z.literal('acceptance'), deliverable: deliverableIdSchema, criteria: z.array(acceptanceCriterionCheckSchema), actions: z.array(actionIntentViewSchema).optional(), /** 报告可修订:字段随成果 schema 渲染,AI 拟值已填 */ fields: z.array(formFieldSchema).default([]), passLabel: z.string().optional(), }); export type AcceptanceWorkOrder = z.infer; export const workOrderSchema = z.discriminatedUnion('mode', [ executionWorkOrderSchema, acceptanceWorkOrderSchema, ]); export type WorkOrder = z.infer; /* ── 对话卡族(决策 / 补全 / 评价) ──────────────────────────────────── */ /** AI 发言的类型标记 —— 对话区可分辨:结论 / 指导 / 判据 */ export const AI_TAGS = ['conclusion', 'guidance', 'criterion'] as const; export const aiTagSchema = z.enum(AI_TAGS); export type AiTag = z.infer; export const AI_TAG_LABEL: Readonly> = { conclusion: '结论', guidance: '指导', criterion: '判据', }; const chatBase = { id: cardIdSchema, at: z.iso.datetime() }; /** 决策卡:分支条件选择表;含自定义收尾项 */ export const decisionCardSchema = z.object({ ...chatBase, k: z.literal('decision'), ask: z.string(), basis: basisSchema, recommend: z.string().optional(), options: z.array( z.object({ value: z.string(), label: z.string(), detail: z.string().optional(), consequence: z.string().optional(), recommended: z.boolean().optional(), }), ), /** 自定义收尾项:人可给出选项之外的处置(附理由) */ allowCustom: z.boolean().default(false), chosen: z .object({ value: z.string(), at: z.iso.datetime(), by: staffIdSchema, note: z.string().optional(), }) .optional(), }); export type DecisionCard = z.infer; /** 补全卡:就地要几个小信息;含「没法填」旁路 —— 两条路必走其一 */ export const elicitationCardSchema = z.object({ ...chatBase, k: z.literal('elicitation'), ask: z.string(), basis: basisSchema.optional(), fields: z.array( z.object({ key: z.string(), label: z.string(), placeholder: z.string().optional() }), ), done: z .object({ at: z.iso.datetime(), by: staffIdSchema, values: z.record(z.string(), z.string()), /** 没法填的理由 —— 人让 AI 迭代一轮需求 */ reason: z.string().optional(), }) .optional(), }); export type ElicitationCard = z.infer; /** 评价卡:任务评价(评分 1–5 + 改进建议) */ export const ratingCardSchema = z.object({ ...chatBase, k: z.literal('rating'), ask: z.string(), basis: basisSchema.optional(), done: z .object({ at: z.iso.datetime(), by: staffIdSchema, score: z.number().int().min(1).max(5), note: z.string().optional(), }) .optional(), }); export type RatingCard = z.infer; export const chatCardSchema = z.discriminatedUnion('k', [ decisionCardSchema, elicitationCardSchema, ratingCardSchema, ]); export type ChatCard = z.infer; /* ── 对话流消息 ───────────────────────────────────────────────────────── */ export const chatMessageSchema = z.discriminatedUnion('k', [ z.object({ ...chatBase, k: z.literal('ai'), text: z.string(), tag: aiTagSchema.optional(), /** 指涉的工单 id —— 对话区点击即让视图区定位到那张工单 */ ref: workOrderIdSchema.optional(), }), z.object({ ...chatBase, k: z.literal('human'), text: z.string(), by: staffIdSchema }), z.object({ ...chatBase, k: z.literal('system'), type: z.enum(['data', 'time', 'human', 'control']), text: z.string(), }), decisionCardSchema, elicitationCardSchema, ratingCardSchema, ]); export type ChatMessage = z.infer; /* ── 回传协议(前端 → 后端;后端以同一 schema 校验,§9.2) ───────────── */ export const workOrderReplySchema = z.discriminatedUnion('mode', [ z.object({ mode: z.literal('execution'), workOrderId: workOrderIdSchema, values: z.record(z.string(), z.string()), }), z.object({ mode: z.literal('acceptance'), workOrderId: workOrderIdSchema, /** 三选一:放行通过 / 驳回重做 / 驳回迭代 —— 驳回必附意见 */ verdict: z.union([z.literal('pass'), rejectionKindSchema]), /** 报告的人工修订(可空 = 全盘采信 AI 拟值) */ values: z.record(z.string(), z.string()).default({}), comment: z.string().optional(), /** 选择组的出口选择:恰发一口,随放行一并确认(AM 1.2) */ selectedPorts: z.record(z.string(), z.string()).default({}), }), ]); export type WorkOrderReply = z.infer; export const chatCardReplySchema = z.discriminatedUnion('k', [ z.object({ k: z.literal('decision'), cardId: cardIdSchema, value: z.string(), note: z.string().optional(), }), z.object({ k: z.literal('elicitation'), cardId: cardIdSchema, values: z.record(z.string(), z.string()).default({}), /** 「没法填」旁路 */ reason: z.string().optional(), }), z.object({ k: z.literal('rating'), cardId: cardIdSchema, score: z.number().int().min(1).max(5), note: z.string().optional(), }), ]); export type ChatCardReply = z.infer; /** * 驳回必附意见 —— 三规矩之一的机检形态(OS 05)。 * 后端回传校验与前端提交校验共用本函数。 */ export function rejectionNeedsComment(reply: WorkOrderReply): boolean { return reply.mode === 'acceptance' && reply.verdict !== 'pass' && !reply.comment?.trim(); } /** 补全卡两条路必走其一:要么填值,要么给「没法填」的理由 */ export function elicitationReplyIsEmpty(reply: ChatCardReply): boolean { if (reply.k !== 'elicitation') return false; const hasValue = Object.values(reply.values).some((v) => v.trim() !== ''); return !hasValue && !reply.reason?.trim(); }