文件预览

hmac_outbound_client.js

查看 Vox 自定义电话 Bot 技能包中的文件内容。

文件内容

resources/hmac_outbound_client.js

'use strict';

const crypto = require('crypto');

const OUTBOUND_PATH = '/vox/v1/outbound';
const TRIAL_OUTBOUND_PATH = '/vox/v2/outbound';

function toGMTDate(date = new Date()) {
  return date.toUTCString();
}

function sortUriParams(uriParams = '') {
  if (!uriParams) return '';
  return String(uriParams).split('&').filter(Boolean).sort().join('&');
}

function buildSignatureMessage({ method, path, uriParams, appId, dateGMT }) {
  const normalizedPath = path.startsWith('/') ? path : `/${path}`;
  return [
    String(method).toUpperCase(),
    normalizedPath,
    sortUriParams(uriParams),
    appId,
    dateGMT,
    `HMAC-APPID:${appId}`,
    ''
  ].join('\n');
}

function signHmacSha256(message, secret) {
  return crypto.createHmac('sha256', secret).update(message, 'utf8').digest('base64');
}

function buildHmacHeaders({ appId, secret, method = 'POST', path = OUTBOUND_PATH, uriParams = '', date = new Date() }) {
  const dateGMT = toGMTDate(date);
  const message = buildSignatureMessage({ method, path, uriParams, appId, dateGMT });
  const signature = signHmacSha256(message, secret);
  return {
    'Content-Type': 'application/json',
    'HMAC-APPID': appId,
    'HMAC-DATE': dateGMT,
    'HMAC-SIGNATURE': signature,
    'HMAC-ALGORITHM': 'hmac-sha256',
    'HMAC-SIGNED-HEADERS': 'HMAC-APPID'
  };
}

function buildOutboundPayload({ credentials, callee, requestId, voiceType, agentProfile }) {
  const payload = {
    botid: credentials.botId || '',
    callee,
    requestId,
    botType: 'custom',
    extra: JSON.stringify({
      voiceType: String(voiceType),
      agent_profile: agentProfile
    })
  };
  if (credentials.appId) payload.appId = credentials.appId;
  return payload;
}

async function callVoxOutbound({ credentials, payload, fetchImpl = globalThis.fetch }) {
  if (!fetchImpl) throw new Error('fetch is not available. Use Node.js >= 18 or pass fetchImpl.');
  const baseUrl = credentials.baseUrl || 'https://vox.teddymobile.cn';
  const path = credentials.trialMode ? TRIAL_OUTBOUND_PATH : OUTBOUND_PATH;
  const url = `${baseUrl.replace(/\/$/, '')}${path}`;
  const headers = credentials.trialMode
    ? { 'Content-Type': 'application/json' }
    : buildHmacHeaders({ appId: credentials.appId, secret: credentials.secret });
  const response = await fetchImpl(url, {
    method: 'POST',
    headers,
    body: JSON.stringify(payload)
  });

  const text = await response.text();
  let body;
  try {
    body = text ? JSON.parse(text) : {};
  } catch (error) {
    body = { raw: text };
  }

  return {
    ok: response.ok && (body.code === undefined || body.code === 0),
    httpStatus: response.status,
    body
  };
}

module.exports = {
  OUTBOUND_PATH,
  TRIAL_OUTBOUND_PATH,
  buildHmacHeaders,
  buildOutboundPayload,
  buildSignatureMessage,
  callVoxOutbound,
  signHmacSha256,
  sortUriParams,
  toGMTDate
};