fix: classify historical dashboard task types
This commit is contained in:
@@ -1842,10 +1842,93 @@ function operationsDashboardOutcomeLabel(kind: OperationsDashboardOutcomeKind):
|
||||
return '进行中';
|
||||
}
|
||||
|
||||
function operationsDashboardBusiness(routeId: unknown): { id: string | null; label: string } {
|
||||
const route = businessRouteById(routeId);
|
||||
return route
|
||||
? { id: route.routeId, label: route.directive }
|
||||
function operationsDashboardRouteFromInstruction(value: unknown) {
|
||||
const instruction = text(value).normalize('NFKC').trim();
|
||||
if (!instruction) return null;
|
||||
const resolved = resolveBusinessRoute(instruction).route;
|
||||
if (resolved) return resolved;
|
||||
const matches = BUSINESS_ROUTES.filter((route) => (
|
||||
[route.directive, ...route.aliases].some((directive) => {
|
||||
if (!instruction.startsWith(directive)) return false;
|
||||
const remainder = instruction.slice(directive.length);
|
||||
return !remainder || /^[\s::;;]/u.test(remainder);
|
||||
})
|
||||
));
|
||||
return matches.length === 1 ? matches[0] : null;
|
||||
}
|
||||
|
||||
function operationsDashboardOperationObject(value: unknown, fallbackText: unknown): Record<string, unknown> {
|
||||
const direct = jsonObject(value);
|
||||
if (Object.keys(direct).length) return direct;
|
||||
const candidate = text(fallbackText).trim();
|
||||
if (!candidate.startsWith('{')) return {};
|
||||
try {
|
||||
return jsonObject(JSON.parse(candidate));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function operationsDashboardRouteFromOperation(value: unknown, fallbackText: unknown) {
|
||||
const operation = operationsDashboardOperationObject(value, fallbackText);
|
||||
const action = text(operation.action).trim();
|
||||
if (!action) return null;
|
||||
const candidates = BUSINESS_ROUTES.filter((route) => route.action === action);
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
const data = jsonObject(operation.data);
|
||||
if (action === 'passenger_list_import') {
|
||||
const referenceKind = text(jsonObject(data.existing_refs).kind || data.target_kind).trim();
|
||||
if (referenceKind === 'independent_order') return businessRouteById('passenger_list_import_independent');
|
||||
if (referenceKind === 'shared_child_order') return businessRouteById('passenger_list_import_shared_child');
|
||||
}
|
||||
if (action === 'arrangement_hotel') {
|
||||
const mode = text(jsonObject(data.arrangement).mode || data.mode).trim();
|
||||
if (mode === 'create') return businessRouteById('arrangement_hotel_create');
|
||||
if (mode === 'update') return businessRouteById('arrangement_hotel_update');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const OPERATIONS_DASHBOARD_FALLBACK_TYPES = [
|
||||
{ key: 'dashboard_order_delete', label: '删除订单', action: 'order_delete' },
|
||||
{ key: 'dashboard_arrangement_hotel', label: '酒店安排', action: 'arrangement_hotel' },
|
||||
{ key: 'dashboard_passenger_list_import', label: '名单导入', action: 'passenger_list_import' },
|
||||
{ key: 'dashboard_order_update', label: '订单修改', action: 'order_update' }
|
||||
] as const;
|
||||
|
||||
function operationsDashboardFallbackType(value: unknown, fallbackText: unknown) {
|
||||
const operation = operationsDashboardOperationObject(value, fallbackText);
|
||||
const action = text(operation.action).trim();
|
||||
return OPERATIONS_DASHBOARD_FALLBACK_TYPES.find((candidate) => candidate.action === action) || null;
|
||||
}
|
||||
|
||||
function operationsDashboardTypeById(value: unknown) {
|
||||
const key = text(value).trim();
|
||||
if (!key) return null;
|
||||
const route = businessRouteById(key);
|
||||
if (route) return { key: route.routeId, label: route.directive };
|
||||
return OPERATIONS_DASHBOARD_FALLBACK_TYPES.find((candidate) => candidate.key === key) || null;
|
||||
}
|
||||
|
||||
function operationsDashboardNeedsInstructionForType(routeId: unknown, operation: unknown): boolean {
|
||||
if (businessRouteById(routeId)) return false;
|
||||
if (operationsDashboardRouteFromOperation(operation, '')) return false;
|
||||
const fallback = operationsDashboardFallbackType(operation, '');
|
||||
return !fallback || fallback.action === 'arrangement_hotel' || fallback.action === 'passenger_list_import';
|
||||
}
|
||||
|
||||
function operationsDashboardBusiness(
|
||||
routeId: unknown,
|
||||
instruction?: unknown,
|
||||
operation?: unknown
|
||||
): { id: string | null; label: string } {
|
||||
const route = businessRouteById(routeId)
|
||||
|| operationsDashboardRouteFromInstruction(instruction)
|
||||
|| operationsDashboardRouteFromOperation(operation, instruction);
|
||||
if (route) return { id: route.routeId, label: route.directive };
|
||||
const fallback = operationsDashboardFallbackType(operation, instruction);
|
||||
return fallback
|
||||
? { id: fallback.key, label: fallback.label }
|
||||
: { id: null, label: '其他任务' };
|
||||
}
|
||||
|
||||
@@ -1955,12 +2038,13 @@ function operationsDashboardTaskProjection(
|
||||
config: AppConfig,
|
||||
row: Record<string, unknown>
|
||||
): OperationsDashboardTaskProjection {
|
||||
const instruction = operationsDashboardLeadershipInstructionText(decryptText(
|
||||
const rawInstruction = decryptText(
|
||||
config,
|
||||
row.original_text_ciphertext as string | null | undefined
|
||||
));
|
||||
).trim();
|
||||
const instruction = operationsDashboardLeadershipInstructionText(rawInstruction);
|
||||
const outcome = operationsDashboardOutcome(row);
|
||||
const business = operationsDashboardBusiness(row.business_route_id);
|
||||
const business = operationsDashboardBusiness(row.business_route_id, rawInstruction, row.operation);
|
||||
const instructionPreview = instruction.replace(/\s+/g, ' ').trim();
|
||||
const resultPreview = outcome.text.replace(/\s+/g, ' ').trim();
|
||||
return {
|
||||
@@ -4608,14 +4692,8 @@ export class TaskService {
|
||||
where += ` AND t.created_by = $${params.length}`;
|
||||
}
|
||||
const businessRouteId = text(options.businessRouteId).trim();
|
||||
if (businessRouteId === 'unclassified') {
|
||||
where += ' AND t.business_route_id IS NULL';
|
||||
} else if (businessRouteId) {
|
||||
if (!businessRouteById(businessRouteId)) {
|
||||
throw new TaskError('operations_dashboard_business_invalid', '看板业务类型无效。', 400);
|
||||
}
|
||||
params.push(businessRouteId);
|
||||
where += ` AND t.business_route_id = $${params.length}`;
|
||||
if (businessRouteId && businessRouteId !== 'unclassified' && !operationsDashboardTypeById(businessRouteId)) {
|
||||
throw new TaskError('operations_dashboard_business_invalid', '看板业务类型无效。', 400);
|
||||
}
|
||||
const statusCondition = operationsStatusSql(status);
|
||||
if (statusCondition) where += ` AND ${statusCondition}`;
|
||||
@@ -4626,6 +4704,7 @@ export class TaskService {
|
||||
const pool = getPool(this.config);
|
||||
const businessOptions = [
|
||||
...BUSINESS_ROUTES.map((route) => ({ key: route.routeId, label: route.directive })),
|
||||
...OPERATIONS_DASHBOARD_FALLBACK_TYPES.map(({ key, label }) => ({ key, label })),
|
||||
{ key: 'unclassified', label: '其他任务' }
|
||||
];
|
||||
const actorQuery = () => pool.query(
|
||||
@@ -4635,44 +4714,60 @@ export class TaskService {
|
||||
ORDER BY is_active DESC, username ASC`,
|
||||
[context.organizationId]
|
||||
);
|
||||
const taskSelect = `t.id, t.task_id, t.original_text_ciphertext,
|
||||
t.status, t.message, t.business_route_id, t.archived_at,
|
||||
t.created_at, t.updated_at, t.success_receipt, t.success_receipt_at,
|
||||
t.error_summary, t.error_summary_at, t.execution_result,
|
||||
t.operation, t.parse_response,
|
||||
const taskBaseSelect = `t.id, t.task_id, t.status, t.business_route_id, t.archived_at,
|
||||
t.created_at, t.updated_at, t.operation,
|
||||
creator.id AS actor_id, creator.username AS actor_username,
|
||||
creator.role AS actor_role, creator.is_active AS actor_is_active,
|
||||
to_char(timezone('Asia/Shanghai', t.created_at), 'YYYY-MM-DD') AS business_day,
|
||||
to_char(timezone('Asia/Shanghai', t.created_at), 'YYYY-MM-DD') AS business_day`;
|
||||
const taskDetailSelect = `${taskBaseSelect},
|
||||
t.original_text_ciphertext, t.message, t.success_receipt, t.success_receipt_at,
|
||||
t.error_summary, t.error_summary_at, t.execution_result, t.parse_response,
|
||||
(SELECT COUNT(*)::int
|
||||
FROM agent_session_messages input_message
|
||||
WHERE input_message.task_id = t.id
|
||||
WHERE input_message.organization_id = t.organization_id
|
||||
AND input_message.task_id = t.id
|
||||
AND input_message.role = 'user'
|
||||
AND input_message.input_source IS DISTINCT FROM 'reparse') AS input_turn_count`;
|
||||
const loadDetailedRows = async (taskRowIds: string[]) => {
|
||||
if (!taskRowIds.length) return [] as Record<string, unknown>[];
|
||||
const detailResult = await pool.query(
|
||||
`SELECT ${taskDetailSelect}
|
||||
FROM tasks t
|
||||
LEFT JOIN users creator ON creator.id = t.created_by
|
||||
WHERE t.organization_id = $1
|
||||
AND t.id = ANY($2::uuid[])`,
|
||||
[context.organizationId, taskRowIds]
|
||||
);
|
||||
return detailResult.rows as Record<string, unknown>[];
|
||||
};
|
||||
|
||||
if (search) {
|
||||
const [candidateResult, actorResult] = await Promise.all([
|
||||
pool.query(
|
||||
`SELECT ${taskSelect}
|
||||
FROM tasks t
|
||||
LEFT JOIN users creator ON creator.id = t.created_by
|
||||
WHERE ${where}
|
||||
ORDER BY t.created_at DESC, t.id DESC
|
||||
LIMIT 2001`,
|
||||
params
|
||||
),
|
||||
actorQuery()
|
||||
]);
|
||||
if (candidateResult.rows.length > 2_000) {
|
||||
throw new TaskError(
|
||||
'operations_dashboard_search_scope_too_large',
|
||||
'关键词查询范围超过 2000 条,请先缩短日期或选择人员、业务类型后再查询。',
|
||||
400
|
||||
);
|
||||
}
|
||||
const candidateRows = candidateResult.rows as Record<string, unknown>[];
|
||||
const [candidateResult, actorResult] = await Promise.all([
|
||||
pool.query(
|
||||
`SELECT ${taskBaseSelect}
|
||||
FROM tasks t
|
||||
LEFT JOIN users creator ON creator.id = t.created_by
|
||||
WHERE ${where}
|
||||
ORDER BY t.created_at DESC, t.id DESC`,
|
||||
params
|
||||
),
|
||||
actorQuery()
|
||||
]);
|
||||
if (search && candidateResult.rows.length > 2_000) {
|
||||
throw new TaskError(
|
||||
'operations_dashboard_search_scope_too_large',
|
||||
'关键词查询范围超过 2000 项任务,请先缩短日期或选择人员、任务类型后再查询。',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const candidateRows = candidateResult.rows as Record<string, unknown>[];
|
||||
const searchValuesByTask = new Map<string, string[]>();
|
||||
let projectionRows = candidateRows;
|
||||
if (search && candidateRows.length) {
|
||||
const taskRowIds = candidateRows.map((row) => text(row.id)).filter(Boolean);
|
||||
const messageResult = taskRowIds.length
|
||||
? await pool.query(
|
||||
const [detailedRows, messageResult] = await Promise.all([
|
||||
loadDetailedRows(taskRowIds),
|
||||
pool.query(
|
||||
`SELECT m.task_id, m.content_ciphertext, actor.username AS actor_username
|
||||
FROM agent_session_messages m
|
||||
LEFT JOIN users actor ON actor.id = m.actor_user_id
|
||||
@@ -4682,8 +4777,9 @@ export class TaskService {
|
||||
AND m.input_source IS DISTINCT FROM 'reparse'`,
|
||||
[context.organizationId, taskRowIds]
|
||||
)
|
||||
: { rows: [] as Record<string, unknown>[] };
|
||||
const searchValuesByTask = new Map<string, string[]>();
|
||||
]);
|
||||
const detailedById = new Map(detailedRows.map((row) => [text(row.id), row]));
|
||||
projectionRows = candidateRows.map((row) => detailedById.get(text(row.id)) || row);
|
||||
for (const row of messageResult.rows as Record<string, unknown>[]) {
|
||||
const key = text(row.task_id);
|
||||
const values = searchValuesByTask.get(key) || [];
|
||||
@@ -4695,201 +4791,90 @@ export class TaskService {
|
||||
);
|
||||
searchValuesByTask.set(key, values);
|
||||
}
|
||||
const matched = candidateRows.flatMap((row) => {
|
||||
const projection = operationsDashboardTaskProjection(this.config, row);
|
||||
const matches = operationsDashboardSearchMatches(search, [
|
||||
projection.task.task_id,
|
||||
projection.task.creator.username,
|
||||
projection.task.business_route_label,
|
||||
projection.instruction,
|
||||
projection.result,
|
||||
...(searchValuesByTask.get(text(row.id)) || [])
|
||||
]);
|
||||
return matches ? [projection.task] : [];
|
||||
});
|
||||
const actors = (actorResult.rows as Record<string, unknown>[]).map((row) => ({
|
||||
id: text(row.id),
|
||||
username: text(row.username),
|
||||
role: taskRole(row.role),
|
||||
is_active: databaseBoolean(row.is_active)
|
||||
} satisfies PublicOperationsDashboardActor));
|
||||
const summary = operationsDashboardSummaryFromTasks(matched);
|
||||
const days = operationsDashboardBreakdownsFromTasks(
|
||||
matched,
|
||||
(task) => ({ key: task.business_day, label: task.business_day }),
|
||||
(left, right) => right.key.localeCompare(left.key)
|
||||
);
|
||||
const businesses = operationsDashboardBreakdownsFromTasks(
|
||||
matched,
|
||||
(task) => ({
|
||||
key: task.business_route_id || 'unclassified',
|
||||
label: task.business_route_label
|
||||
}),
|
||||
(left, right) => right.total - left.total || left.label.localeCompare(right.label, 'zh-CN')
|
||||
);
|
||||
const pageTasks = matched.slice(boundedOffset, boundedOffset + boundedLimit);
|
||||
return {
|
||||
summary,
|
||||
users: operationsDashboardUsersFromTasks(matched),
|
||||
actors,
|
||||
days,
|
||||
businesses,
|
||||
business_options: businessOptions,
|
||||
tasks: pageTasks,
|
||||
range: { from: from.toISOString(), to: to.toISOString() },
|
||||
total: matched.length,
|
||||
offset: boundedOffset,
|
||||
limit: boundedLimit,
|
||||
has_more: boundedOffset + pageTasks.length < matched.length
|
||||
};
|
||||
} else if (candidateRows.length) {
|
||||
const classificationIds = candidateRows
|
||||
.filter((row) => operationsDashboardNeedsInstructionForType(row.business_route_id, row.operation))
|
||||
.map((row) => text(row.id))
|
||||
.filter(Boolean);
|
||||
if (classificationIds.length) {
|
||||
const classificationResult = await pool.query(
|
||||
`SELECT id, original_text_ciphertext
|
||||
FROM tasks
|
||||
WHERE organization_id = $1
|
||||
AND id = ANY($2::uuid[])`,
|
||||
[context.organizationId, classificationIds]
|
||||
);
|
||||
const instructionsById = new Map(
|
||||
(classificationResult.rows as Record<string, unknown>[])
|
||||
.map((row) => [text(row.id), row.original_text_ciphertext])
|
||||
);
|
||||
projectionRows = candidateRows.map((row) => ({
|
||||
...row,
|
||||
original_text_ciphertext: instructionsById.get(text(row.id))
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const listParams = [...params, boundedLimit, boundedOffset];
|
||||
const limitParam = params.length + 1;
|
||||
const offsetParam = params.length + 2;
|
||||
const [summaryResult, userResult, actorResult, dayResult, businessResult, listResult] = await Promise.all([
|
||||
pool.query(
|
||||
`SELECT COUNT(*)::int AS total,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_ACTIVE_SQL})::int AS active,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_COMPLETED_SQL})::int AS completed,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_ATTENTION_SQL})::int AS attention,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_FAILED_SQL})::int AS failed,
|
||||
COUNT(*) FILTER (WHERE t.status = 'cancelled')::int AS cancelled,
|
||||
COUNT(*) FILTER (WHERE t.archived_at IS NOT NULL)::int AS archived,
|
||||
COUNT(DISTINCT t.created_by)::int AS user_count
|
||||
FROM tasks t
|
||||
LEFT JOIN users creator ON creator.id = t.created_by
|
||||
WHERE ${where}`,
|
||||
params
|
||||
),
|
||||
pool.query(
|
||||
`SELECT creator.id AS actor_id, creator.username AS actor_username,
|
||||
creator.role AS actor_role, creator.is_active AS actor_is_active,
|
||||
COUNT(*)::int AS total,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_ACTIVE_SQL})::int AS active,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_COMPLETED_SQL})::int AS completed,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_ATTENTION_SQL})::int AS attention,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_FAILED_SQL})::int AS failed,
|
||||
COUNT(*) FILTER (WHERE t.status = 'cancelled')::int AS cancelled,
|
||||
COUNT(*) FILTER (WHERE t.archived_at IS NOT NULL)::int AS archived,
|
||||
MAX(t.updated_at) AS last_activity_at
|
||||
FROM tasks t
|
||||
JOIN users creator ON creator.id = t.created_by
|
||||
WHERE ${where}
|
||||
GROUP BY creator.id, creator.username, creator.role, creator.is_active
|
||||
ORDER BY total DESC, last_activity_at DESC, creator.username ASC`,
|
||||
params
|
||||
),
|
||||
actorQuery(),
|
||||
pool.query(
|
||||
`SELECT to_char(timezone('Asia/Shanghai', t.created_at), 'YYYY-MM-DD') AS dimension_key,
|
||||
COUNT(*)::int AS total,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_ACTIVE_SQL})::int AS active,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_COMPLETED_SQL})::int AS completed,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_ATTENTION_SQL})::int AS attention,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_FAILED_SQL})::int AS failed,
|
||||
COUNT(*) FILTER (WHERE t.status = 'cancelled')::int AS cancelled
|
||||
FROM tasks t
|
||||
LEFT JOIN users creator ON creator.id = t.created_by
|
||||
WHERE ${where}
|
||||
GROUP BY dimension_key
|
||||
ORDER BY dimension_key DESC`,
|
||||
params
|
||||
),
|
||||
pool.query(
|
||||
`SELECT coalesce(t.business_route_id, 'unclassified') AS dimension_key,
|
||||
COUNT(*)::int AS total,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_ACTIVE_SQL})::int AS active,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_COMPLETED_SQL})::int AS completed,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_ATTENTION_SQL})::int AS attention,
|
||||
COUNT(*) FILTER (WHERE ${OPERATIONS_FAILED_SQL})::int AS failed,
|
||||
COUNT(*) FILTER (WHERE t.status = 'cancelled')::int AS cancelled
|
||||
FROM tasks t
|
||||
LEFT JOIN users creator ON creator.id = t.created_by
|
||||
WHERE ${where}
|
||||
GROUP BY dimension_key
|
||||
ORDER BY total DESC, dimension_key ASC`,
|
||||
params
|
||||
),
|
||||
pool.query(
|
||||
`SELECT ${taskSelect}
|
||||
FROM tasks t
|
||||
LEFT JOIN users creator ON creator.id = t.created_by
|
||||
WHERE ${where}
|
||||
ORDER BY t.created_at DESC, t.id DESC
|
||||
LIMIT $${limitParam} OFFSET $${offsetParam}`,
|
||||
listParams
|
||||
)
|
||||
]);
|
||||
|
||||
const summaryRow = summaryResult.rows[0] as Record<string, unknown> | undefined;
|
||||
const summary: PublicOperationsDashboardSummary = {
|
||||
total: Number(summaryRow?.total || 0),
|
||||
active: Number(summaryRow?.active || 0),
|
||||
completed: Number(summaryRow?.completed || 0),
|
||||
attention: Number(summaryRow?.attention || 0),
|
||||
failed: Number(summaryRow?.failed || 0),
|
||||
cancelled: Number(summaryRow?.cancelled || 0),
|
||||
archived: Number(summaryRow?.archived || 0),
|
||||
user_count: Number(summaryRow?.user_count || 0)
|
||||
};
|
||||
const users = (userResult.rows as Record<string, unknown>[]).map((row) => ({
|
||||
actor: {
|
||||
id: text(row.actor_id),
|
||||
username: text(row.actor_username),
|
||||
role: taskRole(row.actor_role),
|
||||
is_active: databaseBoolean(row.actor_is_active)
|
||||
},
|
||||
total: Number(row.total || 0),
|
||||
active: Number(row.active || 0),
|
||||
completed: Number(row.completed || 0),
|
||||
attention: Number(row.attention || 0),
|
||||
failed: Number(row.failed || 0),
|
||||
cancelled: Number(row.cancelled || 0),
|
||||
archived: Number(row.archived || 0),
|
||||
last_activity_at: row.last_activity_at
|
||||
? new Date(String(row.last_activity_at)).toISOString()
|
||||
: null
|
||||
} satisfies PublicOperationsDashboardUser));
|
||||
const matched = projectionRows.flatMap((row) => {
|
||||
const projection = operationsDashboardTaskProjection(this.config, row);
|
||||
const routeMatches = !businessRouteId
|
||||
|| (businessRouteId === 'unclassified'
|
||||
? !projection.task.business_route_id
|
||||
: projection.task.business_route_id === businessRouteId);
|
||||
if (!routeMatches) return [];
|
||||
if (search && !operationsDashboardSearchMatches(search, [
|
||||
projection.task.creator.username,
|
||||
projection.task.business_route_label,
|
||||
projection.instruction,
|
||||
projection.result,
|
||||
...(searchValuesByTask.get(text(row.id)) || [])
|
||||
])) return [];
|
||||
return [{ row, task: projection.task }];
|
||||
});
|
||||
const matchedTasks = matched.map(({ task }) => task);
|
||||
const actors = (actorResult.rows as Record<string, unknown>[]).map((row) => ({
|
||||
id: text(row.id),
|
||||
username: text(row.username),
|
||||
role: taskRole(row.role),
|
||||
is_active: databaseBoolean(row.is_active)
|
||||
} satisfies PublicOperationsDashboardActor));
|
||||
const breakdown = (row: Record<string, unknown>, label: string): PublicOperationsDashboardBreakdown => ({
|
||||
key: text(row.dimension_key),
|
||||
label,
|
||||
total: Number(row.total || 0),
|
||||
active: Number(row.active || 0),
|
||||
completed: Number(row.completed || 0),
|
||||
attention: Number(row.attention || 0),
|
||||
failed: Number(row.failed || 0),
|
||||
cancelled: Number(row.cancelled || 0)
|
||||
});
|
||||
const days = (dayResult.rows as Record<string, unknown>[]).map((row) => (
|
||||
breakdown(row, text(row.dimension_key))
|
||||
));
|
||||
const businesses = (businessResult.rows as Record<string, unknown>[]).map((row) => {
|
||||
const key = text(row.dimension_key);
|
||||
return breakdown(row, key === 'unclassified' ? '其他任务' : operationsDashboardBusiness(key).label);
|
||||
});
|
||||
const tasks = (listResult.rows as Record<string, unknown>[]).map((row) => (
|
||||
operationsDashboardTaskProjection(this.config, row).task
|
||||
));
|
||||
const summary = operationsDashboardSummaryFromTasks(matchedTasks);
|
||||
const days = operationsDashboardBreakdownsFromTasks(
|
||||
matchedTasks,
|
||||
(task) => ({ key: task.business_day, label: task.business_day }),
|
||||
(left, right) => right.key.localeCompare(left.key)
|
||||
);
|
||||
const businesses = operationsDashboardBreakdownsFromTasks(
|
||||
matchedTasks,
|
||||
(task) => ({
|
||||
key: task.business_route_id || 'unclassified',
|
||||
label: task.business_route_label
|
||||
}),
|
||||
(left, right) => right.total - left.total || left.label.localeCompare(right.label, 'zh-CN')
|
||||
);
|
||||
const pageMatches = matched.slice(boundedOffset, boundedOffset + boundedLimit);
|
||||
let pageTasks = pageMatches.map(({ task }) => task);
|
||||
if (!search && pageMatches.length) {
|
||||
const detailedRows = await loadDetailedRows(pageMatches.map(({ row }) => text(row.id)));
|
||||
const detailedById = new Map(detailedRows.map((row) => [text(row.id), row]));
|
||||
pageTasks = pageMatches.map(({ row, task }) => {
|
||||
const detailed = detailedById.get(text(row.id));
|
||||
return detailed ? operationsDashboardTaskProjection(this.config, detailed).task : task;
|
||||
});
|
||||
}
|
||||
return {
|
||||
summary,
|
||||
users,
|
||||
users: operationsDashboardUsersFromTasks(matchedTasks),
|
||||
actors,
|
||||
days,
|
||||
businesses,
|
||||
business_options: businessOptions,
|
||||
tasks,
|
||||
tasks: pageTasks,
|
||||
range: { from: from.toISOString(), to: to.toISOString() },
|
||||
total: summary.total,
|
||||
total: matchedTasks.length,
|
||||
offset: boundedOffset,
|
||||
limit: boundedLimit,
|
||||
has_more: boundedOffset + tasks.length < summary.total
|
||||
has_more: boundedOffset + pageTasks.length < matchedTasks.length
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4909,7 +4894,8 @@ export class TaskService {
|
||||
to_char(timezone('Asia/Shanghai', t.created_at), 'YYYY-MM-DD') AS business_day,
|
||||
(SELECT COUNT(*)::int
|
||||
FROM agent_session_messages input_message
|
||||
WHERE input_message.task_id = t.id
|
||||
WHERE input_message.organization_id = t.organization_id
|
||||
AND input_message.task_id = t.id
|
||||
AND input_message.role = 'user'
|
||||
AND input_message.input_source IS DISTINCT FROM 'reparse') AS input_turn_count
|
||||
FROM tasks t
|
||||
|
||||
Reference in New Issue
Block a user