fix: sequence AgentBus roster replies
This commit is contained in:
29
control-plane/src/agentbus-delivery.ts
Normal file
29
control-plane/src/agentbus-delivery.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export const AGENTBUS_GENERIC_ACCEPTED_TEXT = '已受理,正在处理。';
|
||||
export const AGENTBUS_ROSTER_WAITING_TEXT = '已识别名单业务,等待 .xls 或 .xlsx 名单附件;附件到齐前不会开始解析或执行。';
|
||||
export const AGENTBUS_ROSTER_ATTACHMENT_RECEIVED_TEXT = '名单附件已收到,正在校验并处理。';
|
||||
export const AGENTBUS_FINAL_REPLY_ELIGIBLE_FIELD = '_final_reply_eligible';
|
||||
|
||||
export interface AgentBusAcceptedDeliveryOptions {
|
||||
text?: string;
|
||||
finalReplyEligible?: boolean;
|
||||
}
|
||||
|
||||
export function createAgentBusAcceptedDeliveryPayload(
|
||||
options: AgentBusAcceptedDeliveryOptions = {}
|
||||
): Record<string, unknown> {
|
||||
const acceptedText = String(options.text || '').trim() || AGENTBUS_GENERIC_ACCEPTED_TEXT;
|
||||
return {
|
||||
event: 'task.progress',
|
||||
status: 'accepted',
|
||||
text: acceptedText,
|
||||
[AGENTBUS_FINAL_REPLY_ELIGIBLE_FIELD]: options.finalReplyEligible !== false
|
||||
};
|
||||
}
|
||||
|
||||
export function publicAgentBusDeliveryPayload(
|
||||
value: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const payload = { ...value };
|
||||
delete payload[AGENTBUS_FINAL_REPLY_ELIGIBLE_FIELD];
|
||||
return payload;
|
||||
}
|
||||
@@ -19,6 +19,11 @@ import {
|
||||
parseAgentBusInputAttachment
|
||||
} from './input-attachment.js';
|
||||
import { diagnosticDurationMs, diagnosticError } from './diagnostics.js';
|
||||
import {
|
||||
AGENTBUS_GENERIC_ACCEPTED_TEXT,
|
||||
AGENTBUS_ROSTER_ATTACHMENT_RECEIVED_TEXT,
|
||||
publicAgentBusDeliveryPayload
|
||||
} from './agentbus-delivery.js';
|
||||
|
||||
const OPEN_READY_STATE = 1;
|
||||
const MAX_COMPLETED_TASK_IDS = 2_048;
|
||||
@@ -487,7 +492,7 @@ function createDurableDeliveryFrame(
|
||||
session: AgentBusSession,
|
||||
directAttachments: AgentBusAttachmentFrame[] = []
|
||||
): AgentBusFrame {
|
||||
const payload = { ...jsonObject(delivery.payload) };
|
||||
const payload = publicAgentBusDeliveryPayload(jsonObject(delivery.payload));
|
||||
const fallbackAttachments = payloadAttachmentReferences(payload)
|
||||
.map((reference) => attachmentMetadataFrame(reference))
|
||||
.filter((attachment): attachment is AgentBusAttachmentFrame => Boolean(attachment));
|
||||
@@ -581,7 +586,7 @@ export function taskResultText(task: PublicTask): string {
|
||||
return '资料不完整,请补充必要信息。';
|
||||
}
|
||||
if (status === 'awaiting_confirmation') {
|
||||
return '已受理,正在处理。';
|
||||
return AGENTBUS_GENERIC_ACCEPTED_TEXT;
|
||||
}
|
||||
if (FAILED_TASK_STATUSES.has(status)) {
|
||||
const failure = task.failure?.failure_message || task.error_summary?.message || task.message;
|
||||
@@ -982,15 +987,16 @@ export class AgentBusListener {
|
||||
)
|
||||
));
|
||||
}
|
||||
const durableDelivery = this.durableDeliveryEnabled();
|
||||
const input: TaskMessageInput = {
|
||||
message,
|
||||
conversationId,
|
||||
taskId: frameTaskId(frame),
|
||||
...(attachments.length ? { attachments } : {}),
|
||||
idempotencyKey: this.durableDeliveryEnabled()
|
||||
idempotencyKey: durableDelivery
|
||||
? `agentbus:${this.channel?.id}:${taskId}`
|
||||
: `agentbus:${taskId}`,
|
||||
...(this.durableDeliveryEnabled() && this.channel
|
||||
...(durableDelivery && this.channel
|
||||
? {
|
||||
channelId: this.channel.id,
|
||||
agentBusRoute: {
|
||||
@@ -1001,7 +1007,6 @@ export class AgentBusListener {
|
||||
}
|
||||
: {})
|
||||
};
|
||||
if (!this.durableDeliveryEnabled()) this.sendProgress(frame, '已受理,正在处理。');
|
||||
const context: TaskContext = {
|
||||
organizationId: this.organizationId,
|
||||
userId: '',
|
||||
@@ -1019,6 +1024,36 @@ export class AgentBusListener {
|
||||
attached: result.attached,
|
||||
created: result.created
|
||||
}, 'AgentBus message ingested into task service');
|
||||
const attachmentResult = result.input_attachment || null;
|
||||
const waitingForRosterAttachment = !attachmentResult && result.task.status === 'awaiting_attachment';
|
||||
const progressText = attachmentResult?.status === 'duplicate'
|
||||
? attachmentResult.message
|
||||
: attachmentResult
|
||||
? AGENTBUS_ROSTER_ATTACHMENT_RECEIVED_TEXT
|
||||
: waitingForRosterAttachment
|
||||
? taskResultText(result.task)
|
||||
: AGENTBUS_GENERIC_ACCEPTED_TEXT;
|
||||
if (!durableDelivery) {
|
||||
this.sendProgress(
|
||||
frame,
|
||||
progressText,
|
||||
waitingForRosterAttachment || attachmentResult?.status === 'duplicate'
|
||||
);
|
||||
}
|
||||
if (waitingForRosterAttachment || attachmentResult?.status === 'duplicate') {
|
||||
this.logger.info({
|
||||
agentbus_event: waitingForRosterAttachment
|
||||
? 'roster_attachment_waiting_reply_ready'
|
||||
: 'attachment_duplicate_reply_ready',
|
||||
inbound_frame_id: taskId,
|
||||
task_id: result.task.task_id,
|
||||
task_status: result.task.status
|
||||
}, waitingForRosterAttachment
|
||||
? 'AgentBus roster attachment waiting reply ready'
|
||||
: 'AgentBus duplicate attachment reply ready');
|
||||
if (durableDelivery) await this.flushDurableDeliveries();
|
||||
return;
|
||||
}
|
||||
const rejectedAttachment = result.input_attachment?.status === 'rejected'
|
||||
? result.input_attachment
|
||||
: null;
|
||||
@@ -1030,7 +1065,7 @@ export class AgentBusListener {
|
||||
task_status: result.task.status,
|
||||
error_code: rejectedAttachment.error_code || 'roster_workbook_processing_failed'
|
||||
}, 'AgentBus roster attachment rejection reply requested');
|
||||
if (this.durableDeliveryEnabled() && this.channel && this.tasks.enqueueAgentBusResult) {
|
||||
if (durableDelivery && this.channel && this.tasks.enqueueAgentBusResult) {
|
||||
await this.tasks.enqueueAgentBusResult.call(this.tasks, this.channel.id, taskId, {
|
||||
event: 'task.result',
|
||||
status: 'failed',
|
||||
@@ -1048,7 +1083,7 @@ export class AgentBusListener {
|
||||
inbound_frame_id: taskId,
|
||||
task_id: result.task.task_id
|
||||
}, 'AgentBus requesting parse queue');
|
||||
if (this.durableDeliveryEnabled()) {
|
||||
if (durableDelivery) {
|
||||
await this.scheduleParseQueue().catch((error) => {
|
||||
this.logger.warn({
|
||||
agentbus_event: 'parse_queue_request_failed',
|
||||
@@ -1298,7 +1333,11 @@ export class AgentBusListener {
|
||||
});
|
||||
}
|
||||
|
||||
private sendProgress(inbound: AgentBusFrame, progressText: string): void {
|
||||
private sendProgress(
|
||||
inbound: AgentBusFrame,
|
||||
progressText: string,
|
||||
completeInbound = false
|
||||
): void {
|
||||
if (!this.socket || this.socket.readyState !== OPEN_READY_STATE || !this.session) {
|
||||
this.logger.warn({
|
||||
agentbus_event: 'progress_skipped',
|
||||
@@ -1321,6 +1360,7 @@ export class AgentBusListener {
|
||||
return;
|
||||
}
|
||||
this.logFrame('outbound_progress_sent', frame, 'AgentBus progress frame sent');
|
||||
if (completeInbound) this.rememberCompleted(text(inbound.id));
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
|
||||
@@ -43,6 +43,12 @@ import {
|
||||
PASSENGER_ROSTER_WORKBOOK_VERSION,
|
||||
type NormalizedPassengerRosterWorkbook
|
||||
} from './passenger-roster-workbook.js';
|
||||
import {
|
||||
AGENTBUS_ROSTER_ATTACHMENT_RECEIVED_TEXT,
|
||||
AGENTBUS_ROSTER_WAITING_TEXT,
|
||||
createAgentBusAcceptedDeliveryPayload,
|
||||
type AgentBusAcceptedDeliveryOptions
|
||||
} from './agentbus-delivery.js';
|
||||
|
||||
export interface TaskEvent {
|
||||
id: number;
|
||||
@@ -1056,7 +1062,7 @@ export function initialTaskIntakeState(routeId: BusinessRouteId | null): {
|
||||
? {
|
||||
status: 'awaiting_attachment',
|
||||
stage: 'intake',
|
||||
message: '已识别名单业务,等待 .xls 或 .xlsx 名单附件;附件到齐前不会开始解析或执行。'
|
||||
message: AGENTBUS_ROSTER_WAITING_TEXT
|
||||
}
|
||||
: { status: 'parse_queued', stage: 'parse', message: '任务已入队,等待解析。' };
|
||||
}
|
||||
@@ -2107,7 +2113,8 @@ export class TaskService {
|
||||
channelId: string | undefined,
|
||||
taskRowId: string,
|
||||
route: TaskMessageInput['agentBusRoute'] | undefined,
|
||||
conversationId: string | null
|
||||
conversationId: string | null,
|
||||
options: AgentBusAcceptedDeliveryOptions = {}
|
||||
): Promise<void> {
|
||||
if (!channelId || !route?.inboundFrameId || !route.inboundFrom) return;
|
||||
await client.query(
|
||||
@@ -2123,7 +2130,7 @@ export class TaskService {
|
||||
route.inboundFrameId,
|
||||
route.inboundFrom,
|
||||
route.conversationId || conversationId,
|
||||
{ event: 'task.progress', status: 'accepted', text: '已受理,正在处理。' }
|
||||
createAgentBusAcceptedDeliveryPayload(options)
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -2910,7 +2917,11 @@ export class TaskService {
|
||||
target.channelId || undefined,
|
||||
target.rowId,
|
||||
selection.agentBusRoute,
|
||||
target.conversationId
|
||||
target.conversationId,
|
||||
{
|
||||
text: '该名单附件已经接收,无需重复发送。',
|
||||
finalReplyEligible: false
|
||||
}
|
||||
);
|
||||
return {
|
||||
row: taskRow,
|
||||
@@ -3095,7 +3106,11 @@ export class TaskService {
|
||||
target.channelId || undefined,
|
||||
target.rowId,
|
||||
selection.agentBusRoute,
|
||||
target.conversationId
|
||||
target.conversationId,
|
||||
{
|
||||
text: AGENTBUS_ROSTER_ATTACHMENT_RECEIVED_TEXT,
|
||||
finalReplyEligible: true
|
||||
}
|
||||
);
|
||||
return { row: updatedRow, event, inputAttachment };
|
||||
});
|
||||
@@ -3229,11 +3244,16 @@ export class TaskService {
|
||||
if (!isPassengerRosterRouteId(routeId)) {
|
||||
throw new TaskError('attachment_not_supported', '只有补充录入名单业务可以接收 Excel 名单附件。', 409);
|
||||
}
|
||||
const baseResult = await this.ingestMessage(context, { ...input, attachments: undefined });
|
||||
const baseResult = await this.ingestMessage(context, {
|
||||
...input,
|
||||
attachments: undefined,
|
||||
agentBusRoute: undefined
|
||||
});
|
||||
return this.attachPassengerRosterAttachment(context, attachment, {
|
||||
taskId: baseResult.task.task_id,
|
||||
conversationId,
|
||||
channelId,
|
||||
agentBusRoute: input.agentBusRoute,
|
||||
created: baseResult.created
|
||||
});
|
||||
}
|
||||
@@ -3414,7 +3434,13 @@ export class TaskService {
|
||||
channelId,
|
||||
text(taskRow.id),
|
||||
input.agentBusRoute,
|
||||
conversationId
|
||||
conversationId,
|
||||
intake.status === 'awaiting_attachment'
|
||||
? {
|
||||
text: intake.message,
|
||||
finalReplyEligible: false
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
const event = await this.emitEvent(client, taskRow, {
|
||||
status: intake.status,
|
||||
@@ -3554,10 +3580,23 @@ export class TaskService {
|
||||
WHERE d.channel_id = $1
|
||||
AND d.delivery_kind = 'accepted'
|
||||
AND d.delivery_status = 'delivered'
|
||||
AND (d.payload ->> '_final_reply_eligible') IS DISTINCT FROM 'false'
|
||||
AND t.status NOT IN (
|
||||
'awaiting_attachment', 'parse_queued', 'parse_running', 'awaiting_confirmation',
|
||||
'confirmed', 'queued', 'accepted', 'running'
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM agentbus_deliveries newer_delivery
|
||||
WHERE newer_delivery.organization_id = d.organization_id
|
||||
AND newer_delivery.channel_id = d.channel_id
|
||||
AND newer_delivery.task_id = d.task_id
|
||||
AND newer_delivery.delivery_kind = 'accepted'
|
||||
AND (newer_delivery.payload ->> '_final_reply_eligible') IS DISTINCT FROM 'false'
|
||||
AND (
|
||||
newer_delivery.created_at > d.created_at
|
||||
OR (newer_delivery.created_at = d.created_at AND newer_delivery.id > d.id)
|
||||
)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM agentbus_deliveries result_delivery
|
||||
WHERE result_delivery.channel_id = d.channel_id
|
||||
|
||||
Reference in New Issue
Block a user