Files
LWLT-AIBOT/control-plane/src/leadership-task-summary.ts
2026-09-07 13:01:01 +08:00

161 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
}