@alipay/alipay-aipay 1.7.0 → 1.7.2
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/README.md +30 -0
- package/dist/commands/install-current.js +15 -7
- package/dist/commands/install.js +20 -16
- package/dist/commands/uninstall.js +8 -3
- package/dist/commands/update.js +34 -16
- package/dist/index.js +10 -4
- package/dist/utils/channels.js +67 -0
- package/dist/utils/config.js +15 -11
- package/dist/utils/file.js +23 -10
- package/dist/utils/npm.js +19 -33
- package/package.json +8 -3
- package/skill/SKILL.md +2 -0
- package/skill/references/normal/json/release-channel.json +1 -0
- package/skill/references/normal/scripts/self_update.mjs +62 -17
- package/skill/references/normal/self-update.md +3 -1
- package/skill-pre/SKILL.md +65 -0
- package/skill-pre/VERSION +1 -0
- package/skill-pre/references/integration/flow.md +602 -0
- package/skill-pre/references/integration/modules/alipay-sdk-reminder.md +281 -0
- package/skill-pre/references/integration/modules/checklist.md +154 -0
- package/skill-pre/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 +199 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +68 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +83 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +80 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +73 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +62 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +132 -0
- package/skill-pre/references/integration/modules/code-examples/csharp/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +640 -0
- package/skill-pre/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 +184 -0
- package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +72 -0
- package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +96 -0
- package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +81 -0
- package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +77 -0
- package/skill-pre/references/integration/modules/code-examples/java/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +69 -0
- package/skill-pre/references/integration/modules/code-examples/java/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +138 -0
- package/skill-pre/references/integration/modules/code-examples/java/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemoController.md +623 -0
- package/skill-pre/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 +135 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +51 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +63 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +59 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +54 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +84 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +71 -0
- package/skill-pre/references/integration/modules/code-examples/nodejs/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +498 -0
- package/skill-pre/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 +157 -0
- package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +59 -0
- package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +73 -0
- package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +69 -0
- package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +65 -0
- package/skill-pre/references/integration/modules/code-examples/php/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +59 -0
- package/skill-pre/references/integration/modules/code-examples/php/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +110 -0
- package/skill-pre/references/integration/modules/code-examples/php/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +806 -0
- package/skill-pre/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 +128 -0
- package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +57 -0
- package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +70 -0
- package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +65 -0
- package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +62 -0
- package/skill-pre/references/integration/modules/code-examples/python/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +51 -0
- package/skill-pre/references/integration/modules/code-examples/python/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +47 -0
- package/skill-pre/references/integration/modules/code-examples/python/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +541 -0
- package/skill-pre/references/integration/modules/interface-contract/aipay-interface-contract.md +52 -0
- package/skill-pre/references/integration/modules/interface-contract/apppay-interface-contract.md +45 -0
- package/skill-pre/references/integration/modules/interface-contract/common-trade-contract.md +42 -0
- package/skill-pre/references/integration/modules/interface-contract/notify-url-contract.md +68 -0
- package/skill-pre/references/integration/modules/interface-contract/webpay-interface-contract.md +44 -0
- package/skill-pre/references/integration/modules/interface-guide.md +61 -0
- package/skill-pre/references/integration/modules/product-decision.md +73 -0
- package/skill-pre/references/integration/modules/sandbox/a2m-sandbox-test.md +235 -0
- package/skill-pre/references/integration/modules/sandbox/alipay-sandbox-tool.md +142 -0
- package/skill-pre/references/integration/modules/sandbox/sandbox-setup-guide.md +239 -0
- package/skill-pre/references/integration/modules/scripts/local_402_sandbox_pay.py +900 -0
- package/skill-pre/references/integration/modules/scripts/sandbox_config.sh +347 -0
- package/skill-pre/references/integration/modules/troubleshooting-flow.md +28 -0
- package/skill-pre/references/normal/alipay-cli-env.md +169 -0
- package/skill-pre/references/normal/conversation-telemetry.md +101 -0
- package/skill-pre/references/normal/full-process-routing.md +92 -0
- package/skill-pre/references/normal/integration-worker.md +90 -0
- package/skill-pre/references/normal/json/customer-messages.json +1 -0
- package/skill-pre/references/normal/json/managed-jq-assets.json +1 -0
- package/skill-pre/references/normal/json/policy-rules.json +1 -0
- package/skill-pre/references/normal/json/release-channel.json +1 -0
- package/skill-pre/references/normal/rejection-guide.md +41 -0
- package/skill-pre/references/normal/scripts/agent_platform.mjs +43 -0
- package/skill-pre/references/normal/scripts/alipay_cli_env.mjs +34 -0
- package/skill-pre/references/normal/scripts/alipay_cli_env.sh +55 -0
- package/skill-pre/references/normal/scripts/alipay_cli_refresh.mjs +129 -0
- package/skill-pre/references/normal/scripts/auth_runtime_runner.mjs +60 -0
- package/skill-pre/references/normal/scripts/checklist_sections.mjs +43 -0
- package/skill-pre/references/normal/scripts/common.sh +365 -0
- package/skill-pre/references/normal/scripts/conversation_telemetry.mjs +105 -0
- package/skill-pre/references/normal/scripts/detect_dev_tool.sh +79 -0
- package/skill-pre/references/normal/scripts/flow_loader.mjs +237 -0
- package/skill-pre/references/normal/scripts/integration_context_runner.mjs +151 -0
- package/skill-pre/references/normal/scripts/integration_message_runner.mjs +1456 -0
- package/skill-pre/references/normal/scripts/interaction_reply.mjs +140 -0
- package/skill-pre/references/normal/scripts/managed_jq.mjs +215 -0
- package/skill-pre/references/normal/scripts/onboarding_discovery_runner.mjs +387 -0
- package/skill-pre/references/normal/scripts/onboarding_message_runner.mjs +1425 -0
- package/skill-pre/references/normal/scripts/onboarding_recovery_runner.mjs +100 -0
- package/skill-pre/references/normal/scripts/open_official_url.sh +205 -0
- package/skill-pre/references/normal/scripts/platform_compat.mjs +42 -0
- package/skill-pre/references/normal/scripts/preview_runtime.mjs +151 -0
- package/skill-pre/references/normal/scripts/project_route_inspector.sh +132 -0
- package/skill-pre/references/normal/scripts/python_script_runner.mjs +58 -0
- package/skill-pre/references/normal/scripts/render_customer_message.mjs +1792 -0
- package/skill-pre/references/normal/scripts/runtime.mjs +271 -0
- package/skill-pre/references/normal/scripts/self_update.mjs +376 -0
- package/skill-pre/references/normal/scripts/telemetry.mjs +2102 -0
- package/skill-pre/references/normal/self-update.md +7 -0
- package/skill-pre/references/normal/telemetry.md +70 -0
- package/skill-pre/references/onboarding/assets/keygen.html +927 -0
- package/skill-pre/references/onboarding/flow.md +607 -0
- package/skill-pre/references/onboarding/modules/app-release.md +90 -0
- package/skill-pre/references/onboarding/modules/authorization.md +43 -0
- package/skill-pre/references/onboarding/modules/error-handling.md +62 -0
- package/skill-pre/references/onboarding/modules/product-sign.md +49 -0
- package/skill-pre/references/onboarding/modules/scripts/app.sh +1485 -0
- package/skill-pre/references/onboarding/modules/scripts/ar_sign_apply.sh +331 -0
- package/skill-pre/references/onboarding/modules/scripts/auth.sh +1040 -0
- package/skill-pre/references/onboarding/modules/scripts/download_key_tool.sh +4 -0
- package/skill-pre/references/onboarding/modules/scripts/error_handler.sh +841 -0
- package/skill-pre/references/onboarding/modules/scripts/mcc.sh +1085 -0
- package/skill-pre/references/onboarding/modules/scripts/network_retry.sh +135 -0
- package/skill-pre/references/onboarding/modules/scripts/query_sign_status.sh +105 -0
- package/skill-pre/references/onboarding/modules/scripts/service.sh +528 -0
- package/skill-pre/references/onboarding/modules/scripts/sign_status_parser.sh +58 -0
- package/skill-pre/references/onboarding/modules/scripts/upload_screenshots.sh +208 -0
- package/skill-pre/references/onboarding/modules/scripts/upload_special_license.sh +195 -0
- package/skill-pre/references/onboarding/modules/service-registration.md +40 -0
- package/skill-pre/references/runtime-entry.md +99 -0
- package/dist/commands/install-current.d.ts +0 -4
- package/dist/commands/install-current.d.ts.map +0 -1
- package/dist/commands/install-current.js.map +0 -1
- package/dist/commands/install.d.ts +0 -10
- package/dist/commands/install.d.ts.map +0 -1
- package/dist/commands/install.js.map +0 -1
- package/dist/commands/prepare-tools.d.ts +0 -5
- package/dist/commands/prepare-tools.d.ts.map +0 -1
- package/dist/commands/prepare-tools.js.map +0 -1
- package/dist/commands/uninstall.d.ts +0 -10
- package/dist/commands/uninstall.d.ts.map +0 -1
- package/dist/commands/uninstall.js.map +0 -1
- package/dist/commands/update.d.ts +0 -10
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types.d.ts +0 -42
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils/alipay-cli.d.ts +0 -20
- package/dist/utils/alipay-cli.d.ts.map +0 -1
- package/dist/utils/alipay-cli.js.map +0 -1
- package/dist/utils/config.d.ts +0 -52
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/file.d.ts +0 -77
- package/dist/utils/file.d.ts.map +0 -1
- package/dist/utils/file.js.map +0 -1
- package/dist/utils/jq.d.ts +0 -17
- package/dist/utils/jq.d.ts.map +0 -1
- package/dist/utils/jq.js.map +0 -1
- package/dist/utils/npm.d.ts +0 -17
- package/dist/utils/npm.d.ts.map +0 -1
- package/dist/utils/npm.js.map +0 -1
- package/dist/utils/shell.d.ts +0 -24
- package/dist/utils/shell.d.ts.map +0 -1
- package/dist/utils/shell.js.map +0 -1
|
@@ -0,0 +1,1456 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
import { isWindowsPlatform } from './platform_compat.mjs';
|
|
12
|
+
import { renderMessage } from './render_customer_message.mjs';
|
|
13
|
+
import { currentRunContext, emitStageMetric, startTelemetryRun, telemetryEnabled } from './telemetry.mjs';
|
|
14
|
+
|
|
15
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const inspectorPath = path.join(scriptDir, 'project_route_inspector.sh');
|
|
17
|
+
|
|
18
|
+
const productVariants = {
|
|
19
|
+
aipay: { variant: 'AIPAY', productName: 'AI 按量付费' },
|
|
20
|
+
webpay: { variant: 'WEBPAY', productName: 'AI 网页应用收款' },
|
|
21
|
+
apppay: { variant: 'APPPAY', productName: 'AI 移动应用收款' }
|
|
22
|
+
};
|
|
23
|
+
const currentProjectAliases = new Set(['.', './', '当前项目', '当前目录', '这个项目', '这个仓库', '这里']);
|
|
24
|
+
const projectMarkerNames = [
|
|
25
|
+
'package.json',
|
|
26
|
+
'pom.xml',
|
|
27
|
+
'build.gradle',
|
|
28
|
+
'build.gradle.kts',
|
|
29
|
+
'settings.gradle',
|
|
30
|
+
'settings.gradle.kts',
|
|
31
|
+
'gradlew',
|
|
32
|
+
'mvnw',
|
|
33
|
+
'pyproject.toml',
|
|
34
|
+
'requirements.txt',
|
|
35
|
+
'Pipfile',
|
|
36
|
+
'poetry.lock',
|
|
37
|
+
'composer.json',
|
|
38
|
+
'artisan',
|
|
39
|
+
'manage.py'
|
|
40
|
+
];
|
|
41
|
+
const projectMarkerNameSet = new Set(projectMarkerNames);
|
|
42
|
+
const skippedProjectDirs = new Set([
|
|
43
|
+
'.git',
|
|
44
|
+
'.hg',
|
|
45
|
+
'.svn',
|
|
46
|
+
'.cache',
|
|
47
|
+
'.gradle',
|
|
48
|
+
'.idea',
|
|
49
|
+
'.next',
|
|
50
|
+
'.nuxt',
|
|
51
|
+
'.pytest_cache',
|
|
52
|
+
'.tox',
|
|
53
|
+
'.Trash',
|
|
54
|
+
'.venv',
|
|
55
|
+
'Library',
|
|
56
|
+
'__pycache__',
|
|
57
|
+
'build',
|
|
58
|
+
'coverage',
|
|
59
|
+
'dist',
|
|
60
|
+
'logs',
|
|
61
|
+
'node_modules',
|
|
62
|
+
'reports',
|
|
63
|
+
'target',
|
|
64
|
+
'test-results',
|
|
65
|
+
'vendor'
|
|
66
|
+
]);
|
|
67
|
+
const sourceFileExtensions = new Set(['.java', '.kt', '.js', '.mjs', '.cjs', '.ts', '.tsx', '.py', '.php', '.cs']);
|
|
68
|
+
const snapshotConfigExtensions = new Set([
|
|
69
|
+
'.config', '.conf', '.csproj', '.fsproj', '.ini', '.properties', '.sln', '.toml', '.vbproj', '.xml', '.yaml', '.yml'
|
|
70
|
+
]);
|
|
71
|
+
const snapshotConfigNames = new Set([
|
|
72
|
+
...projectMarkerNames,
|
|
73
|
+
'.alipay-sandbox.json',
|
|
74
|
+
'.gitignore',
|
|
75
|
+
'composer.lock',
|
|
76
|
+
'dockerfile',
|
|
77
|
+
'gemfile',
|
|
78
|
+
'gemfile.lock',
|
|
79
|
+
'package-lock.json',
|
|
80
|
+
'pnpm-lock.yaml',
|
|
81
|
+
'yarn.lock'
|
|
82
|
+
].map((name) => name.toLowerCase()));
|
|
83
|
+
const webpayMarker = 'alipay\\.trade\\.page\\.pay|AlipayTradePagePay(Request|Model|Response)';
|
|
84
|
+
const apppayMarker = 'alipay\\.trade\\.app\\.pay|AlipayTradeAppPay(Request|Model|Response)';
|
|
85
|
+
const tradeQueryMarker = 'alipay\\.trade\\.query|AlipayTradeQuery(Request|Model|Response)|alipay_trade_query_response';
|
|
86
|
+
const tradeRefundMarker = 'alipay\\.trade\\.refund|AlipayTradeRefund(Request|Model|Response)|alipay_trade_refund_response';
|
|
87
|
+
const tradeRefundQueryMarker = 'alipay\\.trade\\.fastpay\\.refund\\.query|AlipayTradeFastpayRefundQuery(Request|Model|Response)|alipay_trade_fastpay_refund_query_response';
|
|
88
|
+
const tradeCloseMarker = 'alipay\\.trade\\.close|AlipayTradeClose(Request|Model|Response)|alipay_trade_close_response';
|
|
89
|
+
const tradeNotifyMarker = 'notify_url|notifyUrl|NotifyUrl|setNotifyUrl|异步通知';
|
|
90
|
+
const aipayPaymentNeededMarker = 'Payment-Needed';
|
|
91
|
+
const aipayPaymentProofMarker = 'Payment-Proof';
|
|
92
|
+
const aipayPaymentValidationMarker = 'Payment-Validation';
|
|
93
|
+
const aipayPaymentVerifyMarker = 'alipay\\.aipay\\.agent\\.payment\\.verify|AlipayAipayAgentPaymentVerify(Request|Model|Response)|alipay_aipay_agent_payment_verify_response';
|
|
94
|
+
const aipayFulfillmentConfirmMarker = 'alipay\\.aipay\\.agent\\.fulfillment\\.confirm|AlipayAipayAgentFulfillmentConfirm(Request|Model|Response)|alipay_aipay_agent_fulfillment_confirm_response';
|
|
95
|
+
const aipayMarker = `${aipayPaymentNeededMarker}|${aipayPaymentProofMarker}|${aipayPaymentVerifyMarker}|${aipayFulfillmentConfirmMarker}`;
|
|
96
|
+
const internalPrefix = 'ALIPAY_AIPAY_INTERNAL:';
|
|
97
|
+
const receiptKinds = Object.freeze({
|
|
98
|
+
checklist: 'integration-checklist-scope',
|
|
99
|
+
a2mExecution: 'integration-a2m-execution'
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function parseArgs(argv) {
|
|
103
|
+
const command = argv[2];
|
|
104
|
+
const args = {};
|
|
105
|
+
for (let index = 3; index < argv.length; index += 1) {
|
|
106
|
+
const key = argv[index];
|
|
107
|
+
if (!key.startsWith('--')) throw new Error(`未知参数 ${key}`);
|
|
108
|
+
const value = argv[index + 1];
|
|
109
|
+
if (value === undefined || value.startsWith('--')) throw new Error(`参数 ${key} 缺少取值`);
|
|
110
|
+
const name = key.slice(2);
|
|
111
|
+
if (Object.prototype.hasOwnProperty.call(args, name)) throw new Error(`参数 ${key} 重复`);
|
|
112
|
+
args[name] = value;
|
|
113
|
+
index += 1;
|
|
114
|
+
}
|
|
115
|
+
return { command, args };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function required(args, name) {
|
|
119
|
+
const value = args[name];
|
|
120
|
+
if (typeof value !== 'string' || value.length === 0) throw new Error(`缺少参数 --${name}`);
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function optional(args, name) {
|
|
125
|
+
return typeof args[name] === 'string' && args[name].length > 0 ? args[name] : undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function requireCustomerMessageDelivery(messageId, message) {
|
|
129
|
+
process.stderr.write(`${internalPrefix}CUSTOMER_MESSAGE_DELIVERY_REQUIRED=${messageId}\n`);
|
|
130
|
+
return message;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function stripWrappingQuotes(value) {
|
|
134
|
+
const trimmed = value.trim();
|
|
135
|
+
if (
|
|
136
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
137
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
138
|
+
(trimmed.startsWith('`') && trimmed.endsWith('`'))
|
|
139
|
+
) {
|
|
140
|
+
return trimmed.slice(1, -1).trim();
|
|
141
|
+
}
|
|
142
|
+
return trimmed;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sanitizeProjectInput(raw) {
|
|
146
|
+
const value = stripWrappingQuotes(raw);
|
|
147
|
+
if (!value || value.length > 4096 || /[\u0000-\u001f\u007f`|]/.test(value)) {
|
|
148
|
+
throw new Error('project-input 必须是不含控制字符或 Markdown 边界的安全路径描述');
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeBasePath(raw) {
|
|
154
|
+
const basePath = path.resolve(stripWrappingQuotes(raw));
|
|
155
|
+
if (!path.isAbsolute(basePath) || !fs.existsSync(basePath) || !fs.statSync(basePath).isDirectory()) {
|
|
156
|
+
throw new Error('base-path 必须是实际存在的绝对目录');
|
|
157
|
+
}
|
|
158
|
+
return fs.realpathSync(basePath);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function safeReaddir(directory) {
|
|
162
|
+
try {
|
|
163
|
+
return fs.readdirSync(directory, { withFileTypes: true });
|
|
164
|
+
} catch {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function listProjectMarkers(directory) {
|
|
170
|
+
const markers = [];
|
|
171
|
+
for (const entry of safeReaddir(directory)) {
|
|
172
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
173
|
+
if (projectMarkerNameSet.has(entry.name)) {
|
|
174
|
+
markers.push(entry.name);
|
|
175
|
+
} else if (/\.(?:csproj|sln)$/i.test(entry.name)) {
|
|
176
|
+
markers.push(entry.name);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return markers.sort((left, right) => {
|
|
180
|
+
const leftIndex = projectMarkerNames.indexOf(left);
|
|
181
|
+
const rightIndex = projectMarkerNames.indexOf(right);
|
|
182
|
+
if (leftIndex >= 0 || rightIndex >= 0) {
|
|
183
|
+
if (leftIndex < 0) return 1;
|
|
184
|
+
if (rightIndex < 0) return -1;
|
|
185
|
+
return leftIndex - rightIndex;
|
|
186
|
+
}
|
|
187
|
+
return left.localeCompare(right);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isRecognizableProjectRoot(directory) {
|
|
192
|
+
return listProjectMarkers(directory).length > 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function shouldSkipProjectDir(entry) {
|
|
196
|
+
return !entry.isDirectory() || entry.isSymbolicLink() || skippedProjectDirs.has(entry.name) || entry.name.startsWith('.');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseDepth(value) {
|
|
200
|
+
if (value === undefined) return 3;
|
|
201
|
+
if (!/^[0-4]$/.test(value)) throw new Error('max-depth 仅支持 0 到 4');
|
|
202
|
+
return Number(value);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function locateProjectCandidates(searchBasePath, maxDepth = 3, maxCandidates = 20, displayBasePath = searchBasePath, classificationBasePath = displayBasePath) {
|
|
206
|
+
const queue = [{ directory: searchBasePath, depth: 0 }];
|
|
207
|
+
const seen = new Set([searchBasePath]);
|
|
208
|
+
const candidates = [];
|
|
209
|
+
|
|
210
|
+
for (let index = 0; index < queue.length && candidates.length < maxCandidates; index += 1) {
|
|
211
|
+
const { directory, depth } = queue[index];
|
|
212
|
+
const markers = listProjectMarkers(directory);
|
|
213
|
+
if (markers.length > 0) {
|
|
214
|
+
const projectPath = fs.realpathSync(directory);
|
|
215
|
+
candidates.push({
|
|
216
|
+
projectPath,
|
|
217
|
+
relativePath: path.relative(displayBasePath, projectPath) || '.',
|
|
218
|
+
projectSelection: classifyExistingProject(
|
|
219
|
+
projectPath,
|
|
220
|
+
classificationBasePath,
|
|
221
|
+
projectPath === classificationBasePath ? '当前目录' : projectPath
|
|
222
|
+
),
|
|
223
|
+
projectOrigin: 'EXISTING_PROJECT',
|
|
224
|
+
projectOriginLabel: '现有项目',
|
|
225
|
+
markers
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (depth >= maxDepth) continue;
|
|
229
|
+
const childDirs = safeReaddir(directory)
|
|
230
|
+
.filter((entry) => !shouldSkipProjectDir(entry))
|
|
231
|
+
.map((entry) => path.join(directory, entry.name))
|
|
232
|
+
.sort((left, right) => left.localeCompare(right));
|
|
233
|
+
for (const child of childDirs) {
|
|
234
|
+
let realChild;
|
|
235
|
+
try {
|
|
236
|
+
realChild = fs.realpathSync(child);
|
|
237
|
+
} catch {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (seen.has(realChild)) continue;
|
|
241
|
+
seen.add(realChild);
|
|
242
|
+
queue.push({ directory: realChild, depth: depth + 1 });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return candidates;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function formatProjectCandidateHint(basePath, candidates) {
|
|
250
|
+
const visible = candidates.slice(0, 5);
|
|
251
|
+
if (visible.length === 0) return '';
|
|
252
|
+
return visible
|
|
253
|
+
.map((candidate) => {
|
|
254
|
+
const relative = candidate.relativePath || path.relative(basePath, candidate.projectPath) || '.';
|
|
255
|
+
return `${relative}(${candidate.markers.slice(0, 3).join('、')})`;
|
|
256
|
+
})
|
|
257
|
+
.join(';');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function safeInline(value) {
|
|
261
|
+
return value.replace(/[`|\r\n]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function formatLocatedCandidateRows(candidates) {
|
|
265
|
+
if (candidates.length === 0) {
|
|
266
|
+
return '未找到可识别项目候选。可以提供更精确的项目文件夹,或说明要新建的目录。';
|
|
267
|
+
}
|
|
268
|
+
return candidates
|
|
269
|
+
.slice(0, 10)
|
|
270
|
+
.map((candidate, index) => {
|
|
271
|
+
const markers = candidate.markers.slice(0, 3).join('、');
|
|
272
|
+
return `${index + 1}. \`${safeInline(candidate.relativePath)}\`(识别依据:${markers})`;
|
|
273
|
+
})
|
|
274
|
+
.join('\n');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function projectRootError(searchBasePath, prefix, displayBasePath = searchBasePath) {
|
|
278
|
+
const candidates = locateProjectCandidates(searchBasePath, 3, 5, displayBasePath, displayBasePath);
|
|
279
|
+
const markerHint = 'package.json、pom.xml、build.gradle、pyproject.toml、requirements.txt、composer.json、*.csproj 或 *.sln';
|
|
280
|
+
const candidateHint = formatProjectCandidateHint(displayBasePath, candidates);
|
|
281
|
+
if (candidateHint) {
|
|
282
|
+
return new Error(`${prefix};请从候选项目中选择一个具体目录,或提供更精确路径。候选:${candidateHint}。如果要新建项目,请说明“在当前目录新建 pay-demo”或“新建项目”。项目根通常包含 ${markerHint}`);
|
|
283
|
+
}
|
|
284
|
+
return new Error(`${prefix};未在指定目录下找到可识别项目候选。请提供现有服务端项目的具体目录,或说明“在当前目录新建 pay-demo”或“新建项目”。项目根通常包含 ${markerHint}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isDefaultNewProjectRequest(input) {
|
|
288
|
+
return /^(?:新建项目|帮我新建默认项目)$/u.test(input);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function explicitNewArtifactRequest(input) {
|
|
292
|
+
return /^(?:请|麻烦)?(?:给我|帮我)?(?:新建|创建)(?:一个|个|一套)?[^,,。;;!?!?\r\n]{0,80}(?:项目|工程|网站|站点|商城|商店|店铺|应用程序|应用)(?:[,,。;;!?!?].*)?$/iu.test(input);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function looksLikeExplicitPath(value) {
|
|
296
|
+
return /^(?:[A-Za-z]:[\\/]|\\\\|[./~])/u.test(value) ||
|
|
297
|
+
/[\\/]/u.test(value) ||
|
|
298
|
+
/^[A-Za-z0-9._-]+$/u.test(value);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function extractNewProjectPath(input) {
|
|
302
|
+
const prefixed = '(?:请|麻烦)?(?:给我|帮我)?';
|
|
303
|
+
const currentRoot = '(?:当前目录|当前项目|这个项目|这个仓库|这里)';
|
|
304
|
+
const patterns = [
|
|
305
|
+
new RegExp(`^${prefixed}(?:在)?${currentRoot}(?:下|里)?(?:新建|创建)(?:一个)?(?:项目|目录)?\\s+(.+)$`, 'u'),
|
|
306
|
+
new RegExp(`^${prefixed}(?:新建|创建)(?:一个)?(?:项目|目录)(?:到|在)\\s*(.+)$`, 'u')
|
|
307
|
+
];
|
|
308
|
+
for (const pattern of patterns) {
|
|
309
|
+
const match = input.match(pattern);
|
|
310
|
+
if (match?.[1]) {
|
|
311
|
+
const candidate = stripWrappingQuotes(match[1]);
|
|
312
|
+
if (looksLikeExplicitPath(candidate)) return candidate;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const simple = input.match(/^(?:请|麻烦)?(?:给我|帮我)?(?:新建|创建)(?:一个)?\s+(.+)$/u);
|
|
317
|
+
if (simple?.[1]) {
|
|
318
|
+
const candidate = stripWrappingQuotes(simple[1]);
|
|
319
|
+
if (looksLikeExplicitPath(candidate)) return candidate;
|
|
320
|
+
}
|
|
321
|
+
return '';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isExplicitNewProjectRequest(input) {
|
|
325
|
+
return /^(?:帮我)?(?:新建|创建)(?:一个)?(?:默认)?(?:项目|目录)?$/u.test(input) ||
|
|
326
|
+
explicitNewArtifactRequest(input) ||
|
|
327
|
+
extractNewProjectPath(input) !== '';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseExistingProjectPath(input) {
|
|
331
|
+
if (/^(?:请|麻烦)?(?:给我|帮我)?(?:在)?(?:当前目录|当前项目|这个项目|这个仓库|这里)(?:中|内|下|里)?新增.+$/u.test(input)) {
|
|
332
|
+
return '当前项目';
|
|
333
|
+
}
|
|
334
|
+
return input;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function hasDirectoryEntries(directory) {
|
|
338
|
+
try {
|
|
339
|
+
return safeReaddir(directory).length > 0;
|
|
340
|
+
} catch {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function prepareNewProject(projectPath) {
|
|
346
|
+
if (!path.isAbsolute(projectPath)) throw new Error('ROUTE_INSPECT_ERROR:projectPath 必须是绝对路径');
|
|
347
|
+
if (fs.existsSync(projectPath)) {
|
|
348
|
+
const stat = fs.lstatSync(projectPath);
|
|
349
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || hasDirectoryEntries(projectPath)) {
|
|
350
|
+
throw new Error('ROUTE_INSPECT_ERROR:NEW_PROJECT 路径必须尚不存在或为空且不能是符号链接');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
projectSelection: 'NEW_PROJECT',
|
|
355
|
+
projectOrigin: 'NEW_PROJECT',
|
|
356
|
+
projectOriginLabel: '本轮新建项目',
|
|
357
|
+
preparationStatus: 'READY'
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function parseSingleJson(stdout, context) {
|
|
362
|
+
const trimmed = stdout.trim();
|
|
363
|
+
if (!trimmed) throw new Error(`${context} 未输出 JSON`);
|
|
364
|
+
try {
|
|
365
|
+
const parsed = JSON.parse(trimmed);
|
|
366
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error();
|
|
367
|
+
return parsed;
|
|
368
|
+
} catch {
|
|
369
|
+
throw new Error(`${context} 未输出唯一合法 JSON 对象`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function runUnixInspector(args, failureContext, jsonContext) {
|
|
374
|
+
const inspected = spawnSync('bash', [inspectorPath, ...args], {
|
|
375
|
+
encoding: 'utf8',
|
|
376
|
+
shell: false
|
|
377
|
+
});
|
|
378
|
+
if (inspected.status !== 0) {
|
|
379
|
+
const detail = (inspected.stderr || inspected.stdout || '').trim() || `退出码 ${inspected.status}`;
|
|
380
|
+
throw new Error(`${failureContext}失败:${detail}`);
|
|
381
|
+
}
|
|
382
|
+
return parseSingleJson(inspected.stdout, jsonContext);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function parseNewProjectPath(projectPath) {
|
|
386
|
+
return sanitizeProjectInput(projectPath);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function normalizePotentialPath(candidate) {
|
|
390
|
+
let existing = candidate;
|
|
391
|
+
const suffix = [];
|
|
392
|
+
while (!fs.existsSync(existing)) {
|
|
393
|
+
const parent = path.dirname(existing);
|
|
394
|
+
if (parent === existing) break;
|
|
395
|
+
suffix.unshift(path.basename(existing));
|
|
396
|
+
existing = parent;
|
|
397
|
+
}
|
|
398
|
+
const normalizedBase = fs.existsSync(existing) ? fs.realpathSync(existing) : existing;
|
|
399
|
+
return path.join(normalizedBase, ...suffix);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function resolveAgainstBase(input, basePath) {
|
|
403
|
+
const candidate = normalizePotentialPath(path.isAbsolute(input) ? path.resolve(input) : path.resolve(basePath, input));
|
|
404
|
+
if (!path.isAbsolute(input)) {
|
|
405
|
+
const relative = path.relative(basePath, candidate);
|
|
406
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
407
|
+
throw new Error('相对项目路径必须位于 base-path 内;如需目录外项目,请提供绝对路径');
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return candidate;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function resolveSearchBasePath(args, basePath) {
|
|
414
|
+
const rawInput = args['search-input'];
|
|
415
|
+
if (rawInput === undefined) return basePath;
|
|
416
|
+
const input = parseProjectSearchInput(sanitizeProjectInput(rawInput));
|
|
417
|
+
const candidate = currentProjectAliases.has(input) ? basePath : resolveAgainstBase(input, basePath);
|
|
418
|
+
const searchBasePath = fs.existsSync(candidate) ? fs.realpathSync(candidate) : candidate;
|
|
419
|
+
if (!fs.existsSync(searchBasePath) || !fs.statSync(searchBasePath).isDirectory()) {
|
|
420
|
+
throw new Error('search-input 必须指向实际存在的目录;可以使用当前目录、当前目录下子目录或绝对路径');
|
|
421
|
+
}
|
|
422
|
+
return searchBasePath;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function parseProjectSearchInput(input) {
|
|
426
|
+
const trimmed = stripWrappingQuotes(input);
|
|
427
|
+
if (/^(?:帮我)?(?:找|查找|定位)(?:一下)?(?:项目|项目目录|工程|工程目录)$/u.test(trimmed)) {
|
|
428
|
+
return '当前目录';
|
|
429
|
+
}
|
|
430
|
+
const patterns = [
|
|
431
|
+
/^(?:帮我)?(?:在|到)\s*(.+?)\s*(?:下|里|目录下|目录里)?(?:找|查找|定位)(?:一下)?(?:项目|项目目录|工程|工程目录)$/u,
|
|
432
|
+
/^(?:帮我)?(?:找|查找|定位)(?:一下)?\s*(.+?)\s*(?:下|里|目录下|目录里)?(?:的)?(?:项目|项目目录|工程|工程目录)$/u
|
|
433
|
+
];
|
|
434
|
+
for (const pattern of patterns) {
|
|
435
|
+
const match = trimmed.match(pattern);
|
|
436
|
+
if (match?.[1]) return stripWrappingQuotes(match[1]);
|
|
437
|
+
}
|
|
438
|
+
return trimmed;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function classifyExistingProject(projectPath, basePath, rawInput) {
|
|
442
|
+
if (currentProjectAliases.has(rawInput)) return 'CURRENT_PROJECT';
|
|
443
|
+
const relative = path.relative(basePath, projectPath);
|
|
444
|
+
if (relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))) {
|
|
445
|
+
return 'CURRENT_PROJECT';
|
|
446
|
+
}
|
|
447
|
+
return 'OTHER_PROJECT';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isReadableDirectory(directory) {
|
|
451
|
+
try {
|
|
452
|
+
const stat = fs.statSync(directory);
|
|
453
|
+
if (!stat.isDirectory()) return false;
|
|
454
|
+
fs.accessSync(directory, fs.constants.R_OK);
|
|
455
|
+
return true;
|
|
456
|
+
} catch {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function projectFiles(projectPath) {
|
|
462
|
+
const files = [];
|
|
463
|
+
const stack = [projectPath];
|
|
464
|
+
while (stack.length > 0) {
|
|
465
|
+
const directory = stack.pop();
|
|
466
|
+
for (const entry of safeReaddir(directory)) {
|
|
467
|
+
const entryPath = path.join(directory, entry.name);
|
|
468
|
+
if (entry.isDirectory()) {
|
|
469
|
+
if (!entry.isSymbolicLink() && !skippedProjectDirs.has(entry.name)) stack.push(entryPath);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (entry.isSymbolicLink() || !entry.isFile()) continue;
|
|
473
|
+
files.push(entryPath);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function sourceFiles(projectPath, files = projectFiles(projectPath)) {
|
|
480
|
+
return files.filter((file) => sourceFileExtensions.has(path.extname(file).toLowerCase()));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function isSnapshotConfigFile(file) {
|
|
484
|
+
const basename = path.basename(file).toLowerCase();
|
|
485
|
+
return snapshotConfigNames.has(basename)
|
|
486
|
+
|| /^\.env(?:\.|$)/u.test(basename)
|
|
487
|
+
|| /^(?:appsettings(?:\.[^.]+)*|config|jsconfig|launchsettings|settings|tsconfig(?:\.[^.]+)*)\.json$/u.test(basename)
|
|
488
|
+
|| snapshotConfigExtensions.has(path.extname(basename));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function projectSnapshot(projectPath) {
|
|
492
|
+
if (!path.isAbsolute(projectPath) || !isReadableDirectory(projectPath)) {
|
|
493
|
+
throw new Error('项目快照路径必须是可访问的绝对目录');
|
|
494
|
+
}
|
|
495
|
+
const canonicalPath = fs.realpathSync(projectPath);
|
|
496
|
+
const files = projectFiles(canonicalPath);
|
|
497
|
+
const sources = sourceFiles(canonicalPath, files);
|
|
498
|
+
const tracked = new Set([...sources, ...files.filter(isSnapshotConfigFile)]);
|
|
499
|
+
const hash = crypto.createHash('sha256');
|
|
500
|
+
const contents = new Map();
|
|
501
|
+
hash.update(`project=${canonicalPath}\n`);
|
|
502
|
+
for (const file of [...tracked].sort((left, right) => left.localeCompare(right))) {
|
|
503
|
+
const stat = fs.statSync(file);
|
|
504
|
+
const relative = path.relative(canonicalPath, file);
|
|
505
|
+
const content = fs.readFileSync(file);
|
|
506
|
+
contents.set(file, content);
|
|
507
|
+
hash.update(`${relative}\0${stat.size}\0`);
|
|
508
|
+
hash.update(content);
|
|
509
|
+
hash.update('\n');
|
|
510
|
+
}
|
|
511
|
+
return { canonicalPath, sources, contents, fingerprint: hash.digest('hex') };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function projectSnapshotFingerprint(projectPath) {
|
|
515
|
+
return projectSnapshot(projectPath).fingerprint;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function encodeReceipt(payload) {
|
|
519
|
+
return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function decodeReceipt(value, expectedKind) {
|
|
523
|
+
if (typeof value !== 'string' || !value) throw new Error('缺少 receipt');
|
|
524
|
+
let payload;
|
|
525
|
+
try {
|
|
526
|
+
payload = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
527
|
+
} catch {
|
|
528
|
+
throw new Error('receipt 不是合法编码');
|
|
529
|
+
}
|
|
530
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || payload.schema_ver !== 1 || payload.kind !== expectedKind) {
|
|
531
|
+
throw new Error('receipt 类型或版本非法');
|
|
532
|
+
}
|
|
533
|
+
return payload;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function scanPattern(files, pattern) {
|
|
537
|
+
const regex = new RegExp(pattern);
|
|
538
|
+
for (const file of files) {
|
|
539
|
+
let content;
|
|
540
|
+
try {
|
|
541
|
+
content = fs.readFileSync(file, 'utf8');
|
|
542
|
+
} catch {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (regex.test(stripSourceComments(content, path.extname(file).toLowerCase()))) return file;
|
|
546
|
+
}
|
|
547
|
+
return '';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function stripSourceComments(content, extension) {
|
|
551
|
+
if (extension === '.py') {
|
|
552
|
+
content = content.replace(/(^|\n)[ \t]*(?:[rubf]{0,2})?(?:"""[\s\S]*?"""|'''[\s\S]*?''')[ \t]*(?:#[^\r\n]*)?(?=\r?$)/gimu,
|
|
553
|
+
(token) => token.replace(/[^\r\n]/gu, ' '));
|
|
554
|
+
}
|
|
555
|
+
const hashComments = extension === '.py' || extension === '.php';
|
|
556
|
+
const tokens = hashComments
|
|
557
|
+
? /"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`|\/\*[\s\S]*?\*\/|<!--[\s\S]*?-->|\/\/[^\r\n]*|#[^\r\n]*/gu
|
|
558
|
+
: /"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`|\/\*[\s\S]*?\*\/|<!--[\s\S]*?-->|\/\/[^\r\n]*/gu;
|
|
559
|
+
return content.replace(tokens, (token) => {
|
|
560
|
+
const comment = token.startsWith('//') || token.startsWith('/*') || token.startsWith('<!--')
|
|
561
|
+
|| (hashComments && token.startsWith('#'));
|
|
562
|
+
return comment ? token.replace(/[^\r\n]/gu, ' ') : token;
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function implementationSourceFiles(projectPath, files = sourceFiles(projectPath), contents = null) {
|
|
567
|
+
const testDirectories = new Set([
|
|
568
|
+
'__fixtures__', '__tests__', 'fixtures', 'spec', 'specs', 'test', 'testfixtures', 'tests'
|
|
569
|
+
]);
|
|
570
|
+
const generatedDirectories = new Set([
|
|
571
|
+
'.generated', 'bin', 'gen', 'generated', 'generated-sources', 'generated-src', 'obj', 'out'
|
|
572
|
+
]);
|
|
573
|
+
return files.filter((file) => {
|
|
574
|
+
const relative = path.relative(projectPath, file);
|
|
575
|
+
const parts = relative.split(path.sep);
|
|
576
|
+
const lowerParts = parts.map((part) => part.toLowerCase());
|
|
577
|
+
const basename = path.basename(file);
|
|
578
|
+
const lowerBasename = basename.toLowerCase();
|
|
579
|
+
const testDirectory = lowerParts.some((part) => testDirectories.has(part)
|
|
580
|
+
|| /^(?:.*[._-])?(?:integration|unit|functional|acceptance|e2e)?tests?$/u.test(part));
|
|
581
|
+
const generatedDirectory = lowerParts.some((part) => generatedDirectories.has(part));
|
|
582
|
+
const testFile = /(?:^|[._-])(?:test|tests|spec|specs)(?:[._-]|$)/u.test(lowerBasename)
|
|
583
|
+
|| /(?:Test|Tests|Spec|Specs)\.[^.]+$/u.test(basename)
|
|
584
|
+
|| lowerBasename === 'conftest.py';
|
|
585
|
+
const generatedFile = /^generated/u.test(lowerBasename)
|
|
586
|
+
|| /(?:^|[._-])generated(?:[._-]|$)/u.test(lowerBasename)
|
|
587
|
+
|| /\.(?:g|designer)\.[^.]+$/u.test(lowerBasename)
|
|
588
|
+
|| /_pb2(?:_grpc)?\.py$/u.test(lowerBasename);
|
|
589
|
+
let generatedHeader = false;
|
|
590
|
+
try {
|
|
591
|
+
const content = contents?.get(file)?.toString('utf8') ?? fs.readFileSync(file, 'utf8');
|
|
592
|
+
generatedHeader = /^\s*(?:(?:\/\/|#|\/\*+|\*)\s*)?(?:<auto-generated|@generated\b|code generated by|generated by .{0,120}do not edit|this file was generated)/imu
|
|
593
|
+
.test(content.slice(0, 4096));
|
|
594
|
+
} catch {
|
|
595
|
+
generatedHeader = true;
|
|
596
|
+
}
|
|
597
|
+
return !testDirectory && !generatedDirectory && !testFile && !generatedFile && !generatedHeader;
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function firstCodePatternMatch(files, pattern, contents = null) {
|
|
602
|
+
const regex = new RegExp(pattern, 'iu');
|
|
603
|
+
for (const file of files) {
|
|
604
|
+
let content;
|
|
605
|
+
try {
|
|
606
|
+
content = contents?.get(file)?.toString('utf8') ?? fs.readFileSync(file, 'utf8');
|
|
607
|
+
} catch {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const lines = stripSourceComments(content, path.extname(file).toLowerCase()).split(/\r?\n/u);
|
|
611
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
612
|
+
if (regex.test(lines[index])) {
|
|
613
|
+
return { file, line: index + 1 };
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function firstContentPatternMatch(file, content, pattern) {
|
|
621
|
+
const match = new RegExp(pattern, 'iu').exec(content);
|
|
622
|
+
if (!match) return null;
|
|
623
|
+
return { file, line: content.slice(0, match.index).split(/\r?\n/u).length };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function auditFinding(projectPath, code, message, match = null) {
|
|
627
|
+
const finding = { code, message };
|
|
628
|
+
if (match) {
|
|
629
|
+
finding.path = path.relative(projectPath, match.file).replace(/[\u0000-\u001f\u007f]/gu, '?');
|
|
630
|
+
finding.line = match.line;
|
|
631
|
+
}
|
|
632
|
+
return finding;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const implementationRequiredPatterns = Object.freeze({
|
|
636
|
+
AIPAY: [
|
|
637
|
+
['AIPAY_PAYMENT_NEEDED_MISSING', aipayPaymentNeededMarker, '缺少 Payment-Needed 账单响应'],
|
|
638
|
+
['AIPAY_PAYMENT_PROOF_MISSING', aipayPaymentProofMarker, '缺少 Payment-Proof 接收处理'],
|
|
639
|
+
['AIPAY_VERIFY_MISSING', aipayPaymentVerifyMarker, '缺少 alipay.aipay.agent.payment.verify 验付调用'],
|
|
640
|
+
['AIPAY_CONFIRM_MISSING', aipayFulfillmentConfirmMarker, '缺少 alipay.aipay.agent.fulfillment.confirm 履约确认'],
|
|
641
|
+
['AIPAY_VALIDATION_MISSING', aipayPaymentValidationMarker, '缺少 Payment-Validation 成功回执'],
|
|
642
|
+
['AIPAY_ACTIVE_CHECK_MISSING', '\\bactive\\b|getActive\\(|\\.Active\\b', '缺少验付 active 字段处理'],
|
|
643
|
+
['AIPAY_ORDER_CHECK_MISSING', 'out_trade_no|outTradeNo|OutTradeNo', '缺少商户订单号处理'],
|
|
644
|
+
['AIPAY_RESOURCE_CHECK_MISSING', 'resource_id|resourceId|ResourceId', '缺少资源标识处理']
|
|
645
|
+
],
|
|
646
|
+
WEBPAY: [
|
|
647
|
+
['WEBPAY_CREATE_MISSING', webpayMarker, '缺少 alipay.trade.page.pay 下单入口'],
|
|
648
|
+
['TRADE_QUERY_MISSING', tradeQueryMarker, '缺少 alipay.trade.query 交易查询'],
|
|
649
|
+
['TRADE_REFUND_MISSING', tradeRefundMarker, '缺少 alipay.trade.refund 退款'],
|
|
650
|
+
['REFUND_QUERY_MISSING', tradeRefundQueryMarker, '缺少 alipay.trade.fastpay.refund.query 退款查询'],
|
|
651
|
+
['TRADE_CLOSE_MISSING', tradeCloseMarker, '缺少 alipay.trade.close 关闭交易'],
|
|
652
|
+
['TRADE_NOTIFY_MISSING', tradeNotifyMarker, '缺少异步通知处理入口']
|
|
653
|
+
],
|
|
654
|
+
APPPAY: [
|
|
655
|
+
['APPPAY_CREATE_MISSING', apppayMarker, '缺少 alipay.trade.app.pay 下单入口'],
|
|
656
|
+
['TRADE_QUERY_MISSING', tradeQueryMarker, '缺少 alipay.trade.query 交易查询'],
|
|
657
|
+
['TRADE_REFUND_MISSING', tradeRefundMarker, '缺少 alipay.trade.refund 退款'],
|
|
658
|
+
['REFUND_QUERY_MISSING', tradeRefundQueryMarker, '缺少 alipay.trade.fastpay.refund.query 退款查询'],
|
|
659
|
+
['TRADE_CLOSE_MISSING', tradeCloseMarker, '缺少 alipay.trade.close 关闭交易'],
|
|
660
|
+
['TRADE_NOTIFY_MISSING', tradeNotifyMarker, '缺少异步通知处理入口']
|
|
661
|
+
]
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
export function inspectImplementation(variant, projectPath, snapshot = null) {
|
|
665
|
+
if (!Object.prototype.hasOwnProperty.call(implementationRequiredPatterns, variant)) {
|
|
666
|
+
throw new Error('implementation audit variant 非法,仅支持 AIPAY、WEBPAY、APPPAY');
|
|
667
|
+
}
|
|
668
|
+
if (!path.isAbsolute(projectPath) || !isReadableDirectory(projectPath)) {
|
|
669
|
+
throw new Error('implementation audit 项目路径必须是可访问的绝对目录');
|
|
670
|
+
}
|
|
671
|
+
const currentSnapshot = snapshot || projectSnapshot(projectPath);
|
|
672
|
+
const canonicalPath = currentSnapshot.canonicalPath;
|
|
673
|
+
if (canonicalPath !== fs.realpathSync(projectPath)) throw new Error('implementation audit 项目快照路径不匹配');
|
|
674
|
+
const files = implementationSourceFiles(canonicalPath, currentSnapshot.sources, currentSnapshot.contents);
|
|
675
|
+
const findings = [];
|
|
676
|
+
|
|
677
|
+
for (const [code, pattern, message] of implementationRequiredPatterns[variant]) {
|
|
678
|
+
if (!firstCodePatternMatch(files, pattern, currentSnapshot.contents)) {
|
|
679
|
+
findings.push(auditFinding(canonicalPath, code, message));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const commonTradeRelevance = [tradeQueryMarker, tradeRefundMarker, tradeRefundQueryMarker, tradeCloseMarker, tradeNotifyMarker].join('|');
|
|
684
|
+
const targetRelevancePattern = variant === 'AIPAY'
|
|
685
|
+
? aipayMarker
|
|
686
|
+
: `${variant === 'WEBPAY' ? webpayMarker : apppayMarker}|${commonTradeRelevance}`;
|
|
687
|
+
const criticalTodoPattern = '(?:TODO|FIXME)[\\s\\S]{0,240}(?:支付|订单|验付|验签|退款|履约|持久|幂等|payment|order|verify|signature|refund|fulfill|persist|idempot)';
|
|
688
|
+
const fixedResourceFallbackPattern = '(?:resourceIdVerified|resource_id_verified|verifiedResourceId)\\s*=[\\s\\S]{0,240}(?:\\?\\?|\\|\\||\\bor\\b|\\?)[\\s\\S]{0,160}(?:RESOURCE_PATH|ResourcePath|resource\\.path)';
|
|
689
|
+
const proofTradeFallbackPattern = '(?:fulfillmentTradeNo|fulfillment_trade_no)\\s*=[\\s\\S]{0,240}(?:\\?\\?|\\|\\||\\bor\\b|\\?)[\\s\\S]{0,160}(?:tradeNo|trade_no)';
|
|
690
|
+
const mockBypassPattern = '(?:if|when)[\\s\\S]{0,240}(?:mock|test|sandbox)[\\s\\S]{0,320}(?:return\\s+true|bypass|skip|跳过)';
|
|
691
|
+
|
|
692
|
+
for (const file of files) {
|
|
693
|
+
let content;
|
|
694
|
+
try {
|
|
695
|
+
content = currentSnapshot.contents.get(file)?.toString('utf8') ?? fs.readFileSync(file, 'utf8');
|
|
696
|
+
} catch {
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const codeContent = stripSourceComments(content, path.extname(file).toLowerCase());
|
|
700
|
+
const targetRelevant = new RegExp(targetRelevancePattern, 'iu').test(codeContent);
|
|
701
|
+
const checks = [];
|
|
702
|
+
if (targetRelevant || /alipay|支付宝|Payment-Proof/iu.test(codeContent)) {
|
|
703
|
+
checks.push(['PAYMENT_VERIFICATION_BYPASS', mockBypassPattern, '存在测试/沙箱条件下直接绕过支付验签的生产代码']);
|
|
704
|
+
}
|
|
705
|
+
if (targetRelevant) {
|
|
706
|
+
checks.push(['PAYMENT_CRITICAL_TODO', criticalTodoPattern, '支付关键控制仍保留 TODO/FIXME']);
|
|
707
|
+
}
|
|
708
|
+
for (const [code, pattern, message] of checks) {
|
|
709
|
+
const match = firstContentPatternMatch(
|
|
710
|
+
file,
|
|
711
|
+
code === 'PAYMENT_CRITICAL_TODO' ? content : codeContent,
|
|
712
|
+
pattern
|
|
713
|
+
);
|
|
714
|
+
if (match) findings.push(auditFinding(canonicalPath, code, message, match));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (variant === 'AIPAY') {
|
|
718
|
+
const blockChecks = [
|
|
719
|
+
['AIPAY_FIXED_RESOURCE_FALLBACK', fixedResourceFallbackPattern, 'A2M 验付字段缺失时回退到固定资源'],
|
|
720
|
+
['AIPAY_UNVERIFIED_TRADE_FALLBACK', proofTradeFallbackPattern, '履约交易号回退到未经验付响应确认的 Proof 字段']
|
|
721
|
+
];
|
|
722
|
+
for (const [code, pattern, message] of blockChecks) {
|
|
723
|
+
const match = firstContentPatternMatch(file, codeContent, pattern);
|
|
724
|
+
if (match) findings.push(auditFinding(canonicalPath, code, message, match));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const cwdConfig = /(?:process\.cwd\(\)|getcwd\(\)|Directory\.GetCurrentDirectory\(\)|System\.getProperty\(["']user\.dir["']\))[\s\S]{0,240}\.alipay-sandbox\.json|\.alipay-sandbox\.json[\s\S]{0,240}(?:process\.cwd\(\)|getcwd\(\)|Directory\.GetCurrentDirectory\(\)|System\.getProperty\(["']user\.dir["']\))/iu;
|
|
729
|
+
const cwdMatch = cwdConfig.exec(codeContent);
|
|
730
|
+
if (cwdMatch) {
|
|
731
|
+
const line = codeContent.slice(0, cwdMatch.index).split(/\r?\n/u).length;
|
|
732
|
+
findings.push(auditFinding(canonicalPath, 'SANDBOX_CONFIG_CWD_DEPENDENCY',
|
|
733
|
+
'.alipay-sandbox.json 依赖进程当前目录定位', { file, line }));
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const certificateExecution = /certificateExecute|certificate_execute|certExecute|ExecuteCertificate|ExecuteCert/iu.test(codeContent);
|
|
737
|
+
const publicKeyConfiguration = /alipayPublicKey|alipay_public_key|AlipayPublicKey|setAlipayPublicKey/iu.test(codeContent);
|
|
738
|
+
const completeCertificateConfiguration = [
|
|
739
|
+
/appCertPublicKey|app_cert_public_key|AppCertPublicKey/iu,
|
|
740
|
+
/alipayCertPublicKeyRSA2|alipay_cert_public_key|AlipayCertPublicKey/iu,
|
|
741
|
+
/rootCert|root_cert|RootCert/iu
|
|
742
|
+
].every((pattern) => pattern.test(codeContent));
|
|
743
|
+
if (certificateExecution && publicKeyConfiguration && !completeCertificateConfiguration) {
|
|
744
|
+
const match = firstCodePatternMatch(
|
|
745
|
+
[file], 'certificateExecute|certificate_execute|certExecute|ExecuteCertificate|ExecuteCert', currentSnapshot.contents);
|
|
746
|
+
findings.push(auditFinding(canonicalPath, 'CERTIFICATE_MODE_MISMATCH',
|
|
747
|
+
'公钥配置模式调用了证书执行 API,但未配置完整证书链', match));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
findings.sort((left, right) => `${left.code}:${left.path || ''}:${left.line || 0}`
|
|
752
|
+
.localeCompare(`${right.code}:${right.path || ''}:${right.line || 0}`));
|
|
753
|
+
return { projectPath: canonicalPath, variant, findings };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function scanProjectRoute(productType, projectPath, projectSelection) {
|
|
757
|
+
if (!['aipay', 'webpay', 'apppay'].includes(productType)) {
|
|
758
|
+
throw new Error('ROUTE_INSPECT_ERROR:productType 非法');
|
|
759
|
+
}
|
|
760
|
+
if (!['CURRENT_PROJECT', 'OTHER_PROJECT', 'PREPARED_NEW_PROJECT'].includes(projectSelection)) {
|
|
761
|
+
throw new Error('ROUTE_INSPECT_ERROR:projectSelection 非法');
|
|
762
|
+
}
|
|
763
|
+
if (!path.isAbsolute(projectPath)) throw new Error('ROUTE_INSPECT_ERROR:projectPath 必须是绝对路径');
|
|
764
|
+
if (!isReadableDirectory(projectPath)) throw new Error('ROUTE_INSPECT_ERROR:项目路径不可访问');
|
|
765
|
+
|
|
766
|
+
const projectOrigin = projectSelection === 'PREPARED_NEW_PROJECT' ? 'NEW_PROJECT' : 'EXISTING_PROJECT';
|
|
767
|
+
const projectOriginLabel = projectSelection === 'PREPARED_NEW_PROJECT' ? '本轮新建项目' : '现有项目';
|
|
768
|
+
const targetMarker = productType === 'aipay' ? aipayMarker : productType === 'webpay' ? webpayMarker : apppayMarker;
|
|
769
|
+
const files = sourceFiles(projectPath);
|
|
770
|
+
const otherProducts = [];
|
|
771
|
+
|
|
772
|
+
if (scanPattern(files, webpayMarker) && productType !== 'webpay') otherProducts.push('AI 网页应用收款');
|
|
773
|
+
if (scanPattern(files, apppayMarker) && productType !== 'apppay') otherProducts.push('AI 移动应用收款');
|
|
774
|
+
if (scanPattern(files, aipayMarker) && productType !== 'aipay') otherProducts.push('AI 按量付费');
|
|
775
|
+
|
|
776
|
+
const otherProductsText = otherProducts.length > 0 ? otherProducts.join(',') : '无';
|
|
777
|
+
let integrationStatus;
|
|
778
|
+
let evidence;
|
|
779
|
+
|
|
780
|
+
if (!scanPattern(files, targetMarker)) {
|
|
781
|
+
if (otherProductsText === '无') {
|
|
782
|
+
integrationStatus = 'NO_PAYMENT';
|
|
783
|
+
evidence = projectSelection === 'PREPARED_NEW_PROJECT'
|
|
784
|
+
? '固定检查器扫描本轮已初始化的新项目,未发现目标产品或其他受支持支付代码'
|
|
785
|
+
: '固定检查器未发现目标产品或其他受支持支付代码';
|
|
786
|
+
} else {
|
|
787
|
+
integrationStatus = 'OTHER_PRODUCT_ONLY';
|
|
788
|
+
evidence = '固定检查器只发现其他受支持支付产品代码';
|
|
789
|
+
}
|
|
790
|
+
} else {
|
|
791
|
+
const requiredPatterns = productType === 'aipay'
|
|
792
|
+
? [aipayPaymentNeededMarker, aipayPaymentProofMarker, aipayPaymentVerifyMarker, aipayFulfillmentConfirmMarker, aipayPaymentValidationMarker]
|
|
793
|
+
: productType === 'webpay'
|
|
794
|
+
? [webpayMarker, tradeQueryMarker, tradeRefundMarker, tradeRefundQueryMarker, tradeCloseMarker, tradeNotifyMarker]
|
|
795
|
+
: [apppayMarker, tradeQueryMarker, tradeRefundMarker, tradeRefundQueryMarker, tradeCloseMarker, tradeNotifyMarker];
|
|
796
|
+
const missing = requiredPatterns.filter((pattern) => !scanPattern(files, pattern));
|
|
797
|
+
integrationStatus = 'TARGET_PARTIAL';
|
|
798
|
+
evidence = missing.length === 0
|
|
799
|
+
? '固定检查器逐项发现目标产品入口及全部核心接口标记,但静态扫描不能证明配置、安全、测试和 checklist 完成;必须进入 Integration 验证分支'
|
|
800
|
+
: '固定检查器发现目标产品代码,但至少一个核心接口标记缺失;不得按完整集成路由';
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
projectSelection,
|
|
805
|
+
projectOrigin,
|
|
806
|
+
projectOriginLabel,
|
|
807
|
+
integrationStatus,
|
|
808
|
+
evidence,
|
|
809
|
+
otherProducts: otherProductsText
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function runResolveProject(args) {
|
|
814
|
+
const intent = required(args, 'intent');
|
|
815
|
+
if (!['existing', 'new'].includes(intent)) throw new Error('intent 非法,仅支持 existing、new');
|
|
816
|
+
const basePath = normalizeBasePath(required(args, 'base-path'));
|
|
817
|
+
const input = sanitizeProjectInput(required(args, 'project-input'));
|
|
818
|
+
if (args['target-path'] !== undefined) {
|
|
819
|
+
throw new Error('参数 --target-path 已移除;现有项目和新建项目都使用 --project-path 传递真实路径');
|
|
820
|
+
}
|
|
821
|
+
const pathInput = sanitizeProjectInput(required(args, 'project-path'));
|
|
822
|
+
|
|
823
|
+
if (intent === 'existing') {
|
|
824
|
+
if (isExplicitNewProjectRequest(input)) {
|
|
825
|
+
throw new Error('明确新建项目请求必须使用 --intent new,不能改用当前项目');
|
|
826
|
+
}
|
|
827
|
+
const existingInput = parseExistingProjectPath(pathInput);
|
|
828
|
+
const candidate = currentProjectAliases.has(existingInput) ? basePath : resolveAgainstBase(existingInput, basePath);
|
|
829
|
+
const projectPath = fs.existsSync(candidate) ? fs.realpathSync(candidate) : candidate;
|
|
830
|
+
if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
|
|
831
|
+
throw new Error('现有项目路径不可访问;请提供当前目录内相对路径或实际存在的绝对路径');
|
|
832
|
+
}
|
|
833
|
+
if (!isRecognizableProjectRoot(projectPath)) {
|
|
834
|
+
const prefix = currentProjectAliases.has(existingInput)
|
|
835
|
+
? '当前目录不是可识别的项目根,不能直接作为现有项目扫描'
|
|
836
|
+
: '该路径未识别到项目根标记,不能确认是服务端项目根';
|
|
837
|
+
const searchBasePath = currentProjectAliases.has(existingInput) ? basePath : projectPath;
|
|
838
|
+
throw projectRootError(searchBasePath, prefix, basePath);
|
|
839
|
+
}
|
|
840
|
+
return JSON.stringify({
|
|
841
|
+
projectPath,
|
|
842
|
+
projectSelection: classifyExistingProject(projectPath, basePath, existingInput),
|
|
843
|
+
projectOrigin: 'EXISTING_PROJECT',
|
|
844
|
+
projectOriginLabel: '现有项目'
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const newPathInput = parseNewProjectPath(pathInput);
|
|
849
|
+
if (currentProjectAliases.has(newPathInput)) {
|
|
850
|
+
throw new Error('新项目必须明确尚不存在或为空的子目录名');
|
|
851
|
+
}
|
|
852
|
+
const projectPath = resolveAgainstBase(newPathInput, basePath);
|
|
853
|
+
const route = isWindowsPlatform()
|
|
854
|
+
? prepareNewProject(projectPath)
|
|
855
|
+
: runUnixInspector(['prepare-new', projectPath], '新项目路径检查', '新项目路径检查器');
|
|
856
|
+
return JSON.stringify({ projectPath, ...route });
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function runLocateProjects(args) {
|
|
860
|
+
const basePath = normalizeBasePath(required(args, 'base-path'));
|
|
861
|
+
const searchBasePath = resolveSearchBasePath(args, basePath);
|
|
862
|
+
const maxDepth = parseDepth(args['max-depth']);
|
|
863
|
+
const format = args.format || 'json';
|
|
864
|
+
if (!['json', 'message'].includes(format)) throw new Error('format 仅支持 json 或 message');
|
|
865
|
+
const candidates = locateProjectCandidates(searchBasePath, maxDepth, 20, basePath, basePath);
|
|
866
|
+
if (format === 'message') {
|
|
867
|
+
return renderMessage('integration.project_candidates.select', 'DEFAULT', {
|
|
868
|
+
searchBasePath,
|
|
869
|
+
candidateRows: formatLocatedCandidateRows(candidates)
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
return JSON.stringify({
|
|
873
|
+
basePath,
|
|
874
|
+
searchBasePath,
|
|
875
|
+
maxDepth,
|
|
876
|
+
candidates,
|
|
877
|
+
nextAction: '请选择一个候选项目目录作为现有项目,或说明要新建的目录,例如“在当前目录新建 pay-demo”或“新建项目”。'
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function normalizeLanguage(input) {
|
|
882
|
+
const value = input.trim();
|
|
883
|
+
const compact = value.toLowerCase().replace(/[\s_.-]+/g, '');
|
|
884
|
+
if (compact === 'java') return 'Java';
|
|
885
|
+
if (compact === 'python' || compact === 'py' || compact === 'python3') return 'Python';
|
|
886
|
+
if (compact === 'nodejs' || compact === 'node' || compact === 'javascript' || compact === 'js') return 'Node.js';
|
|
887
|
+
if (compact === 'c#' || compact === 'csharp' || compact === 'dotnet' || compact === 'net') return 'C#';
|
|
888
|
+
if (compact === 'php') return 'PHP';
|
|
889
|
+
throw new Error('服务端语言非法,仅支持 Java、Python、Node.js、C#、PHP');
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const existingProjectFrameworkFallback = '沿用现有项目结构';
|
|
893
|
+
const defaultFrameworks = Object.freeze({
|
|
894
|
+
Java: 'Spring Boot',
|
|
895
|
+
PHP: 'Laravel',
|
|
896
|
+
Python: 'FastAPI',
|
|
897
|
+
'C#': 'ASP.NET Core',
|
|
898
|
+
'Node.js': 'Express'
|
|
899
|
+
});
|
|
900
|
+
const frameworkSources = new Set([
|
|
901
|
+
'EXPLICIT',
|
|
902
|
+
'DEFAULT_NEW_PROJECT',
|
|
903
|
+
'PROJECT_INFERRED',
|
|
904
|
+
'PROJECT_STRUCTURE_FALLBACK'
|
|
905
|
+
]);
|
|
906
|
+
|
|
907
|
+
function readProjectMarker(projectPath, relativePath) {
|
|
908
|
+
const file = path.join(projectPath, relativePath);
|
|
909
|
+
try {
|
|
910
|
+
const stat = fs.lstatSync(file);
|
|
911
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2 * 1024 * 1024) return '';
|
|
912
|
+
return fs.readFileSync(file, 'utf8');
|
|
913
|
+
} catch {
|
|
914
|
+
return '';
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function readProjectJson(projectPath, relativePath) {
|
|
919
|
+
const content = readProjectMarker(projectPath, relativePath);
|
|
920
|
+
if (!content) return null;
|
|
921
|
+
try {
|
|
922
|
+
const parsed = JSON.parse(content);
|
|
923
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
924
|
+
} catch {
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function dependencyNames(manifest) {
|
|
930
|
+
const names = new Set();
|
|
931
|
+
for (const key of ['dependencies', 'devDependencies', 'peerDependencies', 'require', 'require-dev']) {
|
|
932
|
+
const dependencies = manifest?.[key];
|
|
933
|
+
if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) continue;
|
|
934
|
+
for (const name of Object.keys(dependencies)) names.add(name.toLowerCase());
|
|
935
|
+
}
|
|
936
|
+
return names;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function firstMatchingFramework(names, rules) {
|
|
940
|
+
for (const [framework, matches] of rules) {
|
|
941
|
+
if (matches.some((name) => names.has(name) || [...names].some((item) => item.startsWith(`${name}/`)))) {
|
|
942
|
+
return framework;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return '';
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function inferFramework(projectPath, language) {
|
|
949
|
+
if (language === 'Node.js') {
|
|
950
|
+
return firstMatchingFramework(dependencyNames(readProjectJson(projectPath, 'package.json')), [
|
|
951
|
+
['NestJS', ['@nestjs/core']],
|
|
952
|
+
['Next.js', ['next']],
|
|
953
|
+
['Express', ['express']],
|
|
954
|
+
['Fastify', ['fastify']],
|
|
955
|
+
['Koa', ['koa']],
|
|
956
|
+
['Hono', ['hono']]
|
|
957
|
+
]);
|
|
958
|
+
}
|
|
959
|
+
if (language === 'PHP') {
|
|
960
|
+
return firstMatchingFramework(dependencyNames(readProjectJson(projectPath, 'composer.json')), [
|
|
961
|
+
['Laravel', ['laravel/framework']],
|
|
962
|
+
['Symfony', ['symfony/framework-bundle', 'symfony/http-kernel']]
|
|
963
|
+
]);
|
|
964
|
+
}
|
|
965
|
+
if (language === 'Java') {
|
|
966
|
+
const content = [
|
|
967
|
+
readProjectMarker(projectPath, 'pom.xml'),
|
|
968
|
+
readProjectMarker(projectPath, 'build.gradle'),
|
|
969
|
+
readProjectMarker(projectPath, 'build.gradle.kts')
|
|
970
|
+
].join('\n').toLowerCase();
|
|
971
|
+
if (/spring-boot|org\.springframework\.boot/u.test(content)) return 'Spring Boot';
|
|
972
|
+
if (/io\.quarkus|quarkus-/u.test(content)) return 'Quarkus';
|
|
973
|
+
if (/io\.micronaut|micronaut-/u.test(content)) return 'Micronaut';
|
|
974
|
+
return '';
|
|
975
|
+
}
|
|
976
|
+
if (language === 'Python') {
|
|
977
|
+
const content = [
|
|
978
|
+
readProjectMarker(projectPath, 'pyproject.toml'),
|
|
979
|
+
readProjectMarker(projectPath, 'requirements.txt'),
|
|
980
|
+
readProjectMarker(projectPath, 'Pipfile')
|
|
981
|
+
].join('\n').toLowerCase();
|
|
982
|
+
if (/(^|[^a-z])django([^a-z]|$)/u.test(content)) return 'Django';
|
|
983
|
+
if (/(^|[^a-z])fastapi([^a-z]|$)/u.test(content)) return 'FastAPI';
|
|
984
|
+
if (/(^|[^a-z])flask([^a-z]|$)/u.test(content)) return 'Flask';
|
|
985
|
+
return '';
|
|
986
|
+
}
|
|
987
|
+
if (language === 'C#') {
|
|
988
|
+
const content = safeReaddir(projectPath)
|
|
989
|
+
.filter((entry) => entry.isFile() && !entry.isSymbolicLink() && entry.name.toLowerCase().endsWith('.csproj'))
|
|
990
|
+
.map((entry) => readProjectMarker(projectPath, entry.name))
|
|
991
|
+
.join('\n').toLowerCase();
|
|
992
|
+
if (/microsoft\.net\.sdk\.web|microsoft\.aspnetcore/u.test(content)) return 'ASP.NET Core';
|
|
993
|
+
return '';
|
|
994
|
+
}
|
|
995
|
+
return '';
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function sanitizeFramework(value) {
|
|
999
|
+
const framework = value.trim();
|
|
1000
|
+
if (!framework || framework.length > 120 || /[\u0000-\u001f\u007f|`]/u.test(framework) || /https?:\/\//iu.test(framework)) {
|
|
1001
|
+
throw new Error('framework 必须是安全的单行框架名称');
|
|
1002
|
+
}
|
|
1003
|
+
return framework;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function deriveFramework(projectPath, projectSelection, language) {
|
|
1007
|
+
if (projectSelection === 'NEW_PROJECT' || projectSelection === 'PREPARED_NEW_PROJECT') {
|
|
1008
|
+
return { framework: defaultFrameworks[language], frameworkSource: 'DEFAULT_NEW_PROJECT' };
|
|
1009
|
+
}
|
|
1010
|
+
const inferred = inferFramework(projectPath, language);
|
|
1011
|
+
return {
|
|
1012
|
+
framework: inferred || existingProjectFrameworkFallback,
|
|
1013
|
+
frameworkSource: inferred ? 'PROJECT_INFERRED' : 'PROJECT_STRUCTURE_FALLBACK'
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function resolveFramework(args, projectPath, projectSelection, language) {
|
|
1018
|
+
const suppliedFramework = typeof args.framework === 'string' && args.framework.length > 0
|
|
1019
|
+
? sanitizeFramework(args.framework)
|
|
1020
|
+
: '';
|
|
1021
|
+
const suppliedSource = typeof args['framework-source'] === 'string' ? args['framework-source'] : '';
|
|
1022
|
+
if (suppliedSource && !frameworkSources.has(suppliedSource)) {
|
|
1023
|
+
throw new Error('framework-source 非法,仅支持 EXPLICIT、DEFAULT_NEW_PROJECT、PROJECT_INFERRED、PROJECT_STRUCTURE_FALLBACK');
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (!suppliedSource) {
|
|
1027
|
+
return suppliedFramework
|
|
1028
|
+
? { framework: suppliedFramework, frameworkSource: 'EXPLICIT' }
|
|
1029
|
+
: deriveFramework(projectPath, projectSelection, language);
|
|
1030
|
+
}
|
|
1031
|
+
if (!suppliedFramework) throw new Error('framework-source 必须与 framework 同时提供');
|
|
1032
|
+
if (suppliedSource === 'EXPLICIT') {
|
|
1033
|
+
return { framework: suppliedFramework, frameworkSource: suppliedSource };
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const derived = deriveFramework(projectPath, projectSelection, language);
|
|
1037
|
+
if (derived.framework !== suppliedFramework || derived.frameworkSource !== suppliedSource) {
|
|
1038
|
+
throw new Error('framework 与当前项目推导事实不一致');
|
|
1039
|
+
}
|
|
1040
|
+
return derived;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function runFrameworkResolve(args) {
|
|
1044
|
+
const language = normalizeLanguage(required(args, 'language'));
|
|
1045
|
+
const projectSelection = required(args, 'project-selection');
|
|
1046
|
+
if (!['NEW_PROJECT', 'PREPARED_NEW_PROJECT', 'CURRENT_PROJECT', 'OTHER_PROJECT'].includes(projectSelection)) {
|
|
1047
|
+
throw new Error('project-selection 非法,仅支持 CURRENT_PROJECT、OTHER_PROJECT、NEW_PROJECT、PREPARED_NEW_PROJECT');
|
|
1048
|
+
}
|
|
1049
|
+
let projectPath = '';
|
|
1050
|
+
if (projectSelection === 'CURRENT_PROJECT' || projectSelection === 'OTHER_PROJECT') {
|
|
1051
|
+
projectPath = fs.realpathSync(required(args, 'project-path'));
|
|
1052
|
+
if (!isReadableDirectory(projectPath)) throw new Error('现有项目路径不可访问');
|
|
1053
|
+
}
|
|
1054
|
+
const resolved = resolveFramework(args, projectPath, projectSelection, language);
|
|
1055
|
+
return JSON.stringify({ language, ...resolved });
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function startConfirmationFacts(args) {
|
|
1059
|
+
const rootIntent = required(args, 'root-intent');
|
|
1060
|
+
if (!['integration_only', 'full_process'].includes(rootIntent)) {
|
|
1061
|
+
throw new Error('root-intent 仅支持 integration_only、full_process');
|
|
1062
|
+
}
|
|
1063
|
+
const productType = required(args, 'product-type');
|
|
1064
|
+
const mapping = productVariants[productType];
|
|
1065
|
+
if (!mapping) throw new Error('product-type 非法,仅支持 aipay、webpay、apppay');
|
|
1066
|
+
|
|
1067
|
+
const projectPath = required(args, 'project-path');
|
|
1068
|
+
const projectSelection = required(args, 'project-selection');
|
|
1069
|
+
const language = normalizeLanguage(required(args, 'language'));
|
|
1070
|
+
const { framework, frameworkSource } = resolveFramework(args, projectPath, projectSelection, language);
|
|
1071
|
+
const productName = args['product-name'] || mapping.productName;
|
|
1072
|
+
if (productName !== mapping.productName) throw new Error('product-name 与 product-type 不匹配');
|
|
1073
|
+
if (!path.isAbsolute(projectPath)) throw new Error('project-path 必须是绝对路径');
|
|
1074
|
+
|
|
1075
|
+
const route = isWindowsPlatform()
|
|
1076
|
+
? scanProjectRoute(productType, projectPath, projectSelection)
|
|
1077
|
+
: runUnixInspector(['scan', productType, projectPath, projectSelection], '项目检查', '项目检查器');
|
|
1078
|
+
if (route.projectSelection !== projectSelection) throw new Error('项目检查结果与本轮 projectSelection 不一致');
|
|
1079
|
+
const existing = projectSelection === 'CURRENT_PROJECT' || projectSelection === 'OTHER_PROJECT';
|
|
1080
|
+
const preparedNew = projectSelection === 'PREPARED_NEW_PROJECT';
|
|
1081
|
+
if (
|
|
1082
|
+
!(preparedNew && route.projectOrigin === 'NEW_PROJECT' && route.projectOriginLabel === '本轮新建项目') &&
|
|
1083
|
+
!(existing && route.projectOrigin === 'EXISTING_PROJECT' && route.projectOriginLabel === '现有项目')
|
|
1084
|
+
) {
|
|
1085
|
+
throw new Error('项目来源与本轮项目选择不一致');
|
|
1086
|
+
}
|
|
1087
|
+
if (!['TARGET_PARTIAL', 'OTHER_PRODUCT_ONLY', 'NO_PAYMENT'].includes(route.integrationStatus)) {
|
|
1088
|
+
throw new Error('项目集成状态不允许发起代码开发确认');
|
|
1089
|
+
}
|
|
1090
|
+
if (typeof route.evidence !== 'string' || typeof route.otherProducts !== 'string') {
|
|
1091
|
+
throw new Error('项目检查结果缺少必要事实字段');
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return {
|
|
1095
|
+
rootIntent,
|
|
1096
|
+
productType,
|
|
1097
|
+
variant: mapping.variant,
|
|
1098
|
+
productName,
|
|
1099
|
+
projectPath: fs.realpathSync(projectPath),
|
|
1100
|
+
projectSelection,
|
|
1101
|
+
language,
|
|
1102
|
+
framework,
|
|
1103
|
+
frameworkSource,
|
|
1104
|
+
route
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function renderStartConfirmation(facts) {
|
|
1109
|
+
return renderMessage('integration.start.confirm', facts.variant, {
|
|
1110
|
+
productName: facts.productName,
|
|
1111
|
+
projectPath: facts.projectPath,
|
|
1112
|
+
projectOriginLabel: facts.route.projectOriginLabel,
|
|
1113
|
+
integrationStatus: facts.route.integrationStatus,
|
|
1114
|
+
otherProducts: facts.route.otherProducts,
|
|
1115
|
+
language: facts.language,
|
|
1116
|
+
framework: facts.framework,
|
|
1117
|
+
nextFlow: 'integration'
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export async function createStartConfirmation(args) {
|
|
1122
|
+
const facts = startConfirmationFacts(args);
|
|
1123
|
+
await startTelemetryRun(facts.rootIntent, facts.productType);
|
|
1124
|
+
return renderStartConfirmation(facts);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
export function createChecklistScopeReceipt(productType, projectPath) {
|
|
1128
|
+
const variant = productVariants[productType]?.variant;
|
|
1129
|
+
if (!variant) throw new Error('checklist scope product-type 非法');
|
|
1130
|
+
const snapshot = projectSnapshot(projectPath);
|
|
1131
|
+
return encodeReceipt({
|
|
1132
|
+
schema_ver: 1,
|
|
1133
|
+
kind: receiptKinds.checklist,
|
|
1134
|
+
variant,
|
|
1135
|
+
project_path: snapshot.canonicalPath,
|
|
1136
|
+
project_fingerprint: snapshot.fingerprint
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function validateChecklistScopeReceipt(receipt, expectedVariant, expectedProjectPath, snapshot) {
|
|
1141
|
+
const payload = decodeReceipt(receipt, receiptKinds.checklist);
|
|
1142
|
+
const canonicalExpectedPath = fs.realpathSync(expectedProjectPath);
|
|
1143
|
+
return payload.variant === expectedVariant
|
|
1144
|
+
&& payload.project_path === canonicalExpectedPath
|
|
1145
|
+
&& snapshot.canonicalPath === canonicalExpectedPath
|
|
1146
|
+
&& snapshot.fingerprint === payload.project_fingerprint;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function a2mExecutionFacts(args) {
|
|
1150
|
+
const projectPath = fs.realpathSync(required(args, 'project-path'));
|
|
1151
|
+
if (!isReadableDirectory(projectPath)) throw new Error('A2M 联调项目路径不可访问');
|
|
1152
|
+
const method = required(args, 'http-method').toUpperCase();
|
|
1153
|
+
if (!['GET', 'POST'].includes(method)) throw new Error('http-method 仅支持 GET 或 POST');
|
|
1154
|
+
let serviceUrl;
|
|
1155
|
+
try {
|
|
1156
|
+
serviceUrl = new URL(required(args, 'service-url'));
|
|
1157
|
+
} catch {
|
|
1158
|
+
throw new Error('service-url 不是合法 URL');
|
|
1159
|
+
}
|
|
1160
|
+
if (!['http:', 'https:'].includes(serviceUrl.protocol) || serviceUrl.username || serviceUrl.password || serviceUrl.hash) {
|
|
1161
|
+
throw new Error('service-url 只允许不含凭据和 fragment 的 HTTP(S) URL');
|
|
1162
|
+
}
|
|
1163
|
+
let requestBodyHash = '';
|
|
1164
|
+
const requestBodyFile = args['request-body-file'];
|
|
1165
|
+
if (method === 'POST') {
|
|
1166
|
+
if (!requestBodyFile || !path.isAbsolute(requestBodyFile)) throw new Error('POST 必须传入绝对 --request-body-file');
|
|
1167
|
+
const stat = fs.lstatSync(requestBodyFile);
|
|
1168
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('request-body-file 必须是普通文件且不能是符号链接');
|
|
1169
|
+
requestBodyHash = crypto.createHash('sha256').update(fs.readFileSync(requestBodyFile)).digest('hex');
|
|
1170
|
+
} else if (requestBodyFile !== undefined) {
|
|
1171
|
+
throw new Error('GET 禁止传入 request-body-file');
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
project_path: projectPath,
|
|
1175
|
+
project_fingerprint: projectSnapshotFingerprint(projectPath),
|
|
1176
|
+
service_url: serviceUrl.href,
|
|
1177
|
+
http_method: method,
|
|
1178
|
+
request_body_sha256: requestBodyHash
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function a2mExecutionReceipt(facts, state = 'READY') {
|
|
1183
|
+
return encodeReceipt({
|
|
1184
|
+
schema_ver: 1,
|
|
1185
|
+
kind: receiptKinds.a2mExecution,
|
|
1186
|
+
state,
|
|
1187
|
+
...facts
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
export function guardA2mExecution(args) {
|
|
1192
|
+
const facts = a2mExecutionFacts(args);
|
|
1193
|
+
const priorReceipt = args['prior-receipt'];
|
|
1194
|
+
if (!priorReceipt) return { decision: 'RUN', receipt: a2mExecutionReceipt(facts) };
|
|
1195
|
+
const prior = decodeReceipt(priorReceipt, receiptKinds.a2mExecution);
|
|
1196
|
+
const unchanged = ['project_path', 'project_fingerprint', 'service_url', 'http_method', 'request_body_sha256']
|
|
1197
|
+
.every((key) => prior[key] === facts[key]);
|
|
1198
|
+
if (!unchanged) return { decision: 'RUN', receipt: a2mExecutionReceipt(facts) };
|
|
1199
|
+
if (prior.state === 'PASSED') return { decision: 'REUSE_PASSED', receipt: priorReceipt };
|
|
1200
|
+
if (prior.state === 'FAILED') return { decision: 'BLOCK_UNCHANGED', receipt: priorReceipt };
|
|
1201
|
+
return { decision: 'UNKNOWN_PREVIOUS_EXECUTION', receipt: priorReceipt };
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
export function completeA2mExecution(receipt, outcome) {
|
|
1205
|
+
if (!['PASSED', 'FAILED', 'INTERRUPTED'].includes(outcome)) {
|
|
1206
|
+
throw new Error('outcome 仅支持 PASSED、FAILED、INTERRUPTED');
|
|
1207
|
+
}
|
|
1208
|
+
const prior = decodeReceipt(receipt, receiptKinds.a2mExecution);
|
|
1209
|
+
if (prior.state !== 'READY') throw new Error('A2M execution receipt 已经终态化');
|
|
1210
|
+
const { schema_ver, kind, state, ...facts } = prior;
|
|
1211
|
+
void schema_ver;
|
|
1212
|
+
void kind;
|
|
1213
|
+
void state;
|
|
1214
|
+
return a2mExecutionReceipt(facts, outcome);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function runContextRequired(args) {
|
|
1218
|
+
const missingItems = required(args, 'missing-items');
|
|
1219
|
+
return renderMessage('integration.context.required', missingItems === '项目路径' ? 'PROJECT_PATH_ONLY' : 'DEFAULT', {
|
|
1220
|
+
missingItems
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function runProductClarify() {
|
|
1225
|
+
return renderMessage('product.clarify', 'DEFAULT', {});
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function runProductUnsupported(args) {
|
|
1229
|
+
return renderMessage('product.unsupported', 'DEFAULT', {
|
|
1230
|
+
productName: required(args, 'product-name')
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function runWindowsSandboxSetup() {
|
|
1235
|
+
return renderMessage('sandbox.windows.manual_setup', 'DEFAULT', {});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function runLocalAcceptanceNotice() {
|
|
1239
|
+
return renderMessage('integration.local_acceptance.notice', 'DEFAULT', {});
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function runA2mTestStart() {
|
|
1243
|
+
return renderMessage('a2m.test.start', 'DEFAULT', {});
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function runSandboxAndroidClient() {
|
|
1247
|
+
return renderMessage('sandbox.android.client', 'DEFAULT', {});
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function runServiceNotReady(args) {
|
|
1251
|
+
return renderMessage('integration.service.not_ready', 'DEFAULT', {
|
|
1252
|
+
recoveryAction: required(args, 'recovery-action')
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function runSafetyCloseout(args) {
|
|
1257
|
+
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
|
+
}));
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function runWindowsSandboxSummary(args) {
|
|
1265
|
+
const environmentRows = required(args, 'environment-rows');
|
|
1266
|
+
const passwordSaveHint = '已写入本地配置文件,默认不在对话中展开';
|
|
1267
|
+
const lines = environmentRows.split('\n');
|
|
1268
|
+
if (lines[2] !== `| 沙箱买家登录密码 | ${passwordSaveHint} |`
|
|
1269
|
+
|| lines[3] !== `| 沙箱买家支付密码 | ${passwordSaveHint} |`) {
|
|
1270
|
+
throw new Error('Windows 沙箱摘要的两个密码行必须只使用固定本地保存提示');
|
|
1271
|
+
}
|
|
1272
|
+
const summary = renderMessage('sandbox.environment.summary', 'DEFAULT', {
|
|
1273
|
+
productName: required(args, 'product-name'),
|
|
1274
|
+
configPath: required(args, 'config-path'),
|
|
1275
|
+
environmentRows
|
|
1276
|
+
});
|
|
1277
|
+
const reminder = renderMessage('sandbox.environment.reminder', 'WINDOWS', {});
|
|
1278
|
+
emitStageMetric('integration.create_sandbox');
|
|
1279
|
+
return requireCustomerMessageDelivery('sandbox.environment.summary', `${summary}\n\n${reminder}`);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
async function readStdin() {
|
|
1283
|
+
let input = '';
|
|
1284
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
1285
|
+
return input.trim() ? JSON.parse(input) : {};
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function checklistInputFromArgs(args) {
|
|
1289
|
+
const mapping = {
|
|
1290
|
+
'overall-result': 'overallResult',
|
|
1291
|
+
'failed-items': 'failedItems',
|
|
1292
|
+
'manual-items': 'manualItems',
|
|
1293
|
+
'sandbox-result': 'sandboxResult',
|
|
1294
|
+
'webpay-experience-result': 'webpayExperienceResult',
|
|
1295
|
+
'sandbox-config-state': 'sandboxConfigState',
|
|
1296
|
+
'blocking-defect-state': 'blockingDefectState',
|
|
1297
|
+
'execution-mode': 'executionMode',
|
|
1298
|
+
'next-flow-reminder': 'nextFlowReminder'
|
|
1299
|
+
};
|
|
1300
|
+
const hasTypedInput = Object.keys(mapping).some((name) => Object.prototype.hasOwnProperty.call(args, name));
|
|
1301
|
+
if (!hasTypedInput) return null;
|
|
1302
|
+
|
|
1303
|
+
const input = {};
|
|
1304
|
+
for (const [argName, fieldName] of Object.entries(mapping)) {
|
|
1305
|
+
if (Object.prototype.hasOwnProperty.call(args, argName)) input[fieldName] = args[argName];
|
|
1306
|
+
}
|
|
1307
|
+
for (const requiredName of [
|
|
1308
|
+
'overall-result',
|
|
1309
|
+
'failed-items',
|
|
1310
|
+
'manual-items',
|
|
1311
|
+
'sandbox-config-state',
|
|
1312
|
+
'blocking-defect-state',
|
|
1313
|
+
'execution-mode',
|
|
1314
|
+
'next-flow-reminder'
|
|
1315
|
+
]) {
|
|
1316
|
+
required(args, requiredName);
|
|
1317
|
+
}
|
|
1318
|
+
return input;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
async function runChecklistResult(args) {
|
|
1322
|
+
const variant = required(args, 'variant');
|
|
1323
|
+
if (!['AIPAY', 'WEBPAY', 'APPPAY'].includes(variant)) throw new Error('variant 非法,仅支持 AIPAY、WEBPAY、APPPAY');
|
|
1324
|
+
if (telemetryEnabled()) {
|
|
1325
|
+
const run = currentRunContext();
|
|
1326
|
+
if (!run?.run_id || run.terminal || run.product_type !== variant.toUpperCase()
|
|
1327
|
+
|| !['integration_only', 'full_process'].includes(run.root_intent)) {
|
|
1328
|
+
throw new Error('checklist 必须在当前产品匹配的 active integration run 内执行');
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (Object.prototype.hasOwnProperty.call(args, 'implementation-audit-receipt')) {
|
|
1332
|
+
throw new Error('implementation-audit-receipt 已废弃');
|
|
1333
|
+
}
|
|
1334
|
+
const finalizesAuditFindings = Object.prototype.hasOwnProperty.call(args, 'finalize-audit-findings');
|
|
1335
|
+
if (finalizesAuditFindings && args['finalize-audit-findings'] !== 'true') {
|
|
1336
|
+
throw new Error('finalize-audit-findings 只允许 true');
|
|
1337
|
+
}
|
|
1338
|
+
const scopeReceipt = required(args, 'scope-receipt');
|
|
1339
|
+
const projectPath = required(args, 'project-path');
|
|
1340
|
+
const snapshot = projectSnapshot(projectPath);
|
|
1341
|
+
if (!validateChecklistScopeReceipt(scopeReceipt, variant, projectPath, snapshot)) {
|
|
1342
|
+
throw new Error('项目在当前 checklist 开始后发生变化,或产品不匹配;必须重新进入 checklist 阶段并重跑受影响检查');
|
|
1343
|
+
}
|
|
1344
|
+
const input = checklistInputFromArgs(args) || await readStdin();
|
|
1345
|
+
const audit = inspectImplementation(variant, projectPath, snapshot);
|
|
1346
|
+
if (audit.findings.length === 0 && finalizesAuditFindings) {
|
|
1347
|
+
throw new Error('finalize-audit-findings 仅用于收口实际 audit findings');
|
|
1348
|
+
}
|
|
1349
|
+
if (audit.findings.length > 0) {
|
|
1350
|
+
if (args['finalize-audit-findings'] !== 'true') {
|
|
1351
|
+
process.stderr.write(`${internalPrefix}CHECKLIST_AUDIT=${JSON.stringify({
|
|
1352
|
+
state: 'REPAIR_REQUIRED',
|
|
1353
|
+
findings: audit.findings
|
|
1354
|
+
})}\n`);
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
const auditItems = audit.findings.map((finding) => {
|
|
1358
|
+
const location = finding.path ? `(${finding.path}${finding.line ? `:${finding.line}` : ''})` : '';
|
|
1359
|
+
return `${finding.message}${location}`;
|
|
1360
|
+
}).join(';');
|
|
1361
|
+
input.overallResult = '未通过';
|
|
1362
|
+
input.blockingDefectState = 'PRESENT';
|
|
1363
|
+
input.failedItems = input.failedItems && input.failedItems !== '无'
|
|
1364
|
+
? `${input.failedItems};${auditItems}`
|
|
1365
|
+
: auditItems;
|
|
1366
|
+
if (input.executionMode === '完整接入') {
|
|
1367
|
+
input.nextFlowReminder = '代码开发校验通过后将自动进入支付产品-产品开通;未通过时停留在代码开发。';
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
emitStageMetric('integration.complete_coding');
|
|
1371
|
+
const rendered = renderMessage('integration.checklist.result', variant, input);
|
|
1372
|
+
return requireCustomerMessageDelivery('integration.checklist.result', rendered);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
export async function runIntegrationMessageCli(argv = process.argv) {
|
|
1376
|
+
const { command, args } = parseArgs(argv);
|
|
1377
|
+
if (command === 'resolve-project') {
|
|
1378
|
+
console.log(runResolveProject(args));
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (command === 'locate-projects') {
|
|
1382
|
+
console.log(runLocateProjects(args));
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if (command === 'start-confirm') {
|
|
1386
|
+
console.log(await createStartConfirmation(args));
|
|
1387
|
+
process.stderr.write(`${internalPrefix}CUSTOMER_MESSAGE_DELIVERY_REQUIRED=integration.start.confirm\n`);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
if (command === 'framework-resolve') {
|
|
1391
|
+
console.log(runFrameworkResolve(args));
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
if (command === 'a2m-execution-guard') {
|
|
1395
|
+
process.stderr.write(`${internalPrefix}A2M_EXECUTION=${JSON.stringify(guardA2mExecution(args))}\n`);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (command === 'a2m-execution-result') {
|
|
1399
|
+
const receipt = completeA2mExecution(required(args, 'receipt'), required(args, 'outcome'));
|
|
1400
|
+
process.stderr.write(`${internalPrefix}A2M_EXECUTION=${JSON.stringify({ decision: 'RECORDED', receipt })}\n`);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (command === 'context-required') {
|
|
1404
|
+
console.log(runContextRequired(args));
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (command === 'product-clarify') {
|
|
1408
|
+
console.log(runProductClarify());
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
if (command === 'product-unsupported') {
|
|
1412
|
+
console.log(runProductUnsupported(args));
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (command === 'windows-sandbox-setup') {
|
|
1416
|
+
console.log(runWindowsSandboxSetup());
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
if (command === 'local-acceptance-notice') {
|
|
1420
|
+
console.log(runLocalAcceptanceNotice());
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (command === 'a2m-test-start') {
|
|
1424
|
+
console.log(runA2mTestStart());
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (command === 'sandbox-android-client') {
|
|
1428
|
+
console.log(runSandboxAndroidClient());
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
if (command === 'service-not-ready') {
|
|
1432
|
+
console.log(runServiceNotReady(args));
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
if (command === 'safety-closeout') {
|
|
1436
|
+
console.log(runSafetyCloseout(args));
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
if (command === 'windows-sandbox-summary') {
|
|
1440
|
+
console.log(runWindowsSandboxSummary(args));
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (command === 'checklist-result') {
|
|
1444
|
+
const rendered = await runChecklistResult(args);
|
|
1445
|
+
if (rendered !== null) console.log(rendered);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
throw new Error('用法: integration_message_runner.mjs <resolve-project|locate-projects|framework-resolve|start-confirm|a2m-execution-guard|a2m-execution-result|context-required|product-clarify|product-unsupported|windows-sandbox-setup|local-acceptance-notice|a2m-test-start|sandbox-android-client|service-not-ready|safety-closeout|windows-sandbox-summary|checklist-result> ...');
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
1452
|
+
runIntegrationMessageCli().catch((error) => {
|
|
1453
|
+
console.error(`INTEGRATION_MESSAGE_ERROR:${error.message}`);
|
|
1454
|
+
process.exitCode = 1;
|
|
1455
|
+
});
|
|
1456
|
+
}
|