mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 05:20:46 +08:00
Add unified IM channel plugin
This commit is contained in:
commit
b8bd681a79
26 changed files with 7901 additions and 0 deletions
156
plugin-src/client/channels/weixin/api.js
Normal file
156
plugin-src/client/channels/weixin/api.js
Normal 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')}`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue