feat: add leader AgentBus task summaries
This commit is contained in:
1 parent
f4664997a8
commit
1a3ab63700
18 files changed
+2158
-19
No files matched your search
@@ -0,0 +1,160 @@
|
||||
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';
|
||||
}
|
||||
Reference in new issue
Block a user