Add unified IM channel plugin

This commit is contained in:
xmanrui 2026-08-15 04:39:56 +08:00
commit b8bd681a79
26 changed files with 7901 additions and 0 deletions

View file

@ -0,0 +1,156 @@
export const WEIXIN_RPC_CHANNEL = '/weixin';
export const WEIXIN_ENDPOINTS = Object.freeze({
status: 'connection.status',
beginProvisioning: 'provision.begin',
pollProvisioning: 'provision.poll',
submitVerification: 'provision.verify',
cancelProvisioning: 'provision.cancel',
reconnectBot: 'bot.reconnect',
deleteBot: 'bot.delete',
});
const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
const PROVISION_STATES = new Set([
'starting',
'pending',
'scanned',
'needs_verification',
'connecting',
'connected',
'expired',
'failed',
'cancelled',
]);
function isRecord(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function string(value, fallback = '') {
return typeof value === 'string' && value.trim() ? value.trim() : fallback;
}
function timestamp(value) {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
export function unwrapRpcResult(result) {
if (!isRecord(result) || typeof result.ok !== 'boolean') {
throw new Error('微信服务返回了无法识别的响应');
}
if (!result.ok) {
const error = new Error(string(result.error?.message, '微信操作失败'));
error.code = string(result.error?.code, 'WEIXIN_RPC_ERROR');
throw error;
}
return result.value;
}
export function safeQrSource(value) {
return typeof value === 'string'
&& /^data:image\/(?:png|webp|svg\+xml)(?:;charset=[^;,]+)?;base64,/i.test(value)
? value
: undefined;
}
export function safeVerificationUrl(value) {
if (typeof value !== 'string') return undefined;
try {
const url = new URL(value);
const host = url.hostname.toLowerCase();
return url.protocol === 'https:'
&& (host === 'weixin.qq.com' || host.endsWith('.weixin.qq.com'))
? url.toString()
: undefined;
} catch {
return undefined;
}
}
export function normalizeProvisioning(value) {
if (!isRecord(value) || !string(value.attemptId)) {
throw new Error('微信扫码服务没有返回有效的绑定任务');
}
const status = PROVISION_STATES.has(value.status) ? value.status : 'failed';
const result = {
attemptId: string(value.attemptId),
status,
expiresAt: timestamp(value.expiresAt) ?? Date.now(),
pollIntervalMs: Math.min(5_000, Math.max(500, Number(value.pollIntervalMs) || 1_000)),
verificationRequired: value.verificationRequired === true || status === 'needs_verification',
};
const verificationUrl = safeVerificationUrl(value.verificationUrl);
const qrCodeDataUrl = safeQrSource(value.qrCodeDataUrl);
if (verificationUrl) result.verificationUrl = verificationUrl;
if (qrCodeDataUrl) result.qrCodeDataUrl = qrCodeDataUrl;
if (string(value.botId)) result.botId = string(value.botId);
if (value.alreadyConnected === true) result.alreadyConnected = true;
if (isRecord(value.error)) {
result.error = {
code: string(value.error.code, 'WEIXIN_PROVISION_FAILED'),
message: string(value.error.message, '微信绑定没有完成'),
};
}
return result;
}
function normalizeBot(value) {
if (!isRecord(value) || !string(value.botId) || !isRecord(value.bot)) return null;
const state = ACCOUNT_STATES.has(value.state) ? value.state : 'error';
const connected = value.connected === true;
return {
botId: string(value.botId),
state: connected ? 'connected' : state,
connected,
configured: value.configured === true,
bot: {
name: string(value.bot.name, '微信机器人'),
accountIdMasked: string(value.bot.accountIdMasked, '已安全保存'),
},
health: {
status: string(value.health?.status, connected ? 'healthy' : 'offline'),
summary: string(value.health?.summary, connected ? '微信连接正常' : '微信连接未就绪'),
lastCheckedAt: timestamp(value.health?.lastCheckedAt),
},
stats: {
messagesReceived: Math.max(0, Number(value.stats?.messagesReceived) || 0),
messagesReplied: Math.max(0, Number(value.stats?.messagesReplied) || 0),
},
error: isRecord(value.error)
? {
code: string(value.error.code, 'WEIXIN_ACCOUNT_ERROR'),
message: string(value.error.message, '微信连接未就绪'),
}
: null,
};
}
export function normalizeSnapshot(value) {
if (!isRecord(value) || !Array.isArray(value.bots)) {
throw new Error('微信服务没有返回有效的账号列表');
}
const bots = value.bots.map(normalizeBot).filter(Boolean);
return {
schemaVersion: Number(value.schemaVersion) || 1,
revision: Number(value.revision) || 0,
state: string(value.state, 'offline'),
bots,
totals: {
configured: bots.length,
connected: bots.filter((bot) => bot.connected).length,
},
provisioning: value.provisioning ? normalizeProvisioning(value.provisioning) : null,
};
}
export function presentError(error) {
return {
code: string(error?.code, 'WEIXIN_ERROR'),
message: string(error?.message, '微信操作失败,请稍后重试'),
};
}
export function formatRemaining(milliseconds) {
const seconds = Math.max(0, Math.ceil(milliseconds / 1_000));
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
}