161 lines
5.5 KiB
TypeScript
161 lines
5.5 KiB
TypeScript
import { businessRouteById } from './business-routes.js';
|
||
|
||
export type LeaderTaskSummaryMilestone = 'final' | 'needs_review' | 'resolved';
|
||
export type LeaderTaskSummaryStatus = 'completed' | 'failed' | 'cancelled' | 'needs_review';
|
||
|
||
export interface LeaderTaskSummaryInput {
|
||
taskId: string;
|
||
status: string;
|
||
businessRouteId: string | null;
|
||
assigneeUsername: string;
|
||
createdAt: string | Date;
|
||
successReceipt?: Record<string, unknown> | null;
|
||
hadNeedsReview?: boolean;
|
||
}
|
||
|
||
export interface LeaderTaskSummaryProjection {
|
||
milestone: LeaderTaskSummaryMilestone;
|
||
deliveryStatus: LeaderTaskSummaryStatus;
|
||
statusLabel: '已完成' | '未完成' | '已取消' | '需人工核验';
|
||
businessLabel: string;
|
||
resultText: string;
|
||
messageText: string;
|
||
}
|
||
|
||
const COMPLETED_STATUSES = new Set(['completed', 'dry_run']);
|
||
const FAILED_STATUSES = new Set([
|
||
'failed',
|
||
'parse_failed',
|
||
'parse_blocked',
|
||
'agent_parse_blocked',
|
||
'blocked',
|
||
'operation_blocked'
|
||
]);
|
||
const NEEDS_REVIEW_STATUSES = new Set([
|
||
'reconciliation_pending',
|
||
'saved_unverified',
|
||
'execution_uncertain',
|
||
'uncertain'
|
||
]);
|
||
|
||
function singleLine(value: unknown, maxLength: number): string {
|
||
return String(value ?? '')
|
||
.normalize('NFKC')
|
||
.replace(/[\u0000-\u001f\u007f]+/gu, ' ')
|
||
.replace(/\s+/gu, ' ')
|
||
.trim()
|
||
.slice(0, maxLength);
|
||
}
|
||
|
||
function identifierValues(...values: unknown[]): string[] {
|
||
const output: string[] = [];
|
||
const visit = (value: unknown): void => {
|
||
if (Array.isArray(value)) {
|
||
value.forEach(visit);
|
||
return;
|
||
}
|
||
if (typeof value !== 'string' && typeof value !== 'number') return;
|
||
const candidate = singleLine(value, 80);
|
||
if (!candidate || !/^(?=.*\d)[A-Za-z0-9._/()-]{4,80}$/u.test(candidate)) return;
|
||
if (/^1[3-9]\d{9}$/u.test(candidate) || /^\d{15,18}[xX]?$/u.test(candidate)) return;
|
||
if (!output.includes(candidate)) output.push(candidate);
|
||
};
|
||
values.forEach(visit);
|
||
return output.slice(0, 10);
|
||
}
|
||
|
||
function completedResult(status: string, receipt: Record<string, unknown>): string {
|
||
if (status === 'dry_run') return '业务方案已生成,尚未正式提交。';
|
||
const groupNumbers = identifierValues(receipt.group_numbers, receipt.group_number);
|
||
const orderNumbers = identifierValues(receipt.order_numbers, receipt.order_number);
|
||
const details = [
|
||
groupNumbers.length ? `团号:${groupNumbers.join('、')}` : '',
|
||
orderNumbers.length ? `订单号:${orderNumbers.join('、')}` : ''
|
||
].filter(Boolean);
|
||
return details.length
|
||
? `业务处理已完成。${details.join(';')}`.slice(0, 300)
|
||
: '业务处理已完成,结果已记录。';
|
||
}
|
||
|
||
export function formatShanghaiTimestamp(value: string | Date): string {
|
||
const date = value instanceof Date ? value : new Date(value);
|
||
if (Number.isNaN(date.getTime())) return '时间未记录';
|
||
const parts = new Intl.DateTimeFormat('en-CA', {
|
||
timeZone: 'Asia/Shanghai',
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
hourCycle: 'h23'
|
||
}).formatToParts(date);
|
||
const part = (type: Intl.DateTimeFormatPartTypes) => (
|
||
parts.find((item) => item.type === type)?.value || ''
|
||
);
|
||
return `${part('year')}-${part('month')}-${part('day')} ${part('hour')}:${part('minute')}:${part('second')}`;
|
||
}
|
||
|
||
export function buildLeaderTaskSummary(
|
||
input: LeaderTaskSummaryInput
|
||
): LeaderTaskSummaryProjection | null {
|
||
const status = singleLine(input.status, 80).toLowerCase();
|
||
let deliveryStatus: LeaderTaskSummaryStatus;
|
||
let statusLabel: LeaderTaskSummaryProjection['statusLabel'];
|
||
let resultText: string;
|
||
if (COMPLETED_STATUSES.has(status)) {
|
||
deliveryStatus = 'completed';
|
||
statusLabel = '已完成';
|
||
resultText = completedResult(status, input.successReceipt || {});
|
||
} else if (FAILED_STATUSES.has(status)) {
|
||
deliveryStatus = 'failed';
|
||
statusLabel = '未完成';
|
||
resultText = '本次工作未完成,请在平台查看业务结果。';
|
||
} else if (status === 'cancelled') {
|
||
deliveryStatus = 'cancelled';
|
||
statusLabel = '已取消';
|
||
resultText = '这项工作已取消。';
|
||
} else if (NEEDS_REVIEW_STATUSES.has(status)) {
|
||
deliveryStatus = 'needs_review';
|
||
statusLabel = '需人工核验';
|
||
resultText = '当前结果需要人工核验,请勿重复提交。';
|
||
} else {
|
||
return null;
|
||
}
|
||
|
||
const milestone: LeaderTaskSummaryMilestone = deliveryStatus === 'needs_review'
|
||
? 'needs_review'
|
||
: input.hadNeedsReview
|
||
? 'resolved'
|
||
: 'final';
|
||
const route = businessRouteById(input.businessRouteId);
|
||
const businessLabel = singleLine(route?.directive || '其他任务', 120) || '其他任务';
|
||
const assignee = singleLine(input.assigneeUsername, 160) || '未知员工';
|
||
const taskId = singleLine(input.taskId, 200) || '任务编号未记录';
|
||
const header = milestone === 'resolved' ? '【员工任务摘要·结果更新】' : '【员工任务摘要】';
|
||
return {
|
||
milestone,
|
||
deliveryStatus,
|
||
statusLabel,
|
||
businessLabel,
|
||
resultText: resultText.slice(0, 300),
|
||
messageText: [
|
||
header,
|
||
`员工:${assignee}`,
|
||
`业务:${businessLabel}`,
|
||
`状态:${statusLabel}`,
|
||
`任务:${taskId}`,
|
||
`提交:${formatShanghaiTimestamp(input.createdAt)}`,
|
||
`结果:${resultText.slice(0, 300)}`
|
||
].join('\n')
|
||
};
|
||
}
|
||
|
||
export function isLeaderTaskSummaryStatus(value: unknown): boolean {
|
||
const status = singleLine(value, 80).toLowerCase();
|
||
return COMPLETED_STATUSES.has(status)
|
||
|| FAILED_STATUSES.has(status)
|
||
|| NEEDS_REVIEW_STATUSES.has(status)
|
||
|| status === 'cancelled';
|
||
}
|