forked from ChaiHao/BPOS-MVP
M1: 底座 —— @contracts 首版 + event / ledger / org / llm
@contracts 首版(§8.1 九个文件): - id 品牌 ID / terminal 终态五名与归侧四标签 / envelope 信封六字段与四类事件 - card 工单两模式 + 对话卡族(形状对齐 2026-07-26 原型定稿) - contract7 契约七件 / ledger 三级寻址与写三分 / process-package 流程包 DSL 与触发器三类 - manifest 活动包清单六区(含 CMMN·DMN·步序声明的资产形状)/ org / api 路由契约 模块(L1): - event 信封存储、归属解析三级短路、投递五诺、监听注册表、三级消费裁定 - ledger schema 自生长(最大化合并/最宽类型/只增不破坏)、任务视图叠加、写三分、冲突检查 - org 岗位授权(客体三类×动作三种)、停用前置检查、会话、种子播种 - llm 四元绑定、ModelGateway 三模式(live/record/replay)—— 两缝之一 app 装配:Hono 服务 + 鉴权中间件 + 治理台/工作台路由;drizzle 迁移;AES-256-GCM 凭证密封 ADR-007 工具链与门禁 · ADR-008 投递队列 · ADR-009 凭证加密 (ADR 编号自 007 续:编码规范 §14 已用到 006,README 写「从 005 续」是笔误) 门禁五道全绿;96 个测试通过,含对真实 PG 的 ledger 集成测试 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
co-authored by
Claude Opus 5
parent
7cd664af3a
commit
14d4fa046a
@@ -0,0 +1,492 @@
|
||||
/**
|
||||
* 人类工具的卡协议(编码规范 §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<typeof toneSchema>;
|
||||
|
||||
/** 依据快照:调用必附上下文(OS 05)—— 裸调用基座必须拒发 */
|
||||
export const basisSchema = z.array(z.object({ k: z.string(), v: z.string() }));
|
||||
export type Basis = z.infer<typeof basisSchema>;
|
||||
|
||||
export const formOptionSchema = z.object({
|
||||
value: z.string(),
|
||||
label: z.string(),
|
||||
negative: z.boolean().optional(),
|
||||
});
|
||||
export type FormOption = z.infer<typeof formOptionSchema>;
|
||||
|
||||
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<typeof formFieldKindSchema>;
|
||||
|
||||
/** 表单字段:结构化交互的原子。卡片表单由本 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<typeof formFieldSchema>;
|
||||
|
||||
/* ── 视图块:视图区的结构化呈现(活动推送,只读) ────────────────────── */
|
||||
|
||||
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<typeof fileDocSchema>;
|
||||
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<typeof viewBlockSchema>;
|
||||
|
||||
/* ── 阶段与过程成果 ───────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* 阶段:交互层的统一进展刻度。
|
||||
* 引擎层的来源随范式而异(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<typeof stageSchema>;
|
||||
|
||||
export const DELIVERABLE_STATES = ['draft', 'pending-acceptance', 'released', 'rejected'] as const;
|
||||
export const deliverableStateSchema = z.enum(DELIVERABLE_STATES);
|
||||
export type DeliverableState = z.infer<typeof deliverableStateSchema>;
|
||||
|
||||
export const DELIVERABLE_STATE_LABEL: Readonly<Record<DeliverableState, string>> = {
|
||||
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<typeof deliverableSchema>;
|
||||
|
||||
/* ── 工单两模式(执行 / 验收) ────────────────────────────────────────── */
|
||||
|
||||
export const WORK_ORDER_MODES = ['execution', 'acceptance'] as const;
|
||||
export const workOrderModeSchema = z.enum(WORK_ORDER_MODES);
|
||||
export type WorkOrderMode = z.infer<typeof workOrderModeSchema>;
|
||||
|
||||
export const WORK_ORDER_MODE_LABEL: Readonly<Record<WorkOrderMode, string>> = {
|
||||
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<typeof workDoneSchema>;
|
||||
|
||||
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<typeof executionWorkOrderSchema>;
|
||||
|
||||
export const acceptanceCriterionCheckSchema = z.object({
|
||||
label: z.string(),
|
||||
level: acceptanceLevelSchema,
|
||||
pass: z.boolean(),
|
||||
});
|
||||
export type AcceptanceCriterionCheck = z.infer<typeof acceptanceCriterionCheckSchema>;
|
||||
|
||||
/** 待落地动作意向:预检全过才上卡,卡面呈现的即已验证的动作清单(OS 7.3) */
|
||||
export const actionIntentViewSchema = z.object({
|
||||
label: z.string(),
|
||||
effect: z.enum(['read', 'write']),
|
||||
detail: z.string(),
|
||||
});
|
||||
export type ActionIntentView = z.infer<typeof actionIntentViewSchema>;
|
||||
|
||||
export const ACTION_EFFECT_LABEL: Readonly<Record<'read' | 'write', string>> = {
|
||||
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<typeof acceptanceWorkOrderSchema>;
|
||||
|
||||
export const workOrderSchema = z.discriminatedUnion('mode', [
|
||||
executionWorkOrderSchema,
|
||||
acceptanceWorkOrderSchema,
|
||||
]);
|
||||
export type WorkOrder = z.infer<typeof workOrderSchema>;
|
||||
|
||||
/* ── 对话卡族(决策 / 补全 / 评价) ──────────────────────────────────── */
|
||||
|
||||
/** AI 发言的类型标记 —— 对话区可分辨:结论 / 指导 / 判据 */
|
||||
export const AI_TAGS = ['conclusion', 'guidance', 'criterion'] as const;
|
||||
export const aiTagSchema = z.enum(AI_TAGS);
|
||||
export type AiTag = z.infer<typeof aiTagSchema>;
|
||||
|
||||
export const AI_TAG_LABEL: Readonly<Record<AiTag, string>> = {
|
||||
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<typeof decisionCardSchema>;
|
||||
|
||||
/** 补全卡:就地要几个小信息;含「没法填」旁路 —— 两条路必走其一 */
|
||||
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<typeof elicitationCardSchema>;
|
||||
|
||||
/** 评价卡:任务评价(评分 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<typeof ratingCardSchema>;
|
||||
|
||||
export const chatCardSchema = z.discriminatedUnion('k', [
|
||||
decisionCardSchema,
|
||||
elicitationCardSchema,
|
||||
ratingCardSchema,
|
||||
]);
|
||||
export type ChatCard = z.infer<typeof chatCardSchema>;
|
||||
|
||||
/* ── 对话流消息 ───────────────────────────────────────────────────────── */
|
||||
|
||||
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<typeof chatMessageSchema>;
|
||||
|
||||
/* ── 回传协议(前端 → 后端;后端以同一 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<typeof workOrderReplySchema>;
|
||||
|
||||
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<typeof chatCardReplySchema>;
|
||||
|
||||
/**
|
||||
* 驳回必附意见 —— 三规矩之一的机检形态(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();
|
||||
}
|
||||
Reference in New Issue
Block a user