企业微信通知增加原始输入

This commit is contained in:
andy committed 2026-09-10 15:58:22 +08:00
1 parent 7ea7c211cc
commit be7d765018
5 files changed
+242 -11

No files matched your search

@@ -61,6 +61,8 @@ test('projection is organization-scoped, future-only, role-safe, source-limited
assert.match(service, /assignee\.organization_id = task\.organization_id/);
assert.match(service, /assignee\.role <> 'admin'/);
assert.match(service, /task\.source IN \('manual', 'agentbus'\)/);
assert.match(service, /task\.original_text_ciphertext,/);
assert.match(service, /originalText: readLeaderSummaryOriginalText\(this\.config, row\.original_text_ciphertext\)/);
assert.match(service, /event\.payload ->> 'archived'.*IS DISTINCT FROM 'true'/s);
assert.match(service, /event\.payload ->> 'restored'.*IS DISTINCT FROM 'true'/s);
assert.match(service, /pg_try_advisory_xact_lock/);
@@ -3,8 +3,12 @@ import test from 'node:test';
import {
buildLeaderTaskSummary,
formatShanghaiTimestamp,
isLeaderTaskSummaryStatus
isLeaderTaskSummaryStatus,
LEADER_SUMMARY_MAX_MESSAGE_BYTES
} from '../src/leadership-task-summary.js';
import { loadConfig } from '../src/config.js';
import { encryptText } from '../src/crypto.js';
import { readLeaderSummaryOriginalText } from '../src/leader-notification-service.js';
const base = {
taskId: 'TASK-20260907-001',
@@ -13,6 +17,141 @@ const base = {
createdAt: '2026-09-07T00:05:06.000Z'
};
const originalHotelInput = [
'安排酒店',
'团号:LW-260915A-B',
'入住日期:2026-09-16',
'离店日期:2026-09-18',
'酒店搜索:示例酒店 twn',
'间数:9',
'备注:请保留原话、全角标点及 两个空格。'
].join('\n');
test('notification shows the original multiline input and omits the generated task-number field', () => {
const summary = buildLeaderTaskSummary({
...base,
status: 'completed',
originalText: originalHotelInput
});
assert.equal(summary?.messageText, [
'【员工任务摘要】',
'员工:employee-a',
'业务:安排酒店',
'状态:已完成',
'提交:2026-09-07 08:05:06',
'',
'原始输入:',
originalHotelInput,
'',
'结果:业务处理已完成,结果已记录。'
].join('\n'));
assert.doesNotMatch(summary?.messageText || '', /TASK-20260907-001|^任务:/mu);
});
test('original input is available even when parsing failed or the business route is unknown', () => {
for (const status of ['parse_failed', 'blocked', 'cancelled', 'saved_unverified', 'dry_run']) {
const summary = buildLeaderTaskSummary({
...base,
businessRouteId: null,
status,
originalText: '这段输入未能解析:日期稍后补充。\n第二行仍须展示。'
});
assert.match(summary?.messageText || '', /业务:其他任务/);
assert.match(summary?.messageText || '', /原始输入:\n这段输入未能解析:日期稍后补充。\n第二行仍须展示。/);
assert.doesNotMatch(summary?.messageText || '', /TASK-20260907-001|^任务:/mu);
}
});
test('result-update messages retain the original input and full result', () => {
const summary = buildLeaderTaskSummary({
...base,
status: 'completed',
hadNeedsReview: true,
originalText: originalHotelInput,
successReceipt: { group_number: 'LW-260915A-B' }
});
assert.equal(summary?.milestone, 'resolved');
assert.ok(summary?.messageText.startsWith('【员工任务摘要·结果更新】'));
assert.ok(summary?.messageText.includes(originalHotelInput));
assert.ok(summary?.messageText.endsWith('结果:业务处理已完成。团号:LW-260915A-B'));
});
test('input formatting preserves punctuation and tabs, normalizes newlines and removes nonprinting controls', () => {
const summary = buildLeaderTaskSummary({
...base,
status: 'completed',
originalText: ' 安排酒店\r\n酒店:A酒店\tTWN\r间数:9\u0000\u0007 '
});
assert.ok(summary?.messageText.includes('原始输入:\n安排酒店\n酒店:A酒店\tTWN\n间数:9\n\n结果:'));
assert.doesNotMatch(summary?.messageText || '', /\r|\u0000|\u0007/);
});
test('missing input has an explicit fallback without inventing input from a receipt', () => {
for (const originalText of [undefined, null, '', ' \n\t ']) {
const summary = buildLeaderTaskSummary({
...base,
status: 'failed',
originalText,
successReceipt: { original_text: '不能作为原始输入的回执内容' }
});
assert.match(summary?.messageText || '', /原始输入:\n原始输入暂不可用,请在平台查看。/);
assert.doesNotMatch(summary?.messageText || '', /不能作为原始输入的回执内容/);
}
});
test('UTF-8 budget retains fitting inputs and visibly truncates only overflowing input', () => {
const options = { ...base, status: 'completed', originalText: 'x' };
const fixedBytes = Buffer.byteLength(buildLeaderTaskSummary(options)!.messageText, 'utf8') - 1;
const available = LEADER_SUMMARY_MAX_MESSAGE_BYTES - fixedBytes;
const fittingInput = 'a'.repeat(available);
const fitting = buildLeaderTaskSummary({ ...options, originalText: fittingInput })!;
assert.equal(Buffer.byteLength(fitting.messageText, 'utf8'), LEADER_SUMMARY_MAX_MESSAGE_BYTES);
assert.ok(fitting.messageText.includes(fittingInput));
assert.doesNotMatch(fitting.messageText, /原文过长/);
const overflowing = buildLeaderTaskSummary({ ...options, originalText: fittingInput + 'a' })!;
assert.ok(Buffer.byteLength(overflowing.messageText, 'utf8') <= LEADER_SUMMARY_MAX_MESSAGE_BYTES);
assert.match(overflowing.messageText, /原文过长,剩余内容请在平台查看/);
assert.ok(overflowing.messageText.endsWith('结果:业务处理已完成,结果已记录。'));
});
test('very long Chinese and emoji input cannot break Unicode or displace the outcome', () => {
const summary = buildLeaderTaskSummary({
...base,
assigneeUsername: '员'.repeat(200),
status: 'completed',
hadNeedsReview: true,
originalText: '酒店🏨中文\n'.repeat(20_000),
successReceipt: { group_numbers: Array.from({ length: 10 }, (_, index) => `GROUP-${index}-${'A'.repeat(60)}`) }
})!;
assert.ok(Buffer.byteLength(summary.messageText, 'utf8') <= LEADER_SUMMARY_MAX_MESSAGE_BYTES);
assert.equal(Buffer.from(summary.messageText).toString('utf8'), summary.messageText);
assert.doesNotMatch(summary.messageText, /\uFFFD/);
assert.match(summary.messageText, /原文过长,剩余内容请在平台查看/);
assert.ok(summary.messageText.endsWith(`结果:${summary.resultText}`));
});
test('worker reads the same encrypted original input for manual and AgentBus tasks', () => {
const config = loadConfig({ NODE_ENV: 'test', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 17).toString('base64') });
const ciphertext = encryptText(config, originalHotelInput);
const originalText = readLeaderSummaryOriginalText(config, ciphertext);
assert.equal(originalText, originalHotelInput);
const summary = buildLeaderTaskSummary({ ...base, status: 'completed', originalText });
assert.ok(summary?.messageText.includes(originalHotelInput));
assert.ok(!summary?.messageText.includes(ciphertext));
});
test('missing, invalid or unreadable ciphertext does not prevent a notification', () => {
const config = loadConfig({ NODE_ENV: 'test', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 17).toString('base64') });
const otherConfig = loadConfig({ NODE_ENV: 'test', FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 18).toString('base64') });
for (const ciphertext of [null, undefined, '', {}, 'not-encrypted', encryptText(otherConfig, originalHotelInput)]) {
const originalText = readLeaderSummaryOriginalText(config, ciphertext);
assert.equal(originalText, null);
const summary = buildLeaderTaskSummary({ ...base, status: 'completed', originalText });
assert.match(summary?.messageText || '', /原始输入暂不可用/);
assert.ok(summary?.messageText.endsWith('结果:业务处理已完成,结果已记录。'));
}
});
test('leader summary projects only stable outcomes', () => {
assert.equal(buildLeaderTaskSummary({ ...base, status: 'running' }), null);
assert.equal(buildLeaderTaskSummary({ ...base, status: 'awaiting_confirmation' }), null);