feat: ship deterministic parser and lifecycle release

This commit is contained in:
inman committed 2026-08-28 17:13:58 +08:00
1 parent 208c434b31
commit 7b5d855b09
176 files changed
+20248 -1362

No files matched your search

+114 -3
View File
@@ -8,6 +8,7 @@ import {
type AgentBusTaskGateway,
createTaskResultFrame,
createTaskProgressFrame,
extractAgentBusBusinessText,
isInboundAgentBusTask,
parseAgentBusFrame,
sessionFromReadyFrame,
@@ -15,7 +16,12 @@ import {
taskResultStatus,
taskResultText
} from '../src/agentbus.js';
import { mergeRuntimeChannelStatuses, type PublicAgentBusChannel } from '../src/agentbus-channels.js';
import {
AgentBusManager,
mergeRuntimeChannelStatuses,
type PublicAgentBusChannel
} from '../src/agentbus-channels.js';
import { resolveBusinessRoute } from '../src/business-routes.js';
import type { AgentBusDelivery, PublicTask } from '../src/task-service.js';
function makeTask(status: string, overrides: Partial<PublicTask> = {}): PublicTask {
@@ -33,6 +39,24 @@ function makeTask(status: string, overrides: Partial<PublicTask> = {}): PublicTa
recovery_count: 0,
session_id_present: false
},
parser: {
route_id: null,
input_contract_version: null,
configured_mode: 'ai',
config_revision: 0,
authoritative_engine: null,
engine_affinity: 'ai',
version: null,
fallback_reason: null,
comparison_status: null,
decision_id: null,
review_status: null,
diff_paths: [],
field_differences: [],
unreviewed_difference_count: 0,
shadow_differences: [],
reparse_count: 0
},
input_request: null,
important_message: null,
operation: null,
@@ -109,6 +133,7 @@ function publicChannelFixture(overrides: Partial<PublicAgentBusChannel> = {}): P
enabled: true,
status: 'disabled',
key_configured: true,
deletable: true,
last_connected_at: null,
last_error: null,
created_at: new Date().toISOString(),
@@ -146,6 +171,32 @@ test('database-managed channels may use the global WebSocket URL without a globa
assert.equal(config.AGENTBUS_WS_TOKEN, undefined);
});
test('WeChat transport envelope exposes only its business text to the global parser', () => {
const businessText = [
'独立团单个下单',
'发团日期:2026-11-02',
'预订客户:示例客户',
'产品搜索:示例产品',
'预估人数:15+1',
'用房数量:TWN 8,SGL 1'
].join('\n');
const wrapped = [
'New WeChat message',
'Conversation: conversation-example-1',
`Text: ${businessText}`
].join('\n');
const extracted = extractAgentBusBusinessText(wrapped);
assert.equal(extracted, businessText);
assert.equal(resolveBusinessRoute(extracted).routeId, 'team_order_create');
const missingConversation = `New WeChat message\nConversation:\nText: ${businessText}`;
const wrongTextLabel = `New WeChat message\nConversation: conversation-example-1\nBody: ${businessText}`;
const emptyBody = 'New WeChat message\nConversation: conversation-example-1\nText: ';
assert.equal(extractAgentBusBusinessText(missingConversation), missingConversation);
assert.equal(extractAgentBusBusinessText(wrongTextLabel), wrongTextLabel);
assert.equal(extractAgentBusBusinessText(emptyBody), emptyBody.trim());
});
test('channel directory prefers the current listener over stale persisted disabled status', () => {
const channel = publicChannelFixture({ status: 'disabled', last_error: '旧进程状态' });
const connected = mergeRuntimeChannelStatuses([channel], [{
@@ -406,6 +457,19 @@ test('AgentBus result text uses the unified important message and preserves the
});
assert.equal(taskResultStatus(erpFailure), 'failed');
assert.equal(taskResultText(erpFailure), '此团还有【应收团款】账,不能取消!');
const uncertainWrite = makeTask('reconciliation_pending', {
important_message: {
kind: 'error',
text: 'ERP 写入结果不确定:write_after_requery_mismatch',
recorded_at: new Date().toISOString()
}
});
assert.equal(taskResultStatus(uncertainWrite), 'failed');
assert.equal(
taskResultText(uncertainWrite),
'ERP 写入已发起,但系统尚未确认最终结果。请勿重复提交同一任务,等待管理员只读核验。'
);
});
test('AgentBus attachment references omit archived visitor XLS and keep only XLSX delivery', () => {
@@ -564,16 +628,27 @@ test('AgentBus listener connects with the documented Authorization header and re
from: 'channel:wechat:user-1',
payload: { text: '格式不符合监听契约' }
}));
const wrappedBusinessText = [
'New WeChat message',
'Conversation: conversation-wechat-1',
'Text: 独立团单个下单',
'发团日期:2026-11-02',
'预订客户:示例客户',
'产品搜索:示例产品',
'预估人数:15+1',
'用房数量:TWN 8,SGL 1'
].join('\n');
socket.emit('message', JSON.stringify({
id: 'channel-event-1',
type: 'event',
from: 'channel:wechat:user-1',
payload: { text: '创建一个新团', reply_policy: { progress: true } }
payload: { text: wrappedBusinessText, reply_policy: { progress: true } }
}));
await new Promise((resolve) => setTimeout(resolve, 50));
assert.equal(received.length, 1);
assert.equal(capturedContext.source, 'agentbus');
assert.equal(received[0].message, wrappedBusinessText.split('\n').slice(2).join('\n').replace(/^Text:\s*/, ''));
assert.equal(received[0].conversationId, 'agentbus:channel:wechat:user-1');
assert.equal(received[0].idempotencyKey, 'agentbus:channel-event-1');
assert.equal(socket.sent.length, 2);
@@ -593,7 +668,9 @@ test('AgentBus listener connects with the documented Authorization header and re
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'session_ready'));
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'frame_ignored' && entry.metadata.ignore_reason === 'unexpected_type:message'));
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'frame_received' && entry.metadata.frame_id === 'channel-event-1'));
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'frame_received' && entry.metadata.text_preview === '创建一个新团'));
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'frame_received' && entry.metadata.text_preview === wrappedBusinessText));
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'task_processing_started'
&& entry.metadata.transport_envelope_unwrapped === true));
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'task_ingested' && entry.metadata.task_id === 'TASK-1'));
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'outbound_progress_sent'));
assert.ok(logs.some((entry) => entry.metadata.agentbus_event === 'outbound_result_sent'));
@@ -617,6 +694,40 @@ test('listener reload stop does not overwrite the channel status as disabled', (
assert.deepEqual(statuses, ['disabled']);
});
test('channel manager replays a reload requested while another reload is in flight', async () => {
const manager = new AgentBusManager({
config: testConfig(),
tasks: {} as AgentBusTaskGateway,
organizationId: 'org-1',
scheduleParseQueue: async () => {}
});
const service = manager.channelService as unknown as {
ensureLegacyChannel: (organizationId: string) => Promise<void>;
listEnabledSecrets: (organizationId: string) => Promise<[]>;
};
let listCalls = 0;
let releaseFirstList: (() => void) | undefined;
service.ensureLegacyChannel = async () => {};
service.listEnabledSecrets = async () => {
listCalls += 1;
if (listCalls === 1) {
await new Promise<void>((resolve) => {
releaseFirstList = resolve;
});
}
return [];
};
const initialReload = manager.start();
while (!releaseFirstList) await new Promise<void>((resolve) => setImmediate(resolve));
const mutationReload = manager.reload();
releaseFirstList();
await Promise.all([initialReload, mutationReload]);
assert.equal(listCalls, 2);
await manager.stop();
});
test('listener shutdown carries its session epoch for stale-writer protection', () => {
const socket = new FakeSocket();
const changes: Array<{ status: string; epoch: number | null }> = [];