三面拆分⑬:触发确认链路 + 运营中心「工作流」区 —— 设计只管设计,运营管细节

**触发确认链路**(用户口述五步的第 4/5 步):
  接收邮件 → 分类识别 → 命中 RFQ 事件 → ④(可选,运营勾)系统消息到指定人
  工作台(带邮件内容)→ ⑤ 工作台点「确认起单」,流程才起。
  · 命中不等于必起:勾了「需人工确认」,装配好的入参存成待确认单
    (sys.trigger_pending,migration 0019),ConfirmDesk 端口把带来件内容的
    系统消息送指定人(eventRef 挂票号,消息列表就地贴确认状态与那枚钮)。
  · 先认领后起单:consumed_at 空才认得到 —— 两人同时点、一人点两次,都只起一条流;
    起单失败退回认领,不留死票。确认人记进 source.by,留痕可查。
  · 登记触发不走确认(登记那一下本身就是人在确认);勾了没指定人 = 配置错误入死信。
  · 4 条单测 + 全链实测:投信 → triggersHit=1 且零直起 → AI 工作台收到确认消息
    → 确认起单 → 二次确认幂等回同一流号 → source.by = AI。

**运营中心重排**(区名即运行时:工作流是流程包的运行时,任务是活动的运行时):
  · 新「工作流」区:在途看板(从任务区迁入)+ 运行配置 —— 每包的触发器
    起单开关 / 确认打勾 / 确认人,和节点负责人(sys.node_owner)。
  · 负责人三层:显式指派 > 运营配置 > 轮值 —— 合并点在 process.start,
    复用既有派发链一行不改。
  · 任务区瘦身成活动运行时(任务/审批/需处置);知识库区改名「数据」
    (与设计中心的知识库解开同名);系统区只剩通道实例。
  · 设计中心触发器详情页摘掉启停按钮 —— 设计只管设计,启停/确认全在运营。
  · 旧深链有改嫁:tasks/board → runs/board,tasks/trg → runs/conf。

reset-runs 补 sys.trigger_pending(待确认单是运行痕 —— 和上回漏 trace_span 同类洞)。
邮件分类 Agent(第 ② 步)现由抽键正则 + S-FEEL 代位,升级路径记进 debt.md。
产品设计.md 运营章、trigger/process 两份 CONTRACT.md 同步。

五闸绿;契约快照随本提交入库。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-29 13:09:16 +08:00
co-authored by Claude Opus 5
parent 70b261d067
commit f9b2c07731
45 changed files with 1176 additions and 115 deletions
+2
View File
@@ -54,6 +54,8 @@ export type ProcessService = {
getWorkflow(id: string): Promise<WorkflowRecord>;
listWorkflows(limit?: number): Promise<WorkflowRecord[]>;
instancesOf(workflowId: string): Promise<NodeInstance[]>;
nodeOwners(packageId: string): Promise<Record<string, string>>;
setNodeOwner(packageId: string, nodeId: string, staffId: string | undefined): Promise<void>;
/** 任务终态到达:完成即继续流转;非完成即走声明的失败走向 */
onTaskTerminal(taskId: string, terminal: TerminalState, ports: Record<string, Record<string, unknown>>): Promise<WorkflowState | undefined>;
applyFailure(workflowId: string, nodeId: string, instance: number, node: Extract<ProcessNode, {
+2
View File
@@ -83,6 +83,8 @@ export type ProcessStore = {
nodeId: string;
instance: number;
} | undefined>;
nodeOwners(packageId: string): Promise<Record<string, string>>;
setNodeOwner(packageId: string, nodeId: string, staffId: string | undefined): Promise<void>;
};
/** 活动契约面 —— app 注入 `activity-registry`(L4):编排校验与起单都要它 */
export type ActivityDirectory = {
+34 -1
View File
@@ -15,9 +15,30 @@ export declare class TriggerInputMissingError extends Error {
export type FireOutcome = {
readonly triggerId: string;
readonly workflowId?: string;
readonly skipped?: 'duplicate' | 'disabled' | 'input-missing';
/** 命中但勾了「需人工确认」:没起单,等指定人点头 —— 这一票的号 */
readonly pendingId?: string;
readonly skipped?: 'duplicate' | 'disabled' | 'input-missing' | 'no-confirmer';
readonly missing?: readonly string[];
};
/** 待确认单:装配好的一次起单,等指定人点「确认起单」 */
export type PendingFire = {
readonly id: string;
readonly triggerId: string;
readonly packageId: string;
/** 确认人快照 —— 确认路由按它认门 */
readonly staffId: string;
readonly subject: string;
readonly data: Record<string, unknown>;
readonly source: {
readonly kind: 'periodic' | 'event' | 'registration';
readonly ref: string;
readonly by?: string;
};
readonly preview: Readonly<Record<string, string>>;
readonly createdAt: string;
readonly consumedAt?: string;
readonly workflowId?: string;
};
export type TriggerService = {
list(): Promise<TriggerConfig[]>;
upsert(config: TriggerConfig): Promise<TriggerConfig>;
@@ -31,4 +52,16 @@ export type TriggerService = {
fireByRegistration(triggerId: string, by: string, data: Record<string, unknown>): Promise<FireOutcome>;
/** 周期触发的到点扫描(由 pg-boss 定时调起) */
sweepPeriodic(now: Date): Promise<FireOutcome[]>;
/** 运营口:要不要人工确认、谁来确认 —— 与设计面的 upsert 分口 */
setConfirm(id: string, confirm: {
required: boolean;
staffId?: string;
}): Promise<void>;
/** 指定人点头 —— 幂等:已被确认过就回那条流的号,绝不起第二条 */
confirmPending(pendingId: string, by: string): Promise<{
workflowId: string;
alreadyStarted: boolean;
}>;
/** 待确认单批读 —— 工作台消息列表就地贴确认状态用 */
pendingByIds(ids: readonly string[]): Promise<PendingFire[]>;
};
+27
View File
@@ -2,6 +2,7 @@
* module: trigger —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
*/
import type { TriggerConfig } from '@contracts';
import type { PendingFire } from './contract.ts';
export type Clock = {
now(): Date;
};
@@ -13,11 +14,37 @@ export type TriggerStore = {
get(id: string): Promise<TriggerConfig | undefined>;
upsert(config: TriggerConfig): Promise<TriggerConfig>;
setEnabled(id: string, enabled: boolean): Promise<void>;
/** 运营口:确认配置 —— 设计面的 upsert 不碰这两列 */
setConfirm(id: string, confirm: {
required: boolean;
staffId?: string;
}): Promise<void>;
remove(id: string): Promise<void>;
/** 幂等:同一键至多起一单 */
claim(key: string): Promise<boolean>;
lastFiredAt(id: string): Promise<Date | undefined>;
markFired(id: string, at: Date): Promise<void>;
createPending(pending: PendingFire): Promise<void>;
getPending(id: string): Promise<PendingFire | undefined>;
pendingByIds(ids: readonly string[]): Promise<PendingFire[]>;
/** 认领:consumed_at 空才认得到 —— 两个人同时点确认,只有一个赢,绝不起两条流 */
claimPending(id: string, at: Date): Promise<boolean>;
/** 起单成功后把流号写回;起单失败时用 releasePending 把认领退回去 */
setPendingWorkflow(id: string, workflowId: string): Promise<void>;
releasePending(id: string): Promise<void>;
};
/**
* 确认台 —— app 注入 `message`(L2):命中但要人点头时,
* 把带来件内容的系统消息送到指定人的工作台。
*/
export type ConfirmDesk = {
request(input: {
pendingId: string;
staffId: string;
triggerName: string;
subject: string;
preview: Readonly<Record<string, string>>;
}): Promise<void>;
};
/** 起单 —— app 注入 `process`(L7 同层?否:trigger 在 process 之上同层,经 app 装配) */
export type ProcessGateway = {
+59
View File
@@ -1325,6 +1325,10 @@ export declare const listTriggersRoute: {
sfeel: z.ZodString;
}, z.core.$strip>], "from">;
}, z.core.$strip>>>;
confirm: z.ZodDefault<z.ZodObject<{
required: z.ZodDefault<z.ZodBoolean>;
staffId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "StaffId", "out">>;
}, z.core.$strip>>;
spec: z.ZodDiscriminatedUnion<[z.ZodObject<{
kind: z.ZodLiteral<"periodic-calendar">;
calendar: z.ZodString;
@@ -1400,6 +1404,10 @@ export declare const upsertTriggerRoute: {
sfeel: z.ZodString;
}, z.core.$strip>], "from">;
}, z.core.$strip>>>;
confirm: z.ZodDefault<z.ZodObject<{
required: z.ZodDefault<z.ZodBoolean>;
staffId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "StaffId", "out">>;
}, z.core.$strip>>;
spec: z.ZodDiscriminatedUnion<[z.ZodObject<{
kind: z.ZodLiteral<"periodic-calendar">;
calendar: z.ZodString;
@@ -1451,6 +1459,10 @@ export declare const upsertTriggerRoute: {
sfeel: z.ZodString;
}, z.core.$strip>], "from">;
}, z.core.$strip>>>;
confirm: z.ZodDefault<z.ZodObject<{
required: z.ZodDefault<z.ZodBoolean>;
staffId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "StaffId", "out">>;
}, z.core.$strip>>;
spec: z.ZodDiscriminatedUnion<[z.ZodObject<{
kind: z.ZodLiteral<"periodic-calendar">;
calendar: z.ZodString;
@@ -2123,6 +2135,53 @@ export declare const listApprovalsRoute: {
* 起一条流(测试台专用):**带节点指派**把整条流定向到指定账号 ——
* 真实任务照旧落到那个人的工作台,测试台只管起单与观测。
*/
/**
* 触发器的**运营配置**:要不要人工确认、谁来确认 —— 与设计面的 upsert 分口,
* 设计动规则不碰这两项,运营动这两项不碰规则。
*/
export declare const setTriggerConfirmRoute: {
readonly method: "POST";
readonly path: "/api/gov/triggers/:id/confirm";
readonly params: z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>;
readonly body: z.ZodObject<{
required: z.ZodBoolean;
staffId: z.ZodOptional<z.ZodString>;
}, z.core.$strip>;
readonly output: z.ZodObject<{
ok: z.ZodBoolean;
}, z.core.$strip>;
};
/** 节点负责人(运营配置):流程包的哪个节点归谁 —— 显式指派 > 这里 > 轮值 */
export declare const listNodeOwnersRoute: {
readonly method: "GET";
readonly path: "/api/gov/packages/:id/owners";
readonly params: z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>;
readonly output: z.ZodObject<{
owners: z.ZodArray<z.ZodObject<{
nodeId: z.ZodString;
staffId: z.ZodString;
}, z.core.$strip>>;
}, z.core.$strip>;
};
export declare const setNodeOwnerRoute: {
readonly method: "PUT";
readonly path: "/api/gov/packages/:id/owners/:nodeId";
readonly params: z.ZodObject<{
id: z.ZodString;
nodeId: z.ZodString;
}, z.core.$strip>;
/** null = 撤掉运营指定,回到轮值 */
readonly body: z.ZodObject<{
staffId: z.ZodNullable<z.ZodString>;
}, z.core.$strip>;
readonly output: z.ZodObject<{
ok: z.ZodBoolean;
}, z.core.$strip>;
};
export declare const testRunRoute: {
readonly method: "POST";
readonly path: "/api/gov/testbed/run";
+22
View File
@@ -2352,6 +2352,11 @@ export declare const systemMessageSchema: z.ZodObject<{
rejectedReason: z.ZodOptional<z.ZodString>;
supersededBy: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>;
confirm: z.ZodOptional<z.ZodObject<{
pendingId: z.ZodString;
consumed: z.ZodBoolean;
workflowId: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>;
}, z.core.$strip>;
export type SystemMessageView = z.infer<typeof systemMessageSchema>;
export declare const listMessagesRoute: {
@@ -2400,6 +2405,11 @@ export declare const listMessagesRoute: {
rejectedReason: z.ZodOptional<z.ZodString>;
supersededBy: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>;
confirm: z.ZodOptional<z.ZodObject<{
pendingId: z.ZodString;
consumed: z.ZodBoolean;
workflowId: z.ZodOptional<z.ZodString>;
}, z.core.$strip>>;
}, z.core.$strip>>;
}, z.core.$strip>;
};
@@ -2427,6 +2437,18 @@ export declare const answerMessageRoute: {
ok: z.ZodBoolean;
}, z.core.$strip>;
};
/** 确认起单 —— 幂等:已被确认过就如实说,并回那条流的号,绝不起第二条 */
export declare const confirmTriggerPendingRoute: {
readonly method: "POST";
readonly path: "/api/desk/trigger-pending/:id/confirm";
readonly params: z.ZodObject<{
id: z.ZodString;
}, z.core.$strip>;
readonly output: z.ZodObject<{
workflowId: z.ZodString;
alreadyStarted: z.ZodBoolean;
}, z.core.$strip>;
};
export type ListMessagesOutput = z.infer<typeof listMessagesRoute.output>;
export declare const minuteResolutionSchema: z.ZodObject<{
id: z.ZodString;
@@ -480,6 +480,10 @@ export declare const triggerConfigSchema: z.ZodObject<{
sfeel: z.ZodString;
}, z.core.$strip>], "from">;
}, z.core.$strip>>>;
confirm: z.ZodDefault<z.ZodObject<{
required: z.ZodDefault<z.ZodBoolean>;
staffId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "StaffId", "out">>;
}, z.core.$strip>>;
spec: z.ZodDiscriminatedUnion<[z.ZodObject<{
kind: z.ZodLiteral<"periodic-calendar">;
calendar: z.ZodString;