文件预览

index.js

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

文件内容

resources/index.js

'use strict';

const { parsePromptToIntent, mergeIntent } = require('./prompt_to_call_intent');
const { checkCompleteness } = require('./intent_completeness');
const { checkSafety } = require('./content_safety_guard');
const { buildAgentProfile } = require('./prompt_to_agent_profile');
const { VOICE_OPTIONS, chooseVoiceType } = require('./voice_type_selector');
const { loadCredentials } = require('./credentials_loader');
const { buildOutboundPayload, callVoxOutbound } = require('./hmac_outbound_client');
const { createRequestId } = require('./request_id');
const { maskPhone } = require('./phone_validator');
const { formatTrialUsage, formatTrialUsageWithRegistration, readTrialState, recordTrialCall } = require('./trial_state');
const { buildTaskBriefing } = require('./task_briefing');
const { buildCallTask } = require('./call_task');
const { analyzeVoiceRequirement } = require('./voice_requirements');
const { buildFailureAdvice, buildResultMeaning } = require('./result_advice');
const { classifyRisk } = require('./risk_classifier');

async function handlePrompt(prompt, options = {}) {
  const previousIntent = options.previousIntent || {};
  const intent = parsePromptToIntent(prompt, previousIntent);
  const env = options.env || process.env;
  const credentialsResult = options.credentials
    ? { ok: true, credentials: options.credentials, missing: [] }
    : loadCredentials(env);
  if (intent.useMode === 'trial' && !options.credentials) {
    credentialsResult.ok = true;
    credentialsResult.missing = [];
    credentialsResult.credentials.appId = '';
    credentialsResult.credentials.secret = '';
    credentialsResult.credentials.trialMode = true;
  }
  const credentials = credentialsResult.ok
    ? credentialsResult.credentials
    : { appId: 'VOX_APP_ID', secret: 'VOX_SECRET', botId: '', baseUrl: 'https://vox.teddymobile.cn', trialMode: false };
  const trialState = credentials.trialMode ? readTrialState(env) : null;
  const safety = checkSafety(intent);

  if (!safety.ok) {
    return {
      status: 'blocked',
      intent,
      message: appendTrialAwareness(`不能发起该电话任务:${safety.reason}`, credentials, trialState)
    };
  }

  if (safety.action === 'allow_with_constraint') {
    intent.constraint = [intent.constraint, safety.reason].filter(Boolean).join(';');
  }

  const completeness = checkCompleteness(intent);
  if (!completeness.complete) {
    const guidance = completeness.missing.includes('useMode')
      ? completeness.question
      : appendTrialAwareness(completeness.question, credentials, trialState);
    const voiceFields = completeness.missing.includes('voiceType') ? buildVoiceChoiceFields(intent) : {};
    const contextFields = completeness.missing.includes('businessContext') ? buildBusinessContextFields(completeness.businessContext) : {};
    return {
      status: 'needs_input',
      intent,
      missing: completeness.missing,
      businessContext: completeness.businessContext,
      ...voiceFields,
      ...contextFields,
      message: guidance
    };
  }

  const voiceType = chooseVoiceType(intent);
  const voiceAnalysis = analyzeVoiceRequirement(intent, voiceType);
  const risk = classifyRisk(intent);
  if (!risk.suitableForCustomBot) {
    return {
      status: risk.level === 'high' ? 'blocked' : 'needs_input',
      intent,
      risk,
      message: appendTrialAwareness(`${risk.reason}\n\n建议:${risk.suggestedAction}`, credentials, trialState)
    };
  }

  const agentProfile = buildAgentProfile(intent);
  const requestId = options.requestId || createRequestId();
  const briefing = buildTaskBriefing({ intent, agentProfile, voice: voiceAnalysis, risk });
  agentProfile.taskBriefing = briefing.taskBriefing;
  agentProfile.voiceGuidance = {
    voiceType: voiceAnalysis.selectedVoiceType,
    voiceName: voiceAnalysis.selectedVoiceName,
    deliveryStyle: voiceAnalysis.deliveryStyle.join('、'),
    scenarioFit: voiceAnalysis.scenarioFit,
    interruptHandling: '用户打断时停止当前话术,先回应用户问题,避免重复上一句话。'
  };
  const callTask = buildCallTask({ intent, agentProfile, voiceType, taskBriefing: briefing.taskBriefing, requestId });

  if (!credentialsResult.ok && !options.noCall) {
    if (intent.useMode === 'formal') {
      return {
        status: 'needs_registration',
        intent,
        missing: credentialsResult.missing,
        message: [
          `你选择了正式账号模式,但当前环境缺少正式 Vox 凭证:${credentialsResult.missing.join(', ')}。`,
          '',
          '请先注册 Vox 企业账号并完成认证,然后配置专属 VOX_APP_ID / VOX_SECRET。',
          `注册入口:${credentials.registerUrl || 'https://vox-ai.teddymobile.cn/trial/apply'}`,
          '如果你只是想先体验,可以回复“试用”,系统会使用推广试用能力。'
        ].join('\n')
      };
    }
    return {
      status: 'missing_credentials',
      intent,
      missing: credentialsResult.missing,
      message: `电话任务信息已完整,但当前环境缺少 Vox 正式外呼凭证:${credentialsResult.missing.join(', ')}。请先配置凭证后再发起外呼。`
    };
  }

  const payload = buildOutboundPayload({
    credentials,
    callee: intent.callee,
    requestId,
    voiceType,
    agentProfile
  });

  if (credentials.trialMode && trialState && trialState.used >= trialState.limit && !options.noCall) {
    const registrationGuide = buildRegistrationGuide(credentials, trialState);
    const visibleTrialState = enrichTrialState(trialState, registrationGuide);
    const registrationFields = buildRegistrationFields(registrationGuide);
    return {
      status: 'needs_registration',
      intent,
      agentProfile,
      voiceType,
      requestId,
      payload,
      callTask,
      taskBriefing: briefing,
      briefingQuality: briefing.briefingQuality,
      risk,
      voiceAnalysis,
      resultMeaning: buildResultMeaning('needs_registration'),
      ...buildSummaryFields({ status: 'needs_registration', intent, agentProfile, voiceAnalysis, requestId, trialState: visibleTrialState, resultMeaning: buildResultMeaning('needs_registration') }),
      trialState: visibleTrialState,
      trialUsage: visibleTrialState.usageTextWithRegistration,
      trial: visibleTrialState.usageTextWithRegistration,
      registrationGuide,
      nextStep: registrationGuide.callToAction,
      ...registrationFields,
      message: appendRegistrationGuide('推广试用额度已用完,暂不能继续使用试用凭证发起外呼。', credentials, trialState)
    };
  }

  if (options.noCall) {
    const registrationGuide = credentials.trialMode ? buildRegistrationGuide(credentials, trialState) : null;
    const visibleTrialState = enrichTrialState(trialState, registrationGuide);
    const registrationFields = buildRegistrationFields(registrationGuide);
    return {
      status: 'ready',
      intent,
      agentProfile,
      voiceType,
      requestId,
      payload,
      callTask,
      taskBriefing: briefing,
      briefingQuality: briefing.briefingQuality,
      risk,
      voiceAnalysis,
      resultMeaning: buildResultMeaning('ready'),
      ...buildSummaryFields({ status: 'ready', intent, agentProfile, voiceAnalysis, requestId, trialState: visibleTrialState, resultMeaning: buildResultMeaning('ready') }),
      trialState: visibleTrialState,
      trialUsage: visibleTrialState ? visibleTrialState.usageTextWithRegistration : '',
      trial: visibleTrialState ? visibleTrialState.usageTextWithRegistration : '',
      registrationGuide,
      nextStep: registrationGuide ? registrationGuide.callToAction : '',
      ...registrationFields,
      message: appendRegistrationGuide('已生成 Vox 自定义 Bot 外呼请求;当前 noCall=true,未调用 Vox。', credentials, trialState)
    };
  }

  const result = await callVoxOutbound({
    credentials,
    payload,
    fetchImpl: options.fetchImpl
  });

  const updatedTrialState = credentials.trialMode
    ? recordTrialCall({ requestId, callee: maskPhone(intent.callee), status: result.ok ? 'accepted' : 'failed' }, env)
    : null;

  const registrationGuide = credentials.trialMode ? buildRegistrationGuide(credentials, updatedTrialState) : null;
  const visibleTrialState = enrichTrialState(updatedTrialState, registrationGuide);
  const registrationFields = buildRegistrationFields(registrationGuide);
  const resultMeaning = buildResultMeaning(result.ok ? 'accepted' : 'failed');
  const failureAdvice = result.ok ? null : buildFailureAdvice(result);

  return {
    status: result.ok ? 'accepted' : 'failed',
    intent,
    agentProfile,
    voiceType,
    requestId,
    payload,
    callTask,
    taskBriefing: briefing,
    briefingQuality: briefing.briefingQuality,
    risk,
    voiceAnalysis,
    resultMeaning,
    resultAdvice: resultMeaning.meaning,
    afterCallNextStep: resultMeaning.whereToCheckResult,
    failureAdvice,
    ...buildSummaryFields({ status: result.ok ? 'accepted' : 'failed', intent, agentProfile, voiceAnalysis, requestId, trialState: visibleTrialState, resultMeaning }),
    vox: result,
    trialState: visibleTrialState,
    trialUsage: visibleTrialState ? visibleTrialState.usageTextWithRegistration : '',
    trial: visibleTrialState ? visibleTrialState.usageTextWithRegistration : '',
    registrationGuide,
    nextStep: registrationGuide ? registrationGuide.callToAction : '',
    ...registrationFields,
    message: formatResultMessage({ result, intent, agentProfile, requestId, credentials, trialState: updatedTrialState })
  };
}

function buildSummaryFields({ status, intent, agentProfile, voiceAnalysis, requestId, trialState, resultMeaning }) {
  const trialRegistrationSuffix = trialState && trialState.registerUrl
    ? `|正式使用请注册:${trialState.registerUrl}`
    : '';
  const rows = [
    ['被叫号码', maskPhone(intent.callee)],
    ['使用方式', intent.useMode === 'trial' ? '推广试用' : '正式账号'],
    ['Bot 角色', agentProfile.role],
    ['音色', `${voiceAnalysis.selectedVoiceName}(${voiceAnalysis.selectedVoiceType})`],
    ['任务目标', agentProfile.goals],
    ['状态', status],
    ['requestId', requestId]
  ];
  if (trialState && trialState.registerUrl) {
    rows.splice(6, 0, ['试用后下一步', `注册正式账号:${trialState.registerUrl}`]);
  }
  if (trialState && trialState.usageTextWithRegistration) {
    rows.push(['试用额度', trialState.registerUrl
      ? `${trialState.usageTextWithRegistration};正式使用请注册:${trialState.registerUrl}`
      : trialState.usageTextWithRegistration]);
  }
  const footerParts = [];
  if (resultMeaning && resultMeaning.meaning) footerParts.push(resultMeaning.meaning);
  if (trialState && trialState.choicesText) footerParts.push(trialState.choicesText);
  return {
    summaryTitle: status === 'accepted'
      ? `已发起 Vox 试用外呼${trialRegistrationSuffix}`
      : `Vox 自定义 Bot 外呼任务${trialRegistrationSuffix}`,
    summaryRows: rows,
    summaryFooter: footerParts.join('\n'),
    nextActionsText: trialState && trialState.choicesText ? trialState.choicesText : '',
    resultAdvice: resultMeaning ? resultMeaning.meaning : '',
    afterCallNextStep: resultMeaning ? resultMeaning.whereToCheckResult : ''
  };
}

function formatResultMessage({ result, intent, agentProfile, requestId, credentials = {}, trialState = null }) {
  if (result.ok) {
    const data = result.body && result.body.data ? result.body.data : {};
    return appendRegistrationGuide([
      '已发起 Vox 自定义 Bot 外呼。',
      '',
      `- 被叫号码:${maskPhone(intent.callee)}`,
      `- Bot 角色:${agentProfile.role}`,
      `- 任务目标:${agentProfile.goals}`,
      `- requestId:${data.requestId || requestId}`,
      `- 状态:${data.status || 'accepted'}`
    ].join('\n'), credentials, trialState);
  }

  const body = result.body || {};
  return appendRegistrationGuide([
    '外呼发起失败。',
    '',
    `- HTTP 状态:${result.httpStatus}`,
    `- code:${body.code === undefined ? 'unknown' : body.code}`,
    `- msg:${body.msg || body.raw || 'unknown'}`,
    `- requestId:${requestId}`
  ].join('\n'), credentials, trialState);
}

function appendRegistrationGuide(message, credentials = {}, trialState = null) {
  if (!credentials.trialMode) return message;
  const usage = formatTrialUsage(trialState);
  const guide = buildRegistrationGuide(credentials, trialState);
  return [
    message,
    '',
    usage,
    `正式使用可注册 Vox 企业账号:${guide.registerUrl}`
  ].filter(Boolean).join('\n');
}

function buildRegistrationGuide(credentials = {}, trialState = null) {
  return {
    title: '正式使用建议',
    callToAction: '如果你希望继续使用电话 Bot,请现在注册 Vox 企业账号。',
    registerUrl: credentials.registerUrl || 'https://vox-ai.teddymobile.cn/trial/apply',
    benefits: '注册后你将获得:专属 VOX_APP_ID / VOX_SECRET、正式外呼额度、企业权限、号码资源和生产接入支持。',
    switchInstruction: '完成注册后,把新的 VOX_APP_ID / VOX_SECRET 替换当前试用配置,即可切换为正式账号。',
    trialUsage: formatTrialUsage(trialState)
  };
}

function enrichTrialState(trialState = null, registrationGuide = null) {
  if (!trialState || !registrationGuide) return trialState;
  return {
    ...trialState,
    usageText: formatTrialUsage(trialState),
    usageTextWithRegistration: formatTrialUsageWithRegistration(trialState, registrationGuide.registerUrl),
    nextStep: registrationGuide.callToAction,
    registerUrl: registrationGuide.registerUrl,
    registrationActionText: `注册正式账号:${registrationGuide.registerUrl}`,
    choicesText: `请选择下一步:[注册正式账号] ${registrationGuide.registerUrl} | [继续试用] | [我已有正式凭证]`
  };
}

function buildRegistrationFields(registrationGuide = null) {
  if (!registrationGuide) {
    return {
      registrationRequired: false,
      registrationTitle: '',
      registrationMessage: '',
      registrationUrl: '',
      registrationBenefits: '',
      registrationSwitchInstruction: '',
      display: null
    };
  }
  const registrationMessage = [
    registrationGuide.title,
    registrationGuide.callToAction,
    `注册入口:${registrationGuide.registerUrl}`,
    registrationGuide.benefits,
    registrationGuide.switchInstruction
  ].join('\n');
  const actions = buildRegistrationActions(registrationGuide);
  return {
    registrationRequired: true,
    registrationTitle: registrationGuide.title,
    registrationMessage,
    registrationUrl: registrationGuide.registerUrl,
    registrationBenefits: registrationGuide.benefits,
    registrationSwitchInstruction: registrationGuide.switchInstruction,
    actions,
    buttons: actions,
    quickReplies: actions,
    suggestedActions: actions,
    choices: actions,
    actionPrompt: '请选择下一步:注册正式账号,或继续使用剩余试用额度。',
    display: {
      registrationGuide: registrationMessage,
      actionPrompt: '请选择下一步:注册正式账号,或继续使用剩余试用额度。',
      actions,
      buttons: actions,
      nextStep: registrationGuide.callToAction,
      registrationUrl: registrationGuide.registerUrl
    }
  };
}

function buildRegistrationActions(registrationGuide = {}) {
  return [
    {
      id: 'register_formal_account',
      type: 'url',
      label: '注册正式账号',
      title: '注册正式账号',
      description: '获取专属 VOX_APP_ID / VOX_SECRET、正式额度、企业权限和号码资源。',
      url: registrationGuide.registerUrl || 'https://vox-ai.teddymobile.cn/trial/apply',
      value: '正式注册'
    },
    {
      id: 'continue_trial',
      type: 'reply',
      label: '继续试用',
      title: '继续试用',
      description: '继续使用当前推广试用额度体验电话 Bot。',
      value: '继续试用'
    },
    {
      id: 'setup_formal_credentials',
      type: 'reply',
      label: '我已有正式凭证',
      title: '我已有正式凭证',
      description: '切换为正式账号模式,并配置专属 VOX_APP_ID / VOX_SECRET。',
      value: '我已有正式凭证'
    }
  ];
}

function buildVoiceChoiceFields(intent = {}) {
  return {
    voiceOptions: VOICE_OPTIONS,
    voiceChoices: VOICE_OPTIONS,
    voiceButtons: VOICE_OPTIONS.map((option) => ({
      id: option.id,
      type: 'reply',
      label: option.label,
      title: option.label,
      description: option.description,
      value: option.value
    })),
    actionPrompt: '请选择 Bot 音色(完整 5 种):',
    display: {
      actionPrompt: '请选择 Bot 音色(完整 5 种):',
      voiceOptions: VOICE_OPTIONS,
      voiceButtons: VOICE_OPTIONS.map((option) => ({
        id: option.id,
        type: 'reply',
        label: option.label,
        description: option.description,
        value: option.value
      })),
      scenario: intent.scenario || ''
    }
  };
}

function buildBusinessContextFields(businessContext = {}) {
  return {
    businessContextRequired: true,
    businessContextQuestion: businessContext.question || '',
    businessContextScenario: businessContext.scenario || 'generic',
    businessContextMissing: businessContext.missing || [],
    businessContextSuggestedFields: businessContext.suggestedFields || [],
    actionPrompt: businessContext.question || '请补充更具体的业务背景。',
    display: {
      actionPrompt: businessContext.question || '请补充更具体的业务背景。',
      businessContext
    }
  };
}

function appendTrialAwareness(message, credentials = {}, trialState = null) {
  if (!credentials.trialMode) return message;
  const usage = formatTrialUsage(trialState);
  return [
    '当前为推广试用模式。',
    usage,
    '',
    message
  ].filter(Boolean).join('\n');
}

module.exports = {
  handlePrompt,
  mergeIntent,
  parsePromptToIntent,
  checkCompleteness,
  checkSafety,
  buildAgentProfile,
  chooseVoiceType,
  buildOutboundPayload,
  formatResultMessage,
  appendRegistrationGuide,
  appendTrialAwareness,
  buildRegistrationGuide,
  buildRegistrationFields,
  buildRegistrationActions,
  buildVoiceChoiceFields,
  buildBusinessContextFields
};