@alipay/alipay-aipay 1.8.3 → 1.8.5

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 (33) hide show
  1. package/dist/commands/install.js +10 -2
  2. package/dist/index.js +6 -1
  3. package/dist/utils/file.js +11 -2
  4. package/dist/utils/profile.js +64 -0
  5. package/package.json +3 -3
  6. package/skill/SKILL.md +2 -0
  7. package/skill/VERSION +1 -1
  8. package/skill/references/integration/flow.md +1 -1
  9. package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +2 -2
  10. package/skill/references/normal/alipay-cli-env.md +2 -2
  11. package/skill/references/normal/scripts/common.sh +6 -2
  12. package/skill/references/normal/scripts/onboarding_message_runner.mjs +80 -250
  13. package/skill/references/normal/scripts/profile.mjs +53 -0
  14. package/skill/references/normal/scripts/session_skill.mjs +17 -2
  15. package/skill/references/normal/self-update.md +2 -0
  16. package/skill/references/onboarding/flow.md +11 -11
  17. package/skill/references/onboarding/modules/product-sign.md +1 -1
  18. package/skill/references/onboarding/modules/scripts/mcc.sh +151 -290
  19. package/skill/references/runtime-entry.md +1 -1
  20. package/skill-pre/SKILL.md +2 -0
  21. package/skill-pre/VERSION +1 -1
  22. package/skill-pre/references/integration/flow.md +1 -1
  23. package/skill-pre/references/integration/modules/sandbox/alipay-sandbox-tool.md +2 -2
  24. package/skill-pre/references/normal/alipay-cli-env.md +2 -2
  25. package/skill-pre/references/normal/scripts/common.sh +6 -2
  26. package/skill-pre/references/normal/scripts/onboarding_message_runner.mjs +80 -250
  27. package/skill-pre/references/normal/scripts/profile.mjs +53 -0
  28. package/skill-pre/references/normal/scripts/session_skill.mjs +17 -2
  29. package/skill-pre/references/normal/self-update.md +2 -0
  30. package/skill-pre/references/onboarding/flow.md +11 -11
  31. package/skill-pre/references/onboarding/modules/product-sign.md +1 -1
  32. package/skill-pre/references/onboarding/modules/scripts/mcc.sh +151 -290
  33. package/skill-pre/references/runtime-entry.md +1 -1
@@ -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 候选详情缓存与本轮选择回执不一致');
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const maxProfileBytes = 64 * 1024;
9
+ const validProfileValue = value => typeof value === 'string' && value.trim() === value
10
+ && [...value].length >= 1 && [...value].length <= 128 && !/[\p{Cc}\p{Cs}]/u.test(value);
11
+
12
+ function readProfileFields(skillDir) {
13
+ try {
14
+ if (typeof skillDir !== 'string' || !path.isAbsolute(skillDir)) return {};
15
+ const file = path.join(skillDir, 'PROFILE');
16
+ const stat = fs.lstatSync(file);
17
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > maxProfileBytes) return {};
18
+ const profile = JSON.parse(fs.readFileSync(file, 'utf8'));
19
+ if (!profile || Array.isArray(profile) || profile.schemaVersion !== 1) return {};
20
+ return {
21
+ chinfo: validProfileValue(profile.chinfo) ? profile.chinfo : '',
22
+ vid: validProfileValue(profile.vid) ? profile.vid : '',
23
+ };
24
+ } catch {
25
+ return {};
26
+ }
27
+ }
28
+
29
+ export function readProfileChinfo(skillDir) { return readProfileFields(skillDir).chinfo || ''; }
30
+ export function readProfileVid(skillDir) { return readProfileFields(skillDir).vid || ''; }
31
+
32
+ export function runProfileCli(argv = process.argv) {
33
+ if (argv.length === 5 && argv[2] === 'read-chinfo' && argv[3] === '--skill-dir') {
34
+ process.stdout.write(readProfileChinfo(argv[4]));
35
+ return 0;
36
+ }
37
+ if (argv.length === 7 && argv[2] === 'merge-biz-info' && argv[3] === '--skill-dir' && argv[5] === '--base-json') {
38
+ try {
39
+ const base = JSON.parse(argv[6]);
40
+ if (!base || Array.isArray(base) || typeof base !== 'object') return 1;
41
+ const fields = readProfileFields(argv[4]);
42
+ if (fields.chinfo) base.chinfo = fields.chinfo;
43
+ if (fields.vid) base.vid = fields.vid;
44
+ process.stdout.write(JSON.stringify(base));
45
+ return 0;
46
+ } catch { return 1; }
47
+ }
48
+ return 1;
49
+ }
50
+
51
+ if (process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))) {
52
+ process.exitCode = runProfileCli();
53
+ }
@@ -35,6 +35,7 @@ function filesUnder(root) {
35
35
  else throw Error('Non-regular session Skill file');
36
36
  };
37
37
  for (const entry of ['SKILL.md','VERSION','references']) walk(entry);
38
+ if (fs.existsSync(path.join(root,'PROFILE'))) walk('PROFILE');
38
39
  return files;
39
40
  }
40
41
  function identity(root, channel, version) {
@@ -48,15 +49,20 @@ function identity(root, channel, version) {
48
49
  (channel==='pre'?'alipay-aipay-pre':'alipay-aipay')) throw Error('Session Skill identity mismatch');
49
50
  }
50
51
 
51
- export function createSessionSkill({sourceDir, channel, version, packageVersion, targetDir, home = os.homedir()}) {
52
+ export function createSessionSkill({sourceDir, channel, version, packageVersion, targetDir, profileContent, home = os.homedir()}) {
52
53
  identity(sourceDir, channel, version);
53
54
  if (!semver.test(packageVersion) || !path.isAbsolute(targetDir)) throw Error('Invalid session install source');
54
- const manifest = filesUnder(sourceDir);
55
55
  const root = privateRoot(home);
56
56
  const skillDir = path.join(root, `${channel}-${version}-${randomUUID()}`);
57
57
  fs.mkdirSync(skillDir, {mode:0o700});
58
58
  try {
59
59
  for (const entry of ['SKILL.md','VERSION','references']) fs.cpSync(path.join(sourceDir,entry),path.join(skillDir,entry),{recursive:true,errorOnExist:true,force:false});
60
+ const selectedProfile = profileContent === undefined ? readProfile(targetDir) : profileContent;
61
+ if (selectedProfile !== null && selectedProfile !== undefined) {
62
+ if (!(Buffer.isBuffer(selectedProfile) || selectedProfile instanceof Uint8Array) || selectedProfile.length > 64 * 1024) throw Error('Invalid PROFILE content');
63
+ fs.writeFileSync(path.join(skillDir,'PROFILE'),selectedProfile,{flag:'wx',mode:0o600});
64
+ }
65
+ const manifest = filesUnder(skillDir);
60
66
  const receipt = {schemaVersion:1, scope:'session', channel, version, packageVersion,
61
67
  skillDir:fs.realpathSync(skillDir), targetDir:path.resolve(targetDir)};
62
68
  fs.writeFileSync(path.join(skillDir,receiptName),JSON.stringify({...receipt,manifest}),{flag:'wx',mode:0o600});
@@ -68,6 +74,15 @@ export function createSessionSkill({sourceDir, channel, version, packageVersion,
68
74
  }
69
75
  }
70
76
 
77
+ function readProfile(skillDir) {
78
+ const file=path.join(skillDir,'PROFILE');
79
+ if (!fs.existsSync(file)) return null;
80
+ plain(file);
81
+ const stat=fs.lstatSync(file);
82
+ if (stat.size > 64 * 1024) throw Error('PROFILE is too large');
83
+ return fs.readFileSync(file);
84
+ }
85
+
71
86
  export function verifySessionSkill(receipt, {home = os.homedir(), channel, version, packageVersion, targetDir} = {}) {
72
87
  if (!receipt || receipt.schemaVersion!==1 || receipt.scope!=='session' || !['stable','pre'].includes(receipt.channel) || !semver.test(receipt.version||'') || !path.isAbsolute(receipt.skillDir||'') ||
73
88
  !path.isAbsolute(receipt.targetDir||'') || !semver.test(receipt.packageVersion||'')) throw Error('Invalid session receipt');
@@ -4,6 +4,8 @@
4
4
 
5
5
  唯一 stderr 终态:`SELF_UPDATE:CURRENT=<合法 SemVer>`、`SELF_UPDATE:UPDATED=<合法 SemVer>`、`SELF_UPDATE:SESSION_READY=<合法 SemVer>`、`SELF_UPDATE:RETRY_WITH_NETWORK`、`SELF_UPDATE:SKIPPED_NETWORK_RESTRICTED`、`SELF_UPDATE:CHECK_FAILED`、`SELF_UPDATE:INSTALL_FAILED`、`SELF_UPDATE:VERIFY_FAILED`。`UPDATED` 用原子替换后的新版,除 `SESSION_READY` 外其余用当前可用版;marker 异常读当前入口,不得称最新或更新成功;停止读旧版,读取同一根目录的新版入口。仅 `RETRY_WITH_NETWORK` 可取权后原样重试一次,拒绝、不支持或再次返回时读当前入口;一次网络恢复外,不重查、取权、用 `sudo`、要求用户操作或写更新状态。每次只查一次 npm latest 的 `version` 和 `alipayAipayChannels`;只比较本渠道 Skill 版本,npm 包版本仅用于精确下载;`VERSION` 缺失、非法、落后或业务入口缺失时只从查询所得精确 npm 包版本安装当前渠道一次。渠道由当前目录的 `references/normal/json/release-channel.json` 与 Skill 名称共同校验;旧正式版缺少渠道文件时只兼容 `stable`,预发版禁止回退正式版。缺少或非法的远端渠道元数据返回 `CHECK_FAILED`,不得按 npm 包版本兜底。不调用公开 `install --target all`;超时终止同一次调用的进程树并清理。
6
6
 
7
+ 公开 npm `install` 是 `PROFILE` 的唯一创建入口:`chinfo` 显式传值时更新为该来源,未传时更新为 `origin`;只有显式 `--vid <值>` 才新增或更新 `vid`,未传时保留已有 `vid`,其他已有扩展字段也保留。自更新只原样迁移当前 Skill 根目录已有的普通 `PROFILE`;不存在时新版及 Windows 会话副本都保持不存在。内部 `npx ... install-current` 不属于用户主动安装,禁止创建 `origin` 或修改 `vid`。源包的 `skill/` 与 `skill-pre/` 不包含 `PROFILE`。
8
+
7
9
  本地正式渠道的 `X.Y.Z-beta`、`X.Y.Z-betaN`(兼容 `-beta.N`)和预发渠道的 `X.Y.Z-pre-beta`、`X.Y.Z-pre-betaN` 均为固定测试版本。完整安装直接返回 `CURRENT` 且不查询 npm;业务入口损坏时返回 `VERIFY_FAILED`,仍不下载修复或转正。更换版本必须由测试人员显式执行 npm 安装器的 `install`,普通 `update` 同样不推进 beta。正常版本分别为 `X.Y.Z` 与 `X.Y.Z-pre`,远端对应渠道若为 beta 或名称不匹配,返回 `CHECK_FAILED`,不自动安装。beta 不参与版本排序,无自动追踪和自动转正。
8
10
 
9
11
  `SESSION_READY` 是 Windows 目录占用或权限失败的有限恢复:只有替换前的备份/暂存准备失败,安装器才从本次精确 npm 包创建并逐文件 SHA-256 复核独立副本;不混合覆盖原目录,不更新全局安装记录。必须同时取得唯一 `ALIPAY_AIPAY_INTERNAL:SELF_UPDATE_SESSION_SKILL_DIR=<JSON 字符串>`,用已核验的绝对路径重绑 `SKILL_DIR`,明确告知“全局目录未更新,当前会话使用已校验的新版本;下次会话仍可能加载旧版”。废弃旧 loaded/CLI 刷新事实,保留本渠道自更新已检查事实,立即读取新路径的 `SKILL.md` 并继续其余启动协议,禁止再次自更新。内测版仍需按新路径/版本完成声明及服务/采集选择,安装意图不得代替采集同意。路径 marker 缺失、冲突或新入口不可读时,不切换、不声称更新成功,使用仍可用的原目录或说明入口不可用。