| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- '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,
- };
|