@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
@@ -750,9 +750,83 @@ export function inspectImplementation(variant, projectPath, snapshot = null) {
750
750
 
751
751
  findings.sort((left, right) => `${left.code}:${left.path || ''}:${left.line || 0}`
752
752
  .localeCompare(`${right.code}:${right.path || ''}:${right.line || 0}`));
753
+ for (const finding of findings) {
754
+ finding.id = crypto.createHash('sha256')
755
+ .update(JSON.stringify([variant, currentSnapshot.fingerprint, finding]))
756
+ .digest('hex');
757
+ }
753
758
  return { projectPath: canonicalPath, variant, findings };
754
759
  }
755
760
 
761
+ function reviewedAuditFindings(audit, rawReview, snapshot) {
762
+ if (rawReview === undefined) return audit.findings.map((finding) => ({ ...finding, reviewDecision: 'UNREVIEWED' }));
763
+ if (typeof rawReview !== 'string' || rawReview.length > 65536) throw new Error('audit-review 必须是受控 JSON 数组');
764
+ let reviews;
765
+ try { reviews = JSON.parse(rawReview); } catch { throw new Error('audit-review 不是合法 JSON'); }
766
+ if (!Array.isArray(reviews)) throw new Error('audit-review 必须是 JSON 数组');
767
+ const findings = new Map(audit.findings.map((finding) => [finding.id, finding]));
768
+ const reviewedIds = new Set();
769
+ const decisions = new Map();
770
+ const excluded = new Set();
771
+ const sources = new Set(implementationSourceFiles(snapshot.canonicalPath, snapshot.sources, snapshot.contents));
772
+ for (const review of reviews) {
773
+ if (!review || typeof review !== 'object' || Array.isArray(review)
774
+ || Object.keys(review).some((key) => !['findingId', 'decision', 'evidence'].includes(key))
775
+ || !findings.has(review.findingId) || reviewedIds.has(review.findingId)) {
776
+ throw new Error('audit-review 必须逐项绑定当前产品和项目快照的唯一 findingId');
777
+ }
778
+ reviewedIds.add(review.findingId);
779
+ if (!['FALSE_POSITIVE', 'CONFIRMED', 'UNRESOLVED'].includes(review.decision)) {
780
+ throw new Error('audit-review decision 仅支持 FALSE_POSITIVE、CONFIRMED、UNRESOLVED');
781
+ }
782
+ decisions.set(review.findingId, review.decision);
783
+ if (!Array.isArray(review.evidence) || review.evidence.length === 0 || review.evidence.length > 20) {
784
+ throw new Error('audit-review 每项必须提供当前生产代码的定位和核对依据');
785
+ }
786
+ for (const evidence of review.evidence) {
787
+ if (!evidence || typeof evidence !== 'object' || Array.isArray(evidence)
788
+ || Object.keys(evidence).some((key) => !['path', 'line', 'reason'].includes(key))
789
+ || typeof evidence.path !== 'string' || !evidence.path || evidence.path.includes('\\')
790
+ || path.posix.isAbsolute(evidence.path) || path.win32.isAbsolute(evidence.path)
791
+ || evidence.path.split('/').some((part) => !part || part === '.' || part === '..')
792
+ || !Number.isInteger(evidence.line) || evidence.line < 1
793
+ || typeof evidence.reason !== 'string' || evidence.reason.trim().length < 12 || evidence.reason.length > 2000
794
+ || /[\u0000-\u001f\u007f]/u.test(evidence.reason)) {
795
+ throw new Error('audit-review evidence 必须包含规范化相对 path、有效 line 和具体单行 reason');
796
+ }
797
+ const file = path.resolve(snapshot.canonicalPath, evidence.path);
798
+ const source = snapshot.contents.get(file)?.toString('utf8');
799
+ const lines = source === undefined ? [] : stripSourceComments(source, path.extname(file).toLowerCase()).split(/\r?\n/u);
800
+ if (!sources.has(file) || !lines?.[evidence.line - 1]?.trim()) {
801
+ throw new Error('audit-review evidence 必须指向当前快照内非空的生产源码行');
802
+ }
803
+ }
804
+ // The runner validates the snapshot and locations; the Agent owns the code reasoning.
805
+ // Unreviewed, confirmed and unresolved findings remain blocking.
806
+ if (review.decision === 'FALSE_POSITIVE') excluded.add(review.findingId);
807
+ }
808
+ return audit.findings.filter((finding) => !excluded.has(finding.id))
809
+ .map((finding) => ({ ...finding, reviewDecision: decisions.get(finding.id) || 'UNREVIEWED' }));
810
+ }
811
+
812
+ function mergeAuditItems(failedItems, findings) {
813
+ let items = failedItems && failedItems !== '无' ? [failedItems] : [];
814
+ if (items[0]?.trim().startsWith('[')) {
815
+ try {
816
+ const parsed = JSON.parse(items[0]);
817
+ if (Array.isArray(parsed) && parsed.every((item) => typeof item === 'string')) items = parsed;
818
+ } catch { /* Preserve plain text. */ }
819
+ }
820
+ items = items.flatMap((item) => item.split(';')).map((item) => item.trim()).filter(Boolean);
821
+ for (const finding of findings) {
822
+ const location = finding.path ? `(${finding.path}${finding.line ? `:${finding.line}` : ''})` : '';
823
+ const prefix = finding.reviewDecision === 'CONFIRMED' ? '' : '静态扫描疑点尚未排除:';
824
+ const item = `${prefix}${finding.message}${location}`;
825
+ if (!items.includes(item)) items.push(item);
826
+ }
827
+ return [...new Set(items)].join(';');
828
+ }
829
+
756
830
  function scanProjectRoute(productType, projectPath, projectSelection) {
757
831
  if (!['aipay', 'webpay', 'apppay'].includes(productType)) {
758
832
  throw new Error('ROUTE_INSPECT_ERROR:productType 非法');
@@ -1255,10 +1329,8 @@ function runServiceNotReady(args) {
1255
1329
 
1256
1330
  function runSafetyCloseout(args) {
1257
1331
  const variant = required(args, 'variant');
1258
- if (!['AIPAY', 'WEBPAY', 'APPPAY'].includes(variant)) throw new Error('variant 非法,仅支持 AIPAY、WEBPAY、APPPAY');
1259
- return requireCustomerMessageDelivery('integration.safety.closeout', renderMessage('integration.safety.closeout', variant, {
1260
- acceptanceSummary: required(args, 'acceptance-summary')
1261
- }));
1332
+ if (variant !== 'APPPAY') throw new Error('步骤 7 提醒仅适用于 APPPAY;AIPAY、WEBPAY 直接进入最终 checklist,上线提醒由最终 summary 输出');
1333
+ return requireCustomerMessageDelivery('integration.safety.closeout', renderMessage('integration.safety.closeout', variant, {}));
1262
1334
  }
1263
1335
 
1264
1336
  function runWindowsSandboxSummary(args) {
@@ -1348,26 +1420,21 @@ async function runChecklistResult(args) {
1348
1420
  }
1349
1421
  const input = checklistInputFromArgs(args) || await readStdin();
1350
1422
  const audit = inspectImplementation(variant, projectPath, snapshot);
1351
- if (audit.findings.length === 0 && finalizesAuditFindings) {
1423
+ const findings = reviewedAuditFindings(audit, args['audit-review'], snapshot);
1424
+ if (findings.length === 0 && finalizesAuditFindings) {
1352
1425
  throw new Error('finalize-audit-findings 仅用于收口实际 audit findings');
1353
1426
  }
1354
- if (audit.findings.length > 0) {
1427
+ if (findings.length > 0) {
1355
1428
  if (args['finalize-audit-findings'] !== 'true') {
1356
1429
  process.stderr.write(`${internalPrefix}CHECKLIST_AUDIT=${JSON.stringify({
1357
1430
  state: 'REPAIR_REQUIRED',
1358
- findings: audit.findings
1431
+ findings
1359
1432
  })}\n`);
1360
1433
  return null;
1361
1434
  }
1362
- const auditItems = audit.findings.map((finding) => {
1363
- const location = finding.path ? `(${finding.path}${finding.line ? `:${finding.line}` : ''})` : '';
1364
- return `${finding.message}${location}`;
1365
- }).join(';');
1366
1435
  input.overallResult = '未通过';
1367
1436
  input.blockingDefectState = 'PRESENT';
1368
- input.failedItems = input.failedItems && input.failedItems !== '无'
1369
- ? `${input.failedItems};${auditItems}`
1370
- : auditItems;
1437
+ input.failedItems = mergeAuditItems(input.failedItems, findings);
1371
1438
  if (input.executionMode === '完整接入') {
1372
1439
  input.nextFlowReminder = '代码开发校验通过后将自动进入支付产品-产品开通;未通过时停留在代码开发。';
1373
1440
  }
@@ -259,13 +259,6 @@ function assertMccQuery(query) {
259
259
  }
260
260
 
261
261
  const mccSelectionReceiptPrefix = 'AIPAY_MCC_SELECTION_V1.';
262
- const ignoredMccQueryTokens = new Set([
263
- '服务', '企业', '个人', '提供', '适用', '用于', '平台', '项目', '申请', '商家', '公司',
264
- '互联', '联网', '互联网', '在线', '用品', '收费', '缴费', '费用', '适用于', '经营',
265
- '业务', '相关', '主体', '从事', '网站', '网页', '购买', '买票', '购票', '票务', '售票',
266
- '预约', '预定', '退票', '支付', '付款', '收款', '下单', '订单', '交易', '销售', '售卖',
267
- '商品', '产品'
268
- ]);
269
262
 
270
263
  function activeMccRunId(env = process.env) {
271
264
  const candidate = env.ALIPAY_AIPAY_RUN_ID
@@ -397,8 +390,10 @@ function mccCatalogForAgent(records) {
397
390
  }
398
391
 
399
392
  function parseAgentCandidateRefs(args, records) {
400
- const raw = optional(args, 'agent-candidate-refs-json');
401
- if (raw === undefined) return { codes: [] };
393
+ if (!Object.prototype.hasOwnProperty.call(args, 'agent-candidate-refs-json')) {
394
+ return { provided: false, codes: [] };
395
+ }
396
+ const raw = args['agent-candidate-refs-json'];
402
397
  let parsed;
403
398
  try {
404
399
  parsed = JSON.parse(raw);
@@ -416,7 +411,7 @@ function parseAgentCandidateRefs(args, records) {
416
411
  }
417
412
  codes.push(refs.get(ref));
418
413
  }
419
- return { codes };
414
+ return { provided: true, codes };
420
415
  }
421
416
 
422
417
  function mccTableSummary(records) {
@@ -454,190 +449,12 @@ function directMccCode(query, records) {
454
449
  return { isCode: true, matches: [...records.keys()].filter((code) => code.endsWith(`_${raw}`)) };
455
450
  }
456
451
 
457
- function mccNgrams(value, size) {
458
- const grams = new Set();
459
- const searchableText = meaningfulMccSearch(value);
460
- for (const segment of searchableText.split(/\s+/u).filter(Boolean)) {
461
- const characters = [...segment];
462
- for (let index = 0; index <= characters.length - size; index += 1) {
463
- const gram = characters.slice(index, index + size).join('');
464
- if (/^[\p{L}\p{N}]+$/u.test(gram)) grams.add(gram);
465
- }
466
- }
467
- return grams;
468
- }
469
-
470
- function meaningfulMccSearch(value) {
471
- let searchableText = normalizeSearch(value);
472
- const ignoredTerms = [...ignoredMccQueryTokens]
473
- .sort((left, right) => [...right].length - [...left].length);
474
- for (const term of ignoredTerms) searchableText = searchableText.replaceAll(term, ' ');
475
- return searchableText
476
- .trim()
477
- .replace(/\s+/gu, ' ')
478
- .split(' ')
479
- .filter((segment) => [...segment].length >= 2)
480
- .join(' ');
481
- }
482
-
483
- function parseMccSemanticTerms(args) {
484
- const raw = optional(args, 'semantic-terms-json');
485
- if (raw === undefined) return [];
486
- let parsed;
487
- try {
488
- parsed = JSON.parse(raw);
489
- } catch {
490
- throw new Error('semantic-terms-json 必须是合法 JSON 字符串数组');
491
- }
492
- if (!Array.isArray(parsed) || parsed.length > 12 || parsed.some((term) => typeof term !== 'string')) {
493
- throw new Error('semantic-terms-json 必须是最多 12 项的 JSON 字符串数组');
494
- }
495
- const normalized = [];
496
- let totalLength = 0;
497
- for (const rawTerm of parsed) {
498
- if (/\u0000|[\u0001-\u001f\u007f]/u.test(rawTerm)) {
499
- throw new Error('semantic-terms-json 包含控制字符');
500
- }
501
- const term = normalizeSearch(rawTerm);
502
- const termLength = [...term.replace(/\s+/gu, '')].length;
503
- if (termLength < 2 || termLength > 40) {
504
- throw new Error('semantic-terms-json 每项长度必须为 2 到 40 个字符');
505
- }
506
- if (/(?:A\d{4}\s*)?B\d{4}/iu.test(term)) {
507
- throw new Error('semantic-terms-json 只能包含业务概念词,禁止提供 MCC 编码');
508
- }
509
- totalLength += termLength;
510
- if (!normalized.includes(term)) normalized.push(term);
511
- }
512
- if (totalLength > 240) throw new Error('semantic-terms-json 总长度不得超过 240 个字符');
513
- return normalized.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
514
- }
515
-
516
- function mccDocumentFrequency(indexedRecords) {
517
- const frequencies = new Map();
518
- for (const indexed of indexedRecords) {
519
- const documentGrams = new Set([
520
- ...indexed.nameBigrams,
521
- ...indexed.nameTrigrams,
522
- ...indexed.businessBigrams,
523
- ...indexed.businessTrigrams
524
- ]);
525
- for (const gram of documentGrams) frequencies.set(gram, (frequencies.get(gram) || 0) + 1);
526
- }
527
- return frequencies;
528
- }
529
-
530
- function weightedMccOverlap(queryGrams, documentGrams, frequencies, recordCount, size) {
531
- let weight = 0;
532
- for (const gram of queryGrams) {
533
- if (!documentGrams.has(gram)) continue;
534
- const documentCount = frequencies.get(gram) || 0;
535
- weight += (Math.log((recordCount + 1) / (documentCount + 1)) + 1) * (size === 3 ? 1.6 : 1);
536
- }
537
- return weight;
538
- }
539
-
540
- function mccPhraseCoverage(haystackSegments, needleSegments) {
541
- let coverage = 0;
542
- for (const needle of needleSegments) {
543
- if (ignoredMccQueryTokens.has(needle)) continue;
544
- const needleLength = [...needle].length;
545
- for (const haystack of haystackSegments) {
546
- if (!haystack.includes(needle)) continue;
547
- coverage = Math.max(coverage, needleLength / [...haystack].length);
548
- }
549
- }
550
- return coverage;
551
- }
552
-
553
- function mccTextEvidence(value, indexed, frequencies, recordCount) {
554
- const normalizedValue = normalizeSearch(value);
555
- const valueSegments = meaningfulMccSearch(normalizedValue).split(' ').filter(Boolean);
556
- const valueBigrams = mccNgrams(normalizedValue, 2);
557
- const valueTrigrams = mccNgrams(normalizedValue, 3);
558
- const exactName = indexed.name === normalizedValue
559
- || (normalizedValue.split(' ').length === 1 && indexed.nameSegments.includes(normalizedValue));
560
- const nameContainsValueCoverage = mccPhraseCoverage(indexed.nameEvidenceSegments, valueSegments);
561
- const valueContainsNameCoverage = mccPhraseCoverage(valueSegments, indexed.nameEvidenceSegments);
562
- const businessContainsValueCoverage = mccPhraseCoverage(indexed.businessEvidenceSegments, valueSegments);
563
- const nameBigram = weightedMccOverlap(valueBigrams, indexed.nameBigrams, frequencies, recordCount, 2);
564
- const nameTrigram = weightedMccOverlap(valueTrigrams, indexed.nameTrigrams, frequencies, recordCount, 3);
565
- const businessBigram = weightedMccOverlap(valueBigrams, indexed.businessBigrams, frequencies, recordCount, 2);
566
- const businessTrigram = weightedMccOverlap(valueTrigrams, indexed.businessTrigrams, frequencies, recordCount, 3);
567
- const matchedBigrams = new Set([
568
- ...[...valueBigrams].filter((gram) => indexed.nameBigrams.has(gram)),
569
- ...[...valueBigrams].filter((gram) => indexed.businessBigrams.has(gram))
570
- ]);
571
- const matchedTrigrams = new Set([
572
- ...[...valueTrigrams].filter((gram) => indexed.nameTrigrams.has(gram)),
573
- ...[...valueTrigrams].filter((gram) => indexed.businessTrigrams.has(gram))
574
- ]);
575
- const rareBigramDocumentLimit = Math.max(1, Math.ceil(recordCount * 0.02));
576
- const hasRareBigram = [...matchedBigrams].some((gram) => (
577
- (frequencies.get(gram) || recordCount) <= rareBigramDocumentLimit
578
- ));
579
- const hasLexicalEvidence = matchedTrigrams.size > 0
580
- || matchedBigrams.size >= 2
581
- || (valueBigrams.size <= 2 && hasRareBigram);
582
- const phraseEvidence = nameContainsValueCoverage > 0
583
- || valueContainsNameCoverage > 0
584
- || businessContainsValueCoverage > 0;
585
- return {
586
- eligible: exactName || phraseEvidence || hasLexicalEvidence,
587
- exactName,
588
- score: (exactName ? 100000 : 0)
589
- + (nameContainsValueCoverage * 6000)
590
- + (valueContainsNameCoverage * 3000)
591
- + (businessContainsValueCoverage * 2500)
592
- + ((nameBigram + nameTrigram) * 120)
593
- + ((businessBigram + businessTrigram) * 40)
594
- };
595
- }
596
-
597
- function deterministicMccMatches(query, records, { limit = 8, semanticTerms = [] } = {}) {
452
+ function directMccName(query, records) {
598
453
  const normalizedQuery = assertMccQuery(query);
599
- const indexedRecords = [...records.values()].map((record) => {
600
- const name = normalizeSearch(record.name);
601
- const businesses = normalizeSearch(record.applicableBusinesses);
602
- return {
603
- record,
604
- name,
605
- nameSegments: name.split(' ').filter(Boolean),
606
- nameEvidenceSegments: meaningfulMccSearch(name).split(' ').filter(Boolean),
607
- businessEvidenceSegments: meaningfulMccSearch(businesses).split(' ').filter(Boolean),
608
- nameBigrams: mccNgrams(name, 2),
609
- nameTrigrams: mccNgrams(name, 3),
610
- businessBigrams: mccNgrams(businesses, 2),
611
- businessTrigrams: mccNgrams(businesses, 3)
612
- };
613
- });
614
- const frequencies = mccDocumentFrequency(indexedRecords);
615
- const scored = [];
616
-
617
- for (const indexed of indexedRecords) {
618
- const originalEvidence = mccTextEvidence(normalizedQuery, indexed, frequencies, indexedRecords.length);
619
- const semanticEvidence = semanticTerms
620
- .map((term) => mccTextEvidence(term, indexed, frequencies, indexedRecords.length))
621
- .filter((evidence) => evidence.eligible)
622
- .sort((left, right) => right.score - left.score)[0];
623
- if (!originalEvidence.eligible && !semanticEvidence) continue;
624
- scored.push({
625
- record: indexed.record,
626
- score: originalEvidence.score + ((semanticEvidence?.score || 0) * 0.7),
627
- exactName: originalEvidence.exactName,
628
- originalEligible: originalEvidence.eligible,
629
- semanticEligible: Boolean(semanticEvidence),
630
- semanticExactName: Boolean(semanticEvidence?.exactName)
631
- });
632
- }
633
-
634
- const exactMatches = scored.filter(({ exactName }) => exactName);
635
- const ranked = exactMatches.length > 0
636
- ? exactMatches
637
- : scored;
638
- return ranked
639
- .sort((left, right) => right.score - left.score || left.record.code.localeCompare(right.record.code))
640
- .slice(0, limit);
454
+ return sortedMccRecords(records)
455
+ .filter((record) => normalizeSearch(record.name) === normalizedQuery)
456
+ .map((record) => record.code)
457
+ .slice(0, 8);
641
458
  }
642
459
 
643
460
  function createMccSelectionReceipt(productType, query, result, mccRunContext = captureMccRunContext()) {
@@ -647,7 +464,6 @@ function createMccSelectionReceipt(productType, query, result, mccRunContext = c
647
464
  mccRunContext.runId,
648
465
  productType,
649
466
  assertMccQuery(query),
650
- JSON.stringify(result.semanticTerms || []),
651
467
  table.sha256,
652
468
  table.recordCount,
653
469
  codes
@@ -655,8 +471,8 @@ function createMccSelectionReceipt(productType, query, result, mccRunContext = c
655
471
  return `${mccSelectionReceiptPrefix}${codes}.${createHash('sha256').update(binding).digest('base64url')}`;
656
472
  }
657
473
 
658
- function resolveMccSelection(productType, query, selectedInput, receipt, records, semanticTerms, agentCandidateCodes, mccRunContext) {
659
- const candidateCodes = mccSelectionReceiptCodes(productType, query, receipt, records, semanticTerms, mccRunContext);
474
+ function resolveMccSelection(productType, query, selectedInput, receipt, records, mccRunContext) {
475
+ const candidateCodes = mccSelectionReceiptCodes(productType, query, receipt, records, mccRunContext);
660
476
  if (!candidateCodes) {
661
477
  const semanticStatus = typeof receipt === 'string' && receipt.startsWith(mccSelectionReceiptPrefix)
662
478
  ? 'SELECTION_RECEIPT_STALE'
@@ -665,10 +481,8 @@ function resolveMccSelection(productType, query, selectedInput, receipt, records
665
481
  query,
666
482
  productType,
667
483
  records,
668
- semanticTerms,
669
484
  semanticStatus,
670
- mccRunContext,
671
- agentCandidateCodes
485
+ mccRunContext
672
486
  );
673
487
  return {
674
488
  ...refreshed,
@@ -685,8 +499,7 @@ function resolveMccSelection(productType, query, selectedInput, receipt, records
685
499
  deterministicCandidateCodes: [],
686
500
  deterministicStrongCandidateCodes: [],
687
501
  deterministicWeakCandidateCodes: [],
688
- semanticCandidateCodes: [],
689
- semanticTerms,
502
+ agentCandidateCodes: [],
690
503
  mergedCandidateCodes: candidateCodes,
691
504
  mccTableSummary: currentTable,
692
505
  candidates: candidateCodes.map((code) => mccCandidateFromRecord(records.get(code), productType, mccRunContext))
@@ -699,15 +512,14 @@ function resolveMccSelection(productType, query, selectedInput, receipt, records
699
512
  deterministicCandidateCodes: [],
700
513
  deterministicStrongCandidateCodes: [],
701
514
  deterministicWeakCandidateCodes: [],
702
- semanticCandidateCodes: [],
703
- semanticTerms,
515
+ agentCandidateCodes: [],
704
516
  mergedCandidateCodes: [selectedCode],
705
517
  mccTableSummary: currentTable,
706
518
  candidates: [candidate]
707
519
  };
708
520
  }
709
521
 
710
- function mccSelectionReceiptCodes(productType, query, receipt, records, semanticTerms, mccRunContext = captureMccRunContext()) {
522
+ function mccSelectionReceiptCodes(productType, query, receipt, records, mccRunContext = captureMccRunContext()) {
711
523
  if (typeof receipt !== 'string' || !receipt.startsWith(mccSelectionReceiptPrefix)) return null;
712
524
  const body = receipt.slice(mccSelectionReceiptPrefix.length);
713
525
  const separator = body.lastIndexOf('.');
@@ -718,8 +530,7 @@ function mccSelectionReceiptCodes(productType, query, receipt, records, semantic
718
530
  || candidateCodes.some((code) => !records.has(code))
719
531
  || receipt !== createMccSelectionReceipt(productType, query, {
720
532
  mergedCandidateCodes: candidateCodes,
721
- mccTableSummary: currentTable,
722
- semanticTerms
533
+ mccTableSummary: currentTable
723
534
  }, mccRunContext)) {
724
535
  return null;
725
536
  }
@@ -730,10 +541,9 @@ function resolveMccResultFromRecords(
730
541
  query,
731
542
  productType,
732
543
  records,
733
- semanticTerms = [],
734
544
  semanticStatus,
735
545
  mccRunContext = captureMccRunContext(),
736
- agentCandidateCodes = []
546
+ agentReview = { provided: false, codes: [] }
737
547
  ) {
738
548
  const directCode = directMccCode(query, records);
739
549
  if (directCode.isCode) {
@@ -746,9 +556,7 @@ function resolveMccResultFromRecords(
746
556
  deterministicCandidateCodes: [code],
747
557
  deterministicStrongCandidateCodes: [code],
748
558
  deterministicWeakCandidateCodes: [],
749
- semanticCandidateCodes: [],
750
559
  agentCandidateCodes: [],
751
- semanticTerms,
752
560
  mergedCandidateCodes: [code],
753
561
  mccTableSummary: mccTableSummary(records),
754
562
  candidates: [candidate]
@@ -761,52 +569,69 @@ function resolveMccResultFromRecords(
761
569
  deterministicCandidateCodes: finalCodes,
762
570
  deterministicStrongCandidateCodes: finalCodes,
763
571
  deterministicWeakCandidateCodes: [],
764
- semanticCandidateCodes: [],
765
572
  agentCandidateCodes: [],
766
- semanticTerms,
767
573
  mergedCandidateCodes: finalCodes,
768
574
  mccTableSummary: mccTableSummary(records),
769
575
  candidates: finalCodes.map((code) => mccCandidateFromRecord(records.get(code), productType, mccRunContext))
770
576
  };
771
577
  }
772
578
 
773
- const matches = deterministicMccMatches(query, records, { limit: 8, semanticTerms });
774
- const deterministicCodes = matches.filter(({ originalEligible }) => originalEligible).map(({ record }) => record.code);
775
- const semanticCodes = matches.filter(({ semanticEligible }) => semanticEligible).map(({ record }) => record.code);
776
- const hasExactSemanticName = matches.some(({ semanticExactName }) => semanticExactName);
777
- const localCodes = matches.map(({ record }) => record.code);
778
- const mergedCodes = [...agentCandidateCodes, ...localCodes]
779
- .filter((code, index, all) => all.indexOf(code) === index);
780
- const finalCodes = mergedCodes.slice(0, 8);
579
+ const exactNameCodes = directMccName(query, records);
580
+ if (exactNameCodes.length > 0) {
581
+ const candidates = exactNameCodes.map((code) => mccCandidateFromRecord(records.get(code), productType, mccRunContext));
582
+ return {
583
+ matchType: candidates.length === 1 ? 'UNIQUE' : 'MULTIPLE',
584
+ semanticStatus: 'NOT_REQUIRED_EXACT_NAME',
585
+ deterministicCandidateCodes: exactNameCodes,
586
+ deterministicStrongCandidateCodes: exactNameCodes,
587
+ deterministicWeakCandidateCodes: [],
588
+ agentCandidateCodes: [],
589
+ mergedCandidateCodes: exactNameCodes,
590
+ mccTableSummary: mccTableSummary(records),
591
+ candidates
592
+ };
593
+ }
594
+
595
+ if (!agentReview.provided) {
596
+ return {
597
+ matchType: 'NONE',
598
+ semanticStatus: semanticStatus || 'AGENT_CATALOG_REVIEW_REQUIRED',
599
+ deterministicCandidateCodes: [],
600
+ deterministicStrongCandidateCodes: [],
601
+ deterministicWeakCandidateCodes: [],
602
+ agentCandidateCodes: [],
603
+ mergedCandidateCodes: [],
604
+ mccTableSummary: mccTableSummary(records),
605
+ candidates: [],
606
+ agentReview: {
607
+ required: true,
608
+ attempt: 1,
609
+ maxAttempts: 2,
610
+ reason: 'NATURAL_LANGUAGE_MATCH',
611
+ instructions: {
612
+ keepExplicitConstraints: true,
613
+ keepUnspecifiedDimensionsOpen: true,
614
+ excludeContradictions: true,
615
+ rankByRelevance: true,
616
+ maxCandidates: 8
617
+ },
618
+ catalog: mccCatalogForAgent(records)
619
+ }
620
+ };
621
+ }
622
+
623
+ const finalCodes = agentReview.codes;
781
624
  const candidates = finalCodes.map((code) => mccCandidateFromRecord(records.get(code), productType, mccRunContext));
782
- const needsAgentReview = agentCandidateCodes.length === 0
783
- && (
784
- matches.length === 0
785
- || deterministicCodes.length !== 1
786
- || (semanticTerms.length > 0 && deterministicCodes.length === 0 && !hasExactSemanticName)
787
- );
788
625
  return {
789
626
  matchType: candidates.length === 1 ? 'UNIQUE' : candidates.length === 0 ? 'NONE' : 'MULTIPLE',
790
- semanticStatus: semanticStatus
791
- || (agentCandidateCodes.length > 0 ? 'AGENT_CATALOG_REVIEW_USED' : semanticTerms.length > 0 ? 'AGENT_SEMANTIC_TERMS_USED' : 'DYNAMIC_CATALOG'),
792
- deterministicCandidateCodes: deterministicCodes,
793
- deterministicStrongCandidateCodes: deterministicCodes,
627
+ semanticStatus: semanticStatus || 'AGENT_CATALOG_REVIEW_USED',
628
+ deterministicCandidateCodes: [],
629
+ deterministicStrongCandidateCodes: [],
794
630
  deterministicWeakCandidateCodes: [],
795
- semanticCandidateCodes: semanticCodes,
796
- agentCandidateCodes,
797
- semanticTerms,
631
+ agentCandidateCodes: finalCodes,
798
632
  mergedCandidateCodes: finalCodes,
799
633
  mccTableSummary: mccTableSummary(records),
800
- candidates,
801
- ...(needsAgentReview ? {
802
- agentReview: {
803
- required: true,
804
- attempt: 1,
805
- maxAttempts: 1,
806
- reason: 'LOW_CONFIDENCE_LOCAL_MATCH',
807
- catalog: mccCatalogForAgent(records)
808
- }
809
- } : {})
634
+ candidates
810
635
  };
811
636
  }
812
637
 
@@ -856,7 +681,9 @@ async function runMccResolvePlan(args) {
856
681
  const selected = optional(args, 'select');
857
682
  const selectionReceipt = optional(args, 'selection-receipt');
858
683
  const selectionState = optional(args, 'selection-state');
859
- const semanticTerms = parseMccSemanticTerms(args);
684
+ if (args['semantic-terms-json'] !== undefined) {
685
+ throw new Error('mcc-resolve-plan 不再接受 --semantic-terms-json;自然语言只使用一次受限目录判断');
686
+ }
860
687
  if ((selected === undefined) !== (selectionReceipt === undefined)) {
861
688
  throw new Error('--select 与 --selection-receipt 必须同时提供');
862
689
  }
@@ -872,8 +699,8 @@ async function runMccResolvePlan(args) {
872
699
  const records = recordsForArgs(args);
873
700
  const agentReview = parseAgentCandidateRefs(args, records);
874
701
  let result = selected === undefined
875
- ? resolveMccResultFromRecords(query, productType, records, semanticTerms, undefined, mccRunContext, agentReview.codes)
876
- : resolveMccSelection(productType, query, selected, selectionReceipt, records, semanticTerms, agentReview.codes, mccRunContext);
702
+ ? resolveMccResultFromRecords(query, productType, records, undefined, mccRunContext, agentReview)
703
+ : resolveMccSelection(productType, query, selected, selectionReceipt, records, mccRunContext);
877
704
  if (selectionState === 'STALE') {
878
705
  result = {
879
706
  ...result,
@@ -910,14 +737,16 @@ async function runMccMatchCodes(args) {
910
737
  }
911
738
  await startTelemetryRun(rootIntent, productType);
912
739
  const query = required(args, 'query');
913
- const semanticTerms = parseMccSemanticTerms(args);
740
+ if (args['semantic-terms-json'] !== undefined) {
741
+ throw new Error('mcc-match-codes 不再接受 --semantic-terms-json;自然语言只使用一次受限目录判断');
742
+ }
914
743
  if (args['agent-mcc-status'] !== undefined || args['agent-mcc-codes'] !== undefined) {
915
744
  throw new Error('mcc-match-codes 不接受 --agent-mcc-status 或 --agent-mcc-codes;MCC 匹配只使用本轮动态 catalog');
916
745
  }
917
746
  const records = recordsForArgs(args);
918
747
  const agentReview = parseAgentCandidateRefs(args, records);
919
748
  const mccRunContext = captureMccRunContext();
920
- const result = resolveMccResultFromRecords(query, productType, records, semanticTerms, undefined, mccRunContext, agentReview.codes);
749
+ const result = resolveMccResultFromRecords(query, productType, records, undefined, mccRunContext, agentReview);
921
750
  const { candidates: _internalCandidates, ...controlResult } = result;
922
751
  process.stderr.write(`ALIPAY_AIPAY_INTERNAL:MCC_RESOLUTION=${JSON.stringify(controlResult)}\n`);
923
752
  return '';
@@ -927,14 +756,15 @@ function runMccValidateSelectionCache(args) {
927
756
  const productType = required(args, 'product-type');
928
757
  productFor({ ...args, 'product-type': productType });
929
758
  const records = recordsForArgs(args);
930
- const semanticTerms = parseMccSemanticTerms(args);
759
+ if (args['semantic-terms-json'] !== undefined) {
760
+ throw new Error('mcc-validate-selection-cache 不再接受 --semantic-terms-json');
761
+ }
931
762
  const mccRunContext = captureMccRunContext();
932
763
  if (!mccSelectionReceiptCodes(
933
764
  productType,
934
765
  required(args, 'query'),
935
766
  required(args, 'selection-receipt'),
936
767
  records,
937
- semanticTerms,
938
768
  mccRunContext
939
769
  )) {
940
770
  throw new Error('MCC 候选详情缓存与本轮选择回执不一致');
@@ -1587,8 +1587,8 @@ export function renderMessage(messageId, variantName, variables, telemetryContex
1587
1587
  if (variables.overallResult === '未通过' && !hasFailedItems) {
1588
1588
  throw new Error('代码开发结果为未通过时必须列出未通过项');
1589
1589
  }
1590
- if (variables.overallResult === '通过' && hasBlockingDefect) {
1591
- throw new Error('代码开发通过时不得存在其他阻塞缺口');
1590
+ if (hasBlockingDefect && variables.overallResult !== '未通过') {
1591
+ throw new Error('存在代码、安全或必要依据阻塞缺口时,代码开发结果必须为未通过');
1592
1592
  }
1593
1593
  if (variables.overallResult !== '通过' && !sandboxPending && !hasBlockingDefect && !manualOnly) {
1594
1594
  throw new Error('沙箱已就绪且没有阻塞缺口时,非通过结果只能来自人工待验证项');
@@ -1614,12 +1614,12 @@ export function renderMessage(messageId, variantName, variables, telemetryContex
1614
1614
  if (variantName === 'WEBPAY' && variables.overallResult === '通过' && variables.webpayExperienceResult === '未提供浏览器付款入口或完整操作指引,需先补齐') {
1615
1615
  throw new Error('AI 网页应用收款未交付付款入口时不得标记代码开发通过');
1616
1616
  }
1617
- if (manualOnly) {
1618
- normalized.failedItems = `人工待验证项未完成:${normalized.manualItems}`;
1619
- }
1620
1617
  normalized.checklistDetails = variables.executionMode === '仅代码开发'
1621
1618
  ? ''
1622
- : `未完成项:${normalized.failedItems}\n人工待验证:${normalized.manualItems}`;
1619
+ : [
1620
+ ...(hasFailedItems ? [`未完成项:${normalized.failedItems}`] : []),
1621
+ ...(hasManualItems ? [`人工待验证:${normalized.manualItems}`] : [])
1622
+ ].join('\n');
1623
1623
  const productName = { AIPAY: 'AI 按量付费', WEBPAY: 'AI 网页应用收款', APPPAY: 'AI 移动应用收款' }[variantName];
1624
1624
  normalized.launchGuidance = variables.executionMode === '仅代码开发'
1625
1625
  ? productionReadinessBlock(productName, {