文件内容
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
};