mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-20 16:10:49 +08:00
feat: add native result-file delivery
This commit is contained in:
parent
2803bbcbab
commit
d472dd5a75
65 changed files with 10205 additions and 255 deletions
153
src/channels/shared/semantic/delivery.mjs
Normal file
153
src/channels/shared/semantic/delivery.mjs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
export const DELIVERY_RECEIPT_SCHEMA_VERSION = 1;
|
||||
|
||||
const ARTIFACT_OUTCOMES = new Set(['sent', 'rejected', 'failed', 'unknown']);
|
||||
const REJECTED_ARTIFACT_ERRORS = new Set([
|
||||
'artifact-changed',
|
||||
'artifact-context-required',
|
||||
'artifact-empty',
|
||||
'artifact-invalid',
|
||||
'artifact-not-file',
|
||||
'artifact-permission-required',
|
||||
'artifact-provider-rejected',
|
||||
'artifact-too-large',
|
||||
'artifact-unavailable',
|
||||
]);
|
||||
|
||||
export function providerMessageIdsFor(value) {
|
||||
if (!value || typeof value !== 'object') return [];
|
||||
const ids = Array.isArray(value.providerMessageIds)
|
||||
? value.providerMessageIds
|
||||
.filter((candidate) => (
|
||||
(typeof candidate === 'string' && candidate.trim())
|
||||
|| Number.isSafeInteger(candidate)
|
||||
))
|
||||
.map(String)
|
||||
: [];
|
||||
const candidates = [
|
||||
value.message_id,
|
||||
value.messageId,
|
||||
value.id,
|
||||
value.ts,
|
||||
value.message?.message_id,
|
||||
value.message?.messageId,
|
||||
value.message?.id,
|
||||
value.message?.ts,
|
||||
value.key?.id,
|
||||
value.data?.message_id,
|
||||
];
|
||||
const id = candidates.find((candidate) => (
|
||||
(typeof candidate === 'string' && candidate.trim())
|
||||
|| Number.isSafeInteger(candidate)
|
||||
));
|
||||
if (id !== undefined) ids.push(String(id));
|
||||
return [...new Set(ids)];
|
||||
}
|
||||
|
||||
function requiredString(value, name) {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
throw new TypeError(`${name} must be a non-empty string`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function providerIds(values) {
|
||||
if (!Array.isArray(values)) throw new TypeError('providerMessageIds must be an array');
|
||||
const ids = [];
|
||||
const seen = new Set();
|
||||
for (const value of values) {
|
||||
const id = requiredString(value, 'providerMessageId');
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
ids.push(id);
|
||||
}
|
||||
return Object.freeze(ids);
|
||||
}
|
||||
|
||||
function artifactResults(values) {
|
||||
if (!Array.isArray(values)) throw new TypeError('artifacts must be an array');
|
||||
return Object.freeze(values.map((value) => {
|
||||
if (!value || typeof value !== 'object') {
|
||||
throw new TypeError('artifact result must be an object');
|
||||
}
|
||||
const artifactId = requiredString(value.artifactId, 'artifactId');
|
||||
if (!ARTIFACT_OUTCOMES.has(value.outcome)) {
|
||||
throw new TypeError('artifact outcome must be sent, rejected, failed, or unknown');
|
||||
}
|
||||
const reason = value.reason === undefined
|
||||
? undefined
|
||||
: requiredString(value.reason, 'artifact reason');
|
||||
return Object.freeze({
|
||||
artifactId,
|
||||
outcome: value.outcome,
|
||||
...(reason === undefined ? {} : { reason }),
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export function artifactOutcomeForError(error) {
|
||||
const code = typeof error === 'string' ? error : error?.code;
|
||||
if (code === 'artifact-delivery-uncertain') return 'unknown';
|
||||
if (REJECTED_ARTIFACT_ERRORS.has(code)) return 'rejected';
|
||||
return 'failed';
|
||||
}
|
||||
|
||||
export function createDeliveryReceipt({
|
||||
deliveryId,
|
||||
presentation,
|
||||
providerMessageIds = [],
|
||||
artifacts = [],
|
||||
}) {
|
||||
return Object.freeze({
|
||||
schemaVersion: DELIVERY_RECEIPT_SCHEMA_VERSION,
|
||||
deliveryId: requiredString(deliveryId, 'deliveryId'),
|
||||
presentation: requiredString(presentation, 'presentation'),
|
||||
providerMessageIds: providerIds(providerMessageIds),
|
||||
artifacts: artifactResults(artifacts),
|
||||
});
|
||||
}
|
||||
|
||||
export function createArtifactFailureReceipt({
|
||||
artifactId,
|
||||
deliveryId,
|
||||
error,
|
||||
presentation = 'text-fallback',
|
||||
providerMessageIds = [],
|
||||
}) {
|
||||
const code = typeof error === 'string' ? error : error?.code;
|
||||
const reason = typeof code === 'string' && code
|
||||
? code
|
||||
: 'artifact-provider-failed';
|
||||
return createDeliveryReceipt({
|
||||
deliveryId,
|
||||
presentation,
|
||||
providerMessageIds,
|
||||
artifacts: [{
|
||||
artifactId,
|
||||
outcome: artifactOutcomeForError(error),
|
||||
reason,
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
export function mergeDeliveryReceipts({ deliveryId, presentation, receipts }) {
|
||||
if (!Array.isArray(receipts) || receipts.length === 0) {
|
||||
throw new TypeError('receipts must contain at least one delivery receipt');
|
||||
}
|
||||
const messageIds = [];
|
||||
const artifacts = new Map();
|
||||
for (const receipt of receipts) {
|
||||
if (!receipt || receipt.schemaVersion !== DELIVERY_RECEIPT_SCHEMA_VERSION) {
|
||||
throw new TypeError('receipt must use DeliveryReceipt schema version 1');
|
||||
}
|
||||
messageIds.push(...(receipt.providerMessageIds ?? []));
|
||||
for (const artifact of receipt.artifacts ?? []) {
|
||||
artifacts.set(artifact.artifactId, artifact);
|
||||
}
|
||||
}
|
||||
return createDeliveryReceipt({
|
||||
deliveryId,
|
||||
presentation,
|
||||
providerMessageIds: messageIds,
|
||||
artifacts: [...artifacts.values()],
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue