@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.
- package/package.json +5 -2
- package/skill/SKILL.md +2 -2
- package/skill/VERSION +1 -1
- package/skill/references/integration/flow.md +109 -41
- package/skill/references/integration/modules/checklist.md +25 -17
- 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
- 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
- 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
- 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
- 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
- package/skill/references/integration/modules/interface-contract/aipay-interface-contract.md +3 -4
- package/skill/references/integration/modules/interface-contract/apppay-interface-contract.md +4 -4
- package/skill/references/integration/modules/interface-contract/common-trade-contract.md +1 -1
- package/skill/references/integration/modules/interface-contract/notify-url-contract.md +4 -4
- package/skill/references/integration/modules/interface-contract/webpay-interface-contract.md +4 -4
- package/skill/references/integration/modules/interface-guide.md +15 -13
- package/skill/references/integration/modules/product-decision.md +13 -13
- package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +2 -2
- package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +1 -1
- package/skill/references/integration/modules/scripts/sandbox_config.sh +1 -1
- package/skill/references/integration/modules/troubleshooting-flow.md +3 -3
- package/skill/references/normal/alipay-cli-env.md +2 -2
- package/skill/references/normal/full-process-routing.md +7 -5
- package/skill/references/normal/json/customer-messages.json +56 -51
- package/skill/references/normal/json/policy-rules.json +6 -6
- package/skill/references/normal/rejection-guide.md +1 -1
- package/skill/references/normal/scripts/alipay_cli_env.mjs +6 -2
- package/skill/references/normal/scripts/alipay_cli_env.sh +36 -2
- package/skill/references/normal/scripts/common.sh +64 -0
- package/skill/references/normal/scripts/flow_loader.mjs +241 -0
- package/skill/references/normal/scripts/integration_context_runner.mjs +166 -0
- package/skill/references/normal/scripts/integration_message_runner.mjs +237 -15
- package/skill/references/normal/scripts/interaction_reply.mjs +133 -0
- package/skill/references/normal/scripts/onboarding_discovery_runner.mjs +224 -0
- package/skill/references/normal/scripts/onboarding_message_runner.mjs +27 -12
- package/skill/references/normal/scripts/onboarding_recovery_runner.mjs +96 -0
- package/skill/references/normal/scripts/project_route_inspector.sh +3 -3
- package/skill/references/normal/scripts/python_script_runner.mjs +1 -1
- package/skill/references/normal/scripts/render_customer_message.mjs +67 -42
- package/skill/references/normal/scripts/telemetry.mjs +54 -5
- package/skill/references/normal/telemetry.md +26 -0
- package/skill/references/onboarding/flow.md +166 -152
- package/skill/references/onboarding/modules/app-release.md +39 -386
- package/skill/references/onboarding/modules/authorization.md +25 -270
- package/skill/references/onboarding/modules/error-handling.md +50 -253
- package/skill/references/onboarding/modules/product-sign.md +24 -289
- package/skill/references/onboarding/modules/scripts/app.sh +58 -21
- package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +15 -12
- package/skill/references/onboarding/modules/scripts/auth.sh +6 -1
- package/skill/references/onboarding/modules/scripts/error_handler.sh +1 -0
- package/skill/references/onboarding/modules/scripts/query_sign_status.sh +5 -3
- package/skill/references/onboarding/modules/scripts/service.sh +3 -2
- package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +24 -12
- package/skill/references/onboarding/modules/service-registration.md +20 -312
- 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: '
|
|
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
|
-
|
|
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('
|
|
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
|
|
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
|
|
692
|
+
return {
|
|
693
|
+
productType,
|
|
694
|
+
variant: mapping.variant,
|
|
636
695
|
productName,
|
|
637
|
-
projectPath,
|
|
638
|
-
|
|
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
|
-
|
|
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
|
+
}
|