@alipay/alipay-aipay 1.4.9 → 1.5.0

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 (55) hide show
  1. package/package.json +5 -2
  2. package/skill/SKILL.md +2 -2
  3. package/skill/VERSION +1 -1
  4. package/skill/references/integration/flow.md +109 -41
  5. package/skill/references/integration/modules/checklist.md +25 -17
  6. package/skill/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +1 -1
  7. package/skill/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +1 -1
  8. package/skill/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +1 -1
  9. package/skill/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +1 -1
  10. package/skill/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +1 -1
  11. package/skill/references/integration/modules/interface-contract/aipay-interface-contract.md +3 -4
  12. package/skill/references/integration/modules/interface-contract/apppay-interface-contract.md +4 -4
  13. package/skill/references/integration/modules/interface-contract/common-trade-contract.md +1 -1
  14. package/skill/references/integration/modules/interface-contract/notify-url-contract.md +4 -4
  15. package/skill/references/integration/modules/interface-contract/webpay-interface-contract.md +4 -4
  16. package/skill/references/integration/modules/interface-guide.md +15 -13
  17. package/skill/references/integration/modules/product-decision.md +13 -13
  18. package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +2 -2
  19. package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +1 -1
  20. package/skill/references/integration/modules/scripts/sandbox_config.sh +1 -1
  21. package/skill/references/integration/modules/troubleshooting-flow.md +3 -3
  22. package/skill/references/normal/alipay-cli-env.md +2 -2
  23. package/skill/references/normal/full-process-routing.md +7 -5
  24. package/skill/references/normal/json/customer-messages.json +56 -51
  25. package/skill/references/normal/json/policy-rules.json +6 -6
  26. package/skill/references/normal/rejection-guide.md +1 -1
  27. package/skill/references/normal/scripts/alipay_cli_env.mjs +6 -2
  28. package/skill/references/normal/scripts/alipay_cli_env.sh +36 -2
  29. package/skill/references/normal/scripts/common.sh +64 -0
  30. package/skill/references/normal/scripts/flow_loader.mjs +241 -0
  31. package/skill/references/normal/scripts/integration_context_runner.mjs +166 -0
  32. package/skill/references/normal/scripts/integration_message_runner.mjs +237 -15
  33. package/skill/references/normal/scripts/interaction_reply.mjs +133 -0
  34. package/skill/references/normal/scripts/onboarding_discovery_runner.mjs +224 -0
  35. package/skill/references/normal/scripts/onboarding_message_runner.mjs +27 -12
  36. package/skill/references/normal/scripts/onboarding_recovery_runner.mjs +96 -0
  37. package/skill/references/normal/scripts/project_route_inspector.sh +3 -3
  38. package/skill/references/normal/scripts/python_script_runner.mjs +1 -1
  39. package/skill/references/normal/scripts/render_customer_message.mjs +67 -42
  40. package/skill/references/normal/scripts/telemetry.mjs +54 -5
  41. package/skill/references/normal/telemetry.md +26 -0
  42. package/skill/references/onboarding/flow.md +166 -152
  43. package/skill/references/onboarding/modules/app-release.md +39 -386
  44. package/skill/references/onboarding/modules/authorization.md +25 -270
  45. package/skill/references/onboarding/modules/error-handling.md +50 -253
  46. package/skill/references/onboarding/modules/product-sign.md +24 -289
  47. package/skill/references/onboarding/modules/scripts/app.sh +58 -21
  48. package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +15 -12
  49. package/skill/references/onboarding/modules/scripts/auth.sh +6 -1
  50. package/skill/references/onboarding/modules/scripts/error_handler.sh +1 -0
  51. package/skill/references/onboarding/modules/scripts/query_sign_status.sh +5 -3
  52. package/skill/references/onboarding/modules/scripts/service.sh +3 -2
  53. package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +24 -12
  54. package/skill/references/onboarding/modules/service-registration.md +20 -312
  55. package/skill/references/runtime-entry.md +17 -16
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import crypto from 'node:crypto';
3
4
  import { spawnSync } from 'node:child_process';
4
5
  import fs from 'node:fs';
5
6
  import os from 'node:os';
@@ -15,9 +16,9 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
15
16
  const inspectorPath = path.join(scriptDir, 'project_route_inspector.sh');
16
17
 
17
18
  const productVariants = {
18
- aipay: { variant: 'AIPAY', productName: '按量付费' },
19
- webpay: { variant: 'WEBPAY', productName: '网站支付' },
20
- apppay: { variant: 'APPPAY', productName: 'APP 支付' }
19
+ aipay: { variant: 'AIPAY', productName: 'AI 按量付费' },
20
+ webpay: { variant: 'WEBPAY', productName: 'AI 网页应用收款' },
21
+ apppay: { variant: 'APPPAY', productName: 'AI 移动应用收款' }
21
22
  };
22
23
  const currentProjectAliases = new Set(['.', './', '当前项目', '当前目录', '这个项目', '这个仓库', '这里']);
23
24
  const projectMarkerNames = [
@@ -75,6 +76,12 @@ const aipayPaymentValidationMarker = 'Payment-Validation';
75
76
  const aipayPaymentVerifyMarker = 'alipay\\.aipay\\.agent\\.payment\\.verify|AlipayAipayAgentPaymentVerify(Request|Model|Response)|alipay_aipay_agent_payment_verify_response';
76
77
  const aipayFulfillmentConfirmMarker = 'alipay\\.aipay\\.agent\\.fulfillment\\.confirm|AlipayAipayAgentFulfillmentConfirm(Request|Model|Response)|alipay_aipay_agent_fulfillment_confirm_response';
77
78
  const aipayMarker = `${aipayPaymentNeededMarker}|${aipayPaymentProofMarker}|${aipayPaymentVerifyMarker}|${aipayFulfillmentConfirmMarker}`;
79
+ const internalPrefix = 'ALIPAY_AIPAY_INTERNAL:';
80
+ const receiptKinds = Object.freeze({
81
+ startConfirmation: 'integration-start-confirmation',
82
+ checklist: 'integration-checklist-scope',
83
+ a2mExecution: 'integration-a2m-execution'
84
+ });
78
85
 
79
86
  function parseArgs(argv) {
80
87
  const command = argv[2];
@@ -84,7 +91,9 @@ function parseArgs(argv) {
84
91
  if (!key.startsWith('--')) throw new Error(`未知参数 ${key}`);
85
92
  const value = argv[index + 1];
86
93
  if (value === undefined || value.startsWith('--')) throw new Error(`参数 ${key} 缺少取值`);
87
- args[key.slice(2)] = value;
94
+ const name = key.slice(2);
95
+ if (Object.prototype.hasOwnProperty.call(args, name)) throw new Error(`参数 ${key} 重复`);
96
+ args[name] = value;
88
97
  index += 1;
89
98
  }
90
99
  return { command, args };
@@ -452,6 +461,54 @@ function sourceFiles(projectPath) {
452
461
  return files.sort((left, right) => left.localeCompare(right));
453
462
  }
454
463
 
464
+ function projectSnapshotFingerprint(projectPath) {
465
+ if (!path.isAbsolute(projectPath) || !isReadableDirectory(projectPath)) {
466
+ throw new Error('项目快照路径必须是可访问的绝对目录');
467
+ }
468
+ const canonicalPath = fs.realpathSync(projectPath);
469
+ const tracked = new Set(sourceFiles(canonicalPath));
470
+ for (const name of [...projectMarkerNames, '.alipay-sandbox.json', '.gitignore']) {
471
+ const candidate = path.join(canonicalPath, name);
472
+ try {
473
+ if (fs.statSync(candidate).isFile() && !fs.lstatSync(candidate).isSymbolicLink()) tracked.add(candidate);
474
+ } catch {
475
+ // Missing optional project facts do not enter the snapshot.
476
+ }
477
+ }
478
+ const hash = crypto.createHash('sha256');
479
+ hash.update(`project=${canonicalPath}\n`);
480
+ for (const file of [...tracked].sort((left, right) => left.localeCompare(right))) {
481
+ const stat = fs.statSync(file);
482
+ const relative = path.relative(canonicalPath, file);
483
+ hash.update(`${relative}\0${stat.size}\0`);
484
+ if (path.basename(file) === '.alipay-sandbox.json') {
485
+ hash.update(`${stat.mtimeMs}\n`);
486
+ } else {
487
+ hash.update(fs.readFileSync(file));
488
+ hash.update('\n');
489
+ }
490
+ }
491
+ return hash.digest('hex');
492
+ }
493
+
494
+ function encodeReceipt(payload) {
495
+ return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
496
+ }
497
+
498
+ function decodeReceipt(value, expectedKind) {
499
+ if (typeof value !== 'string' || !value) throw new Error('缺少 receipt');
500
+ let payload;
501
+ try {
502
+ payload = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
503
+ } catch {
504
+ throw new Error('receipt 不是合法编码');
505
+ }
506
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload) || payload.schema_ver !== 1 || payload.kind !== expectedKind) {
507
+ throw new Error('receipt 类型或版本非法');
508
+ }
509
+ return payload;
510
+ }
511
+
455
512
  function scanPattern(files, pattern) {
456
513
  const regex = new RegExp(pattern);
457
514
  for (const file of files) {
@@ -484,9 +541,9 @@ function scanProjectRoute(productType, projectPath, projectSelection) {
484
541
  const files = sourceFiles(projectPath);
485
542
  const otherProducts = [];
486
543
 
487
- if (scanPattern(files, webpayMarker) && productType !== 'webpay') otherProducts.push('网站支付');
488
- if (scanPattern(files, apppayMarker) && productType !== 'apppay') otherProducts.push('APP 支付');
489
- if (scanPattern(files, aipayMarker) && productType !== 'aipay') otherProducts.push('按量付费');
544
+ if (scanPattern(files, webpayMarker) && productType !== 'webpay') otherProducts.push('AI 网页应用收款');
545
+ if (scanPattern(files, apppayMarker) && productType !== 'apppay') otherProducts.push('AI 移动应用收款');
546
+ if (scanPattern(files, aipayMarker) && productType !== 'aipay') otherProducts.push('AI 按量付费');
490
547
 
491
548
  const otherProductsText = otherProducts.length > 0 ? otherProducts.join(',') : '无';
492
549
  let integrationStatus;
@@ -600,7 +657,7 @@ function normalizeLanguage(input) {
600
657
  throw new Error('服务端语言非法,仅支持 Java、Python、Node.js、C#、PHP');
601
658
  }
602
659
 
603
- function runStartConfirm(args) {
660
+ function startConfirmationFacts(args) {
604
661
  const productType = required(args, 'product-type');
605
662
  const mapping = productVariants[productType];
606
663
  if (!mapping) throw new Error('product-type 非法,仅支持 aipay、webpay、apppay');
@@ -632,18 +689,163 @@ function runStartConfirm(args) {
632
689
  throw new Error('项目检查结果缺少必要事实字段');
633
690
  }
634
691
 
635
- return renderMessage('integration.start.confirm', mapping.variant, {
692
+ return {
693
+ productType,
694
+ variant: mapping.variant,
636
695
  productName,
637
- projectPath,
638
- projectOriginLabel: route.projectOriginLabel,
639
- integrationStatus: route.integrationStatus,
640
- otherProducts: route.otherProducts,
696
+ projectPath: fs.realpathSync(projectPath),
697
+ projectSelection,
641
698
  language,
642
699
  framework,
700
+ route,
701
+ projectFingerprint: projectSnapshotFingerprint(projectPath)
702
+ };
703
+ }
704
+
705
+ function confirmationReceipt(facts) {
706
+ return encodeReceipt({
707
+ schema_ver: 1,
708
+ kind: receiptKinds.startConfirmation,
709
+ product_type: facts.productType,
710
+ product_name: facts.productName,
711
+ project_path: facts.projectPath,
712
+ project_selection: facts.projectSelection,
713
+ language: facts.language,
714
+ framework: facts.framework,
715
+ project_fingerprint: facts.projectFingerprint,
716
+ route: {
717
+ project_origin: facts.route.projectOrigin,
718
+ project_origin_label: facts.route.projectOriginLabel,
719
+ integration_status: facts.route.integrationStatus,
720
+ evidence: facts.route.evidence,
721
+ other_products: facts.route.otherProducts
722
+ }
723
+ });
724
+ }
725
+
726
+ function renderStartConfirmation(facts) {
727
+ return renderMessage('integration.start.confirm', facts.variant, {
728
+ productName: facts.productName,
729
+ projectPath: facts.projectPath,
730
+ projectOriginLabel: facts.route.projectOriginLabel,
731
+ integrationStatus: facts.route.integrationStatus,
732
+ otherProducts: facts.route.otherProducts,
733
+ language: facts.language,
734
+ framework: facts.framework,
643
735
  nextFlow: 'integration'
644
736
  });
645
737
  }
646
738
 
739
+ export function createStartConfirmation(args) {
740
+ const facts = startConfirmationFacts(args);
741
+ return { message: renderStartConfirmation(facts), receipt: confirmationReceipt(facts) };
742
+ }
743
+
744
+ export function validateStartConfirmationReceipt(receipt) {
745
+ const saved = decodeReceipt(receipt, receiptKinds.startConfirmation);
746
+ const facts = startConfirmationFacts({
747
+ 'product-type': saved.product_type,
748
+ 'product-name': saved.product_name,
749
+ 'project-path': saved.project_path,
750
+ 'project-selection': saved.project_selection,
751
+ language: saved.language,
752
+ framework: saved.framework
753
+ });
754
+ const currentReceipt = confirmationReceipt(facts);
755
+ if (currentReceipt === receipt) return { valid: true, receipt, productType: saved.product_type };
756
+ return {
757
+ valid: false,
758
+ receipt: currentReceipt,
759
+ message: renderStartConfirmation(facts),
760
+ productType: saved.product_type
761
+ };
762
+ }
763
+
764
+ export function createChecklistScopeReceipt(projectPath) {
765
+ const canonicalPath = fs.realpathSync(required({ projectPath }, 'projectPath'));
766
+ return encodeReceipt({
767
+ schema_ver: 1,
768
+ kind: receiptKinds.checklist,
769
+ project_path: canonicalPath,
770
+ project_fingerprint: projectSnapshotFingerprint(canonicalPath)
771
+ });
772
+ }
773
+
774
+ function validateChecklistScopeReceipt(receipt, expectedProjectPath) {
775
+ const payload = decodeReceipt(receipt, receiptKinds.checklist);
776
+ const canonicalExpectedPath = fs.realpathSync(expectedProjectPath);
777
+ return payload.project_path === canonicalExpectedPath
778
+ && projectSnapshotFingerprint(canonicalExpectedPath) === payload.project_fingerprint;
779
+ }
780
+
781
+ function a2mExecutionFacts(args) {
782
+ const projectPath = fs.realpathSync(required(args, 'project-path'));
783
+ if (!isReadableDirectory(projectPath)) throw new Error('A2M 联调项目路径不可访问');
784
+ const method = required(args, 'http-method').toUpperCase();
785
+ if (!['GET', 'POST'].includes(method)) throw new Error('http-method 仅支持 GET 或 POST');
786
+ let serviceUrl;
787
+ try {
788
+ serviceUrl = new URL(required(args, 'service-url'));
789
+ } catch {
790
+ throw new Error('service-url 不是合法 URL');
791
+ }
792
+ if (!['http:', 'https:'].includes(serviceUrl.protocol) || serviceUrl.username || serviceUrl.password || serviceUrl.hash) {
793
+ throw new Error('service-url 只允许不含凭据和 fragment 的 HTTP(S) URL');
794
+ }
795
+ let requestBodyHash = '';
796
+ const requestBodyFile = args['request-body-file'];
797
+ if (method === 'POST') {
798
+ if (!requestBodyFile || !path.isAbsolute(requestBodyFile)) throw new Error('POST 必须传入绝对 --request-body-file');
799
+ const stat = fs.lstatSync(requestBodyFile);
800
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('request-body-file 必须是普通文件且不能是符号链接');
801
+ requestBodyHash = crypto.createHash('sha256').update(fs.readFileSync(requestBodyFile)).digest('hex');
802
+ } else if (requestBodyFile !== undefined) {
803
+ throw new Error('GET 禁止传入 request-body-file');
804
+ }
805
+ return {
806
+ project_path: projectPath,
807
+ project_fingerprint: projectSnapshotFingerprint(projectPath),
808
+ service_url: serviceUrl.href,
809
+ http_method: method,
810
+ request_body_sha256: requestBodyHash
811
+ };
812
+ }
813
+
814
+ function a2mExecutionReceipt(facts, state = 'READY') {
815
+ return encodeReceipt({
816
+ schema_ver: 1,
817
+ kind: receiptKinds.a2mExecution,
818
+ state,
819
+ ...facts
820
+ });
821
+ }
822
+
823
+ export function guardA2mExecution(args) {
824
+ const facts = a2mExecutionFacts(args);
825
+ const priorReceipt = args['prior-receipt'];
826
+ if (!priorReceipt) return { decision: 'RUN', receipt: a2mExecutionReceipt(facts) };
827
+ const prior = decodeReceipt(priorReceipt, receiptKinds.a2mExecution);
828
+ const unchanged = ['project_path', 'project_fingerprint', 'service_url', 'http_method', 'request_body_sha256']
829
+ .every((key) => prior[key] === facts[key]);
830
+ if (!unchanged) return { decision: 'RUN', receipt: a2mExecutionReceipt(facts) };
831
+ if (prior.state === 'PASSED') return { decision: 'REUSE_PASSED', receipt: priorReceipt };
832
+ if (prior.state === 'FAILED') return { decision: 'BLOCK_UNCHANGED', receipt: priorReceipt };
833
+ return { decision: 'UNKNOWN_PREVIOUS_EXECUTION', receipt: priorReceipt };
834
+ }
835
+
836
+ export function completeA2mExecution(receipt, outcome) {
837
+ if (!['PASSED', 'FAILED', 'INTERRUPTED'].includes(outcome)) {
838
+ throw new Error('outcome 仅支持 PASSED、FAILED、INTERRUPTED');
839
+ }
840
+ const prior = decodeReceipt(receipt, receiptKinds.a2mExecution);
841
+ if (prior.state !== 'READY') throw new Error('A2M execution receipt 已经终态化');
842
+ const { schema_ver, kind, state, ...facts } = prior;
843
+ void schema_ver;
844
+ void kind;
845
+ void state;
846
+ return a2mExecutionReceipt(facts, outcome);
847
+ }
848
+
647
849
  function runContextRequired(args) {
648
850
  return renderMessage('integration.context.required', 'DEFAULT', {
649
851
  missingItems: required(args, 'missing-items')
@@ -732,6 +934,11 @@ function checklistInputFromArgs(args) {
732
934
  async function runChecklistResult(args) {
733
935
  const variant = required(args, 'variant');
734
936
  if (!['AIPAY', 'WEBPAY', 'APPPAY'].includes(variant)) throw new Error('variant 非法,仅支持 AIPAY、WEBPAY、APPPAY');
937
+ const scopeReceipt = required(args, 'scope-receipt');
938
+ const projectPath = required(args, 'project-path');
939
+ if (!validateChecklistScopeReceipt(scopeReceipt, projectPath)) {
940
+ throw new Error('项目在当前 checklist 开始后发生变化;必须重新进入 checklist 阶段并重跑受影响检查');
941
+ }
735
942
  const input = checklistInputFromArgs(args) || await readStdin();
736
943
  const rendered = renderMessage('integration.checklist.result', variant, input);
737
944
  if (input.executionMode === '仅代码开发') {
@@ -751,7 +958,22 @@ async function main() {
751
958
  return;
752
959
  }
753
960
  if (command === 'start-confirm') {
754
- console.log(runStartConfirm(args));
961
+ const result = createStartConfirmation(args);
962
+ console.log(result.message);
963
+ process.stderr.write(`${internalPrefix}PROJECT_CONFIRM_RECEIPT=${result.receipt}\n`);
964
+ return;
965
+ }
966
+ if (command === 'checklist-scope-receipt') {
967
+ process.stderr.write(`${internalPrefix}CHECKLIST_SCOPE_RECEIPT=${createChecklistScopeReceipt(required(args, 'project-path'))}\n`);
968
+ return;
969
+ }
970
+ if (command === 'a2m-execution-guard') {
971
+ process.stderr.write(`${internalPrefix}A2M_EXECUTION=${JSON.stringify(guardA2mExecution(args))}\n`);
972
+ return;
973
+ }
974
+ if (command === 'a2m-execution-result') {
975
+ const receipt = completeA2mExecution(required(args, 'receipt'), required(args, 'outcome'));
976
+ process.stderr.write(`${internalPrefix}A2M_EXECUTION=${JSON.stringify({ decision: 'RECORDED', receipt })}\n`);
755
977
  return;
756
978
  }
757
979
  if (command === 'context-required') {
@@ -790,7 +1012,7 @@ async function main() {
790
1012
  console.log(await runChecklistResult(args));
791
1013
  return;
792
1014
  }
793
- throw new Error('用法: integration_message_runner.mjs <resolve-project|locate-projects|start-confirm|context-required|product-clarify|windows-sandbox-setup|local-acceptance-notice|a2m-test-start|service-not-ready|safety-closeout|windows-sandbox-summary|checklist-result> ...');
1015
+ throw new Error('用法: integration_message_runner.mjs <resolve-project|locate-projects|start-confirm|checklist-scope-receipt|a2m-execution-guard|a2m-execution-result|context-required|product-clarify|windows-sandbox-setup|local-acceptance-notice|a2m-test-start|service-not-ready|safety-closeout|windows-sandbox-summary|checklist-result> ...');
794
1016
  }
795
1017
 
796
1018
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
@@ -0,0 +1,133 @@
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 internalPrefix = 'ALIPAY_AIPAY_INTERNAL:';
9
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
10
+ const catalogPath = path.resolve(scriptDir, '../json/customer-messages.json');
11
+ const questionPattern = /[??]|(?:吗|么|嘛|呢)$|是否|能否|可否|要不要|是不是|好了吗|完成了吗|为什么|为何|怎么|怎样|如何|什么意思|什么(?:方式|时候)?|哪个|哪种|何时|什么时候|多少|可不可以|能不能|行不行/u;
12
+ const englishQuestionPattern = /\b(?:can|could|would|should|why|how|what|when|whether)\b/iu;
13
+ const uncertaintyPattern = /可能|大概|也许|或许|不确定|说不好|看情况|再看看|考虑一下|好像|似乎|应该|有风险|有问题|担心|顾虑/u;
14
+ const englishUncertaintyPattern = /\b(?:maybe|perhaps|probably|possibly|unsure)\b/iu;
15
+ const rejectionPattern = /(?:不(?:是|需要|要|用|必|能|可|再)?|别|无(?:需|须|法))(?:再)?(?:继续|推进|执行|处理|开始|提交|确认|同意|允许|批准|创建|操作|往下|下一步|通过|完成)|不可以|不行|不同意|不确认|不允许|不批准|不通过|未确认|未完成|未通过|没(?:有)?完成|还没(?:有)?(?:好|完成|确认|准备)|拒绝|取消|停止|暂停|稍后|晚点|待会|等一下|等等|算了|否定/u;
16
+ const englishRejectionPattern = /\b(?:no|nope|not|don't|do\s+not|stop|cancel|wait|change)\b/iu;
17
+ const conditionalOrChangePattern = /如果|假如|要是|除非|只要|之后再|以后再|但是|不过|只是|前提|再说|(?:修改|调整|改一下|改成|换成|重选|重新选择)|先.{0,20}(?:再|然后)/u;
18
+ const affirmativeIntentPattern = /继续|推进|执行|开始|确认(?:无误)?|确定|同意|允许|批准|可以|没(?:有)?问题|无异议|通过|(?:已经|已)(?:完成|好了)|完成(?:了)?|搞定(?:了)?|弄好了|处理好了|就这么办|往下(?:走|做)|下一步|按(?:此|这个|该|当前)(?:方案)?(?:做|办|执行|推进|处理)|照(?:此|这个|该|当前)(?:方案)?(?:做|办|执行|推进|处理)/u;
19
+ const englishAffirmativeIntentPattern = /\b(?:ok(?:ay)?|yes|confirm(?:ed)?|continue|proceed|approve(?:d)?|go\s+ahead)\b/iu;
20
+ const simpleAffirmativePattern = /^(?:行|行的|行吧)$/u;
21
+ const englishAffirmativeReplies = new Set(['ok', 'okay', 'yes', 'y']);
22
+ const safePrefixes = ['那么就', '那就', '麻烦', '烦请', '嗯嗯', '好的', '我们', '请', '那', '就', '嗯', '我'];
23
+ const safeSuffixes = ['谢谢', '就行', '即可', '吧', '呀', '啊', '哈', '了'];
24
+
25
+ function affirmativeReplies() {
26
+ const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
27
+ const replies = catalog.interactionReplySets?.affirmative;
28
+ if (!Array.isArray(replies) || replies.length === 0 || replies.some((reply) => typeof reply !== 'string' || !reply)) {
29
+ throw new Error('customer-messages affirmative 回复集合无效');
30
+ }
31
+ return new Set(replies.map(normalizeAffirmativeReply));
32
+ }
33
+
34
+ export function normalizeAffirmativeReply(value) {
35
+ if (typeof value !== 'string') return '';
36
+ return value
37
+ .normalize('NFKC')
38
+ .trim()
39
+ .replace(/[。!!]+$/gu, '')
40
+ .trim()
41
+ .toLowerCase();
42
+ }
43
+
44
+ export function isAffirmativeReply(value) {
45
+ const normalized = normalizeAffirmativeReply(value);
46
+ if (normalized === '') return false;
47
+ const replies = affirmativeReplies();
48
+ let compact = normalized.replace(/[\s,,.。!!、;;::]+/gu, '');
49
+ const intentText = compact
50
+ .replace(/(?:不需要|无需|不用)(?:再)?(?:修改|调整|改动|改|确认)/gu, '无异议')
51
+ .replace(/没(?:有)?问题/gu, '无异议');
52
+ if (
53
+ questionPattern.test(intentText)
54
+ || englishQuestionPattern.test(normalized)
55
+ || uncertaintyPattern.test(intentText)
56
+ || englishUncertaintyPattern.test(normalized)
57
+ || rejectionPattern.test(intentText)
58
+ || englishRejectionPattern.test(normalized)
59
+ || conditionalOrChangePattern.test(intentText)
60
+ ) return false;
61
+ if (
62
+ replies.has(normalized)
63
+ || replies.has(compact)
64
+ || simpleAffirmativePattern.test(compact)
65
+ || affirmativeIntentPattern.test(compact)
66
+ || englishAffirmativeIntentPattern.test(normalized)
67
+ ) return true;
68
+
69
+ let changed = true;
70
+ while (changed && compact) {
71
+ changed = false;
72
+ for (const prefix of safePrefixes) {
73
+ if (compact.startsWith(prefix)) {
74
+ compact = compact.slice(prefix.length);
75
+ changed = true;
76
+ break;
77
+ }
78
+ }
79
+ for (const suffix of safeSuffixes) {
80
+ if (compact.endsWith(suffix)) {
81
+ compact = compact.slice(0, -suffix.length);
82
+ changed = true;
83
+ break;
84
+ }
85
+ }
86
+ }
87
+ if (compact === '') return false;
88
+ if (
89
+ replies.has(compact)
90
+ || simpleAffirmativePattern.test(compact)
91
+ || affirmativeIntentPattern.test(compact)
92
+ || englishAffirmativeIntentPattern.test(compact)
93
+ ) return true;
94
+
95
+ // Only collapse repeated Latin keystrokes after all negative/ambiguous guards.
96
+ const deduplicatedLatin = compact.replace(/([a-z])\1+/gu, '$1');
97
+ return englishAffirmativeReplies.has(deduplicatedLatin);
98
+ }
99
+
100
+ function parseArgs(argv) {
101
+ const command = argv[2];
102
+ const args = {};
103
+ for (let index = 3; index < argv.length; index += 1) {
104
+ const token = argv[index];
105
+ if (!token.startsWith('--')) throw new Error(`未知参数 ${token}`);
106
+ const value = argv[index + 1];
107
+ if (value === undefined || value.startsWith('--')) throw new Error(`参数 ${token} 缺少取值`);
108
+ const name = token.slice(2);
109
+ if (Object.prototype.hasOwnProperty.call(args, name)) throw new Error(`参数 ${token} 重复`);
110
+ args[name] = value;
111
+ index += 1;
112
+ }
113
+ return { command, args };
114
+ }
115
+
116
+ function main() {
117
+ const { command, args } = parseArgs(process.argv);
118
+ if (command !== 'confirm' || typeof args.reply !== 'string' || !args.reply) {
119
+ throw new Error('用法: interaction_reply.mjs confirm --reply <用户原始回复>');
120
+ }
121
+ const decision = isAffirmativeReply(args.reply) ? 'CONFIRMED' : 'REJECTED';
122
+ process.stderr.write(`${internalPrefix}REPLY_DECISION=${decision}\n`);
123
+ if (decision === 'REJECTED') process.exitCode = 2;
124
+ }
125
+
126
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
127
+ try {
128
+ main();
129
+ } catch (error) {
130
+ process.stderr.write(`${internalPrefix}REPLY_VALIDATION_ERROR=${String(error?.message || error).replace(/[\r\n]/gu, ' ')}\n`);
131
+ process.exitCode = 1;
132
+ }
133
+ }