dsh-im-ops/plugin-src/host/delivery-suggestions.mjs
2026-08-30 11:24:53 +08:00

135 lines
4.3 KiB
JavaScript

const CHANNELS = new Set([
'weixin',
'feishu',
'dingtalk',
'wecom',
'qq',
'slack',
'telegram',
'discord',
'whatsapp',
]);
function isRecord(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function opaqueId(value) {
return typeof value === 'string'
&& value.length > 0
&& value.length <= 512
&& value.trim() === value
&& !/[\s:\u0000-\u001f\u007f]/u.test(value);
}
function afterPrefix(key, prefix) {
const marker = `${prefix}:`;
if (!key.startsWith(marker)) return null;
const value = key.slice(marker.length);
return opaqueId(value) ? value : null;
}
function simpleSuggestion(key, definitions) {
for (const [prefix, kind, field] of definitions) {
const value = afterPrefix(key, prefix);
if (value) return { kind, route: { [field]: value } };
}
return null;
}
function feishuSuggestion(key) {
const openId = afterPrefix(key, 'p2p');
if (openId?.startsWith('ou_')) {
return { kind: 'user', route: { openId } };
}
return simpleSuggestion(key, [['group', 'group', 'chatId']]);
}
function slackSuggestion(key) {
const directChannel = afterPrefix(key, 'direct');
if (directChannel && /^[A-Za-z0-9_-]{1,128}$/.test(directChannel)) {
return { kind: 'conversation', route: { channelId: directChannel } };
}
const match = /^group:([^:]+):(\d{1,20}(?:\.\d{1,20})?)$/.exec(key);
if (!match || !/^[A-Za-z0-9_-]{1,128}$/.test(match[1])) return null;
return { kind: 'thread', route: { channelId: match[1], threadTs: match[2] } };
}
function telegramSuggestion(key) {
const match = /^(direct|group):(-?\d+)(?::([1-9]\d*))?$/.exec(key);
if (!match) return null;
const chatId = Number(match[2]);
if (!Number.isSafeInteger(chatId)) return null;
if (match[1] === 'direct' && match[3] !== undefined) return null;
if (match[3] === undefined) return { kind: 'chat', route: { chatId: match[2] } };
const messageThreadId = Number(match[3]);
if (!Number.isSafeInteger(messageThreadId) || messageThreadId <= 0) return null;
return { kind: 'topic', route: { chatId: match[2], messageThreadId } };
}
function discordSuggestion(key) {
const match = /^(?:direct|group):(\d{1,32})$/.exec(key);
return match ? { kind: 'channel', route: { channelId: match[1] } } : null;
}
function whatsappSuggestion(key) {
const direct = /^direct:(\d{5,32}@(s\.whatsapp\.net|lid))$/.exec(key);
if (direct) return { kind: 'user', route: { jid: direct[1] } };
const group = /^group:(\d{5,32}(?:-\d{1,32})?@g\.us)$/.exec(key);
return group ? { kind: 'group', route: { jid: group[1] } } : null;
}
/** Convert one persisted conversation key into a stable proactive-delivery route. */
export function deliverySuggestionFromConversationKey(channel, key) {
if (!CHANNELS.has(channel) || typeof key !== 'string') return null;
switch (channel) {
case 'weixin':
return simpleSuggestion(key, [['p2p', 'user', 'toUserId']]);
case 'feishu':
return feishuSuggestion(key);
case 'dingtalk':
return simpleSuggestion(key, [
['p2p', 'user', 'userId'],
['group', 'group', 'openConversationId'],
]);
case 'wecom':
return simpleSuggestion(key, [
['direct', 'user', 'chatId'],
['group', 'group', 'chatId'],
]);
case 'qq':
return simpleSuggestion(key, [
['c2c', 'user', 'userOpenId'],
['group', 'group', 'groupOpenId'],
]);
case 'slack':
return slackSuggestion(key);
case 'telegram':
return telegramSuggestion(key);
case 'discord':
return discordSuggestion(key);
case 'whatsapp':
return whatsappSuggestion(key);
default:
return null;
}
}
/**
* Extract and de-duplicate stable delivery routes from a persisted sessions map.
* Session ids and every other state field are intentionally ignored.
*/
export function deliverySuggestionsFromSessions(channel, sessions) {
if (!CHANNELS.has(channel) || !isRecord(sessions)) return [];
const suggestions = [];
const seen = new Set();
for (const key of Object.keys(sessions)) {
const suggestion = deliverySuggestionFromConversationKey(channel, key);
if (!suggestion) continue;
const identity = JSON.stringify(suggestion);
if (seen.has(identity)) continue;
seen.add(identity);
suggestions.push(suggestion);
}
return suggestions;
}