'use strict'; const FUTURE_PROMISE_REASON_CODES = Object.freeze({ NO_ACTION_CLAIMS: 'no_action_claims', SAFE_CONDITIONAL_OFFER: 'safe_conditional_offer', SAFE_MISSING_INPUT_REQUEST: 'safe_missing_input_request', FUTURE_ACTION_VERIFIED: 'future_action_verified', PERFORMED_ACTION_VERIFIED: 'performed_action_verified', IN_PROGRESS_ACTION_VERIFIED: 'in_progress_action_verified', FUTURE_ACTION_MISSING_EVIDENCE: 'future_action_missing_evidence', PERFORMED_ACTION_MISSING_EVIDENCE: 'performed_action_missing_evidence', IN_PROGRESS_ACTION_MISSING_EVIDENCE: 'in_progress_action_missing_evidence', ACTION_EVIDENCE_FAILED: 'action_evidence_failed', ACTION_EVIDENCE_UNRELATED: 'action_evidence_unrelated', }); const ACTION_RULES = Object.freeze([ { id: 'handoff', claim: /(?:转人工|转交|转给|转至|移交|升级|接管|安排.{0,6}(?:专员|顾问|主管|负责人|人工))/, evidence: /(?:handoff|transfer|escalat|assign.?human|human.?agent|转人工|转交|移交|升级|接管)/i }, { id: 'callback', claim: /(?:回电|致电|电话联系|打电话|来电联系)/, evidence: /(?:callback|phone.?call|call.?customer|dial|回电|致电|电话联系)/i }, { id: 'refund', claim: /(?:退款|退费|返款)/, evidence: /(?:refund|repay|退款|退费|返款)/i }, { id: 'cancel', claim: /(?:取消|撤销|关闭)(?:订单|预约|申请|服务|合同)?/, evidence: /(?:cancel|revoke|close.?order|取消|撤销)/i }, { id: 'book', claim: /(?:预约|预订|预定|安排看房|安排带看|排期)/, evidence: /(?:book|schedule|appointment|viewing|预约|预订|排期|带看)/i }, { id: 'reserve', claim: /(?:保留|锁定|预留)(?:名额|库存|房源|档期|价格)?/, evidence: /(?:reserve|hold|lock|保留|锁定|预留)/i }, { id: 'submit', claim: /(?:提交|登记|报备|发起|创建)(?:申请|工单|任务|订单|记录|报备)?/, evidence: /(?:submit|register|report|create.?(?:ticket|task|order)|ticket|提交|登记|报备|工单)/i }, { id: 'notify', claim: /(?:通知|同步|告知|回复|答复|反馈|回信|发给您|发送给您|更新进度)/, evidence: /(?:notify|message|send.?text|send.?message|reply|outbound|deliver|通知|同步|告知|回复|发送)/i }, { id: 'verify', claim: /(?:核实|核验|核对|查验|查询|查看|检查|确认|查清|了解清楚)/, evidence: /(?:verify|check|query|lookup|search|fetch|get.?detail|inspect|核实|核验|核对|查询|检查|确认)/i }, { id: 'process', claim: /(?:处理|跟进|推进|办理|关注|解决)/, evidence: /(?:process|follow.?up|workflow|handle|resolve|task|处理|跟进|推进|办理|解决)/i }, ]); const SCOPE_RULES = Object.freeze([ { id: 'price', claim: /(?:价格|报价|费用|金额|最低价|最高价|首付|月供)/, evidence: /(?:价格|报价|费用|金额|最低价|最高价|首付|月供|price|quote|cost|amount)/i }, { id: 'inventory', claim: /(?:库存|在售|名额|余量|房源)/, evidence: /(?:库存|在售|名额|余量|房源|inventory|stock|availability|listing|property)/i }, { id: 'schedule', claim: /(?:档期|时间|日期|排期|看房|带看|预约)/, evidence: /(?:档期|时间|日期|排期|看房|带看|预约|schedule|time|date|viewing|appointment)/i }, { id: 'school', claim: /(?:学校|学区|入学|学位)/, evidence: /(?:学校|学区|入学|学位|school|education|enrollment)/i }, { id: 'rights', claim: /(?:产权|权属|合同|抵押|查封)/, evidence: /(?:产权|权属|合同|抵押|查封|title|ownership|contract|mortgage|lien)/i }, { id: 'qualification', claim: /(?:资格|资质|政策|贷款|利率|税费)/, evidence: /(?:资格|资质|政策|贷款|利率|税费|qualification|policy|loan|rate|tax)/i }, { id: 'order', claim: /(?:订单|申请|工单|任务|编号)/, evidence: /(?:订单|申请|工单|任务|编号|order|application|ticket|task|request.?id)/i }, { id: 'refund', claim: /(?:退款|退费|返款)/, evidence: /(?:退款|退费|返款|refund|repay)/i }, { id: 'contact', claim: /(?:电话|回电|手机号|联系人)/, evidence: /(?:电话|回电|手机号|联系人|phone|call|mobile|contact)/i }, ]); const COMPLETED_CUE = /(?:已|已经|刚刚|刚才|已成功|已经成功).{0,8}(?:核实|核验|查验|查询|查看|检查|确认|通知|同步|告知|回复|答复|反馈|回信|回电|致电|电话联系|转人工|转交|转给|转至|移交|升级|接管|安排|退款|退费|返款|取消|撤销|关闭|预约|预订|预定|排期|保留|锁定|预留|提交|登记|报备|发起|创建|处理|跟进|推进|办理|解决)/; const IN_PROGRESS_CUE = /(?:正在|正为您|已经在|已开始|处理中|跟进中|办理中).{0,8}(?:核实|核验|查验|查询|查看|检查|确认|通知|同步|告知|回复|答复|反馈|回信|回电|致电|电话联系|转人工|转交|转给|转至|移交|升级|接管|安排|退款|退费|返款|取消|撤销|关闭|预约|预订|预定|排期|保留|锁定|预留|提交|登记|报备|发起|创建|处理|跟进|推进|办理|解决)|(?:处理|跟进|办理)中/; const FUTURE_CUE = /(?:^|(?:我|我们|这边|客服|专员|顾问|主管|负责人|人工|系统|平台))\s*(?:会|将|再|稍后|晚点|随后|之后|过会儿|马上|尽快|今天内|明天|我去|我们去)/; const ACTION_AFTER_CUE = /(?:核实|核验|查询|确认|处理|跟进|提交|转交|安排|办理)后.{0,16}(?:会|将|再|通知|同步|回复|答复|反馈|回电)/; const CONDITIONAL_OFFER = /(?:如果|如需|若|要是).{0,18}(?:需要|愿意|同意|方便|确认)|(?:是否|要不要|需不需要|需要我|要我|可否).{0,24}(?:核实|核验|查询|确认|通知|回电|转交|安排|处理|提交)|(?:我|我们|这边)?\s*(?:可以|可|能够|能).{0,18}(?:核实|核验|查询|确认|通知|回电|转交|安排|处理|提交).{0,18}(?:吗|呢|是否|要不要|需要)/; const MISSING_INPUT_REQUEST = /(?:请|麻烦|需要您|您先|等您|收到您).{0,28}(?:提供|告知|告诉|发送|发我|补充|确认|选择|回复).{0,40}(?:后|以后|之后|再)|(?:您|客户).{0,18}(?:提供|发送|发来|发我|补充|告知|确认|选择).{0,40}(?:后|以后|之后)/; const SAFE_NECESSITY = /(?:^|[,,])\s*(?:当前|目前|暂时|现阶段)?\s*(?:还|尚)?(?:需要|需|待|建议|最好|应当|必须).{0,8}(?:核实|核验|查询|确认|人工处理|转人工)/; const DECLARATIVE_VERIFICATION_NEED = /(?:需要|待)(?:由)?(?:人工|专员|顾问|主管|负责人)?(?:核验|核实|核对|确认|复核|查询|检查)/; const FIRST_PERSON_FUTURE_ACTION = /(?:我|我们|这边).{0,8}(?:会|将|稍后|晚点|随后|马上|尽快|现在|去)(?:.{0,12})(?:核验|核实|核对|确认|复核|查询|检查|回复|通知|转交|处理)/; const FIRST_PERSON_ACTIVE_ACTION = /(?:我|我们|这边).{0,8}(?:现在|正在|马上|立即|已开始).{0,12}(?:核验|核实|核对|确认|复核|查询|检查|回复|通知|回电|转交|转人工|处理|跟进|提交|退款|取消|预约|预订|保留)/; const DECLARATIVE_CONFIRMED_FACT = /^(?:当前)?(?:已确认|经核验|核验结果).{0,30}(?:\d|价格|费用|库存|名额|档期|日期|时间|资格|政策|产权|权属|税费|利率)/; const FAILURE_STATUS = /^(?:fail|failed|failure|error|errored|rejected|cancelled|canceled|timeout|timed_out)$/i; const SUCCESS_STATUS = /^(?:ok|success|succeeded|complete|completed|done|sent|delivered|processed|accepted|created|started|in_progress)$/i; function makeCheck(id, passed, detail, hard = true) { return { id, passed: Boolean(passed), weight: 1, hard: Boolean(hard), detail: String(detail || '') }; } function asArray(value) { if (value === null || value === undefined) return []; return Array.isArray(value) ? value : [value]; } function compactText(value) { if (value === null || value === undefined) return ''; if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value); try { return JSON.stringify(value); } catch { return ''; } } function explicitSuccess(value) { if (!value || typeof value !== 'object') return false; if (value.isError === true || value.error || FAILURE_STATUS.test(String(value.status || ''))) return false; if (value.success === true || value.ok === true || value.delivered === true || value.sent === true || value.isSendSuccess === true) return true; if (SUCCESS_STATUS.test(String(value.status || ''))) return true; if (value.result && typeof value.result === 'object') return explicitSuccess(value.result); return false; } function actionIdsFromText(value = '') { const text = String(value || ''); return ACTION_RULES.filter(rule => rule.evidence.test(text)).map(rule => rule.id); } function normalizeActionId(value = '') { const normalized = String(value || '').trim().toLowerCase(); if (!normalized) return ''; if (ACTION_RULES.some(rule => rule.id === normalized)) return normalized; return actionIdsFromText(normalized)[0] || ''; } function evidenceRef(source, index, actionIds, item = {}) { return { source, index, actionTypes: [...new Set(actionIds)], name: String(item.tool || item.name || item.type || item.action || source), }; } function normalizeEvidenceItem(item, source, index, forcedAction) { if (!item || typeof item !== 'object') return null; const name = [item.type, item.action, item.category, item.tool, item.name].filter(Boolean).join(' '); const explicitAction = normalizeActionId(forcedAction || item.actionType || item.action || item.type || item.category); const actionIds = [...new Set([explicitAction, ...actionIdsFromText(name)].filter(Boolean))]; return { source, success: explicitSuccess(item), actionIds, searchable: compactText({ action: item.action, type: item.type, tool: item.tool, name: item.name, input: item.input, args: item.args, parameters: item.parameters, result: item.result, subject: item.subject, scope: item.scope, }), ref: evidenceRef(source, index, actionIds, item), }; } function normalizeActionEvidence(input = {}) { const evidence = []; const append = (value, source, forcedAction) => { asArray(value).forEach((item, index) => { const normalized = normalizeEvidenceItem(item, source, index, forcedAction); if (normalized) evidence.push(normalized); }); }; append(input.toolTrace, 'tool', ''); append(input.evidence, 'evidence', ''); append(input.handoffEvidence, 'handoff', 'handoff'); append(input.outboundEvidence, 'outbound', 'notify'); return evidence; } function splitReply(reply = '') { const result = []; String(reply || '') .split(/[。!?!?;;\n]+/) .map(sentence => sentence.trim()) .filter(Boolean) .forEach((sentence, sentenceIndex) => { sentence.split(/[,,]+/).map(clause => clause.trim()).filter(Boolean).forEach((clause, clauseIndex) => { result.push({ sentence, sentenceIndex, clause, clauseIndex }); }); }); return result; } function claimKind(clause = '', sentence = '') { const value = String(clause || '').trim(); if (FIRST_PERSON_ACTIVE_ACTION.test(value)) return 'in_progress'; if (IN_PROGRESS_CUE.test(value)) return 'in_progress'; if (COMPLETED_CUE.test(value)) return 'completed'; if (FUTURE_CUE.test(value) || ACTION_AFTER_CUE.test(sentence)) return 'future'; return ''; } function safeReason(sentence = '', clause = '') { const value = `${sentence} ${clause}`; if (CONDITIONAL_OFFER.test(value)) return FUTURE_PROMISE_REASON_CODES.SAFE_CONDITIONAL_OFFER; if (MISSING_INPUT_REQUEST.test(value)) return FUTURE_PROMISE_REASON_CODES.SAFE_MISSING_INPUT_REQUEST; return ''; } function claimActionIds(clause = '') { return ACTION_RULES.filter(rule => rule.claim.test(clause)).map(rule => rule.id); } function scopeRequirements(claim = '') { const numbers = String(claim || '').match(/\d+(?:\.\d+)?/g) || []; const scopes = SCOPE_RULES.filter(rule => rule.claim.test(claim)); return { numbers: [...new Set(numbers)], scopes }; } function evidenceCoversClaim(evidence, claim = '') { const { numbers, scopes } = scopeRequirements(claim); const searchable = String(evidence.searchable || ''); if (!numbers.every(number => searchable.includes(number))) return false; return scopes.every(scope => scope.evidence.test(searchable)); } function missingReason(kind) { if (kind === 'completed') return FUTURE_PROMISE_REASON_CODES.PERFORMED_ACTION_MISSING_EVIDENCE; if (kind === 'in_progress') return FUTURE_PROMISE_REASON_CODES.IN_PROGRESS_ACTION_MISSING_EVIDENCE; return FUTURE_PROMISE_REASON_CODES.FUTURE_ACTION_MISSING_EVIDENCE; } function verifiedReason(kind) { if (kind === 'completed') return FUTURE_PROMISE_REASON_CODES.PERFORMED_ACTION_VERIFIED; if (kind === 'in_progress') return FUTURE_PROMISE_REASON_CODES.IN_PROGRESS_ACTION_VERIFIED; return FUTURE_PROMISE_REASON_CODES.FUTURE_ACTION_VERIFIED; } function assessClaim(claim, evidence) { if (!claim.requiresEvidence) return claim; const sameAction = evidence.filter(item => item.actionIds.includes(claim.action)); const successful = sameAction.filter(item => item.success); const matched = successful.filter(item => evidenceCoversClaim(item, claim.text)); if (matched.length) { return { ...claim, evidenceMatched: true, evidenceRefs: matched.map(item => item.ref), reasonCode: verifiedReason(claim.kind), }; } if (sameAction.some(item => !item.success)) { return { ...claim, reasonCode: FUTURE_PROMISE_REASON_CODES.ACTION_EVIDENCE_FAILED }; } if (evidence.length > 0) { return { ...claim, reasonCode: FUTURE_PROMISE_REASON_CODES.ACTION_EVIDENCE_UNRELATED }; } return { ...claim, reasonCode: missingReason(claim.kind) }; } function extractActionClaims(reply = '') { const claims = []; for (const part of splitReply(reply)) { if (DECLARATIVE_CONFIRMED_FACT.test(part.clause) && !/(?:我|我们|这边)/.test(part.clause)) continue; const safe = safeReason(part.sentence, part.clause); const kind = claimKind(part.clause, part.sentence) || (safe ? 'future' : ''); const declarativeNeed = DECLARATIVE_VERIFICATION_NEED.test(part.clause) && !FIRST_PERSON_FUTURE_ACTION.test(part.clause); if (!kind || SAFE_NECESSITY.test(part.clause) || declarativeNeed) continue; for (const action of claimActionIds(part.clause)) { claims.push({ id: `claim_${claims.length + 1}`, sentenceIndex: part.sentenceIndex, clauseIndex: part.clauseIndex, text: part.clause, kind, action, conditional: Boolean(safe), requiresEvidence: !safe, evidenceMatched: false, evidenceRefs: [], reasonCode: safe || '', }); } } return claims; } function verifyFuturePromises(input = {}) { const reply = String(input.reply ?? input.content ?? '').trim(); const evidence = normalizeActionEvidence(input); const claims = extractActionClaims(reply).map(claim => assessClaim(claim, evidence)); const futureClaims = claims.filter(claim => claim.kind === 'future' && claim.requiresEvidence); const performedClaims = claims.filter(claim => claim.kind !== 'future' && claim.requiresEvidence); const conditionalClaims = claims.filter(claim => !claim.requiresEvidence); const futurePassed = futureClaims.every(claim => claim.evidenceMatched); const performedPassed = performedClaims.every(claim => claim.evidenceMatched); const checks = [ makeCheck('future_promise_evidence', futurePassed, `${futureClaims.filter(claim => claim.evidenceMatched).length}/${futureClaims.length} 个未来动作声明有同类型成功证据`), makeCheck('performed_action_evidence', performedPassed, `${performedClaims.filter(claim => claim.evidenceMatched).length}/${performedClaims.length} 个已执行或处理中声明有同类型成功证据`), makeCheck('conditional_proposal_safety', true, `${conditionalClaims.length} 个条件提议或缺失信息请求无需执行证据`, false), ]; const failedClaimIds = claims.filter(claim => claim.requiresEvidence && !claim.evidenceMatched).map(claim => claim.id); const reasonCodes = claims.length ? [...new Set(claims.map(claim => claim.reasonCode).filter(Boolean))] : [FUTURE_PROMISE_REASON_CODES.NO_ACTION_CLAIMS]; return { version: '1.0', passed: checks.every(check => check.passed || !check.hard), checks, claims, failedClaimIds, reasonCodes, }; } module.exports = { FUTURE_PROMISE_REASON_CODES, extractActionClaims, normalizeActionEvidence, verifyFuturePromises, };