mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
Claim in-flight IM turns before ownership flips active so approvals no longer stick only in DSH Web, auto-recover stale session models once, and let WhatsApp bots pick a shared default model for new sessions. Co-authored-by: Cursor <cursoragent@cursor.com>
161 lines
6.7 KiB
JavaScript
161 lines
6.7 KiB
JavaScript
import { normalizeAgentPresetCatalog, normalizeAgentPresetId, SET_AGENT_PRESET_ENDPOINT } from '../../agent-preset.js';
|
|
import {
|
|
normalizeDefaultModelSelection,
|
|
normalizeHostModelCatalog,
|
|
SET_DEFAULT_MODEL_ENDPOINT,
|
|
} from '../../default-model.js';
|
|
import { normalizeLastMessageError } from '../../last-message-error.js';
|
|
import { normalizeAccessGrant } from '../../../../src/channels/shared/access-grant.mjs';
|
|
import { normalizeAccessPolicy } from '../../../../src/channels/shared/access-policy.mjs';
|
|
import { normalizeContextEnhancementConfig } from '../../../../src/channels/shared/context-enhancement.mjs';
|
|
import { normalizeGroupSessionScope } from '../../../../src/channels/shared/session-scope.mjs';
|
|
|
|
export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
|
|
|
|
export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|
status: 'connection.status',
|
|
beginProvisioning: 'provision.begin',
|
|
pollProvisioning: 'provision.poll',
|
|
cancelProvisioning: 'provision.cancel',
|
|
reconnectBot: 'bot.reconnect',
|
|
deleteBot: 'bot.delete',
|
|
setAccessPolicy: 'bot.access-policy.set',
|
|
setAccessGrant: 'bot.access-grant.set',
|
|
resolveAccessPending: 'bot.access-pending.resolve',
|
|
refreshAccessGroupTitles: 'bot.access-group-titles.refresh',
|
|
setGroupSessionScope: 'bot.group-session-scope.set',
|
|
setWorkspace: 'bot.workspace.set',
|
|
setAgentPreset: SET_AGENT_PRESET_ENDPOINT,
|
|
setDefaultModel: SET_DEFAULT_MODEL_ENDPOINT,
|
|
setContextEnhancement: 'bot.context-enhancement.set',
|
|
resolveChannelPeer: 'bot.session.channel-peer',
|
|
});
|
|
|
|
const PROVISION_STATES = new Set(['starting', 'pending', 'connecting', 'connected', 'failed', 'cancelled']);
|
|
const BOT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
|
|
const QR_DATA_URL = /^data:image\/(?:png|webp);base64,[a-z\d+/]+={0,2}$/i;
|
|
|
|
function isRecord(value) {
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function text(value, fallback, max = 240) {
|
|
return typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : fallback;
|
|
}
|
|
|
|
function id(value) {
|
|
const result = text(value, '', 128);
|
|
return /^[a-z\d_-]+$/i.test(result) ? result : undefined;
|
|
}
|
|
|
|
function timestamp(value) {
|
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
const parsed = typeof value === 'string' ? Date.parse(value) : Number.NaN;
|
|
return Number.isNaN(parsed) ? undefined : parsed;
|
|
}
|
|
|
|
export function unwrapRpcResult(result) {
|
|
if (!isRecord(result) || typeof result.ok !== 'boolean') {
|
|
throw new Error('WhatsApp 服务返回了无法识别的响应');
|
|
}
|
|
if (!result.ok) {
|
|
const error = new Error(text(result.error?.message, 'WhatsApp 操作失败'));
|
|
error.code = text(result.error?.code, 'WHATSAPP_RPC_ERROR', 80);
|
|
throw error;
|
|
}
|
|
return result.value;
|
|
}
|
|
|
|
export function safeQrSource(value) {
|
|
return typeof value === 'string' && value.length <= 2 * 1024 * 1024 && QR_DATA_URL.test(value)
|
|
? value : undefined;
|
|
}
|
|
|
|
export function normalizeProvisioning(value, now = Date.now()) {
|
|
const source = isRecord(value?.provisioning) ? value.provisioning : value;
|
|
if (!isRecord(source)) throw new Error('WhatsApp 服务没有返回扫码进度');
|
|
const attemptId = id(source.attemptId);
|
|
if (!attemptId) throw new Error('WhatsApp 服务没有返回有效的扫码任务');
|
|
const reported = text(source.status, 'failed', 32);
|
|
const result = {
|
|
attemptId,
|
|
status: PROVISION_STATES.has(reported) ? reported : 'failed',
|
|
expiresAt: timestamp(source.expiresAt) ?? now + 60_000,
|
|
pollIntervalMs: Math.min(5_000, Math.max(500, Number(source.pollIntervalMs) || 1_000)),
|
|
qrRevision: Number.isSafeInteger(source.qrRevision) ? source.qrRevision : 0,
|
|
};
|
|
const qrCodeDataUrl = safeQrSource(source.qrCodeDataUrl);
|
|
if (qrCodeDataUrl) result.qrCodeDataUrl = qrCodeDataUrl;
|
|
if (id(source.botId)) result.botId = id(source.botId);
|
|
if (isRecord(source.error)) result.error = {
|
|
code: text(source.error.code, 'WHATSAPP_PROVISION_FAILED', 80),
|
|
message: text(source.error.message, 'WhatsApp 没有接入完成'),
|
|
};
|
|
return result;
|
|
}
|
|
|
|
function normalizeBot(value) {
|
|
if (!isRecord(value) || !id(value.botId)) return undefined;
|
|
const connected = value.connected === true;
|
|
const state = BOT_STATES.has(value.state) ? value.state : 'offline';
|
|
return {
|
|
botId: id(value.botId),
|
|
connected,
|
|
state: connected ? 'connected' : state,
|
|
workspace: text(value.workspace, '', 4_096),
|
|
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
|
defaultModel: normalizeDefaultModelSelection(value.defaultModel),
|
|
contextEnhancement: normalizeContextEnhancementConfig(value.contextEnhancement),
|
|
...(Object.hasOwn(value, 'accessPolicy')
|
|
? { accessPolicy: normalizeAccessPolicy(value.accessPolicy) }
|
|
: {}),
|
|
...(Object.hasOwn(value, 'accessGrant')
|
|
? { accessGrant: normalizeAccessGrant(value.accessGrant) }
|
|
: {}),
|
|
...(Object.hasOwn(value, 'groupSessionScope')
|
|
? { groupSessionScope: normalizeGroupSessionScope(value.groupSessionScope) }
|
|
: {}),
|
|
bot: {
|
|
name: text(value.bot?.name, 'WhatsApp机器人', 100),
|
|
idMasked: text(value.bot?.idMasked, 'WhatsApp账号', 140),
|
|
},
|
|
health: {
|
|
summary: text(value.health?.summary, connected
|
|
? 'WhatsApp Web 关联设备运行正常' : 'WhatsApp 连接尚未就绪'),
|
|
lastCheckedAt: timestamp(value.health?.lastCheckedAt),
|
|
},
|
|
lastMessageError: normalizeLastMessageError(value.lastMessageError),
|
|
error: isRecord(value.error) ? {
|
|
code: text(value.error.code, 'WHATSAPP_ACCOUNT_ERROR', 80),
|
|
message: text(value.error.message, 'WhatsApp 连接尚未就绪'),
|
|
} : null,
|
|
};
|
|
}
|
|
|
|
export function normalizeSnapshot(value) {
|
|
const source = isRecord(value?.snapshot) ? value.snapshot : value;
|
|
if (!isRecord(source) || !Array.isArray(source.bots)) {
|
|
throw new Error('WhatsApp 服务没有返回有效的机器人列表');
|
|
}
|
|
const bots = source.bots.map(normalizeBot).filter(Boolean);
|
|
return {
|
|
revision: Number.isSafeInteger(source.revision) ? source.revision : 0,
|
|
bots,
|
|
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
|
|
provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
|
|
agentPresetCatalog: normalizeAgentPresetCatalog(source.agentPresetCatalog),
|
|
modelCatalog: normalizeHostModelCatalog(source.modelCatalog),
|
|
};
|
|
}
|
|
|
|
export function presentError(error) {
|
|
return {
|
|
code: text(error?.code, 'WHATSAPP_ERROR', 80),
|
|
message: text(error?.message, 'WhatsApp 操作失败,请稍后重试'),
|
|
};
|
|
}
|
|
|
|
export function formatRemaining(milliseconds) {
|
|
const seconds = Math.max(0, Math.ceil(Number(milliseconds) / 1_000) || 0);
|
|
return `${String(Math.floor(seconds / 60)).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`;
|
|
}
|