@alipay/alipay-aipay 1.8.2 → 1.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +3 -3
  2. package/skill/VERSION +1 -1
  3. package/skill/references/integration/flow.md +27 -30
  4. package/skill/references/integration/modules/checklist.md +4 -6
  5. package/skill/references/integration/modules/sandbox/sandbox-setup-guide.md +2 -2
  6. package/skill/references/normal/integration-worker.md +1 -1
  7. package/skill/references/normal/json/customer-messages.json +1 -1
  8. package/skill/references/normal/scripts/flow_loader.mjs +9 -0
  9. package/skill/references/normal/scripts/integration_message_runner.mjs +81 -14
  10. package/skill/references/normal/scripts/onboarding_message_runner.mjs +80 -250
  11. package/skill/references/normal/scripts/render_customer_message.mjs +6 -6
  12. package/skill/references/onboarding/flow.md +9 -9
  13. package/skill/references/onboarding/modules/product-sign.md +1 -1
  14. package/skill/references/onboarding/modules/scripts/mcc.sh +151 -290
  15. package/skill/references/runtime-entry.md +2 -2
  16. package/skill-pre/VERSION +1 -1
  17. package/skill-pre/references/integration/flow.md +27 -30
  18. package/skill-pre/references/integration/modules/checklist.md +4 -6
  19. package/skill-pre/references/integration/modules/sandbox/sandbox-setup-guide.md +2 -2
  20. package/skill-pre/references/normal/integration-worker.md +1 -1
  21. package/skill-pre/references/normal/json/customer-messages.json +1 -1
  22. package/skill-pre/references/normal/scripts/flow_loader.mjs +9 -0
  23. package/skill-pre/references/normal/scripts/integration_message_runner.mjs +81 -14
  24. package/skill-pre/references/normal/scripts/onboarding_message_runner.mjs +80 -250
  25. package/skill-pre/references/normal/scripts/render_customer_message.mjs +6 -6
  26. package/skill-pre/references/onboarding/flow.md +9 -9
  27. package/skill-pre/references/onboarding/modules/product-sign.md +1 -1
  28. package/skill-pre/references/onboarding/modules/scripts/mcc.sh +151 -290
  29. package/skill-pre/references/runtime-entry.md +2 -2
@@ -244,13 +244,6 @@ function assertMccQuery(query) {
244
244
  }
245
245
 
246
246
  const mccSelectionReceiptPrefix = 'AIPAY_MCC_SELECTION_V1.';
247
- const ignoredMccQueryTokens = new Set([
248
- '服务', '企业', '个人', '提供', '适用', '用于', '平台', '项目', '申请', '商家', '公司',
249
- '互联', '联网', '互联网', '在线', '用品', '收费', '缴费', '费用', '适用于', '经营',
250
- '业务', '相关', '主体', '从事', '网站', '网页', '购买', '买票', '购票', '票务', '售票',
251
- '预约', '预定', '退票', '支付', '付款', '收款', '下单', '订单', '交易', '销售', '售卖',
252
- '商品', '产品'
253
- ]);
254
247
 
255
248
  function activeMccRunId(env = process.env) {
256
249
  const candidate = env.ALIPAY_AIPAY_RUN_ID
@@ -378,8 +371,10 @@ function mccCatalogForAgent(records) {
378
371
  }
379
372
 
380
373
  function parseAgentCandidateRefs(args, records) {
381
- const raw = optional(args, 'agent-candidate-refs-json');
382
- if (raw === undefined) return { codes: [] };
374
+ if (!Object.prototype.hasOwnProperty.call(args, 'agent-candidate-refs-json')) {
375
+ return { provided: false, codes: [] };
376
+ }
377
+ const raw = args['agent-candidate-refs-json'];
383
378
  let parsed;
384
379
  try {
385
380
  parsed = JSON.parse(raw);
@@ -397,7 +392,7 @@ function parseAgentCandidateRefs(args, records) {
397
392
  }
398
393
  codes.push(refs.get(ref));
399
394
  }
400
- return { codes };
395
+ return { provided: true, codes };
401
396
  }
402
397
 
403
398
  function mccTableSummary(records) {
@@ -435,190 +430,12 @@ function directMccCode(query, records) {
435
430
  return { isCode: true, matches: [...records.keys()].filter((code) => code.endsWith(`_${raw}`)) };
436
431
  }
437
432
 
438
- function mccNgrams(value, size) {
439
- const grams = new Set();
440
- const searchableText = meaningfulMccSearch(value);
441
- for (const segment of searchableText.split(/\s+/u).filter(Boolean)) {
442
- const characters = [...segment];
443
- for (let index = 0; index <= characters.length - size; index += 1) {
444
- const gram = characters.slice(index, index + size).join('');
445
- if (/^[\p{L}\p{N}]+$/u.test(gram)) grams.add(gram);
446
- }
447
- }
448
- return grams;
449
- }
450
-
451
- function meaningfulMccSearch(value) {
452
- let searchableText = normalizeSearch(value);
453
- const ignoredTerms = [...ignoredMccQueryTokens]
454
- .sort((left, right) => [...right].length - [...left].length);
455
- for (const term of ignoredTerms) searchableText = searchableText.replaceAll(term, ' ');
456
- return searchableText
457
- .trim()
458
- .replace(/\s+/gu, ' ')
459
- .split(' ')
460
- .filter((segment) => [...segment].length >= 2)
461
- .join(' ');
462
- }
463
-
464
- function parseMccSemanticTerms(args) {
465
- const raw = optional(args, 'semantic-terms-json');
466
- if (raw === undefined) return [];
467
- let parsed;
468
- try {
469
- parsed = JSON.parse(raw);
470
- } catch {
471
- throw new Error('semantic-terms-json 必须是合法 JSON 字符串数组');
472
- }
473
- if (!Array.isArray(parsed) || parsed.length > 12 || parsed.some((term) => typeof term !== 'string')) {
474
- throw new Error('semantic-terms-json 必须是最多 12 项的 JSON 字符串数组');
475
- }
476
- const normalized = [];
477
- let totalLength = 0;
478
- for (const rawTerm of parsed) {
479
- if (/\u0000|[\u0001-\u001f\u007f]/u.test(rawTerm)) {
480
- throw new Error('semantic-terms-json 包含控制字符');
481
- }
482
- const term = normalizeSearch(rawTerm);
483
- const termLength = [...term.replace(/\s+/gu, '')].length;
484
- if (termLength < 2 || termLength > 40) {
485
- throw new Error('semantic-terms-json 每项长度必须为 2 到 40 个字符');
486
- }
487
- if (/(?:A\d{4}\s*)?B\d{4}/iu.test(term)) {
488
- throw new Error('semantic-terms-json 只能包含业务概念词,禁止提供 MCC 编码');
489
- }
490
- totalLength += termLength;
491
- if (!normalized.includes(term)) normalized.push(term);
492
- }
493
- if (totalLength > 240) throw new Error('semantic-terms-json 总长度不得超过 240 个字符');
494
- return normalized.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
495
- }
496
-
497
- function mccDocumentFrequency(indexedRecords) {
498
- const frequencies = new Map();
499
- for (const indexed of indexedRecords) {
500
- const documentGrams = new Set([
501
- ...indexed.nameBigrams,
502
- ...indexed.nameTrigrams,
503
- ...indexed.businessBigrams,
504
- ...indexed.businessTrigrams
505
- ]);
506
- for (const gram of documentGrams) frequencies.set(gram, (frequencies.get(gram) || 0) + 1);
507
- }
508
- return frequencies;
509
- }
510
-
511
- function weightedMccOverlap(queryGrams, documentGrams, frequencies, recordCount, size) {
512
- let weight = 0;
513
- for (const gram of queryGrams) {
514
- if (!documentGrams.has(gram)) continue;
515
- const documentCount = frequencies.get(gram) || 0;
516
- weight += (Math.log((recordCount + 1) / (documentCount + 1)) + 1) * (size === 3 ? 1.6 : 1);
517
- }
518
- return weight;
519
- }
520
-
521
- function mccPhraseCoverage(haystackSegments, needleSegments) {
522
- let coverage = 0;
523
- for (const needle of needleSegments) {
524
- if (ignoredMccQueryTokens.has(needle)) continue;
525
- const needleLength = [...needle].length;
526
- for (const haystack of haystackSegments) {
527
- if (!haystack.includes(needle)) continue;
528
- coverage = Math.max(coverage, needleLength / [...haystack].length);
529
- }
530
- }
531
- return coverage;
532
- }
533
-
534
- function mccTextEvidence(value, indexed, frequencies, recordCount) {
535
- const normalizedValue = normalizeSearch(value);
536
- const valueSegments = meaningfulMccSearch(normalizedValue).split(' ').filter(Boolean);
537
- const valueBigrams = mccNgrams(normalizedValue, 2);
538
- const valueTrigrams = mccNgrams(normalizedValue, 3);
539
- const exactName = indexed.name === normalizedValue
540
- || (normalizedValue.split(' ').length === 1 && indexed.nameSegments.includes(normalizedValue));
541
- const nameContainsValueCoverage = mccPhraseCoverage(indexed.nameEvidenceSegments, valueSegments);
542
- const valueContainsNameCoverage = mccPhraseCoverage(valueSegments, indexed.nameEvidenceSegments);
543
- const businessContainsValueCoverage = mccPhraseCoverage(indexed.businessEvidenceSegments, valueSegments);
544
- const nameBigram = weightedMccOverlap(valueBigrams, indexed.nameBigrams, frequencies, recordCount, 2);
545
- const nameTrigram = weightedMccOverlap(valueTrigrams, indexed.nameTrigrams, frequencies, recordCount, 3);
546
- const businessBigram = weightedMccOverlap(valueBigrams, indexed.businessBigrams, frequencies, recordCount, 2);
547
- const businessTrigram = weightedMccOverlap(valueTrigrams, indexed.businessTrigrams, frequencies, recordCount, 3);
548
- const matchedBigrams = new Set([
549
- ...[...valueBigrams].filter((gram) => indexed.nameBigrams.has(gram)),
550
- ...[...valueBigrams].filter((gram) => indexed.businessBigrams.has(gram))
551
- ]);
552
- const matchedTrigrams = new Set([
553
- ...[...valueTrigrams].filter((gram) => indexed.nameTrigrams.has(gram)),
554
- ...[...valueTrigrams].filter((gram) => indexed.businessTrigrams.has(gram))
555
- ]);
556
- const rareBigramDocumentLimit = Math.max(1, Math.ceil(recordCount * 0.02));
557
- const hasRareBigram = [...matchedBigrams].some((gram) => (
558
- (frequencies.get(gram) || recordCount) <= rareBigramDocumentLimit
559
- ));
560
- const hasLexicalEvidence = matchedTrigrams.size > 0
561
- || matchedBigrams.size >= 2
562
- || (valueBigrams.size <= 2 && hasRareBigram);
563
- const phraseEvidence = nameContainsValueCoverage > 0
564
- || valueContainsNameCoverage > 0
565
- || businessContainsValueCoverage > 0;
566
- return {
567
- eligible: exactName || phraseEvidence || hasLexicalEvidence,
568
- exactName,
569
- score: (exactName ? 100000 : 0)
570
- + (nameContainsValueCoverage * 6000)
571
- + (valueContainsNameCoverage * 3000)
572
- + (businessContainsValueCoverage * 2500)
573
- + ((nameBigram + nameTrigram) * 120)
574
- + ((businessBigram + businessTrigram) * 40)
575
- };
576
- }
577
-
578
- function deterministicMccMatches(query, records, { limit = 8, semanticTerms = [] } = {}) {
433
+ function directMccName(query, records) {
579
434
  const normalizedQuery = assertMccQuery(query);
580
- const indexedRecords = [...records.values()].map((record) => {
581
- const name = normalizeSearch(record.name);
582
- const businesses = normalizeSearch(record.applicableBusinesses);
583
- return {
584
- record,
585
- name,
586
- nameSegments: name.split(' ').filter(Boolean),
587
- nameEvidenceSegments: meaningfulMccSearch(name).split(' ').filter(Boolean),
588
- businessEvidenceSegments: meaningfulMccSearch(businesses).split(' ').filter(Boolean),
589
- nameBigrams: mccNgrams(name, 2),
590
- nameTrigrams: mccNgrams(name, 3),
591
- businessBigrams: mccNgrams(businesses, 2),
592
- businessTrigrams: mccNgrams(businesses, 3)
593
- };
594
- });
595
- const frequencies = mccDocumentFrequency(indexedRecords);
596
- const scored = [];
597
-
598
- for (const indexed of indexedRecords) {
599
- const originalEvidence = mccTextEvidence(normalizedQuery, indexed, frequencies, indexedRecords.length);
600
- const semanticEvidence = semanticTerms
601
- .map((term) => mccTextEvidence(term, indexed, frequencies, indexedRecords.length))
602
- .filter((evidence) => evidence.eligible)
603
- .sort((left, right) => right.score - left.score)[0];
604
- if (!originalEvidence.eligible && !semanticEvidence) continue;
605
- scored.push({
606
- record: indexed.record,
607
- score: originalEvidence.score + ((semanticEvidence?.score || 0) * 0.7),
608
- exactName: originalEvidence.exactName,
609
- originalEligible: originalEvidence.eligible,
610
- semanticEligible: Boolean(semanticEvidence),
611
- semanticExactName: Boolean(semanticEvidence?.exactName)
612
- });
613
- }
614
-
615
- const exactMatches = scored.filter(({ exactName }) => exactName);
616
- const ranked = exactMatches.length > 0
617
- ? exactMatches
618
- : scored;
619
- return ranked
620
- .sort((left, right) => right.score - left.score || left.record.code.localeCompare(right.record.code))
621
- .slice(0, limit);
435
+ return sortedMccRecords(records)
436
+ .filter((record) => normalizeSearch(record.name) === normalizedQuery)
437
+ .map((record) => record.code)
438
+ .slice(0, 8);
622
439
  }
623
440
 
624
441
  function createMccSelectionReceipt(productType, query, result, mccRunContext = captureMccRunContext()) {
@@ -628,7 +445,6 @@ function createMccSelectionReceipt(productType, query, result, mccRunContext = c
628
445
  mccRunContext.runId,
629
446
  productType,
630
447
  assertMccQuery(query),
631
- JSON.stringify(result.semanticTerms || []),
632
448
  table.sha256,
633
449
  table.recordCount,
634
450
  codes
@@ -636,8 +452,8 @@ function createMccSelectionReceipt(productType, query, result, mccRunContext = c
636
452
  return `${mccSelectionReceiptPrefix}${codes}.${createHash('sha256').update(binding).digest('base64url')}`;
637
453
  }
638
454
 
639
- function resolveMccSelection(productType, query, selectedInput, receipt, records, semanticTerms, agentCandidateCodes, mccRunContext) {
640
- const candidateCodes = mccSelectionReceiptCodes(productType, query, receipt, records, semanticTerms, mccRunContext);
455
+ function resolveMccSelection(productType, query, selectedInput, receipt, records, mccRunContext) {
456
+ const candidateCodes = mccSelectionReceiptCodes(productType, query, receipt, records, mccRunContext);
641
457
  if (!candidateCodes) {
642
458
  const semanticStatus = typeof receipt === 'string' && receipt.startsWith(mccSelectionReceiptPrefix)
643
459
  ? 'SELECTION_RECEIPT_STALE'
@@ -646,10 +462,8 @@ function resolveMccSelection(productType, query, selectedInput, receipt, records
646
462
  query,
647
463
  productType,
648
464
  records,
649
- semanticTerms,
650
465
  semanticStatus,
651
- mccRunContext,
652
- agentCandidateCodes
466
+ mccRunContext
653
467
  );
654
468
  return {
655
469
  ...refreshed,
@@ -666,8 +480,7 @@ function resolveMccSelection(productType, query, selectedInput, receipt, records
666
480
  deterministicCandidateCodes: [],
667
481
  deterministicStrongCandidateCodes: [],
668
482
  deterministicWeakCandidateCodes: [],
669
- semanticCandidateCodes: [],
670
- semanticTerms,
483
+ agentCandidateCodes: [],
671
484
  mergedCandidateCodes: candidateCodes,
672
485
  mccTableSummary: currentTable,
673
486
  candidates: candidateCodes.map((code) => mccCandidateFromRecord(records.get(code), productType, mccRunContext))
@@ -680,15 +493,14 @@ function resolveMccSelection(productType, query, selectedInput, receipt, records
680
493
  deterministicCandidateCodes: [],
681
494
  deterministicStrongCandidateCodes: [],
682
495
  deterministicWeakCandidateCodes: [],
683
- semanticCandidateCodes: [],
684
- semanticTerms,
496
+ agentCandidateCodes: [],
685
497
  mergedCandidateCodes: [selectedCode],
686
498
  mccTableSummary: currentTable,
687
499
  candidates: [candidate]
688
500
  };
689
501
  }
690
502
 
691
- function mccSelectionReceiptCodes(productType, query, receipt, records, semanticTerms, mccRunContext = captureMccRunContext()) {
503
+ function mccSelectionReceiptCodes(productType, query, receipt, records, mccRunContext = captureMccRunContext()) {
692
504
  if (typeof receipt !== 'string' || !receipt.startsWith(mccSelectionReceiptPrefix)) return null;
693
505
  const body = receipt.slice(mccSelectionReceiptPrefix.length);
694
506
  const separator = body.lastIndexOf('.');
@@ -699,8 +511,7 @@ function mccSelectionReceiptCodes(productType, query, receipt, records, semantic
699
511
  || candidateCodes.some((code) => !records.has(code))
700
512
  || receipt !== createMccSelectionReceipt(productType, query, {
701
513
  mergedCandidateCodes: candidateCodes,
702
- mccTableSummary: currentTable,
703
- semanticTerms
514
+ mccTableSummary: currentTable
704
515
  }, mccRunContext)) {
705
516
  return null;
706
517
  }
@@ -711,10 +522,9 @@ function resolveMccResultFromRecords(
711
522
  query,
712
523
  productType,
713
524
  records,
714
- semanticTerms = [],
715
525
  semanticStatus,
716
526
  mccRunContext = captureMccRunContext(),
717
- agentCandidateCodes = []
527
+ agentReview = { provided: false, codes: [] }
718
528
  ) {
719
529
  const directCode = directMccCode(query, records);
720
530
  if (directCode.isCode) {
@@ -727,9 +537,7 @@ function resolveMccResultFromRecords(
727
537
  deterministicCandidateCodes: [code],
728
538
  deterministicStrongCandidateCodes: [code],
729
539
  deterministicWeakCandidateCodes: [],
730
- semanticCandidateCodes: [],
731
540
  agentCandidateCodes: [],
732
- semanticTerms,
733
541
  mergedCandidateCodes: [code],
734
542
  mccTableSummary: mccTableSummary(records),
735
543
  candidates: [candidate]
@@ -742,52 +550,69 @@ function resolveMccResultFromRecords(
742
550
  deterministicCandidateCodes: finalCodes,
743
551
  deterministicStrongCandidateCodes: finalCodes,
744
552
  deterministicWeakCandidateCodes: [],
745
- semanticCandidateCodes: [],
746
553
  agentCandidateCodes: [],
747
- semanticTerms,
748
554
  mergedCandidateCodes: finalCodes,
749
555
  mccTableSummary: mccTableSummary(records),
750
556
  candidates: finalCodes.map((code) => mccCandidateFromRecord(records.get(code), productType, mccRunContext))
751
557
  };
752
558
  }
753
559
 
754
- const matches = deterministicMccMatches(query, records, { limit: 8, semanticTerms });
755
- const deterministicCodes = matches.filter(({ originalEligible }) => originalEligible).map(({ record }) => record.code);
756
- const semanticCodes = matches.filter(({ semanticEligible }) => semanticEligible).map(({ record }) => record.code);
757
- const hasExactSemanticName = matches.some(({ semanticExactName }) => semanticExactName);
758
- const localCodes = matches.map(({ record }) => record.code);
759
- const mergedCodes = [...agentCandidateCodes, ...localCodes]
760
- .filter((code, index, all) => all.indexOf(code) === index);
761
- const finalCodes = mergedCodes.slice(0, 8);
560
+ const exactNameCodes = directMccName(query, records);
561
+ if (exactNameCodes.length > 0) {
562
+ const candidates = exactNameCodes.map((code) => mccCandidateFromRecord(records.get(code), productType, mccRunContext));
563
+ return {
564
+ matchType: candidates.length === 1 ? 'UNIQUE' : 'MULTIPLE',
565
+ semanticStatus: 'NOT_REQUIRED_EXACT_NAME',
566
+ deterministicCandidateCodes: exactNameCodes,
567
+ deterministicStrongCandidateCodes: exactNameCodes,
568
+ deterministicWeakCandidateCodes: [],
569
+ agentCandidateCodes: [],
570
+ mergedCandidateCodes: exactNameCodes,
571
+ mccTableSummary: mccTableSummary(records),
572
+ candidates
573
+ };
574
+ }
575
+
576
+ if (!agentReview.provided) {
577
+ return {
578
+ matchType: 'NONE',
579
+ semanticStatus: semanticStatus || 'AGENT_CATALOG_REVIEW_REQUIRED',
580
+ deterministicCandidateCodes: [],
581
+ deterministicStrongCandidateCodes: [],
582
+ deterministicWeakCandidateCodes: [],
583
+ agentCandidateCodes: [],
584
+ mergedCandidateCodes: [],
585
+ mccTableSummary: mccTableSummary(records),
586
+ candidates: [],
587
+ agentReview: {
588
+ required: true,
589
+ attempt: 1,
590
+ maxAttempts: 2,
591
+ reason: 'NATURAL_LANGUAGE_MATCH',
592
+ instructions: {
593
+ keepExplicitConstraints: true,
594
+ keepUnspecifiedDimensionsOpen: true,
595
+ excludeContradictions: true,
596
+ rankByRelevance: true,
597
+ maxCandidates: 8
598
+ },
599
+ catalog: mccCatalogForAgent(records)
600
+ }
601
+ };
602
+ }
603
+
604
+ const finalCodes = agentReview.codes;
762
605
  const candidates = finalCodes.map((code) => mccCandidateFromRecord(records.get(code), productType, mccRunContext));
763
- const needsAgentReview = agentCandidateCodes.length === 0
764
- && (
765
- matches.length === 0
766
- || deterministicCodes.length !== 1
767
- || (semanticTerms.length > 0 && deterministicCodes.length === 0 && !hasExactSemanticName)
768
- );
769
606
  return {
770
607
  matchType: candidates.length === 1 ? 'UNIQUE' : candidates.length === 0 ? 'NONE' : 'MULTIPLE',
771
- semanticStatus: semanticStatus
772
- || (agentCandidateCodes.length > 0 ? 'AGENT_CATALOG_REVIEW_USED' : semanticTerms.length > 0 ? 'AGENT_SEMANTIC_TERMS_USED' : 'DYNAMIC_CATALOG'),
773
- deterministicCandidateCodes: deterministicCodes,
774
- deterministicStrongCandidateCodes: deterministicCodes,
608
+ semanticStatus: semanticStatus || 'AGENT_CATALOG_REVIEW_USED',
609
+ deterministicCandidateCodes: [],
610
+ deterministicStrongCandidateCodes: [],
775
611
  deterministicWeakCandidateCodes: [],
776
- semanticCandidateCodes: semanticCodes,
777
- agentCandidateCodes,
778
- semanticTerms,
612
+ agentCandidateCodes: finalCodes,
779
613
  mergedCandidateCodes: finalCodes,
780
614
  mccTableSummary: mccTableSummary(records),
781
- candidates,
782
- ...(needsAgentReview ? {
783
- agentReview: {
784
- required: true,
785
- attempt: 1,
786
- maxAttempts: 1,
787
- reason: 'LOW_CONFIDENCE_LOCAL_MATCH',
788
- catalog: mccCatalogForAgent(records)
789
- }
790
- } : {})
615
+ candidates
791
616
  };
792
617
  }
793
618
 
@@ -837,7 +662,9 @@ async function runMccResolvePlan(args) {
837
662
  const selected = optional(args, 'select');
838
663
  const selectionReceipt = optional(args, 'selection-receipt');
839
664
  const selectionState = optional(args, 'selection-state');
840
- const semanticTerms = parseMccSemanticTerms(args);
665
+ if (args['semantic-terms-json'] !== undefined) {
666
+ throw new Error('mcc-resolve-plan 不再接受 --semantic-terms-json;自然语言只使用一次受限目录判断');
667
+ }
841
668
  if ((selected === undefined) !== (selectionReceipt === undefined)) {
842
669
  throw new Error('--select 与 --selection-receipt 必须同时提供');
843
670
  }
@@ -853,8 +680,8 @@ async function runMccResolvePlan(args) {
853
680
  const records = recordsForArgs(args);
854
681
  const agentReview = parseAgentCandidateRefs(args, records);
855
682
  let result = selected === undefined
856
- ? resolveMccResultFromRecords(query, productType, records, semanticTerms, undefined, mccRunContext, agentReview.codes)
857
- : resolveMccSelection(productType, query, selected, selectionReceipt, records, semanticTerms, agentReview.codes, mccRunContext);
683
+ ? resolveMccResultFromRecords(query, productType, records, undefined, mccRunContext, agentReview)
684
+ : resolveMccSelection(productType, query, selected, selectionReceipt, records, mccRunContext);
858
685
  if (selectionState === 'STALE') {
859
686
  result = {
860
687
  ...result,
@@ -891,14 +718,16 @@ async function runMccMatchCodes(args) {
891
718
  }
892
719
  await startTelemetryRun(rootIntent, productType);
893
720
  const query = required(args, 'query');
894
- const semanticTerms = parseMccSemanticTerms(args);
721
+ if (args['semantic-terms-json'] !== undefined) {
722
+ throw new Error('mcc-match-codes 不再接受 --semantic-terms-json;自然语言只使用一次受限目录判断');
723
+ }
895
724
  if (args['agent-mcc-status'] !== undefined || args['agent-mcc-codes'] !== undefined) {
896
725
  throw new Error('mcc-match-codes 不接受 --agent-mcc-status 或 --agent-mcc-codes;MCC 匹配只使用本轮动态 catalog');
897
726
  }
898
727
  const records = recordsForArgs(args);
899
728
  const agentReview = parseAgentCandidateRefs(args, records);
900
729
  const mccRunContext = captureMccRunContext();
901
- const result = resolveMccResultFromRecords(query, productType, records, semanticTerms, undefined, mccRunContext, agentReview.codes);
730
+ const result = resolveMccResultFromRecords(query, productType, records, undefined, mccRunContext, agentReview);
902
731
  const { candidates: _internalCandidates, ...controlResult } = result;
903
732
  process.stderr.write(`ALIPAY_AIPAY_INTERNAL:MCC_RESOLUTION=${JSON.stringify(controlResult)}\n`);
904
733
  return '';
@@ -908,14 +737,15 @@ function runMccValidateSelectionCache(args) {
908
737
  const productType = required(args, 'product-type');
909
738
  productFor({ ...args, 'product-type': productType });
910
739
  const records = recordsForArgs(args);
911
- const semanticTerms = parseMccSemanticTerms(args);
740
+ if (args['semantic-terms-json'] !== undefined) {
741
+ throw new Error('mcc-validate-selection-cache 不再接受 --semantic-terms-json');
742
+ }
912
743
  const mccRunContext = captureMccRunContext();
913
744
  if (!mccSelectionReceiptCodes(
914
745
  productType,
915
746
  required(args, 'query'),
916
747
  required(args, 'selection-receipt'),
917
748
  records,
918
- semanticTerms,
919
749
  mccRunContext
920
750
  )) {
921
751
  throw new Error('MCC 候选详情缓存与本轮选择回执不一致');
@@ -1578,8 +1578,8 @@ export function renderMessage(messageId, variantName, variables, telemetryContex
1578
1578
  if (variables.overallResult === '未通过' && !hasFailedItems) {
1579
1579
  throw new Error('代码开发结果为未通过时必须列出未通过项');
1580
1580
  }
1581
- if (variables.overallResult === '通过' && hasBlockingDefect) {
1582
- throw new Error('代码开发通过时不得存在其他阻塞缺口');
1581
+ if (hasBlockingDefect && variables.overallResult !== '未通过') {
1582
+ throw new Error('存在代码、安全或必要依据阻塞缺口时,代码开发结果必须为未通过');
1583
1583
  }
1584
1584
  if (variables.overallResult !== '通过' && !sandboxPending && !hasBlockingDefect && !manualOnly) {
1585
1585
  throw new Error('沙箱已就绪且没有阻塞缺口时,非通过结果只能来自人工待验证项');
@@ -1605,12 +1605,12 @@ export function renderMessage(messageId, variantName, variables, telemetryContex
1605
1605
  if (variantName === 'WEBPAY' && variables.overallResult === '通过' && variables.webpayExperienceResult === '未提供浏览器付款入口或完整操作指引,需先补齐') {
1606
1606
  throw new Error('AI 网页应用收款未交付付款入口时不得标记代码开发通过');
1607
1607
  }
1608
- if (manualOnly) {
1609
- normalized.failedItems = `人工待验证项未完成:${normalized.manualItems}`;
1610
- }
1611
1608
  normalized.checklistDetails = variables.executionMode === '仅代码开发'
1612
1609
  ? ''
1613
- : `未完成项:${normalized.failedItems}\n人工待验证:${normalized.manualItems}`;
1610
+ : [
1611
+ ...(hasFailedItems ? [`未完成项:${normalized.failedItems}`] : []),
1612
+ ...(hasManualItems ? [`人工待验证:${normalized.manualItems}`] : [])
1613
+ ].join('\n');
1614
1614
  const productName = { AIPAY: 'AI 按量付费', WEBPAY: 'AI 网页应用收款', APPPAY: 'AI 移动应用收款' }[variantName];
1615
1615
  normalized.launchGuidance = variables.executionMode === '仅代码开发'
1616
1616
  ? productionReadinessBlock(productName, {
@@ -13,7 +13,7 @@
13
13
 
14
14
  步骤顺序固定为方案展示后自动授权,再执行适用只读查询,最后处理独立分支。查询失败只阻断对应分支,不当作空列表;其他成功分支可继续。
15
15
 
16
- 方案展示前只按本轮 `mcc.sh resolve-plan` 结果陈述 MCC 名称、描述和特殊资质。Agent 可从用户原始经营描述提炼有依据的行业概念和同义表达;本地词面结果不明确时,runner 会把同一次 `queryMcc` 返回的当前产品 MCC 名称和描述以一次性目录复核 marker 交给 Agent,Agent 只能返回目录引用,runner 再映射并校验为当前动态目录中的候选。`ar-support.queryMcc`、按需 `mccsearchservice.batchQueryMcc` 及候选快照的完整执行和失效规则见步骤 2;无查询依据的结论禁止推断。
16
+ 方案展示前只按本轮 `mcc.sh resolve-plan` 结果陈述 MCC 名称、描述和特殊资质。合法 MCC 编码和规范化后的完整类目名称由 runner 确定性匹配;其他自然语言由 Agent 对同一次 `queryMcc` 返回的当前产品完整 MCC 名称和描述做一次受限目录判断。Agent 只能返回目录引用,runner 再映射并校验为当前动态目录中的候选。`ar-support.queryMcc`、按需 `mccsearchservice.batchQueryMcc` 及候选快照的完整执行和失效规则见步骤 2;无查询依据的结论禁止推断。
17
17
 
18
18
  ---
19
19
 
@@ -103,29 +103,29 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message prod
103
103
 
104
104
  产品明确但无经营描述/MCC:执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-clarify --product-type "$PRODUCT_TYPE" --root-intent "$ROOT_INTENT" --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"`。该命令必须先创建或复用正式 telemetry run 并记录 `clarify_intent`;缺少或非法 `ROOT_INTENT` 时停止,不得继续 MCC 或授权。
105
105
 
106
- 已有经营描述、二级类目名称或编码时,以原话作为 `MCC_SEARCH_QUERY` 进入唯一 MCC 解析/方案入口。非精确编码时,Agent 应先理解原话;遇到行业俗称、场景表达或目录不一定直接出现的用词时,在首次调用中一并传入 0 到 12 个有原话依据的行业概念、经营对象或同义表达,形成 `MCC_SEMANTIC_TERMS_JSON`;只传概念,不补原话无依据的行业。没有可靠扩展时省略,不追加用户确认。runner 在完整动态类目内做 NFKC、泛词剥离、标点边界内二/三元词组召回和动态稀有度排序;原始精确编码/完整名称决定性优先,其余原话与 Agent 辅助概念联合评分,语义项按 `0.7` 权重参与,名称证据高于描述,其他候选最多 8 项。若本地结果为 `NONE`、候选冲突或仅由低置信语义词产生,runner 只发出一次 `MCC_AGENT_REVIEW_REQUIRED`,其中包含当前动态目录的稳定引用、名称和描述;Agent 必须基于该目录返回最多 8 个引用,并在同一原始 query 下用 `--agent-candidate-refs-json` 重跑,runner 只接受本轮目录中的引用。目录复核不新增 MCP 或网络调用,也不替代本地校验;禁止业务词到 MCC 的硬编码点映射、自由提交 MCC code、远程语义或额外网络调用。
106
+ 已有经营描述、二级类目名称或编码时,以原话作为 `MCC_SEARCH_QUERY` 进入唯一 MCC 解析/方案入口。合法 MCC 编码或规范化后与完整二级类目名称相等时由 runner 直接匹配,不调用 Agent。其他自然语言由 runner 输出一次 `MCC_AGENT_REVIEW_REQUIRED`,其中包含当前动态目录的稳定引用、名称、描述及固定判断约束;Agent 阅读用户完整原话和完整目录,只返回按匹配度排序的 0 到 8 个目录引用。用户明确提供的行业、商品、服务对象、经营方式和场景都是筛选条件,与明确条件冲突的候选必须排除;用户没有说明的细分维度保持开放,不能擅自排除或补充经营范围。候选超过 8 项时只保留最相关的 8 项。禁止业务词到 MCC 的硬编码点映射、自由提交 MCC code、远程语义或额外网络调用。
107
107
 
108
- 同一 `run + 用户输入 + 原始 MCC_SEARCH_QUERY + 产品` 的语义扩充最多重试 2 次。首次未提供语义词且结果为 `NONE`,或已提供语义词但结果仍为 `NONE` 且仍有预算时,`mcc.sh` 只输出唯一内部 `MCC_SEMANTIC_RETRY_REQUIRED`,stdout 为空;Agent 必须保留原始 query、复用同一 `USER_INPUT_ID`,根据原话补充此前未使用且有依据的概念后原样重跑本命令,不向用户展示中间无匹配。后续重试提交的语义词必须保留此前语义词并至少新增一个此前未使用的词;未新增或替换原语义词时不消耗额度,直接进入既有无匹配分支。`MCC_SEMANTIC_RETRY_REQUIRED` 的 `attempt` 从 1 到 2,达到 2 后不再产生该 marker;仍为 `NONE` 时输出下方既有无匹配文案并停止。精确 MCC 编码不存在不进入语义重试;网络/CLI 失败仍只走各自既有恢复分支。语义重试计数只保存在当前 run 的私有缓存中,不随 MCC 树缓存 TTL 过期,读写异常按已耗尽处理,禁止通过更换 query、重置参数或重复调用绕过上限。
108
+ 同一 `run + 产品 + USER_INPUT_ID + 原始 MCC_SEARCH_QUERY + 动态目录摘要` 的自然语言判断只允许一次;同一 `USER_INPUT_ID` 不得通过改写 query 重新发起判断。Agent 返回非 JSON 字符串数组、目录外或重复引用、超过 8 项等协议错误时,runner 只输出一次 `MCC_AGENT_REPAIR_REQUIRED`;Agent 必须基于 marker 中的同一目录和明确错误原因,只修正格式、去重、排序或裁剪为最多 8 项,再用同一 query、`USER_INPUT_ID` 和 `--agent-candidate-refs-json` 原样重跑。修复不得重新解释用户意图、扩大经营范围、重新查询目录或改变用户原话。第二次仍非法、判断或修复状态重复、动态目录变化、状态不可安全记录时立即输出既有 MCC 暂时无法处理正文并停止,不回退字符匹配、不再次调用 Agent、不进入授权或签约。合法空数组表示当前目录无匹配,直接输出既有无匹配文案并等待用户补充描述,不触发修复。
109
109
 
110
- `queryMcc` 树和多候选快照使用默认 1800 秒、本轮 run 私有缓存;无有效 run 不缓存。快照绑定产品、salesCode、原查询、规范化语义词、选择回执、候选树事实及按需取得的资格详情;有效命中原样复用,失效才重查。runner 只接受最多 12 项、单项 2 到 40 字符、总计不超过 240 字符的 JSON 字符串数组并拒绝 MCC 编码;Agent 参数错误时基于原话更正或省略,不要求用户重述。两种 MCP 请求体不变;`batchQueryMcc` 最多一次,只传树内命中且 `needSpecialQual=true` 的候选编码。
110
+ `queryMcc` 树和多候选快照使用默认 1800 秒、本轮 run 私有缓存;无有效 run 不缓存。快照绑定产品、salesCode、原查询、选择回执、候选树事实及按需取得的资格详情;有效命中原样复用,失效才重查。两种 MCP 请求体不变;`batchQueryMcc` 最多一次,只传 Agent 最终候选中 `needSpecialQual=true` 的编码。
111
111
 
112
112
  ```bash
113
- node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" mcc resolve-plan --query "$MCC_SEARCH_QUERY" --product-type "$PRODUCT_TYPE" --root-intent "$ROOT_INTENT" [--semantic-terms-json "$MCC_SEMANTIC_TERMS_JSON"] [--agent-candidate-refs-json "$MCC_AGENT_CANDIDATE_REFS_JSON"] --input-id "$USER_INPUT_ID" [--telemetry-run-id "$RUN_ID" --telemetry-history-count "$HISTORY_COUNT"] --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"
113
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" mcc resolve-plan --query "$MCC_SEARCH_QUERY" --product-type "$PRODUCT_TYPE" --root-intent "$ROOT_INTENT" [--agent-candidate-refs-json "$MCC_AGENT_CANDIDATE_REFS_JSON"] --input-id "$USER_INPUT_ID" [--telemetry-run-id "$RUN_ID" --telemetry-history-count "$HISTORY_COUNT"] --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"
114
114
  ```
115
115
 
116
- 只收唯一 `ALIPAY_AIPAY_INTERNAL:MCC_QUERY_RESULT=SUCCESS` 与 `ALIPAY_AIPAY_INTERNAL:MCC_RESOLUTION=<json>`。若同时收到唯一 `ALIPAY_AIPAY_INTERNAL:MCC_AGENT_REVIEW_REQUIRED=<json>` 且 stdout 为空,Agent 读取其中稳定引用、名称和描述,最多返回 8 个当前目录引用后用同一 query、语义词和 `USER_INPUT_ID` 原样重跑一次;不得把该 marker 或中间状态展示给用户。目录复核最多一次,不消耗语义扩充预算;未返回有效引用时沿用本地结果并继续既有 `NONE`/候选分支。若同时收到唯一 `ALIPAY_AIPAY_INTERNAL:MCC_SEMANTIC_RETRY_REQUIRED=<json>` 且 stdout 为空,按上面的规则在同一工作回合自动重跑;不得把该 marker 或中间状态展示给用户。`UNIQUE` 恰一项,并有唯一 `CUSTOMER_MESSAGE_DELIVERY_REQUIRED=onboarding.plan.confirm`、`MCC_CONTEXT_RECEIPT`;先把 stdout 作为可见进行中消息原文送达,随后同一回合进入 `execute`,不等待回复。`MULTIPLE` 送候选后等待;预算耗尽的 `NONE` 送既有无匹配文案后等待用户重新描述。禁止旧 `mcc-search`/`mcc-candidates`、手写候选或近似追问。
116
+ 只收唯一 `ALIPAY_AIPAY_INTERNAL:MCC_QUERY_RESULT=SUCCESS` 与适用的内部结果。若同时收到唯一 `MCC_AGENT_REVIEW_REQUIRED` 且 stdout 为空,Agent 按上面的判断约束返回 0 到 8 个当前目录引用并原样重跑一次;若收到唯一 `MCC_AGENT_REPAIR_REQUIRED` 且 stdout 为空,只做一次协议修复并原样重跑。两个 marker 和中间状态都不得展示给用户。`UNIQUE` 恰一项,并有唯一 `CUSTOMER_MESSAGE_DELIVERY_REQUIRED=onboarding.plan.confirm`、`MCC_CONTEXT_RECEIPT`;先把 stdout 作为可见进行中消息原文送达,随后同一回合进入 `execute`,不等待回复。`MULTIPLE` 送候选后等待;合法 `NONE` 送既有无匹配文案后等待用户重新描述。禁止旧 `mcc-search`/`mcc-candidates`、手写候选或近似追问。
117
117
 
118
118
  `MCC_FLOW:RETRY_WITH_NETWORK`:联网后原样重试同一 `mcc resolve-plan`。`CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION` 且 stdout 空:给同命令 CLI 私有状态目录权限后原样重试。失败停 MCC;禁无匹配兜底或进方案/授权/签约。
119
119
 
120
- `ALIPAY_AIPAY_INTERNAL:MCC_QUERY_RESULT=FAILED` 且无 `MCC_FLOW:RETRY_WITH_NETWORK`:送stdout,停MCC;非无匹配,禁候选/方案/授权/签约。
120
+ `ALIPAY_AIPAY_INTERNAL:MCC_QUERY_RESULT=FAILED` 且无 `MCC_FLOW:RETRY_WITH_NETWORK`:送 stdout,停 MCC;非无匹配,禁候选/方案/授权/签约。只有用户按 stdout 指引重新发送经营描述后才开始新一轮匹配:先按统一前置规则计数,把该聊天消息原话作为新的 `MCC_SEARCH_QUERY`,使用该消息新的 `USER_INPUT_ID` 调用 `mcc resolve-plan`;不得复用上次的目录引用、修复 marker 或选择回执。即使用户原样重发经营描述,新 `USER_INPUT_ID` 也允许重新匹配。Agent 不得在没有新用户消息时自动发起第二轮,也不得因再次失败循环重试。
121
121
 
122
- `MULTIPLE` 还须取得唯一 `MCC_SELECTION_RECEIPT`。用户回复序号、当前候选完整名称或编码时,以原始 `MCC_SEARCH_QUERY` 和首次检索使用的同一 `MCC_SEMANTIC_TERMS_JSON` 重跑同一入口:`node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" mcc resolve-plan --query "$MCC_SEARCH_QUERY" --product-type "$PRODUCT_TYPE" --root-intent "$ROOT_INTENT" [--semantic-terms-json "$MCC_SEMANTIC_TERMS_JSON"] --select "$SELECTED_MCC_CODE" --selection-receipt "$MCC_SELECTION_RECEIPT" --input-id "$USER_INPUT_ID" --telemetry-run-id "$RUN_ID" --telemetry-history-count "$HISTORY_COUNT" --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"`。有效详情快照直接生成方案,不再调用 `queryMcc` 或 `batchQueryMcc`;runner 仍校验当前 run、产品、原话、规范化语义词、候选和事实哈希。receipt 或快照失效时不得沿用或自动套用旧选择,必须基于完整当前产品目录重新召回并至少重新展示一次当前候选。
122
+ `MULTIPLE` 还须取得唯一 `MCC_SELECTION_RECEIPT`。用户回复序号、当前候选完整名称或编码时,以原始 `MCC_SEARCH_QUERY` 重跑同一入口:`node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" mcc resolve-plan --query "$MCC_SEARCH_QUERY" --product-type "$PRODUCT_TYPE" --root-intent "$ROOT_INTENT" --select "$SELECTED_MCC_CODE" --selection-receipt "$MCC_SELECTION_RECEIPT" --input-id "$USER_INPUT_ID" --telemetry-run-id "$RUN_ID" --telemetry-history-count "$HISTORY_COUNT" --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"`。有效详情快照直接生成方案,不再调用 `queryMcc` 或 `batchQueryMcc`;runner 仍校验当前 run、产品、原话、候选和事实哈希。receipt 或快照失效时不得沿用或自动套用旧选择,必须基于完整当前产品目录重新判断并至少重新展示一次当前候选。
123
123
 
124
124
  用户指定的 MCC 不在候选中时,按该 MCC 重新查询,不沿用旧候选回执,也不要要求用户换类目。
125
125
 
126
126
  `salesCode`、`scope` 和基础材料只取“产品映射”。方案只由 `mcc resolve-plan` 或已有唯一 MCC 事实时的 `onboarding-message plan-confirm --root-intent "$ROOT_INTENT" --mcc-context-receipt ... [--telemetry-run-id "$RUN_ID" --telemetry-history-count "$HISTORY_COUNT"]` 生成,展示产品/scope、MCC、特殊资质、企业账号边界及商家平台链接。已有 active run 时必须传 telemetry 参数;如果该命令首次创建 run,则尚无 marker,省略参数并由同一进程把本次 plan stage 的 history 固定为 0,命令返回后取得后续使用的 `RUN_ID`。`ROOT_INTENT` 取当前路线根意图,只能是 `onboarding_only` 或 `full_process`。特殊资质只来自 `qualificationGroups`:多个 group 是 OR 关系;同一 group 内多个 qualifications 是 AND 关系;`attaType` 为空或 null 的 qualification 只展示 `qualName`,不生成上传项。禁止补充模板外资格或审核规则。
127
127
 
128
- 不得保留“先运行旧字符串搜索,无匹配后再临时让 Agent 猜”的无边界两阶段流程,或重复询问已知信息;目录复核是完整 `queryMcc` 事实上的一次受限 Agent 判断,必须在 `MCC_AGENT_REVIEW_REQUIRED` marker 后立即完成,最多一次且不增加用户确认。语义重试只允许按上一段登记的两次预算执行。语义概念、目录引用与原话必须在同一次 `resolve-plan` 中提交并统一校验。唯一事实校验通过、方案 marker 唯一且 stdout 已送达后,执行 `onboarding-execute-ready --product-type "$PRODUCT_TYPE"`;缺值/冲突/非唯一继续澄清,产品/MCC 变更重渲染。`execute` loader 只继承 `clarify_intent` 已记录的正式 run 事实,不再单独记录 stage,不代表新增用户回复,禁手工上报;Agent 在收到每条聊天框用户消息时,必须先按 `runtime-entry.md` 的统一前置命令计数,不按是否与 Skill 相关筛选,`summary` 收口后不再计入当前 run,`full_process` 不重置。
128
+ 不得保留“先运行旧字符串搜索,无匹配后再临时让 Agent 猜”的两阶段流程,或重复询问已知信息;自然语言只允许完整动态目录上的一次受限 Agent 判断和一次协议修复,二者预算由本轮私有状态原子固定。唯一事实校验通过、方案 marker 唯一且 stdout 已送达后,执行 `onboarding-execute-ready --product-type "$PRODUCT_TYPE"`;缺值/冲突/非唯一继续澄清,产品/MCC 变更重渲染。`execute` loader 只继承 `clarify_intent` 已记录的正式 run 事实,不再单独记录 stage,不代表新增用户回复,禁手工上报;Agent 在收到每条聊天框用户消息时,必须先按 `runtime-entry.md` 的统一前置命令计数,不按是否与 Skill 相关筛选,`summary` 收口后不再计入当前 run,`full_process` 不重置。
129
129
 
130
130
  <!-- ALIPAY_AIPAY_FLOW:ONBOARDING_ENTRY:END -->
131
131
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  `queryMcc`、签约查询和签约提交使用 `{"request":{...},"ctx":{}}`;`batchQueryMcc` 使用 `{"request":{"mccCodes":[...]}}`。完整 payload 只在脚本中维护;Agent 不从本模块自行构造 MCP 调用。
15
15
 
16
- `queryMcc` 成功树按同一 run、产品短时缓存,并逐项提供二级 MCC 编码、名称、描述和布尔型 `needSpecialQual`;任一必需字段缺失、为空或类型异常均失败关闭,不从其他字段回退。非精确编码时,Agent 应先理解用户原话,并在行业俗称或场景表达与目录用词可能不同时于首次调用一并提供有原话依据的行业概念或同义表达;本地词面结果为 `NONE`、候选冲突或低置信时,runner 可发出一次目录复核 marker,携带同一次 `queryMcc` 返回的当前产品 MCC 名称、描述和稳定引用;Agent 只返回该 marker 中的目录引用,runner 映射并校验为当前动态目录中的候选,不接受自由提交 MCC code。稳定引用由完整动态目录生成,并随候选裁剪和资格详情合并原样携带;最终方案 runner 不得按缩减后的候选数组重新编号。目录复核不新增 MCP/网络调用,语义扩充仍按 flow 最多完成两次。候选确定后,仅把其中 `needSpecialQual=true` 的编码集中交给一次 `batchQueryMcc` 获取特殊资质项;没有此类候选时不调用。多候选的完整树事实和资格详情按 run、产品、原查询、规范化语义词和选择回执缓存,用户选择时携带同一语义词且有效命中则两种 MCC 方法都不重调。缓存默认 1800 秒、无有效 run 不落盘;过期、损坏、符号链接、跨 run、语义词变化或事实哈希不一致时忽略并从完整当前产品目录重查,不修补缓存事实,不做产品级全量预取。
16
+ `queryMcc` 成功树按同一 run、产品短时缓存,并逐项提供二级 MCC 编码、名称、描述和布尔型 `needSpecialQual`;任一必需字段缺失、为空或类型异常均失败关闭,不从其他字段回退。合法 MCC 编码和规范化完整类目名称由 runner 确定性匹配;其他自然语言由 Agent 基于用户完整原话和同一次 `queryMcc` 返回的当前产品完整 MCC 名称、描述做一次受限判断,只返回按匹配度排序的最多 8 个稳定目录引用。用户明确约束用于排除冲突候选,未说明的维度保持开放。稳定引用由完整动态目录生成,并随候选裁剪和资格详情合并原样携带;最终方案 runner 不得按缩减后的候选数组重新编号。非法 Agent 结果只允许一次协议修复,第二次失败立即停止,不回退字符匹配或重复 Agent 判断。候选确定后,仅把其中 `needSpecialQual=true` 的编码集中交给一次 `batchQueryMcc` 获取特殊资质项;没有此类候选时不调用。多候选的完整树事实和资格详情按 run、产品、原查询和选择回执缓存,有效选择时两种 MCC 方法都不重调。缓存默认 1800 秒、无有效 run 不落盘;过期、损坏、符号链接、跨 run 或事实哈希不一致时忽略并从完整当前产品目录重新判断,不修补缓存事实,不做产品级全量预取。
17
17
 
18
18
  ## 签约状态
19
19