@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,1792 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { currentRunContext, emitMessageMetric } from './telemetry.mjs';
|
|
9
|
+
|
|
10
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const normalDir = path.resolve(scriptDir, '..');
|
|
12
|
+
const jsonDir = path.join(normalDir, 'json');
|
|
13
|
+
const catalogPath = path.join(jsonDir, 'customer-messages.json');
|
|
14
|
+
const policyPath = path.join(jsonDir, 'policy-rules.json');
|
|
15
|
+
const sensitiveNamePattern = /(private.?key|public.?key|token|password|payment.?proof|signature|authorization|full.?response)/i;
|
|
16
|
+
const sensitiveValuePattern = /-----BEGIN (?:RSA )?(?:PRIVATE|PUBLIC) KEY-----|Payment-Proof:\s*\S+|Authorization:\s*Bearer\s+\S+|["'](?:password|passwd|secret|api[_-]?key|access[_-]?token)["']\s*:/i;
|
|
17
|
+
const placeholderPattern = /\{\{([A-Za-z][A-Za-z0-9_]*)\}\}/g;
|
|
18
|
+
const allowedVariableTypes = new Set([
|
|
19
|
+
'applicationCandidates',
|
|
20
|
+
'command', 'customerErrorDetails', 'customerErrorText', 'enum', 'enumList', 'existingPath', 'mccCode', 'officialTemporaryUrl', 'path',
|
|
21
|
+
'runtimeUrl', 'sandboxCredential', 'sandboxTableRows', 'serviceCandidates', 'mccCandidates', 'singleLine', 'string'
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function allowedVariableNames(definitions) {
|
|
25
|
+
return [...definitions.values()]
|
|
26
|
+
.filter((definition) => !definition.derived)
|
|
27
|
+
.map((definition) => definition.name)
|
|
28
|
+
.join('、') || '无';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cloneInputVariable(definition) {
|
|
32
|
+
const cloned = {
|
|
33
|
+
name: definition.name,
|
|
34
|
+
type: definition.type,
|
|
35
|
+
required: Boolean(definition.required)
|
|
36
|
+
};
|
|
37
|
+
if (Array.isArray(definition.values)) cloned.values = [...definition.values];
|
|
38
|
+
if (definition.urlKind) cloned.urlKind = definition.urlKind;
|
|
39
|
+
if (definition.derived) cloned.derived = true;
|
|
40
|
+
return cloned;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setVariableRule(variables, name, updates) {
|
|
44
|
+
const variable = variables.find((entry) => entry.name === name);
|
|
45
|
+
if (!variable) return;
|
|
46
|
+
Object.assign(variable, updates);
|
|
47
|
+
if (updates.values) variable.values = [...updates.values];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadJson(file) {
|
|
51
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function keyToolManualUrl(policy) {
|
|
55
|
+
const value = policy.officialUrls?.keyToolManual;
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = new URL(value);
|
|
59
|
+
} catch {
|
|
60
|
+
throw new Error('policy-rules.json 缺少合法 officialUrls.keyToolManual');
|
|
61
|
+
}
|
|
62
|
+
if (parsed.protocol !== 'https:' || parsed.hostname !== 'opendocs.alipay.com'
|
|
63
|
+
|| parsed.pathname !== '/isv/02kipk' || parsed.search || parsed.hash
|
|
64
|
+
|| parsed.username || parsed.password || parsed.port) {
|
|
65
|
+
throw new Error('policy-rules.json officialUrls.keyToolManual 不符合固定官方地址约束');
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function derivedCatalogText(definitions, name) {
|
|
71
|
+
const definition = definitions.get(name);
|
|
72
|
+
const values = definition?.values;
|
|
73
|
+
if (!definition?.derived || definition.type !== 'string'
|
|
74
|
+
|| !Array.isArray(values) || values.length !== 1
|
|
75
|
+
|| typeof values[0] !== 'string' || !values[0]) {
|
|
76
|
+
throw new Error(`消息目录缺少合法派生文案 ${name}`);
|
|
77
|
+
}
|
|
78
|
+
return values[0];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function allowedRepliesFor(catalog, message) {
|
|
82
|
+
if (!message.allowedReplySet) return message.allowedReplies || [];
|
|
83
|
+
const replies = catalog.interactionReplySets?.[message.allowedReplySet];
|
|
84
|
+
if (!Array.isArray(replies) || replies.length === 0 || replies.some((reply) => typeof reply !== 'string' || !reply)) {
|
|
85
|
+
throw new Error(`${message.messageId}: allowedReplySet 无效`);
|
|
86
|
+
}
|
|
87
|
+
return replies;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function safeMccDisplayText(value, fieldName) {
|
|
91
|
+
const normalized = typeof value === 'string'
|
|
92
|
+
? value.replace(/[\u0000-\u001f\u007f]+/gu, ' ').replace(/\s+/gu, ' ').trim()
|
|
93
|
+
: value;
|
|
94
|
+
if (typeof normalized !== 'string' || normalized.length === 0
|
|
95
|
+
|| /[|`]/u.test(normalized) || /https?:\/\//iu.test(normalized)
|
|
96
|
+
|| normalized.includes('{{') || normalized.includes('}}') || /<INTERNAL_/iu.test(normalized)
|
|
97
|
+
|| sensitiveValuePattern.test(normalized)) {
|
|
98
|
+
throw new Error(`${fieldName} 不是安全的 MCC 展示文本`);
|
|
99
|
+
}
|
|
100
|
+
return normalized;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeQualificationGroups(groups, { allowEmpty = true } = {}) {
|
|
104
|
+
if (groups === undefined || groups === null) return [];
|
|
105
|
+
if (!Array.isArray(groups)) throw new Error('qualificationGroups 必须是数组');
|
|
106
|
+
if (!allowEmpty && groups.length === 0) throw new Error('特殊资质类目缺少 qualificationGroups');
|
|
107
|
+
return groups.map((group, groupIndex) => {
|
|
108
|
+
if (!group || typeof group !== 'object' || Array.isArray(group)) {
|
|
109
|
+
throw new Error(`qualificationGroups[${groupIndex}] 必须是对象`);
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(group.qualifications)) {
|
|
112
|
+
throw new Error(`qualificationGroups[${groupIndex}].qualifications 必须是数组`);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
qualifications: group.qualifications.map((qualification, qualificationIndex) => {
|
|
116
|
+
if (!qualification || typeof qualification !== 'object' || Array.isArray(qualification)) {
|
|
117
|
+
throw new Error(`qualificationGroups[${groupIndex}].qualifications[${qualificationIndex}] 必须是对象`);
|
|
118
|
+
}
|
|
119
|
+
const attaType = qualification.attaType === null || qualification.attaType === undefined || qualification.attaType === ''
|
|
120
|
+
? null
|
|
121
|
+
: safeMccDisplayText(String(qualification.attaType), '资质材料类型');
|
|
122
|
+
const qualName = safeMccDisplayText(String(qualification.qualName || ''), '资质名称');
|
|
123
|
+
const normalized = { attaType, qualName };
|
|
124
|
+
for (const [source, target] of [['qualId', 'qualId'], ['qualType', 'qualType'], ['status', 'status']]) {
|
|
125
|
+
if (qualification[source] !== undefined && qualification[source] !== null && qualification[source] !== '') {
|
|
126
|
+
normalized[target] = safeMccDisplayText(String(qualification[source]), `资质字段 ${source}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return normalized;
|
|
130
|
+
})
|
|
131
|
+
};
|
|
132
|
+
}).filter((group) => group.qualifications.length > 0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function qualificationGroupSummary(group) {
|
|
136
|
+
return group.qualifications
|
|
137
|
+
.map((qualification) => qualification.attaType
|
|
138
|
+
? `${qualification.qualName}(需上传材料)`
|
|
139
|
+
: `${qualification.qualName}(无需上传,按文字条件核验)`)
|
|
140
|
+
.join(' + ');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function mccQualificationSummary(input) {
|
|
144
|
+
const needSpecialQual = input.needSpecialQual === true;
|
|
145
|
+
const groups = normalizeQualificationGroups(input.qualificationGroups);
|
|
146
|
+
if (!needSpecialQual && groups.length === 0) return '无需特殊资质';
|
|
147
|
+
if (needSpecialQual && groups.length === 0) return '需要特殊资质,但当前查询未返回具体资质要求';
|
|
148
|
+
return `可任选一种资质组合:${groups.map((group, index) => `${index + 1}. ${qualificationGroupSummary(group)}`).join(';')}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeMccContextPayload(payload) {
|
|
152
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw new Error('MCC 上下文回执载荷无效');
|
|
153
|
+
const schema = payload.schema_ver;
|
|
154
|
+
if (schema !== 1) throw new Error('MCC 上下文回执版本无效');
|
|
155
|
+
const normalized = {
|
|
156
|
+
schema_ver: 1,
|
|
157
|
+
product_type: safeMccDisplayText(String(payload.product_type || ''), '产品类型'),
|
|
158
|
+
sales_code: safeMccDisplayText(String(payload.sales_code || ''), '产品码'),
|
|
159
|
+
mcc_code: safeMccDisplayText(String(payload.mcc_code || ''), 'MCC 编码'),
|
|
160
|
+
mcc_name: safeMccDisplayText(String(payload.mcc_name || ''), 'MCC 名称'),
|
|
161
|
+
need_special_qual: payload.need_special_qual === true,
|
|
162
|
+
qualification_groups: normalizeQualificationGroups(payload.qualification_groups)
|
|
163
|
+
};
|
|
164
|
+
if (normalized.qualification_groups.some((group) => group.qualifications.length === 0)) {
|
|
165
|
+
throw new Error('MCC 上下文回执包含空特殊资质组合');
|
|
166
|
+
}
|
|
167
|
+
if (normalized.need_special_qual !== (normalized.qualification_groups.length > 0)) {
|
|
168
|
+
throw new Error('MCC 上下文回执特殊资质标记与资质组合不一致');
|
|
169
|
+
}
|
|
170
|
+
if (!['aipay', 'webpay', 'apppay'].includes(normalized.product_type)) throw new Error('MCC 上下文回执产品类型无效');
|
|
171
|
+
const policy = loadJson(policyPath);
|
|
172
|
+
const product = authorizationProduct(policy, (entry) => entry.salesCode === normalized.sales_code);
|
|
173
|
+
if (!product) {
|
|
174
|
+
throw new Error('MCC 上下文回执产品码无效');
|
|
175
|
+
}
|
|
176
|
+
if (productTypeForProductName(product.productName) !== normalized.product_type) {
|
|
177
|
+
throw new Error('MCC 上下文回执产品类型和产品码不一致');
|
|
178
|
+
}
|
|
179
|
+
if (!/^A\d{4}_B\d{4}$/u.test(normalized.mcc_code)) throw new Error('MCC 上下文回执 MCC 编码无效');
|
|
180
|
+
if (payload.mcc_description !== undefined && payload.mcc_description !== null && payload.mcc_description !== '') {
|
|
181
|
+
normalized.mcc_description = safeMccDisplayText(String(payload.mcc_description), 'MCC 描述');
|
|
182
|
+
}
|
|
183
|
+
if (payload.run_id !== undefined && payload.run_id !== null && payload.run_id !== '') {
|
|
184
|
+
const runId = String(payload.run_id);
|
|
185
|
+
if (/[\u0000-\u001f\u007f,^=]/u.test(runId)) throw new Error('MCC 上下文回执 run_id 无效');
|
|
186
|
+
normalized.run_id = runId;
|
|
187
|
+
}
|
|
188
|
+
return normalized;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function createMccContextReceipt(input) {
|
|
192
|
+
const payload = normalizeMccContextPayload(input);
|
|
193
|
+
const encoded = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
|
|
194
|
+
const digest = createHash('sha256').update(encoded).digest('base64url');
|
|
195
|
+
return `AIPAY_MCC_CONTEXT_V1.${encoded}.${digest}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function verifyMccContextReceipt(receipt) {
|
|
199
|
+
if (typeof receipt !== 'string' || !receipt.startsWith('AIPAY_MCC_CONTEXT_V1.')) {
|
|
200
|
+
throw new Error('MCC 上下文回执格式无效');
|
|
201
|
+
}
|
|
202
|
+
const parts = receipt.split('.');
|
|
203
|
+
if (parts.length !== 3 || !parts[1] || !parts[2]) throw new Error('MCC 上下文回执格式无效');
|
|
204
|
+
const digest = createHash('sha256').update(parts[1]).digest('base64url');
|
|
205
|
+
if (digest !== parts[2]) throw new Error('MCC 上下文回执校验失败');
|
|
206
|
+
let payload;
|
|
207
|
+
try {
|
|
208
|
+
payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
209
|
+
} catch {
|
|
210
|
+
throw new Error('MCC 上下文回执载荷不是合法 JSON');
|
|
211
|
+
}
|
|
212
|
+
return normalizeMccContextPayload(payload);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function activeMccRunId(env = process.env) {
|
|
216
|
+
const explicit = env.ALIPAY_AIPAY_RUN_ID || env.ALIPAY_AIPAY_FLOW_RUN_ID || '';
|
|
217
|
+
const candidate = explicit || currentRunContext(env)?.run_id || '';
|
|
218
|
+
if (typeof candidate !== 'string' || !candidate || /[\u0000-\u001f\u007f,^=]/u.test(candidate)) return '';
|
|
219
|
+
return candidate;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeExpectedMccRunId(value) {
|
|
223
|
+
if (value === undefined || value === null || value === '') return '';
|
|
224
|
+
const candidate = String(value);
|
|
225
|
+
if (/[\u0000-\u001f\u007f,^=]/u.test(candidate)) throw new Error('MCC 上下文期望 run_id 无效');
|
|
226
|
+
return candidate;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function expectedMccRunIdFromContext(context = {}) {
|
|
230
|
+
return normalizeExpectedMccRunId(
|
|
231
|
+
context.mccRunId
|
|
232
|
+
|| context.expectedMccRunId
|
|
233
|
+
|| context.mcc_run_id
|
|
234
|
+
|| context.expected_mcc_run_id
|
|
235
|
+
|| ''
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function assertCurrentMccRun(receiptPayload, env = process.env, expectedRunId = '') {
|
|
240
|
+
const currentRunId = normalizeExpectedMccRunId(expectedRunId) || activeMccRunId(env);
|
|
241
|
+
if (receiptPayload.run_id && receiptPayload.run_id !== currentRunId) {
|
|
242
|
+
throw new Error('MCC 上下文回执不属于当前 run');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function signingMccContext(input) {
|
|
247
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('签约 MCC 上下文必须是对象');
|
|
248
|
+
const productType = String(input.productType || '');
|
|
249
|
+
const salesCode = String(input.salesCode || '');
|
|
250
|
+
const mccCode = String(input.mccCode || '');
|
|
251
|
+
const receiptPayload = verifyMccContextReceipt(input.mccContextReceipt);
|
|
252
|
+
assertCurrentMccRun(receiptPayload);
|
|
253
|
+
const policy = loadJson(policyPath);
|
|
254
|
+
const product = authorizationProduct(policy, (entry) => entry.salesCode === salesCode);
|
|
255
|
+
if (!product || productTypeForProductName(product.productName) !== productType
|
|
256
|
+
|| receiptPayload.product_type !== productType
|
|
257
|
+
|| receiptPayload.sales_code !== salesCode
|
|
258
|
+
|| receiptPayload.mcc_code !== mccCode) {
|
|
259
|
+
throw new Error('特殊资质上下文与当前产品或 MCC 不一致');
|
|
260
|
+
}
|
|
261
|
+
const groups = receiptPayload.qualification_groups;
|
|
262
|
+
const requiresSpecialQualification = receiptPayload.need_special_qual === true || groups.length > 0;
|
|
263
|
+
if (receiptPayload.need_special_qual === true && groups.length === 0) {
|
|
264
|
+
throw new Error('当前 MCC 要求特殊资质,但查询结果没有可选择的资质组合');
|
|
265
|
+
}
|
|
266
|
+
const rawGroupIndex = input.qualificationGroupIndex;
|
|
267
|
+
if (!requiresSpecialQualification) {
|
|
268
|
+
if (rawGroupIndex !== undefined && rawGroupIndex !== null && rawGroupIndex !== '') {
|
|
269
|
+
throw new Error('当前 MCC 无特殊资质时不得指定资质组合');
|
|
270
|
+
}
|
|
271
|
+
return { receiptPayload, expectedLicenseTypes: [] };
|
|
272
|
+
}
|
|
273
|
+
const groupIndex = Number(rawGroupIndex);
|
|
274
|
+
if (!Number.isInteger(groupIndex) || groupIndex < 1 || groupIndex > groups.length) {
|
|
275
|
+
throw new Error('特殊资质组合序号无效');
|
|
276
|
+
}
|
|
277
|
+
const expectedLicenseTypes = groups[groupIndex - 1].qualifications
|
|
278
|
+
.filter((qualification) => qualification.attaType)
|
|
279
|
+
.map((qualification) => qualification.attaType);
|
|
280
|
+
return { receiptPayload, expectedLicenseTypes };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function validateSpecialLicenseUploadRequest(input) {
|
|
284
|
+
const expectedLicenseTypes = specialLicenseTypesForUploadRequest(input);
|
|
285
|
+
if (!Array.isArray(input.licenseTypes)
|
|
286
|
+
|| input.licenseTypes.some((value) => typeof value !== 'string' || !value || /[\u0000-\u001f\u007f]/u.test(value))
|
|
287
|
+
|| JSON.stringify(input.licenseTypes) !== JSON.stringify(expectedLicenseTypes)) {
|
|
288
|
+
throw new Error('待上传特殊资质类型与所选 MCC 资质组合不一致');
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function specialLicenseTypesForUploadRequest(input) {
|
|
294
|
+
const { expectedLicenseTypes } = signingMccContext({
|
|
295
|
+
...input,
|
|
296
|
+
salesCode: input?.salesCode || verifyMccContextReceipt(input?.mccContextReceipt).sales_code,
|
|
297
|
+
mccCode: input?.mccCode || verifyMccContextReceipt(input?.mccContextReceipt).mcc_code
|
|
298
|
+
});
|
|
299
|
+
if (expectedLicenseTypes.length === 0) throw new Error('所选资质组合没有需要上传的材料');
|
|
300
|
+
return expectedLicenseTypes;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function validateSigningSpecialLicenseContext(input) {
|
|
304
|
+
const { expectedLicenseTypes } = signingMccContext(input);
|
|
305
|
+
if (!Array.isArray(input.specialLicense)) throw new Error('specialLicense 必须是数组');
|
|
306
|
+
const normalized = input.specialLicense.map((item) => {
|
|
307
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)
|
|
308
|
+
|| Object.keys(item).sort().join(',') !== 'licensePic,licenseType'
|
|
309
|
+
|| typeof item.licenseType !== 'string' || !item.licenseType
|
|
310
|
+
|| typeof item.licensePic !== 'string' || !item.licensePic
|
|
311
|
+
|| /[\u0000-\u001f\u007f]/u.test(item.licenseType)
|
|
312
|
+
|| /[\u0000-\u001f\u007f]/u.test(item.licensePic)) {
|
|
313
|
+
throw new Error('specialLicense 项必须且只能包含安全的 licenseType 和 licensePic');
|
|
314
|
+
}
|
|
315
|
+
return { licenseType: item.licenseType, licensePic: item.licensePic };
|
|
316
|
+
});
|
|
317
|
+
if (JSON.stringify(normalized.map((item) => item.licenseType)) !== JSON.stringify(expectedLicenseTypes)) {
|
|
318
|
+
throw new Error('specialLicense 与所选 MCC 资质组合不一致');
|
|
319
|
+
}
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function authorizationProduct(policy, predicate) {
|
|
324
|
+
const products = policy.authorizationProducts || [];
|
|
325
|
+
return products.find(predicate);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function productTypeForProductName(productName) {
|
|
329
|
+
return {
|
|
330
|
+
'AI 按量付费': 'aipay',
|
|
331
|
+
'AI 网页应用收款': 'webpay',
|
|
332
|
+
'AI 移动应用收款': 'apppay'
|
|
333
|
+
}[productName] || '';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function validateUrl(value, policy, urlKind, expected = {}) {
|
|
337
|
+
if (/[\u0000-\u0020\u007f`()\[\]<>|]/.test(value)) {
|
|
338
|
+
throw new Error('officialTemporaryUrl 包含不允许的 Markdown 边界');
|
|
339
|
+
}
|
|
340
|
+
let parsed;
|
|
341
|
+
try {
|
|
342
|
+
parsed = new URL(value);
|
|
343
|
+
} catch {
|
|
344
|
+
throw new Error('officialTemporaryUrl 不是合法 URL');
|
|
345
|
+
}
|
|
346
|
+
const allowed = policy.temporaryUrlAllowlist[urlKind];
|
|
347
|
+
if (!allowed || parsed.protocol !== 'https:' || parsed.hostname !== allowed.host || parsed.pathname !== allowed.path || parsed.username || parsed.password || parsed.port || parsed.hash) {
|
|
348
|
+
throw new Error(`officialTemporaryUrl 不符合 ${urlKind} allowlist`);
|
|
349
|
+
}
|
|
350
|
+
if (urlKind === 'authorization') {
|
|
351
|
+
const required = allowed.requiredQueryParams || ['deviceCode', 'productCode', 'mccCode'];
|
|
352
|
+
const optional = allowed.optionalQueryParams || [];
|
|
353
|
+
const allowedKeys = new Set([...required, ...optional]);
|
|
354
|
+
const actualKeys = [...parsed.searchParams.keys()];
|
|
355
|
+
if (actualKeys.some((key) => !allowedKeys.has(key))) throw new Error('authorization URL 包含未允许的查询参数');
|
|
356
|
+
for (const key of required) {
|
|
357
|
+
if (parsed.searchParams.getAll(key).length !== 1 || !parsed.searchParams.get(key)) {
|
|
358
|
+
throw new Error('authorization URL 缺少必要查询参数或参数重复');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for (const key of optional) {
|
|
362
|
+
if (parsed.searchParams.getAll(key).length > 1) throw new Error('authorization URL 可选参数重复');
|
|
363
|
+
}
|
|
364
|
+
const productCode = parsed.searchParams.get('productCode');
|
|
365
|
+
if (!authorizationProduct(policy, (product) => product.salesCode === productCode)) {
|
|
366
|
+
throw new Error('authorization URL productCode 不是受支持产品');
|
|
367
|
+
}
|
|
368
|
+
if (!/^A\d{4}_B\d{4}$/.test(parsed.searchParams.get('mccCode'))) {
|
|
369
|
+
throw new Error('authorization URL mccCode 格式无效');
|
|
370
|
+
}
|
|
371
|
+
for (const [key, expectedValue] of Object.entries(expected)) {
|
|
372
|
+
if (expectedValue !== undefined && parsed.searchParams.get(key) !== expectedValue) {
|
|
373
|
+
throw new Error(`authorization URL ${key} 与当前授权上下文不一致`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (urlKind === 'publicKeyConfirmation') {
|
|
378
|
+
const required = allowed.requiredQueryParams || ['keyConfirmToken'];
|
|
379
|
+
const optional = allowed.optionalQueryParams || [];
|
|
380
|
+
const allowedKeys = new Set([...required, ...optional]);
|
|
381
|
+
const actualKeys = [...parsed.searchParams.keys()];
|
|
382
|
+
if (actualKeys.some((key) => !allowedKeys.has(key))) throw new Error('publicKeyConfirmation URL 包含未允许的查询参数');
|
|
383
|
+
for (const key of required) {
|
|
384
|
+
if (parsed.searchParams.getAll(key).length !== 1 || !parsed.searchParams.get(key)) {
|
|
385
|
+
throw new Error('publicKeyConfirmation URL 缺少必要查询参数或参数重复');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
for (const key of optional) {
|
|
389
|
+
if (parsed.searchParams.getAll(key).length > 1) throw new Error('publicKeyConfirmation URL 可选参数重复');
|
|
390
|
+
}
|
|
391
|
+
if (/[\r\n]/.test(parsed.searchParams.get('keyConfirmToken'))) throw new Error('publicKeyConfirmation URL keyConfirmToken 无效');
|
|
392
|
+
}
|
|
393
|
+
if (urlKind === 'windowsOnboardingRedirect') {
|
|
394
|
+
const required = allowed.requiredQueryParams || ['from', 'reason', 'platform_id'];
|
|
395
|
+
const allowedKeys = new Set(required);
|
|
396
|
+
const actualKeys = [...parsed.searchParams.keys()];
|
|
397
|
+
if (actualKeys.some((key) => !allowedKeys.has(key))) throw new Error('windowsOnboardingRedirect URL 包含未允许的查询参数');
|
|
398
|
+
for (const key of required) {
|
|
399
|
+
if (parsed.searchParams.getAll(key).length !== 1 || !parsed.searchParams.get(key)) {
|
|
400
|
+
throw new Error('windowsOnboardingRedirect URL 缺少必要查询参数或参数重复');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (parsed.searchParams.get('from') !== 'alipay-aipay-skill') {
|
|
404
|
+
throw new Error('windowsOnboardingRedirect URL from 无效');
|
|
405
|
+
}
|
|
406
|
+
if (parsed.searchParams.get('reason') !== 'alipay-aipay-skill-onboarding-not-support-windows') {
|
|
407
|
+
throw new Error('windowsOnboardingRedirect URL reason 无效');
|
|
408
|
+
}
|
|
409
|
+
if (/[\u0000-\u001f\u007f,^=]/u.test(parsed.searchParams.get('platform_id'))) {
|
|
410
|
+
throw new Error('windowsOnboardingRedirect URL platform_id 无效');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return parsed;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function validateAuthorizationContext(input) {
|
|
417
|
+
const policy = loadJson(policyPath);
|
|
418
|
+
const required = ['productName', 'salesCode', 'scope', 'mccCode', 'mccName'];
|
|
419
|
+
for (const key of required) {
|
|
420
|
+
if (typeof input[key] !== 'string' || !input[key]) throw new Error(`授权上下文缺少 ${key}`);
|
|
421
|
+
}
|
|
422
|
+
const product = authorizationProduct(policy, (entry) => entry.salesCode === input.salesCode);
|
|
423
|
+
if (!product || product.productName !== input.productName || product.scope !== input.scope) {
|
|
424
|
+
throw new Error('产品名称、salesCode 和 scope 不属于同一固定产品映射');
|
|
425
|
+
}
|
|
426
|
+
if (!/^A\d{4}_B\d{4}$/u.test(input.mccCode)) throw new Error('MCC 编码格式无效');
|
|
427
|
+
safeMccDisplayText(input.mccName, 'MCC 名称');
|
|
428
|
+
if (typeof input.mccContextReceipt !== 'string' || input.mccContextReceipt.length === 0) {
|
|
429
|
+
throw new Error('MCC 授权上下文必须提供本轮 MCC_CONTEXT_RECEIPT');
|
|
430
|
+
}
|
|
431
|
+
const receipt = verifyMccContextReceipt(input.mccContextReceipt);
|
|
432
|
+
assertCurrentMccRun(receipt);
|
|
433
|
+
const expectedProductType = productTypeForProductName(product.productName);
|
|
434
|
+
if (receipt.product_type !== expectedProductType
|
|
435
|
+
|| receipt.sales_code !== input.salesCode
|
|
436
|
+
|| receipt.mcc_code !== input.mccCode
|
|
437
|
+
|| receipt.mcc_name !== input.mccName) {
|
|
438
|
+
throw new Error('MCC 上下文回执与授权参数不一致');
|
|
439
|
+
}
|
|
440
|
+
if (input.deviceCode !== undefined && (typeof input.deviceCode !== 'string' || !input.deviceCode || /[\r\n]/.test(input.deviceCode))) {
|
|
441
|
+
throw new Error('本次授权 deviceCode 无效');
|
|
442
|
+
}
|
|
443
|
+
if (input.officialUrl !== undefined) {
|
|
444
|
+
if (typeof input.deviceCode !== 'string' || !input.deviceCode || /[\r\n]/.test(input.deviceCode)) {
|
|
445
|
+
throw new Error('本次授权 deviceCode 无效');
|
|
446
|
+
}
|
|
447
|
+
if (typeof input.officialUrl !== 'string' || !input.officialUrl) throw new Error('缺少本次授权 URL');
|
|
448
|
+
validateUrl(input.officialUrl, policy, 'authorization', {
|
|
449
|
+
deviceCode: input.deviceCode,
|
|
450
|
+
productCode: input.salesCode,
|
|
451
|
+
mccCode: input.mccCode,
|
|
452
|
+
...(input.platform ? { platform: input.platform } : {})
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return { product, mccContext: receipt };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function buildAuthorizationUrl(input) {
|
|
459
|
+
validateAuthorizationContext(input);
|
|
460
|
+
if (typeof input.deviceCode !== 'string' || !input.deviceCode || /[\r\n]/.test(input.deviceCode)) {
|
|
461
|
+
throw new Error('本次授权 deviceCode 无效');
|
|
462
|
+
}
|
|
463
|
+
if (input.platform !== undefined && (typeof input.platform !== 'string' || /[\r\n]/.test(input.platform))) {
|
|
464
|
+
throw new Error('授权 platform 无效');
|
|
465
|
+
}
|
|
466
|
+
const url = new URL('https://aipay.alipay.com/cli-auth');
|
|
467
|
+
url.searchParams.set('deviceCode', input.deviceCode);
|
|
468
|
+
url.searchParams.set('productCode', input.salesCode);
|
|
469
|
+
url.searchParams.set('mccCode', input.mccCode);
|
|
470
|
+
if (input.platform && input.platform !== 'unknown') url.searchParams.set('platform', input.platform);
|
|
471
|
+
const value = url.toString();
|
|
472
|
+
validateAuthorizationContext({ ...input, officialUrl: value });
|
|
473
|
+
return value;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function buildWindowsOnboardingRedirectUrl(platformId) {
|
|
477
|
+
if (typeof platformId !== 'string' || !platformId || /[\u0000-\u001f\u007f,^=]/u.test(platformId)) {
|
|
478
|
+
throw new Error('Windows 产品开通引导缺少合法的当前流程 run_id');
|
|
479
|
+
}
|
|
480
|
+
const url = new URL('https://aipay.alipay.com/');
|
|
481
|
+
url.searchParams.set('from', 'alipay-aipay-skill');
|
|
482
|
+
url.searchParams.set('reason', 'alipay-aipay-skill-onboarding-not-support-windows');
|
|
483
|
+
url.searchParams.set('platform_id', platformId);
|
|
484
|
+
const value = url.toString();
|
|
485
|
+
validateUrl(value, loadJson(policyPath), 'windowsOnboardingRedirect');
|
|
486
|
+
return value;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function validateValue(definition, value, policy) {
|
|
490
|
+
if (value === undefined || value === null || value === '') {
|
|
491
|
+
if (definition.required) throw new Error(`缺少必填变量 ${definition.name}`);
|
|
492
|
+
if (definition.type === 'applicationCandidates' || definition.type === 'serviceCandidates' || definition.type === 'customerErrorDetails') return [];
|
|
493
|
+
return '不在本轮范围';
|
|
494
|
+
}
|
|
495
|
+
if (definition.type === 'applicationCandidates' || definition.type === 'serviceCandidates' || definition.type === 'mccCandidates' || definition.type === 'customerErrorDetails') {
|
|
496
|
+
if (!Array.isArray(value)) throw new Error(`变量 ${definition.name} 必须是候选数组`);
|
|
497
|
+
if (definition.type === 'customerErrorDetails') {
|
|
498
|
+
if (value.length > 100) throw new Error(`变量 ${definition.name} 错误详情数量超过安全上限`);
|
|
499
|
+
return value.map((item) => {
|
|
500
|
+
if (typeof item !== 'string' || !item || item.length > 1000 || /[\r\n]/.test(item)
|
|
501
|
+
|| item.includes('{{') || item.includes('}}') || /<INTERNAL_/i.test(item) || sensitiveValuePattern.test(item)) {
|
|
502
|
+
throw new Error(`变量 ${definition.name} 包含不安全的错误详情`);
|
|
503
|
+
}
|
|
504
|
+
return escapeMarkdownText(item);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return value;
|
|
508
|
+
}
|
|
509
|
+
if (typeof value !== 'string') throw new Error(`变量 ${definition.name} 必须是字符串`);
|
|
510
|
+
if (value.includes('{{') || value.includes('}}') || /<INTERNAL_/i.test(value)) {
|
|
511
|
+
throw new Error(`变量 ${definition.name} 包含禁止内容`);
|
|
512
|
+
}
|
|
513
|
+
if (definition.type !== 'sandboxCredential' && sensitiveValuePattern.test(value)) {
|
|
514
|
+
throw new Error(`变量 ${definition.name} 包含可复用敏感内容`);
|
|
515
|
+
}
|
|
516
|
+
switch (definition.type) {
|
|
517
|
+
case 'enum':
|
|
518
|
+
if (!definition.values.includes(value)) {
|
|
519
|
+
throw new Error(`变量 ${definition.name} 不在允许枚举中;允许值:${definition.values.join('、')}`);
|
|
520
|
+
}
|
|
521
|
+
break;
|
|
522
|
+
case 'enumList': {
|
|
523
|
+
const items = value.split('、');
|
|
524
|
+
if (items.some((item) => !item || !definition.values.includes(item)) || new Set(items).size !== items.length) {
|
|
525
|
+
throw new Error(`变量 ${definition.name} 包含未允许或重复的列表项;允许值:${definition.values.join('、')}`);
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case 'mccCode':
|
|
530
|
+
if (!/^A\d{4}_B\d{4}$/.test(value)) throw new Error(`变量 ${definition.name} 不是合法 MCC 编码`);
|
|
531
|
+
break;
|
|
532
|
+
case 'officialTemporaryUrl':
|
|
533
|
+
validateUrl(value, policy, definition.urlKind);
|
|
534
|
+
break;
|
|
535
|
+
case 'runtimeUrl': {
|
|
536
|
+
if (value.length > 4096 || /[\u0000-\u0020\u007f`()\[\]<>|]/.test(value)) {
|
|
537
|
+
throw new Error(`变量 ${definition.name} 必须是不含空白、控制字符或 Markdown 边界的 URL`);
|
|
538
|
+
}
|
|
539
|
+
let parsed;
|
|
540
|
+
try { parsed = new URL(value); } catch { throw new Error(`变量 ${definition.name} 不是合法 URL`); }
|
|
541
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error(`变量 ${definition.name} 只允许 HTTP(S) URL`);
|
|
542
|
+
if (parsed.username || parsed.password) throw new Error(`变量 ${definition.name} 禁止 URL 用户信息`);
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case 'sandboxCredential':
|
|
546
|
+
if (value.length > 256 || /[\r\n]/.test(value)) throw new Error(`变量 ${definition.name} 不是合法沙箱凭据`);
|
|
547
|
+
break;
|
|
548
|
+
case 'sandboxTableRows':
|
|
549
|
+
if (value.length > 4096 || !value.split('\n').every((line) => /^\| [^|]+ \| [^|]+ \|$/.test(line))) {
|
|
550
|
+
throw new Error(`变量 ${definition.name} 不是合法沙箱摘要行`);
|
|
551
|
+
}
|
|
552
|
+
break;
|
|
553
|
+
case 'existingPath':
|
|
554
|
+
if (value.length > 4096 || /[\u0000-\u001f\u007f`|]/.test(value) || !path.isAbsolute(value) || !fs.existsSync(value)) {
|
|
555
|
+
throw new Error(`变量 ${definition.name} 必须是安全且实际存在的绝对路径`);
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
case 'path':
|
|
559
|
+
if (value.length > 4096 || /[\u0000-\u001f\u007f`|]/.test(value) || !path.isAbsolute(value)) {
|
|
560
|
+
throw new Error(`变量 ${definition.name} 必须是安全绝对路径`);
|
|
561
|
+
}
|
|
562
|
+
break;
|
|
563
|
+
case 'singleLine':
|
|
564
|
+
if ((definition.name !== 'mccQualification' && value.length > 1000)
|
|
565
|
+
|| /[\u0000-\u001f\u007f|`]/.test(value) || /https?:\/\//i.test(value)) {
|
|
566
|
+
throw new Error(`变量 ${definition.name} 必须是不含控制字符、Markdown 代码边界或 URL 的安全单行文本`);
|
|
567
|
+
}
|
|
568
|
+
break;
|
|
569
|
+
case 'command':
|
|
570
|
+
if (value.length > 2000 || /[\u0000-\u001f\u007f`]/.test(value)) {
|
|
571
|
+
throw new Error(`变量 ${definition.name} 必须是安全单行命令`);
|
|
572
|
+
}
|
|
573
|
+
break;
|
|
574
|
+
case 'customerErrorText':
|
|
575
|
+
if (value.length > 1000 || /[\r\n]/.test(value)) throw new Error(`变量 ${definition.name} 不是安全单行错误文本`);
|
|
576
|
+
value = escapeMarkdownText(value);
|
|
577
|
+
break;
|
|
578
|
+
case 'string':
|
|
579
|
+
if (definition.name === 'mccContextReceipt') {
|
|
580
|
+
verifyMccContextReceipt(value);
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
if (value.length > 12000 || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)) {
|
|
584
|
+
throw new Error(`变量 ${definition.name} 包含不允许的控制字符或内容过长`);
|
|
585
|
+
}
|
|
586
|
+
break;
|
|
587
|
+
default:
|
|
588
|
+
throw new Error(`变量 ${definition.name} 使用未知类型 ${definition.type}`);
|
|
589
|
+
}
|
|
590
|
+
return value;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function escapeMarkdownText(value) {
|
|
594
|
+
return value
|
|
595
|
+
.replace(/([\\`\[\]<>])/g, '\\$1')
|
|
596
|
+
.replace(/\|/g, '\\|');
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function appKeySaveLine(appId, savePath) {
|
|
600
|
+
if (typeof savePath !== 'string' || savePath === '') return '';
|
|
601
|
+
if (savePath.length > 1000 || /[\u0000-\u001f\u007f`|]/u.test(savePath) || /https?:\/\//i.test(savePath)) {
|
|
602
|
+
throw new Error('支付宝公钥保存路径必须是安全单行路径');
|
|
603
|
+
}
|
|
604
|
+
return `当前 appId: ${appId} 对应的支付宝公钥保存在:${savePath}`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function candidateText(value, fallback) {
|
|
608
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
609
|
+
if (!['string', 'number', 'boolean'].includes(typeof value)) throw new Error('候选字段必须是可显示的标量');
|
|
610
|
+
const text = String(value);
|
|
611
|
+
if (text.length > 4096 || text.includes('{{') || text.includes('}}') || /<INTERNAL_/i.test(text) || sensitiveValuePattern.test(text)) {
|
|
612
|
+
throw new Error('候选字段包含禁止内容或过长');
|
|
613
|
+
}
|
|
614
|
+
return escapeMarkdownText(text.replace(/[\u0000-\u001f\u007f]/g, ' '));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function validateCandidateKeys(candidate, allowedKeys) {
|
|
618
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) throw new Error('候选项必须是 JSON 对象');
|
|
619
|
+
for (const key of Object.keys(candidate)) {
|
|
620
|
+
if (!allowedKeys.has(key)) throw new Error(`候选项包含未知字段 ${key}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function applicationCandidateTable(variantName, candidates) {
|
|
625
|
+
const allowedKeys = new Set(['appId', 'appName', 'status']);
|
|
626
|
+
for (const candidate of candidates) validateCandidateKeys(candidate, allowedKeys);
|
|
627
|
+
const online = candidates.filter((candidate) => candidate.status === 'ON_LINE');
|
|
628
|
+
const pending = candidates.filter((candidate) => candidate.status !== 'ON_LINE');
|
|
629
|
+
if (variantName === 'EMPTY' && candidates.length !== 0) throw new Error('应用空列表消息与候选事实不一致');
|
|
630
|
+
if (variantName === 'PENDING_ONLY' && (pending.length === 0 || online.length !== 0)) throw new Error('应用未上线列表消息与候选事实不一致');
|
|
631
|
+
if (variantName === 'ONLINE_WITH_OPTIONAL_PENDING' && online.length === 0) throw new Error('应用上线列表消息缺少可复用候选');
|
|
632
|
+
for (const candidate of online) {
|
|
633
|
+
if (typeof candidate.appId !== 'string' || !candidate.appId || /[\u0000-\u001f\u007f]/.test(candidate.appId)) {
|
|
634
|
+
throw new Error('可复用应用候选 appId 缺失或包含控制字符');
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (variantName === 'EMPTY') return '';
|
|
638
|
+
const table = (rows, onlineTable) => [
|
|
639
|
+
onlineTable ? '| 应用ID | 应用名称 | 状态 |' : '| 应用ID | 应用名称 | 实际状态 |',
|
|
640
|
+
onlineTable ? '|--------|----------|------|' : '|--------|----------|----------|',
|
|
641
|
+
...rows.map((candidate) => `| ${candidateText(candidate.appId, '未知')} | ${candidateText(candidate.appName, '未知')} | ${onlineTable ? '已上线' : candidateText(candidate.status, '未知')} |`)
|
|
642
|
+
];
|
|
643
|
+
if (variantName === 'PENDING_ONLY') return table(pending, false).join('\n');
|
|
644
|
+
const lines = table(online, true);
|
|
645
|
+
if (pending.length > 0) {
|
|
646
|
+
lines.push('', '另有以下尚未上线的同类型应用,当前不可复用:', '', ...table(pending, false), '', '新建前请结合以上状态避免重复创建。');
|
|
647
|
+
}
|
|
648
|
+
return lines.join('\n');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function serviceCandidateTable(variantName, candidates) {
|
|
652
|
+
const allowedKeys = new Set(['serviceId', 'serviceName', 'serviceDesc', 'pricing', 'serviceStatus', 'status', 'resourceUrl']);
|
|
653
|
+
for (const candidate of candidates) {
|
|
654
|
+
validateCandidateKeys(candidate, allowedKeys);
|
|
655
|
+
if (typeof candidate.serviceId !== 'string' || !candidate.serviceId || /[\u0000-\u001f\u007f]/.test(candidate.serviceId)) {
|
|
656
|
+
throw new Error('服务候选 serviceId 缺失或包含控制字符');
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (variantName === 'EMPTY' && candidates.length !== 0) throw new Error('服务空列表消息与候选事实不一致');
|
|
660
|
+
if (variantName === 'DEFAULT' && candidates.length === 0) throw new Error('服务候选消息缺少候选事实');
|
|
661
|
+
if (variantName === 'EMPTY') return '';
|
|
662
|
+
const statusDisplay = (candidate) => {
|
|
663
|
+
const status = candidate.serviceStatus ?? candidate.status ?? '未知';
|
|
664
|
+
return { ACTIVE: '已上线', PENDING: '审核中', REJECTED: '审核拒绝' }[status] || candidateText(status, '未知');
|
|
665
|
+
};
|
|
666
|
+
const pricingDisplay = (candidate) => {
|
|
667
|
+
if (candidate.pricing === undefined || candidate.pricing === null || candidate.pricing === '' || candidate.pricing === '未知') {
|
|
668
|
+
return '未知';
|
|
669
|
+
}
|
|
670
|
+
return `${candidateText(candidate.pricing, '未知')}元/次`;
|
|
671
|
+
};
|
|
672
|
+
return [
|
|
673
|
+
'| 服务ID | 服务名称 | 描述 | 价格 | 实际状态 | 服务地址 |',
|
|
674
|
+
'|--------|----------|------|------|----------|----------|',
|
|
675
|
+
...candidates.map((candidate) => `| ${candidateText(candidate.serviceId, '未知')} | ${candidateText(candidate.serviceName, '未知')} | ${candidateText(candidate.serviceDesc, '无描述')} | ${pricingDisplay(candidate)} | ${statusDisplay(candidate)} | ${candidateText(candidate.resourceUrl, '未知')} |`)
|
|
676
|
+
].join('\n');
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function normalizedProductType(value) {
|
|
680
|
+
const raw = String(value || '').toLowerCase();
|
|
681
|
+
return ['aipay', 'webpay', 'apppay'].includes(raw) ? raw : '';
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function mccCandidateTable(variantName, candidates, expectedProductType = '') {
|
|
685
|
+
const allowedKeys = new Set(['mccCode', 'mccName', 'applicableBusinesses', 'qualification', 'needSpecialQual', 'qualificationGroups', 'mccContextReceipt']);
|
|
686
|
+
const productType = normalizedProductType(expectedProductType);
|
|
687
|
+
for (const candidate of candidates) {
|
|
688
|
+
validateCandidateKeys(candidate, allowedKeys);
|
|
689
|
+
if (typeof candidate.mccCode !== 'string' || !/^A\d{4}_B\d{4}$/.test(candidate.mccCode)) {
|
|
690
|
+
throw new Error('MCC 候选 mccCode 缺失或格式无效');
|
|
691
|
+
}
|
|
692
|
+
safeMccDisplayText(candidate.mccName, 'MCC 候选名称');
|
|
693
|
+
safeMccDisplayText(candidate.applicableBusinesses, 'MCC 候选适用商家');
|
|
694
|
+
safeMccDisplayText(candidate.qualification, 'MCC 候选特殊资质');
|
|
695
|
+
normalizeQualificationGroups(candidate.qualificationGroups);
|
|
696
|
+
if (typeof candidate.mccContextReceipt !== 'string' || candidate.mccContextReceipt.length === 0) {
|
|
697
|
+
throw new Error('MCC 候选必须携带本轮上下文回执');
|
|
698
|
+
}
|
|
699
|
+
const receipt = verifyMccContextReceipt(candidate.mccContextReceipt);
|
|
700
|
+
assertCurrentMccRun(receipt);
|
|
701
|
+
const candidateGroups = normalizeQualificationGroups(candidate.qualificationGroups);
|
|
702
|
+
const expectedQualification = mccQualificationSummary({
|
|
703
|
+
needSpecialQual: receipt.need_special_qual,
|
|
704
|
+
qualificationGroups: receipt.qualification_groups
|
|
705
|
+
});
|
|
706
|
+
if (receipt.mcc_code !== candidate.mccCode
|
|
707
|
+
|| receipt.mcc_name !== candidate.mccName
|
|
708
|
+
|| (receipt.mcc_description || '') !== candidate.applicableBusinesses) {
|
|
709
|
+
throw new Error('MCC 候选上下文回执与候选不一致');
|
|
710
|
+
}
|
|
711
|
+
if ((productType && receipt.product_type !== productType)
|
|
712
|
+
|| JSON.stringify(candidateGroups) !== JSON.stringify(receipt.qualification_groups)
|
|
713
|
+
|| candidate.qualification !== expectedQualification) {
|
|
714
|
+
throw new Error('MCC 候选产品或特殊资质与上下文回执不一致');
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (variantName !== 'DEFAULT') throw new Error(`onboarding.mcc.candidates 不支持 variant ${variantName}`);
|
|
718
|
+
if (candidates.length === 0 || candidates.length > 8) throw new Error('MCC 候选数量必须在 1 到 8 之间');
|
|
719
|
+
return [
|
|
720
|
+
'| 序号 | MCC 二级类目 | 适用商家 | 特殊资质 |',
|
|
721
|
+
'|---|---|---|---|',
|
|
722
|
+
...candidates.map((candidate, index) => `| ${index + 1} | ${candidateText(candidate.mccName, '未知')}(\`${candidate.mccCode}\`) | ${candidateText(candidate.applicableBusinesses, '未知')} | ${candidate.qualification === '-' ? '当前查询未返回特殊资质说明' : candidateText(candidate.qualification, '未知')} |`)
|
|
723
|
+
].join('\n');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function hasValue(variables, name) {
|
|
727
|
+
return typeof variables[name] === 'string' && variables[name].length > 0;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function normalizeServicePricing(value) {
|
|
731
|
+
if (/[\r\n\t]/.test(value)) {
|
|
732
|
+
throw new Error('服务单价与 service.sh 校验规则不一致;请使用数字金额,模板会追加“元/次”单位');
|
|
733
|
+
}
|
|
734
|
+
const match = value.match(/^ *([0-9]+(?:\.[0-9]+)?) *(?:元\/次)? *$/u);
|
|
735
|
+
if (!match || Number(match[1]) < 0.01) {
|
|
736
|
+
throw new Error('服务单价与 service.sh 校验规则不一致;请使用数字金额,模板会追加“元/次”单位');
|
|
737
|
+
}
|
|
738
|
+
return match[1];
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function validateServicePayload(variables, normalized) {
|
|
742
|
+
normalized.servicePricing = normalizeServicePricing(variables.servicePricing);
|
|
743
|
+
let schema;
|
|
744
|
+
try { schema = JSON.parse(variables.serviceSchema); } catch { throw new Error('服务请求示例不是合法 JSON'); }
|
|
745
|
+
normalized.serviceSchema = JSON.stringify(schema);
|
|
746
|
+
if (normalized.serviceSchema.includes('```')) throw new Error('服务请求示例包含 Markdown 代码围栏');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function validateServiceCreateSummary(variantName, variables, normalized) {
|
|
750
|
+
if (variantName !== 'DEFAULT') throw new Error(`service.create.summary 不支持 variant ${variantName}`);
|
|
751
|
+
for (const name of ['serviceName', 'serviceDescription', 'serviceUrl', 'servicePricing', 'serviceSchema']) {
|
|
752
|
+
if (!hasValue(variables, name)) throw new Error(`服务创建摘要缺少 ${name}`);
|
|
753
|
+
}
|
|
754
|
+
validateServicePayload(variables, normalized);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function validateIntegrationStart(variantName, variables) {
|
|
758
|
+
const expectedProducts = { AIPAY: 'AI 按量付费', WEBPAY: 'AI 网页应用收款', APPPAY: 'AI 移动应用收款' };
|
|
759
|
+
if (expectedProducts[variantName] !== variables.productName) {
|
|
760
|
+
throw new Error('integration.start.confirm variant 与产品不匹配');
|
|
761
|
+
}
|
|
762
|
+
if (variables.nextFlow !== 'integration') {
|
|
763
|
+
throw new Error('integration.start.confirm 只能进入支付产品-代码开发');
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function validateSandboxEnvironmentRows(variables) {
|
|
768
|
+
const expectedLabels = [
|
|
769
|
+
'沙箱支付宝应用ID(appID)',
|
|
770
|
+
'沙箱买家测试账号',
|
|
771
|
+
'沙箱买家登录密码',
|
|
772
|
+
'沙箱买家支付密码'
|
|
773
|
+
];
|
|
774
|
+
const rows = variables.environmentRows.split('\n').map((line) => {
|
|
775
|
+
const match = /^\| ([^|]+) \| ([^|]+) \|$/u.exec(line);
|
|
776
|
+
return match ? { label: match[1], value: match[2] } : null;
|
|
777
|
+
});
|
|
778
|
+
if (rows.length !== expectedLabels.length
|
|
779
|
+
|| rows.some((row, index) => !row || row.label !== expectedLabels[index] || !row.value.trim())) {
|
|
780
|
+
throw new Error('sandbox.environment.summary 必须按固定顺序传入四项已校验沙箱字段');
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function validateWriteConfirmation(variantName, variables, normalized) {
|
|
785
|
+
const common = ['sessionSummary', 'subjectSummary', 'productName', 'actionTypes'];
|
|
786
|
+
const service = ['serviceId', 'serviceName', 'serviceDescription', 'serviceUrl', 'servicePricing', 'serviceSchema'];
|
|
787
|
+
const definitions = {
|
|
788
|
+
SERVICE_UPDATE_ONLY: { actions: '服务修改', product: 'AI 按量付费', required: ['serviceId', 'serviceName', 'serviceDescription', 'serviceUrl', 'servicePricing', 'serviceSchema'], allowed: [...common, ...service] }
|
|
789
|
+
};
|
|
790
|
+
const rule = definitions[variantName];
|
|
791
|
+
if (!rule) throw new Error(`onboarding.write.confirm 不支持 variant ${variantName}`);
|
|
792
|
+
if (variables.actionTypes !== rule.actions) throw new Error('写确认 variant 与 actionTypes 不匹配');
|
|
793
|
+
if (rule.product && variables.productName !== rule.product) throw new Error('服务写确认只适用于 AI 按量付费');
|
|
794
|
+
for (const name of rule.required) {
|
|
795
|
+
if (!hasValue(variables, name)) throw new Error(`写确认缺少 ${name}`);
|
|
796
|
+
}
|
|
797
|
+
const allowed = new Set(rule.allowed);
|
|
798
|
+
for (const [name, value] of Object.entries(variables)) {
|
|
799
|
+
if (value !== undefined && value !== null && value !== '' && !allowed.has(name)) {
|
|
800
|
+
throw new Error(`写确认包含当前动作不适用的变量 ${name}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
validateServicePayload(variables, normalized);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function productionSpecificLines(productName) {
|
|
808
|
+
if (productName === 'AI 按量付费') {
|
|
809
|
+
return [
|
|
810
|
+
'4. AI 按量付费 `serviceId`:把沙箱 `api_mock_service_id` 替换为真实 `serviceId`,`serviceId` 获取方式是:登录支付宝 AI 付站点(https://aipay.alipay.com/?from=alipay-aipay-skill) → 控制台 → 服务管理 → 复制审核已通过的目标服务 ID。',
|
|
811
|
+
'5. AI 按量付费商户标识:`seller_id`/`seller_unique_id` 填收款商户 PID/2088,PID 获取方式是:登录支付宝商家平台(https://b.alipay.com/page/portal/home) → 点击头像 → 复制账户名下方 2088 开头的商家 ID;`seller_app_id` 填生产 `appId`。',
|
|
812
|
+
'6. AI 按量付费验收:A2M 不使用 `notify_url`;上线前用生产应用私钥重新签名 402 账单,并验证 `Payment-Proof`、金额/订单/资源一致、`trade_no` 防重复履约和 `alipay.aipay.agent.fulfillment.confirm` 异步回执。'
|
|
813
|
+
];
|
|
814
|
+
}
|
|
815
|
+
if (productName === 'AI 移动应用收款') {
|
|
816
|
+
return [
|
|
817
|
+
'4. AI 移动应用收款 `notify_url`:生成生产 `orderStr` 时传入商家已部署的公网 HTTPS `notify_url`;未传不会触发异步通知。',
|
|
818
|
+
'5. AI 移动应用收款验收:`notify_url` 完成验签,校验 `app_id`/`out_trade_no`/`total_amount`/`trade_status`,幂等处理并返回 `success`;支付结果以异步通知或 `alipay.trade.query` 补偿查询为准。客户端 SDK 使用服务端 `orderStr` 调起支付,客户端结果只做展示,不能作为付款成功依据。'
|
|
819
|
+
];
|
|
820
|
+
}
|
|
821
|
+
return [
|
|
822
|
+
'4. AI 网页应用收款 `notify_url`:生产下单时传入商家已部署的公网 HTTPS `notify_url`;该地址是商家后端通知地址,不是支付宝生成的 URL。',
|
|
823
|
+
'5. AI 网页应用收款验收:`notify_url` 完成验签,校验 `app_id`/`out_trade_no`/`total_amount`/`trade_status`,幂等处理并返回 `success`;支付结果以异步通知或 `alipay.trade.query` 补偿查询为准,同步回跳或前台页面不能作为付款成功依据。',
|
|
824
|
+
'6. AI 网页应用收款限制:如需解除 AI 网页应用收款的收款限制,请前往 支付宝商家平台 - 签约管理 补全收款信息:https://b.alipay.com/page/ar-center/ar-manage'
|
|
825
|
+
];
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function productionConfigSourceLines() {
|
|
829
|
+
return [
|
|
830
|
+
'1. 产品开通确认:前往支付宝 AI 付站点查看产品开通情况(https://aipay.alipay.com/?from=alipay-aipay-skill)。',
|
|
831
|
+
'2. 应用上线确认:前往支付宝 AI 付站点“一站式接入 → 选择产品 → 密钥配置”,确认目标支付宝应用“已上线”。',
|
|
832
|
+
'3. 替换项目中支付宝应用配置为生产:把生产环境的支付宝应用配置写入业务系统环境变量、受保护配置文件或密钥管理系统。确保 appId、应用私钥、支付宝公钥同属一套生产环境支付宝应用,同时修改项目中的支付宝网关为生产网关:https://openapi.alipay.com/gateway.do。'
|
|
833
|
+
];
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function productionReadinessBlock(productName, context) {
|
|
837
|
+
const statusLines = [];
|
|
838
|
+
if (!context.productBranchesComplete) {
|
|
839
|
+
statusLines.push('- 还不能正式收款,产品签约、服务市场或应用发布仍有上表待办。');
|
|
840
|
+
}
|
|
841
|
+
if (context.integrationResult === '不在本轮执行范围') {
|
|
842
|
+
statusLines.push('- 本轮只完成产品开通流程收口,不代表业务系统已经具备收款代码;先完成支付产品-代码开发、项目部署和生产参数验收。');
|
|
843
|
+
} else if (context.integrationResult === '部分完成,沙箱配置待完成') {
|
|
844
|
+
statusLines.push('- 代码开发仍有沙箱配置缺口;可继续产品开通流程,但不能进入真实生产收款。');
|
|
845
|
+
} else if (context.integrationResult === '部分完成,人工待验证') {
|
|
846
|
+
statusLines.push('- 代码开发还有人工待验证项;完成后才能判断是否可进入真实生产上线。');
|
|
847
|
+
}
|
|
848
|
+
if (statusLines.length > 0) {
|
|
849
|
+
return [
|
|
850
|
+
'【当前状态】',
|
|
851
|
+
...statusLines,
|
|
852
|
+
'',
|
|
853
|
+
'【上线前有以下关键点】',
|
|
854
|
+
...productionConfigSourceLines(),
|
|
855
|
+
...productionSpecificLines(productName),
|
|
856
|
+
'',
|
|
857
|
+
'【提示】',
|
|
858
|
+
'- 如找不到配置替换位置,可以试试对 Agent 说:“我要上线了,帮我找一下项目里支付宝生产配置应该改哪些地方”。让 Agent 定位文件/变量名,但不要把生产应用私钥、账号密码、支付凭证或未脱敏通知内容直接发给 Agent。'
|
|
859
|
+
].join('\n');
|
|
860
|
+
}
|
|
861
|
+
return [
|
|
862
|
+
'【当前状态】',
|
|
863
|
+
'- 代码开发和产品开通流程已完成,但当前 Agent 没有替你切生产配置,也不会默认发起真实生产交易。',
|
|
864
|
+
'',
|
|
865
|
+
'【上线前有以下关键点】',
|
|
866
|
+
...productionConfigSourceLines(),
|
|
867
|
+
...productionSpecificLines(productName),
|
|
868
|
+
'',
|
|
869
|
+
'【提示】',
|
|
870
|
+
'- 如找不到配置替换位置,可以试试对 Agent 说:“我要上线了,帮我找一下项目里支付宝生产配置应该改哪些地方”。让 Agent 定位文件/变量名,但不要把生产应用私钥、账号密码、支付凭证或未脱敏通知内容直接发给 Agent。'
|
|
871
|
+
].join('\n');
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function normalizeProcessPartialResultSpacing(rendered) {
|
|
875
|
+
const lines = [];
|
|
876
|
+
let previousBlank = false;
|
|
877
|
+
for (const rawLine of rendered.split('\n')) {
|
|
878
|
+
const line = rawLine.trimEnd();
|
|
879
|
+
const blank = line === '';
|
|
880
|
+
if (blank && previousBlank) continue;
|
|
881
|
+
lines.push(line);
|
|
882
|
+
previousBlank = blank;
|
|
883
|
+
}
|
|
884
|
+
return lines.join('\n').trimEnd();
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function validateVariantInputShape(messageId, variantName, variables) {
|
|
888
|
+
if (messageId === 'onboarding.discovery.summary') {
|
|
889
|
+
if (variantName === 'AIPAY') {
|
|
890
|
+
if (!hasValue(variables, 'serviceStatus')) {
|
|
891
|
+
throw new Error('onboarding.discovery.summary/AIPAY 缺少 serviceStatus 字段;允许变量:signingStatus、serviceStatus、applicationStatus');
|
|
892
|
+
}
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (Object.prototype.hasOwnProperty.call(variables, 'serviceStatus')) {
|
|
896
|
+
throw new Error(`onboarding.discovery.summary/${variantName} 禁止传入 serviceStatus 字段;AI 网页应用收款和 AI 移动应用收款的服务市场由模板固定为“无需处理”,请从 JSON 删除 serviceStatus 字段;允许变量:signingStatus、applicationStatus`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const managedEntrypoints = {
|
|
902
|
+
'preview.notice': { command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" preview notice', reason: '预发版启动声明由 preview 托管。', allowedCallers: [] },
|
|
903
|
+
'application.key.methods': { command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" preview key-options', reason: '预发版公钥获取方式由 preview 和应用消息托管。', allowedCallers: [] },
|
|
904
|
+
'application.key.local.opened': { command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" preview open-keygen', reason: '仅受控本地页面 opener 可生成此消息。', allowedCallers: [] },
|
|
905
|
+
'application.key.local.fallback': { command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" preview key-tool-fallback', reason: '本地页面失败降级。', allowedCallers: [] },
|
|
906
|
+
'application.key.tool.manual': { command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" key-tool', reason: '预发版只提供手动下载安装引导。', allowedCallers: [] },
|
|
907
|
+
'product.unsupported': {
|
|
908
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message product-unsupported --product-name <产品名>',
|
|
909
|
+
reason: 'runner 托管,禁手写。'
|
|
910
|
+
},
|
|
911
|
+
'product.clarify': {
|
|
912
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message product-clarify,或代码开发上下文使用同一入口执行 integration-message product-clarify',
|
|
913
|
+
reason: 'runner 托管,禁直调 renderer。'
|
|
914
|
+
},
|
|
915
|
+
'onboarding.enterprise.unsupported': {
|
|
916
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message enterprise-unsupported --variant <ONBOARDING_ONLY|FULL_PROCESS>',
|
|
917
|
+
reason: 'Onboarding runner 托管。'
|
|
918
|
+
},
|
|
919
|
+
'onboarding.windows.redirect': {
|
|
920
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry',
|
|
921
|
+
reason: 'runner 按真实平台输出。'
|
|
922
|
+
},
|
|
923
|
+
'integration.start.confirm': {
|
|
924
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message start-confirm --root-intent <integration_only|full_process> --product-type <aipay|webpay|apppay> --project-path <absPath> --project-selection <CURRENT_PROJECT|OTHER_PROJECT|PREPARED_NEW_PROJECT> --language <language> --framework <framework>',
|
|
925
|
+
reason: '绑定同次项目扫描事实。'
|
|
926
|
+
},
|
|
927
|
+
'integration.checklist.result': {
|
|
928
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message checklist-result --variant <AIPAY|WEBPAY|APPPAY> --project-path <absPath> --scope-receipt <receipt> --overall-result <result> --failed-items <items> --manual-items <items> ...',
|
|
929
|
+
reason: 'runner 校验 scope 并执行最终 audit。'
|
|
930
|
+
},
|
|
931
|
+
'integration.project_candidates.select': {
|
|
932
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message locate-projects --base-path <absDir> --search-input <current|relativeSubdir|absDir> --format message',
|
|
933
|
+
reason: 'runner 轻量定位候选。'
|
|
934
|
+
},
|
|
935
|
+
'onboarding.mcc.clarify': {
|
|
936
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-clarify --product-type <aipay|webpay|apppay> --root-intent <onboarding_only|full_process>',
|
|
937
|
+
reason: 'runner 绑定产品。'
|
|
938
|
+
},
|
|
939
|
+
'onboarding.mcc.no_match': {
|
|
940
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" mcc resolve-plan --query <business-description> --product-type <aipay|webpay|apppay> --root-intent <onboarding_only|full_process>',
|
|
941
|
+
reason: 'mcc.sh 查询当前产品支持的 MCC 树和详情后托管输出无匹配恢复。'
|
|
942
|
+
},
|
|
943
|
+
'onboarding.mcc.candidates': {
|
|
944
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" mcc resolve-plan --query <business-description> --product-type <aipay|webpay|apppay> --root-intent <onboarding_only|full_process>',
|
|
945
|
+
reason: 'mcc.sh 绑定当前产品支持类目、详情、候选快照和特殊资质。'
|
|
946
|
+
},
|
|
947
|
+
'onboarding.discovery.summary': {
|
|
948
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-discovery run --product-type <aipay|webpay|apppay> --sales-code <SALES_CODE>',
|
|
949
|
+
reason: 'runner 执行发现并输出摘要。'
|
|
950
|
+
},
|
|
951
|
+
'onboarding.plan.confirm': {
|
|
952
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" mcc resolve-plan --query <经营描述|MCC编码> --product-type <aipay|webpay|apppay>,或已持有唯一动态 MCC 回执时使用 onboarding-message plan-confirm --root-intent <onboarding_only|full_process> --product-type <...> --mcc-code <...> --mcc-name <...> --mcc-qualification <...> --mcc-context-receipt <...>',
|
|
953
|
+
reason: 'mcc.sh 绑定当前产品支持类目、详情和特殊资质;plan-confirm 只用于已验证唯一事实。'
|
|
954
|
+
},
|
|
955
|
+
'materials.category.collect': {
|
|
956
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message material-collect --category <signing|service|application> --state <INITIAL|PARTIAL|INVALID|APP_WEB_INITIAL|APP_MOBILE_INITIAL> --product-type <aipay|webpay|apppay> [--missing-fields <字段列表>] [--special-qualification-json <qualificationGroups>]',
|
|
957
|
+
reason: 'runner 归一化“签约资料”“AI 网页应用收款签约截图”“mobilePlatform”等别名。'
|
|
958
|
+
},
|
|
959
|
+
'service.create.summary': {
|
|
960
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message service-create-summary --service-name <name> --service-description <desc> --service-url <url> --service-pricing <amount> --service-schema <json>',
|
|
961
|
+
reason: 'runner 托管服务创建摘要。'
|
|
962
|
+
},
|
|
963
|
+
'onboarding.write.confirm': {
|
|
964
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message service-update-confirm --session-summary <summary> --subject-summary <summary> --service-id <id> --service-name <name> --service-description <desc> --service-url <url> --service-pricing <amount> --service-schema <json>',
|
|
965
|
+
reason: 'runner 绑定服务修改摘要。'
|
|
966
|
+
},
|
|
967
|
+
'signing.operation.result': {
|
|
968
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message signing-result --actual-status <status> [--fee-display <fee>] --next-action <action>',
|
|
969
|
+
reason: 'Onboarding runner 托管。'
|
|
970
|
+
},
|
|
971
|
+
'service.operation.result': {
|
|
972
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message service-result --service-id <id> --actual-result <result>',
|
|
973
|
+
reason: 'Onboarding runner 托管。'
|
|
974
|
+
},
|
|
975
|
+
'application.harmony.manual_create': {
|
|
976
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message application-harmony-manual-create',
|
|
977
|
+
reason: 'Onboarding runner 托管。'
|
|
978
|
+
},
|
|
979
|
+
'application.reuse.no_key': {
|
|
980
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message application-reuse-no-key --app-id <appId>',
|
|
981
|
+
reason: 'runner 绑定 appId。'
|
|
982
|
+
},
|
|
983
|
+
'application.operation.result': {
|
|
984
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message application-result --app-id <id> --actual-status <status> [--next-action <action>] [--app-key-save-path <path>]',
|
|
985
|
+
reason: 'Onboarding runner 托管;人工配置下一步目录固定。'
|
|
986
|
+
},
|
|
987
|
+
'application.candidates.facts': {
|
|
988
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message application-candidates --application-type <WEBAPP|MOBILEAPP> --candidates-json <APP_CANDIDATES_JSON> [--selection-state <initial|invalid>]',
|
|
989
|
+
reason: 'runner 绑定候选快照。'
|
|
990
|
+
},
|
|
991
|
+
'application.candidate.select': {
|
|
992
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message application-candidates --application-type <WEBAPP|MOBILEAPP> --candidates-json <APP_CANDIDATES_JSON> [--selection-state <initial|invalid>]',
|
|
993
|
+
reason: 'runner 绑定候选快照。'
|
|
994
|
+
},
|
|
995
|
+
'application.candidate.invalid': {
|
|
996
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message application-candidates --application-type <WEBAPP|MOBILEAPP> --candidates-json <APP_CANDIDATES_JSON> --selection-state invalid',
|
|
997
|
+
reason: 'runner 绑定非空候选快照。'
|
|
998
|
+
},
|
|
999
|
+
'service.candidates.facts': {
|
|
1000
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message service-candidates --candidates-json <SERVICE_CANDIDATES_JSON> [--selection-state <initial|invalid>]',
|
|
1001
|
+
reason: 'runner 绑定服务候选。'
|
|
1002
|
+
},
|
|
1003
|
+
'service.candidate.invalid': {
|
|
1004
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message service-candidates --candidates-json <SERVICE_CANDIDATES_JSON> --selection-state invalid',
|
|
1005
|
+
reason: 'runner 绑定非空服务候选。'
|
|
1006
|
+
},
|
|
1007
|
+
'service.candidate.select': {
|
|
1008
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message service-candidates --candidates-json <SERVICE_CANDIDATES_JSON> [--selection-state <initial|invalid>]',
|
|
1009
|
+
reason: 'runner 绑定服务候选。'
|
|
1010
|
+
},
|
|
1011
|
+
'process.partial_result': {
|
|
1012
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message closeout --product-type <aipay|webpay|apppay> --root-intent <onboarding_only|full_process> --signing-result <result> --application-result <result> --remaining-actions <actions> [--integration-result <result>] [--service-result <result>]',
|
|
1013
|
+
reason: 'runner 合并最终收口,禁手写。'
|
|
1014
|
+
},
|
|
1015
|
+
'sandbox.android.client': {
|
|
1016
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message sandbox-android-client',
|
|
1017
|
+
reason: 'Integration runner 托管。'
|
|
1018
|
+
},
|
|
1019
|
+
'auth.page': {
|
|
1020
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" auth init --defer-open --scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name> --mcc-context-receipt <receipt>,页面 stdout 独立送达后使用同一入口执行 auth open ...',
|
|
1021
|
+
reason: 'auth.sh 绑定上下文和 URL。',
|
|
1022
|
+
allowedCallers: ['auth.sh']
|
|
1023
|
+
},
|
|
1024
|
+
'auth.open.manual': {
|
|
1025
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" auth manual-open',
|
|
1026
|
+
reason: 'auth.sh 托管手动打开提示。',
|
|
1027
|
+
allowedCallers: ['auth.sh']
|
|
1028
|
+
},
|
|
1029
|
+
'auth.pending': {
|
|
1030
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" auth wait|confirm [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name> --mcc-context-receipt <receipt>]',
|
|
1031
|
+
reason: 'auth.sh 实际查询结果。',
|
|
1032
|
+
allowedCallers: ['auth.sh']
|
|
1033
|
+
},
|
|
1034
|
+
'auth.expired': {
|
|
1035
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" auth confirm [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name> --mcc-context-receipt <receipt>]',
|
|
1036
|
+
reason: 'auth.sh 实际查询结果。',
|
|
1037
|
+
allowedCallers: ['auth.sh']
|
|
1038
|
+
},
|
|
1039
|
+
'auth.mismatch': {
|
|
1040
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" auth mismatch --defer-open [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name> --mcc-context-receipt <receipt>],页面 stdout 独立送达后使用同一入口执行 auth open ...',
|
|
1041
|
+
reason: 'auth.sh 托管 mismatch 恢复。',
|
|
1042
|
+
allowedCallers: ['auth.sh']
|
|
1043
|
+
},
|
|
1044
|
+
'error.mcp.auth': {
|
|
1045
|
+
command: '由 onboarding modules/scripts/error_handler.sh 根据实际认证错误和 logout 结果托管输出',
|
|
1046
|
+
reason: 'error_handler.sh 托管。',
|
|
1047
|
+
allowedCallers: ['error_handler.sh']
|
|
1048
|
+
},
|
|
1049
|
+
'error.mcp.service': {
|
|
1050
|
+
command: '由 onboarding modules/scripts/error_handler.sh 根据实际服务错误托管输出',
|
|
1051
|
+
reason: 'error_handler.sh 托管。',
|
|
1052
|
+
allowedCallers: ['error_handler.sh']
|
|
1053
|
+
},
|
|
1054
|
+
'error.backend': {
|
|
1055
|
+
command: '由 onboarding modules/scripts/error_handler.sh 根据实际后端错误托管输出',
|
|
1056
|
+
reason: 'error_handler.sh 托管。',
|
|
1057
|
+
allowedCallers: ['error_handler.sh']
|
|
1058
|
+
},
|
|
1059
|
+
'error.service.unstable': {
|
|
1060
|
+
command: '由 onboarding modules/scripts/error_handler.sh 根据实际服务错误托管输出',
|
|
1061
|
+
reason: 'error_handler.sh 托管。',
|
|
1062
|
+
allowedCallers: ['error_handler.sh']
|
|
1063
|
+
},
|
|
1064
|
+
'error.cli': {
|
|
1065
|
+
command: '由 onboarding modules/scripts/error_handler.sh 根据实际 CLI 错误托管输出',
|
|
1066
|
+
reason: 'error_handler.sh 托管。',
|
|
1067
|
+
allowedCallers: ['error_handler.sh']
|
|
1068
|
+
},
|
|
1069
|
+
'application.key.page': {
|
|
1070
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" app key --product-type <aipay|webpay|apppay> --defer-open <appId> <publicKey>,页面 stdout 独立送达后使用同一入口执行 app open-key-page <appId>',
|
|
1071
|
+
reason: 'app.sh 校验 URL 并托管页面。',
|
|
1072
|
+
allowedCallers: ['app.sh']
|
|
1073
|
+
},
|
|
1074
|
+
'application.key.pending': {
|
|
1075
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" app verify-key --product-type <aipay|webpay|apppay> <appId> [publicKey] 或使用同一入口执行 app verify-key-and-audit --product-type <aipay|webpay|apppay> <appId> [publicKey]',
|
|
1076
|
+
reason: 'app.sh 实际查询结果。',
|
|
1077
|
+
allowedCallers: ['app.sh']
|
|
1078
|
+
},
|
|
1079
|
+
'key_tool.download.result': {
|
|
1080
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" key-tool',
|
|
1081
|
+
reason: '预发版不再下载工具或输出下载成功消息,只提供手动安装引导。',
|
|
1082
|
+
allowedCallers: []
|
|
1083
|
+
},
|
|
1084
|
+
'key_tool.download.fallback': {
|
|
1085
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" key-tool',
|
|
1086
|
+
reason: '预发版不再下载工具,手动安装引导由 preview_runtime.mjs 托管。',
|
|
1087
|
+
allowedCallers: []
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
function publicManagedEntrypoint(messageId) {
|
|
1092
|
+
const managed = managedEntrypoints[messageId];
|
|
1093
|
+
if (!managed) return null;
|
|
1094
|
+
return { command: managed.command, reason: managed.reason };
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function addManagedEntrypointHint(messageId, error) {
|
|
1098
|
+
const managed = managedEntrypoints[messageId];
|
|
1099
|
+
if (!managed) return error;
|
|
1100
|
+
return new Error(`${error.message};该消息运行时应使用托管入口:${managed.command};${managed.reason}`);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function assertManagedCliAllowed(messageId) {
|
|
1104
|
+
const managed = managedEntrypoints[messageId];
|
|
1105
|
+
if (!managed) return;
|
|
1106
|
+
const caller = process.env.ALIPAY_AIPAY_RENDERER_MANAGED_CALLER || '';
|
|
1107
|
+
if (managed.allowedCallers?.includes(caller)) return;
|
|
1108
|
+
throw new Error(`${messageId} 由托管入口输出,禁止直接调用 renderer;请使用托管入口:${managed.command};${managed.reason}`);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export function getMessageSchema(messageId, variantName = 'DEFAULT') {
|
|
1112
|
+
const catalog = loadJson(catalogPath);
|
|
1113
|
+
const message = catalog.messages.find((entry) => entry.messageId === messageId);
|
|
1114
|
+
if (!message) throw new Error(`未知 messageId ${messageId}`);
|
|
1115
|
+
if (message.owner !== 'catalog_owned') throw new Error(`${messageId} 由脚本直接输出,没有 renderer 输入 schema`);
|
|
1116
|
+
const acceptsRuntimeChecklistVariant =
|
|
1117
|
+
messageId === 'integration.checklist.result' &&
|
|
1118
|
+
['AIPAY', 'WEBPAY', 'APPPAY'].includes(variantName);
|
|
1119
|
+
if (!acceptsRuntimeChecklistVariant && !message.variants?.[variantName]) {
|
|
1120
|
+
throw new Error(`${messageId} 不支持 variant ${variantName}`);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
let inputVariables = (message.variables || [])
|
|
1124
|
+
.filter((definition) => !definition.derived)
|
|
1125
|
+
.map(cloneInputVariable);
|
|
1126
|
+
const variantRules = [];
|
|
1127
|
+
|
|
1128
|
+
if (messageId === 'onboarding.discovery.summary') {
|
|
1129
|
+
if (variantName === 'AIPAY') {
|
|
1130
|
+
setVariableRule(inputVariables, 'serviceStatus', { required: true });
|
|
1131
|
+
variantRules.push('AIPAY 必须传入 serviceStatus。');
|
|
1132
|
+
} else if (['WEBPAY', 'APPPAY'].includes(variantName)) {
|
|
1133
|
+
inputVariables = inputVariables.filter((variable) => variable.name !== 'serviceStatus');
|
|
1134
|
+
variantRules.push('WEBPAY/APPPAY 禁止传入 serviceStatus;服务市场由模板固定为“无需处理”。');
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (messageId === 'materials.category.collect') {
|
|
1139
|
+
const expectedStatus = {
|
|
1140
|
+
INITIAL: '待补充',
|
|
1141
|
+
SERVICE_INITIAL: '待补充',
|
|
1142
|
+
APP_WEB_INITIAL: '待补充',
|
|
1143
|
+
APP_MOBILE_INITIAL: '待补充',
|
|
1144
|
+
PARTIAL: '部分已提供,待补充',
|
|
1145
|
+
INVALID: '校验失败,需更正'
|
|
1146
|
+
}[variantName];
|
|
1147
|
+
if (!expectedStatus) throw new Error(`${messageId} 不支持 variant ${variantName}`);
|
|
1148
|
+
setVariableRule(inputVariables, 'currentStatus', { values: [expectedStatus] });
|
|
1149
|
+
variantRules.push(`${variantName} 的 currentStatus 固定为“${expectedStatus}”。`);
|
|
1150
|
+
if (variantName === 'SERVICE_INITIAL') {
|
|
1151
|
+
setVariableRule(inputVariables, 'categoryName', { values: ['服务资料'] });
|
|
1152
|
+
setVariableRule(inputVariables, 'missingFields', { values: ['服务名称、服务描述、服务地址、服务单价、请求示例JSON'] });
|
|
1153
|
+
variantRules.push('SERVICE_INITIAL 只允许 categoryName=服务资料 且收集全部五项服务资料。');
|
|
1154
|
+
} else if (variantName === 'APP_WEB_INITIAL') {
|
|
1155
|
+
setVariableRule(inputVariables, 'categoryName', { values: ['应用资料'] });
|
|
1156
|
+
setVariableRule(inputVariables, 'missingFields', { values: ['应用公钥'] });
|
|
1157
|
+
variantRules.push('APP_WEB_INITIAL 只允许收集应用公钥。');
|
|
1158
|
+
} else if (variantName === 'APP_MOBILE_INITIAL') {
|
|
1159
|
+
setVariableRule(inputVariables, 'categoryName', { values: ['应用资料'] });
|
|
1160
|
+
setVariableRule(inputVariables, 'missingFields', { values: ['应用平台和对应资料、应用公钥'] });
|
|
1161
|
+
variantRules.push('APP_MOBILE_INITIAL 只允许一次收集应用平台对应资料和应用公钥。');
|
|
1162
|
+
} else if (variantName === 'INITIAL') {
|
|
1163
|
+
const missingFields = inputVariables.find((variable) => variable.name === 'missingFields');
|
|
1164
|
+
if (missingFields?.values) missingFields.values = missingFields.values.filter((value) => value !== '应用平台和对应资料');
|
|
1165
|
+
variantRules.push('移动应用首次收集必须使用 APP_MOBILE_INITIAL。');
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (messageId === 'service.create.summary' || messageId === 'onboarding.write.confirm') {
|
|
1170
|
+
setVariableRule(inputVariables, 'servicePricing', {
|
|
1171
|
+
format: '纯数字金额,例如 0.08;模板负责追加“元/次”,不要传入单位。'
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (messageId === 'application.operation.result') {
|
|
1176
|
+
if (variantName === 'ALIPAY_PUBLIC_KEY_MANUAL_CONFIGURATION') {
|
|
1177
|
+
setVariableRule(inputVariables, 'actualStatus', { values: ['需人工配置'] });
|
|
1178
|
+
setVariableRule(inputVariables, 'nextAction', { required: false });
|
|
1179
|
+
setVariableRule(inputVariables, 'appKeySavePath', { required: false });
|
|
1180
|
+
variantRules.push('支付宝公钥本地导出失败使用该变体;下一步由消息目录固定,禁止通过 nextAction 传入 URL 或自由文案。');
|
|
1181
|
+
} else {
|
|
1182
|
+
setVariableRule(inputVariables, 'nextAction', { required: true });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
if (messageId === 'onboarding.write.confirm' && variantName === 'SERVICE_UPDATE_ONLY') {
|
|
1187
|
+
for (const requiredName of ['serviceId', 'serviceName', 'serviceDescription', 'serviceUrl', 'servicePricing', 'serviceSchema']) {
|
|
1188
|
+
setVariableRule(inputVariables, requiredName, { required: true });
|
|
1189
|
+
}
|
|
1190
|
+
setVariableRule(inputVariables, 'productName', { values: ['AI 按量付费'] });
|
|
1191
|
+
setVariableRule(inputVariables, 'actionTypes', { values: ['服务修改'] });
|
|
1192
|
+
variantRules.push('SERVICE_UPDATE_ONLY 只用于 AI 按量付费服务修改;签约提交、服务创建、应用创建和应用提审不得进入该确认消息。');
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
return {
|
|
1196
|
+
schemaVersion: 1,
|
|
1197
|
+
messageId,
|
|
1198
|
+
variant: variantName,
|
|
1199
|
+
owner: message.owner,
|
|
1200
|
+
blocking: Boolean(message.blocking),
|
|
1201
|
+
replyMode: message.replyMode || 'exact',
|
|
1202
|
+
allowedReplies: allowedRepliesFor(catalog, message),
|
|
1203
|
+
inputHintType: message.inputHintType || null,
|
|
1204
|
+
postRenderHintType: message.postRenderHintType || null,
|
|
1205
|
+
managedEntrypoint: publicManagedEntrypoint(messageId),
|
|
1206
|
+
inputVariables,
|
|
1207
|
+
variantRules
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
export function validateCatalog() {
|
|
1212
|
+
const catalog = loadJson(catalogPath);
|
|
1213
|
+
const policy = loadJson(policyPath);
|
|
1214
|
+
const errors = [];
|
|
1215
|
+
const messages = new Map();
|
|
1216
|
+
const policyIds = new Set(policy.rules.map((rule) => rule.policyId));
|
|
1217
|
+
const officialUrls = new Set(Object.values(policy.officialUrls));
|
|
1218
|
+
let manualKeyToolUrl = '';
|
|
1219
|
+
try {
|
|
1220
|
+
manualKeyToolUrl = keyToolManualUrl(policy);
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
errors.push(error.message);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const inputHints = catalog.interactionPrompts || {};
|
|
1226
|
+
if (typeof inputHints.confirmInputHint !== 'string' || !inputHints.confirmInputHint) {
|
|
1227
|
+
errors.push('缺少统一确认输入提示');
|
|
1228
|
+
}
|
|
1229
|
+
if (typeof inputHints.planAdjustmentHint !== 'string' || !inputHints.planAdjustmentHint) {
|
|
1230
|
+
errors.push('缺少统一方案调整提示');
|
|
1231
|
+
}
|
|
1232
|
+
if (typeof inputHints.authorizationCompletionInputHint !== 'string' || !inputHints.authorizationCompletionInputHint) {
|
|
1233
|
+
errors.push('缺少统一授权完成输入提示');
|
|
1234
|
+
}
|
|
1235
|
+
if (typeof inputHints.publicKeyConfirmationCompletionInputHint !== 'string' || !inputHints.publicKeyConfirmationCompletionInputHint) {
|
|
1236
|
+
errors.push('缺少统一应用公钥确认完成输入提示');
|
|
1237
|
+
}
|
|
1238
|
+
if (typeof inputHints.completionInputHint !== 'string' || !inputHints.completionInputHint) {
|
|
1239
|
+
errors.push('缺少统一完成输入提示');
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const productNames = new Set();
|
|
1243
|
+
const salesCodes = new Set();
|
|
1244
|
+
for (const product of policy.authorizationProducts || []) {
|
|
1245
|
+
if (!product.productName || !product.salesCode || !product.scope) errors.push('authorizationProducts 存在不完整映射');
|
|
1246
|
+
if (productNames.has(product.productName)) errors.push(`authorizationProducts 重复 productName: ${product.productName}`);
|
|
1247
|
+
if (salesCodes.has(product.salesCode)) errors.push(`authorizationProducts 重复 salesCode: ${product.salesCode}`);
|
|
1248
|
+
productNames.add(product.productName);
|
|
1249
|
+
salesCodes.add(product.salesCode);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
for (const message of catalog.messages) {
|
|
1253
|
+
let allowedReplies = [];
|
|
1254
|
+
try {
|
|
1255
|
+
allowedReplies = allowedRepliesFor(catalog, message);
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
errors.push(error.message);
|
|
1258
|
+
}
|
|
1259
|
+
if (messages.has(message.messageId)) errors.push(`重复 messageId: ${message.messageId}`);
|
|
1260
|
+
messages.set(message.messageId, message);
|
|
1261
|
+
if (!['catalog_owned', 'script_owned'].includes(message.owner)) errors.push(`${message.messageId}: owner 无效`);
|
|
1262
|
+
if (message.blocking && allowedReplies.length > 0 && !['confirm', 'authorizationComplete', 'publicKeyConfirmationComplete', 'complete'].includes(message.inputHintType)) {
|
|
1263
|
+
errors.push(`${message.messageId}: 阻塞回复消息必须声明合法 inputHintType`);
|
|
1264
|
+
}
|
|
1265
|
+
if (message.postRenderHintType !== undefined && !['planAdjustment', 'scanReturn'].includes(message.postRenderHintType)) {
|
|
1266
|
+
errors.push(`${message.messageId}: postRenderHintType 无效`);
|
|
1267
|
+
}
|
|
1268
|
+
if (message.postRenderHintType && (message.blocking || JSON.stringify(message.allowedReplies || []) !== '[]')) {
|
|
1269
|
+
errors.push(`${message.messageId}: postRenderHintType 只允许用于非阻塞且无回复枚举的消息`);
|
|
1270
|
+
}
|
|
1271
|
+
for (const id of message.requiredPolicyIds || []) {
|
|
1272
|
+
if (!policyIds.has(id)) errors.push(`${message.messageId}: 未知 policyId ${id}`);
|
|
1273
|
+
}
|
|
1274
|
+
const variableNames = new Set((message.variables || []).map((variable) => variable.name));
|
|
1275
|
+
for (const variable of message.variables || []) {
|
|
1276
|
+
if (!allowedVariableTypes.has(variable.type)) errors.push(`${message.messageId}: 未知变量类型 ${variable.type}`);
|
|
1277
|
+
if (variable.derived && variable.required) errors.push(`${message.messageId}: 派生变量不得声明为必填输入`);
|
|
1278
|
+
if (variable.derived && (!Array.isArray(variable.values) || variable.values.length !== 1)) {
|
|
1279
|
+
errors.push(`${message.messageId}: 派生变量必须声明唯一目录值`);
|
|
1280
|
+
}
|
|
1281
|
+
if (sensitiveNamePattern.test(variable.name) && !['officialTemporaryUrl', 'sandboxCredential'].includes(variable.type)) {
|
|
1282
|
+
errors.push(`${message.messageId}: 禁止的敏感变量名 ${variable.name}`);
|
|
1283
|
+
}
|
|
1284
|
+
for (const value of variable.values || []) {
|
|
1285
|
+
if (typeof value !== 'string') continue;
|
|
1286
|
+
for (const match of value.matchAll(/https:\/\/[^\s)`),。;:]+/g)) {
|
|
1287
|
+
if (!officialUrls.has(match[0])) errors.push(`${message.messageId}: 变量 ${variable.name} 静态 URL 未登记 ${match[0]}`);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (message.replyMode && !['exact', 'freeform', 'runtime_enum'].includes(message.replyMode)) errors.push(`${message.messageId}: replyMode 无效`);
|
|
1292
|
+
if (message.owner === 'script_owned') {
|
|
1293
|
+
if (!message.scriptSource || !message.snapshotId || message.variants) {
|
|
1294
|
+
errors.push(`${message.messageId}: script_owned 必须只有 scriptSource/snapshotId`);
|
|
1295
|
+
}
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
if (!message.variants || Object.keys(message.variants).length === 0) {
|
|
1299
|
+
errors.push(`${message.messageId}: catalog_owned 缺少 variants`);
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
for (const [variantName, variant] of Object.entries(message.variants)) {
|
|
1303
|
+
if (!Array.isArray(variant.templateLines)) errors.push(`${message.messageId}/${variantName}: templateLines 必须是数组`);
|
|
1304
|
+
const template = (variant.templateLines || []).join('\n');
|
|
1305
|
+
for (const match of template.matchAll(placeholderPattern)) {
|
|
1306
|
+
if (!variableNames.has(match[1])) errors.push(`${message.messageId}/${variantName}: 未声明变量 ${match[1]}`);
|
|
1307
|
+
}
|
|
1308
|
+
for (const match of template.matchAll(/https:\/\/[^\s)`),。;:]+/g)) {
|
|
1309
|
+
if (!officialUrls.has(match[0])) errors.push(`${message.messageId}/${variantName}: 静态 URL 未登记 ${match[0]}`);
|
|
1310
|
+
}
|
|
1311
|
+
if (manualKeyToolUrl && template.includes(manualKeyToolUrl)) {
|
|
1312
|
+
errors.push(`${message.messageId}/${variantName}: 密钥工具手动地址必须使用 {{keyToolManual}},禁止在消息模板内复制 URL`);
|
|
1313
|
+
}
|
|
1314
|
+
if (/<INTERNAL_/i.test(template)) errors.push(`${message.messageId}/${variantName}: 模板包含内部标签`);
|
|
1315
|
+
if (message.blocking && allowedReplies.length > 0 && /(?:回复|输入)\s*`?1`?/.test(template)) {
|
|
1316
|
+
errors.push(`${message.messageId}/${variantName}: 阻塞输入提示必须由 renderer 统一追加`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
for (const message of catalog.messages) {
|
|
1322
|
+
if (message.fallbackMessageId && !messages.has(message.fallbackMessageId)) {
|
|
1323
|
+
errors.push(`${message.messageId}: fallback 不存在 ${message.fallbackMessageId}`);
|
|
1324
|
+
}
|
|
1325
|
+
const seen = new Set([message.messageId]);
|
|
1326
|
+
let current = message;
|
|
1327
|
+
while (current?.fallbackMessageId) {
|
|
1328
|
+
if (seen.has(current.fallbackMessageId)) {
|
|
1329
|
+
errors.push(`${message.messageId}: fallback 存在环`);
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
seen.add(current.fallbackMessageId);
|
|
1333
|
+
current = messages.get(current.fallbackMessageId);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return errors;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
export function renderMessage(messageId, variantName, variables, telemetryContext = {}) {
|
|
1340
|
+
const catalog = loadJson(catalogPath);
|
|
1341
|
+
const policy = loadJson(policyPath);
|
|
1342
|
+
const message = catalog.messages.find((entry) => entry.messageId === messageId);
|
|
1343
|
+
if (!message) throw new Error(`未知 messageId ${messageId}`);
|
|
1344
|
+
if (message.owner !== 'catalog_owned') throw new Error(`${messageId} 由脚本直接输出,禁止 renderer 复制`);
|
|
1345
|
+
const catalogVariantName = messageId === 'integration.checklist.result'
|
|
1346
|
+
? `${variantName}_${variables.overallResult === '通过' ? 'PASSED' : 'INCOMPLETE'}`
|
|
1347
|
+
: variantName;
|
|
1348
|
+
const variant = message.variants[catalogVariantName];
|
|
1349
|
+
if (!variant) throw new Error(`${messageId} 不支持 variant ${variantName}`);
|
|
1350
|
+
const definitions = new Map(message.variables.map((definition) => [definition.name, definition]));
|
|
1351
|
+
const normalized = {};
|
|
1352
|
+
try {
|
|
1353
|
+
for (const key of Object.keys(variables)) {
|
|
1354
|
+
if (!definitions.has(key)) throw new Error(`出现未声明变量 ${key};允许变量:${allowedVariableNames(definitions)}`);
|
|
1355
|
+
if (definitions.get(key).derived) throw new Error(`派生变量 ${key} 只能由 renderer 生成`);
|
|
1356
|
+
}
|
|
1357
|
+
validateVariantInputShape(messageId, variantName, variables);
|
|
1358
|
+
for (const definition of message.variables) {
|
|
1359
|
+
normalized[definition.name] = definition.derived ? '' : validateValue(definition, variables[definition.name], policy);
|
|
1360
|
+
}
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
throw addManagedEntrypointHint(messageId, error);
|
|
1363
|
+
}
|
|
1364
|
+
if (messageId === 'application.candidates.facts') {
|
|
1365
|
+
normalized.candidateTable = applicationCandidateTable(variantName, variables.applicationCandidates);
|
|
1366
|
+
}
|
|
1367
|
+
if (definitions.has('applicationKeyMethods')) normalized.applicationKeyMethods = renderMessage('application.key.methods', 'DEFAULT', {});
|
|
1368
|
+
if (definitions.has('keyToolManual')) normalized.keyToolManual = keyToolManualUrl(policy);
|
|
1369
|
+
if (definitions.has('keyToolFormatHint')) {
|
|
1370
|
+
normalized.keyToolFormatHint = derivedCatalogText(definitions, 'keyToolFormatHint');
|
|
1371
|
+
}
|
|
1372
|
+
if (messageId === 'service.candidates.facts') {
|
|
1373
|
+
normalized.candidateTable = serviceCandidateTable(variantName, variables.serviceCandidates);
|
|
1374
|
+
}
|
|
1375
|
+
if (messageId === 'onboarding.mcc.candidates') {
|
|
1376
|
+
normalized.candidateTable = mccCandidateTable(variantName, variables.mccCandidates, telemetryContext.productType);
|
|
1377
|
+
}
|
|
1378
|
+
if (messageId === 'application.operation.result') {
|
|
1379
|
+
if (variantName === 'ALIPAY_PUBLIC_KEY_MANUAL_CONFIGURATION') {
|
|
1380
|
+
if (variables.actualStatus !== '需人工配置') {
|
|
1381
|
+
throw new Error('支付宝公钥人工配置结果必须使用 actualStatus=需人工配置');
|
|
1382
|
+
}
|
|
1383
|
+
if (variables.nextAction !== undefined && variables.nextAction !== '') {
|
|
1384
|
+
throw new Error('支付宝公钥人工配置下一步由消息目录固定,禁止传入 nextAction');
|
|
1385
|
+
}
|
|
1386
|
+
if (variables.appKeySavePath !== undefined && variables.appKeySavePath !== '') {
|
|
1387
|
+
throw new Error('需人工配置表示支付宝公钥未本地保存,禁止传入保存路径');
|
|
1388
|
+
}
|
|
1389
|
+
normalized.manualConfigurationNextAction = definitions.get('manualConfigurationNextAction')?.values?.[0] || '';
|
|
1390
|
+
if (!normalized.manualConfigurationNextAction) throw new Error('application.operation.result 缺少支付宝公钥人工配置下一步');
|
|
1391
|
+
} else if (!variables.nextAction) {
|
|
1392
|
+
throw new Error('application.operation.result 默认变体必须传入 nextAction');
|
|
1393
|
+
}
|
|
1394
|
+
if (variables.actualStatus === '已上线且配置完整' && (variables.appKeySavePath === undefined || variables.appKeySavePath === '')) {
|
|
1395
|
+
throw new Error('application.operation.result 在已上线且配置完整时必须传入支付宝公钥保存路径');
|
|
1396
|
+
}
|
|
1397
|
+
normalized.appKeySaveLine = appKeySaveLine(normalized.appId, variables.appKeySavePath);
|
|
1398
|
+
}
|
|
1399
|
+
if (messageId === 'sandbox.environment.summary') validateSandboxEnvironmentRows(variables);
|
|
1400
|
+
if (messageId === 'error.backend') {
|
|
1401
|
+
normalized.errorSceneLine = variables.errorScene || variables.errorSpecific
|
|
1402
|
+
? `📋 错误场景:${variables.errorScene ? normalized.errorScene : '未知'} / ${variables.errorSpecific ? normalized.errorSpecific : '未知'}`
|
|
1403
|
+
: '';
|
|
1404
|
+
normalized.checkedErrorsBlock = normalized.checkedErrors.length > 0
|
|
1405
|
+
? ['📋 详细校验错误:', ...normalized.checkedErrors.map((detail) => ` - ${detail}`)].join('\n')
|
|
1406
|
+
: '';
|
|
1407
|
+
normalized.bizTipsLine = variables.bizTips ? `💡 提示:${normalized.bizTips}` : '';
|
|
1408
|
+
normalized.appMaxLine = variables.appMax === 'YES'
|
|
1409
|
+
? '📌 处理建议:当前主体下应用数量已达到上限,无法继续新建应用。请优先复用已有上线应用;如必须新建,请前往支付宝开放平台处理应用配额或联系支付宝技术支持。'
|
|
1410
|
+
: '';
|
|
1411
|
+
}
|
|
1412
|
+
if (messageId === 'error.cli') {
|
|
1413
|
+
normalized.bizTipsLine = variables.bizTips ? `💡 ${normalized.bizTips}` : '';
|
|
1414
|
+
normalized.checkedErrorsBlock = normalized.checkedErrors.length > 0
|
|
1415
|
+
? ['📋 详细校验错误:', ...normalized.checkedErrors.map((detail) => ` - ${detail}`)].join('\n')
|
|
1416
|
+
: '';
|
|
1417
|
+
}
|
|
1418
|
+
const hasMccInput =
|
|
1419
|
+
variables.mccCode !== undefined ||
|
|
1420
|
+
variables.mccName !== undefined ||
|
|
1421
|
+
variables.mccQualification !== undefined ||
|
|
1422
|
+
variables.mccContextReceipt !== undefined;
|
|
1423
|
+
if (hasMccInput) {
|
|
1424
|
+
if (!definitions.has('mccCode') || !definitions.has('mccName')) throw new Error(`${messageId} 的 MCC 变量声明不完整`);
|
|
1425
|
+
const dynamicReceipt = variables.mccContextReceipt
|
|
1426
|
+
? verifyMccContextReceipt(variables.mccContextReceipt)
|
|
1427
|
+
: null;
|
|
1428
|
+
if (!dynamicReceipt) throw new Error('MCC 消息必须提供本轮 MCC_CONTEXT_RECEIPT');
|
|
1429
|
+
assertCurrentMccRun(dynamicReceipt, process.env, expectedMccRunIdFromContext(telemetryContext));
|
|
1430
|
+
const expectedReceiptProductType = messageId === 'onboarding.plan.confirm'
|
|
1431
|
+
? { AIPAY: 'aipay', WEBPAY: 'webpay', APPPAY: 'apppay' }[variantName]
|
|
1432
|
+
: '';
|
|
1433
|
+
if (expectedReceiptProductType && dynamicReceipt.product_type !== expectedReceiptProductType) {
|
|
1434
|
+
throw new Error('MCC 上下文回执与方案产品不一致');
|
|
1435
|
+
}
|
|
1436
|
+
if (dynamicReceipt.mcc_code !== variables.mccCode || dynamicReceipt.mcc_name !== variables.mccName) {
|
|
1437
|
+
throw new Error('MCC 上下文回执与消息变量不一致');
|
|
1438
|
+
}
|
|
1439
|
+
normalized.mccName = dynamicReceipt.mcc_name;
|
|
1440
|
+
normalized.mccCode = dynamicReceipt.mcc_code;
|
|
1441
|
+
if (definitions.has('mccQualification') && variables.mccQualification !== undefined) {
|
|
1442
|
+
const expectedQualification = mccQualificationSummary({
|
|
1443
|
+
needSpecialQual: dynamicReceipt.need_special_qual,
|
|
1444
|
+
qualificationGroups: dynamicReceipt.qualification_groups
|
|
1445
|
+
});
|
|
1446
|
+
if (variables.mccQualification !== expectedQualification) throw new Error('MCC 特殊资质不匹配动态上下文回执');
|
|
1447
|
+
normalized.mccQualification = expectedQualification;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (messageId === 'auth.page') {
|
|
1451
|
+
const product = authorizationProduct(policy, (entry) => entry.productName === variables.productName);
|
|
1452
|
+
if (!product) throw new Error('授权消息产品不在固定映射中');
|
|
1453
|
+
validateAuthorizationContext({
|
|
1454
|
+
productName: variables.productName,
|
|
1455
|
+
salesCode: product.salesCode,
|
|
1456
|
+
scope: product.scope,
|
|
1457
|
+
mccCode: variables.mccCode,
|
|
1458
|
+
mccName: variables.mccName,
|
|
1459
|
+
deviceCode: variables.deviceCode,
|
|
1460
|
+
officialUrl: variables.officialUrl,
|
|
1461
|
+
mccContextReceipt: variables.mccContextReceipt
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
if (messageId === 'integration.start.confirm') {
|
|
1465
|
+
validateIntegrationStart(variantName, variables);
|
|
1466
|
+
normalized.integrationStatus = {
|
|
1467
|
+
TARGET_PARTIAL: '已发现目标产品相关代码,需要继续核验或补全',
|
|
1468
|
+
OTHER_PRODUCT_ONLY: '未发现目标产品代码,已检测到其他支付能力',
|
|
1469
|
+
NO_PAYMENT: '未发现支付代码'
|
|
1470
|
+
}[variables.integrationStatus];
|
|
1471
|
+
}
|
|
1472
|
+
if (messageId === 'integration.checklist.result') {
|
|
1473
|
+
const sandboxPending = variables.sandboxConfigState !== 'READY';
|
|
1474
|
+
const hasBlockingDefect = variables.blockingDefectState === 'PRESENT';
|
|
1475
|
+
if (variantName === 'AIPAY' && !variables.sandboxResult) throw new Error('AI 按量付费 checklist 缺少沙箱联调结果');
|
|
1476
|
+
if (variantName !== 'AIPAY' && variables.sandboxResult) throw new Error('非 AI 按量付费 checklist 禁止传入 AI 按量付费沙箱结果');
|
|
1477
|
+
if (variantName === 'WEBPAY' && !variables.webpayExperienceResult) throw new Error('AI 网页应用收款 checklist 缺少沙箱体验实际结果');
|
|
1478
|
+
if (variantName !== 'WEBPAY' && variables.webpayExperienceResult) throw new Error('非 AI 网页应用收款 checklist 禁止传入 AI 网页应用收款沙箱体验结果');
|
|
1479
|
+
const hasFailedItems = variables.failedItems !== '无';
|
|
1480
|
+
const hasManualItems = variables.manualItems !== '无';
|
|
1481
|
+
const manualOnly = !hasFailedItems && hasManualItems;
|
|
1482
|
+
const expectedReminder = variables.executionMode === '仅代码开发'
|
|
1483
|
+
? '正式上线前还需完成支付产品-产品开通。'
|
|
1484
|
+
: sandboxPending && variables.overallResult === '部分通过' && !hasBlockingDefect
|
|
1485
|
+
? '沙箱配置尚未完成,但不阻塞本轮继续支付产品-产品开通;完成配置后再进行沙箱测试。'
|
|
1486
|
+
: !sandboxPending && variables.overallResult === '部分通过' && !hasBlockingDefect && manualOnly
|
|
1487
|
+
? '代码开发自动校验已完成,仍有人工待验证项;本轮继续产品开通。'
|
|
1488
|
+
: '代码开发校验通过后将自动进入支付产品-产品开通;未通过时停留在代码开发。';
|
|
1489
|
+
if (variables.nextFlowReminder !== expectedReminder) throw new Error('代码开发执行模式与后续衔接不一致');
|
|
1490
|
+
if (variables.overallResult === '通过' && (hasFailedItems || hasManualItems)) {
|
|
1491
|
+
throw new Error('代码开发结果为通过时不得存在未通过项或人工待验证项');
|
|
1492
|
+
}
|
|
1493
|
+
if (variables.overallResult === '部分通过' && !hasFailedItems && !hasManualItems) {
|
|
1494
|
+
throw new Error('代码开发结果为部分通过时必须存在未通过项或人工待验证项');
|
|
1495
|
+
}
|
|
1496
|
+
if (variables.overallResult === '未通过' && !hasFailedItems) {
|
|
1497
|
+
throw new Error('代码开发结果为未通过时必须列出未通过项');
|
|
1498
|
+
}
|
|
1499
|
+
if (variables.overallResult === '通过' && hasBlockingDefect) {
|
|
1500
|
+
throw new Error('代码开发通过时不得存在其他阻塞缺口');
|
|
1501
|
+
}
|
|
1502
|
+
if (variables.overallResult !== '通过' && !sandboxPending && !hasBlockingDefect && !manualOnly) {
|
|
1503
|
+
throw new Error('沙箱已就绪且没有阻塞缺口时,非通过结果只能来自人工待验证项');
|
|
1504
|
+
}
|
|
1505
|
+
if (variables.overallResult === '未通过' && !hasBlockingDefect) {
|
|
1506
|
+
throw new Error('代码开发未通过时必须登记其他阻塞缺口');
|
|
1507
|
+
}
|
|
1508
|
+
if (sandboxPending && variables.overallResult === '通过') {
|
|
1509
|
+
throw new Error('沙箱配置未完成时不得标记代码开发通过');
|
|
1510
|
+
}
|
|
1511
|
+
if (sandboxPending && !hasFailedItems) {
|
|
1512
|
+
throw new Error('沙箱配置未完成时必须列入未通过或部分通过项');
|
|
1513
|
+
}
|
|
1514
|
+
if (sandboxPending && variantName === 'AIPAY' && variables.sandboxResult !== 'AI 按量付费沙箱服务端联调未通过') {
|
|
1515
|
+
throw new Error('沙箱配置未完成时 AI 按量付费联调不得标记通过');
|
|
1516
|
+
}
|
|
1517
|
+
if (sandboxPending && variantName === 'WEBPAY' && variables.webpayExperienceResult !== '未提供浏览器付款入口或完整操作指引,需先补齐') {
|
|
1518
|
+
throw new Error('沙箱配置未完成时 AI 网页应用收款不得标记已交付付款体验');
|
|
1519
|
+
}
|
|
1520
|
+
if (variantName === 'AIPAY' && variables.overallResult === '通过' && variables.sandboxResult !== 'AI 按量付费沙箱服务端联调通过') {
|
|
1521
|
+
throw new Error('AI 按量付费代码开发通过必须有沙箱服务端联调通过结论');
|
|
1522
|
+
}
|
|
1523
|
+
if (variantName === 'WEBPAY' && variables.overallResult === '通过' && variables.webpayExperienceResult === '未提供浏览器付款入口或完整操作指引,需先补齐') {
|
|
1524
|
+
throw new Error('AI 网页应用收款未交付付款入口时不得标记代码开发通过');
|
|
1525
|
+
}
|
|
1526
|
+
if (manualOnly) {
|
|
1527
|
+
normalized.failedItems = `人工待验证项未完成:${normalized.manualItems}`;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
if (messageId === 'materials.category.collect') {
|
|
1531
|
+
const expectedStatus = {
|
|
1532
|
+
INITIAL: '待补充',
|
|
1533
|
+
SERVICE_INITIAL: '待补充',
|
|
1534
|
+
APP_WEB_INITIAL: '待补充',
|
|
1535
|
+
APP_MOBILE_INITIAL: '待补充',
|
|
1536
|
+
PARTIAL: '部分已提供,待补充',
|
|
1537
|
+
INVALID: '校验失败,需更正'
|
|
1538
|
+
}[variantName];
|
|
1539
|
+
if (!expectedStatus || variables.currentStatus !== expectedStatus) {
|
|
1540
|
+
throw new Error('materials.category.collect variant 与材料状态不匹配');
|
|
1541
|
+
}
|
|
1542
|
+
const isServiceInitial = variantName === 'SERVICE_INITIAL';
|
|
1543
|
+
if (isServiceInitial && (
|
|
1544
|
+
variables.categoryName !== '服务资料'
|
|
1545
|
+
|| variables.missingFields !== '服务名称、服务描述、服务地址、服务单价、请求示例JSON'
|
|
1546
|
+
)) {
|
|
1547
|
+
throw new Error('SERVICE_INITIAL 只允许收集全部五项服务资料');
|
|
1548
|
+
}
|
|
1549
|
+
const isWebInitial = variantName === 'APP_WEB_INITIAL';
|
|
1550
|
+
if (isWebInitial && (variables.categoryName !== '应用资料' || variables.missingFields !== '应用公钥')) {
|
|
1551
|
+
throw new Error('APP_WEB_INITIAL 只允许收集应用公钥');
|
|
1552
|
+
}
|
|
1553
|
+
const isMobileInitial = variantName === 'APP_MOBILE_INITIAL';
|
|
1554
|
+
if (isMobileInitial && (variables.categoryName !== '应用资料' || variables.missingFields !== '应用平台和对应资料、应用公钥')) {
|
|
1555
|
+
throw new Error('APP_MOBILE_INITIAL 只允许一次收集应用平台对应资料和应用公钥');
|
|
1556
|
+
}
|
|
1557
|
+
if (variantName === 'INITIAL' && variables.missingFields.split('、').includes('应用平台和对应资料')) {
|
|
1558
|
+
throw new Error('移动应用首次收集必须使用 APP_MOBILE_INITIAL');
|
|
1559
|
+
}
|
|
1560
|
+
normalized.missingFields = normalized.missingFields.replace(
|
|
1561
|
+
'应用平台和对应资料',
|
|
1562
|
+
'平台字段(IOS=bundleId;ANDROID=appPackage+appSign;ALL=bundleId+appPackage+appSign)'
|
|
1563
|
+
);
|
|
1564
|
+
normalized.paymentPageHint = variables.missingFields.split('、').includes('支付页截图')
|
|
1565
|
+
? definitions.get('paymentPageHint')?.values?.[0] || ''
|
|
1566
|
+
: '';
|
|
1567
|
+
normalized.serviceUrlHint = variantName === 'INVALID'
|
|
1568
|
+
&& variables.categoryName === '服务资料'
|
|
1569
|
+
&& variables.missingFields.split('、').includes('服务地址')
|
|
1570
|
+
? definitions.get('serviceUrlHint')?.values?.[0] || ''
|
|
1571
|
+
: '';
|
|
1572
|
+
normalized.servicePricingHint = variantName === 'INVALID'
|
|
1573
|
+
&& variables.categoryName === '服务资料'
|
|
1574
|
+
&& variables.missingFields.split('、').includes('服务单价')
|
|
1575
|
+
? definitions.get('servicePricingHint')?.values?.[0] || ''
|
|
1576
|
+
: '';
|
|
1577
|
+
normalized.specialQualificationHint = variables.specialQualificationHint || '';
|
|
1578
|
+
}
|
|
1579
|
+
if (messageId === 'process.partial_result') {
|
|
1580
|
+
const statusQueryHintDefinition = definitions.get('statusQueryHint');
|
|
1581
|
+
const statusQueryHint = statusQueryHintDefinition?.values?.[0];
|
|
1582
|
+
if (!statusQueryHintDefinition?.derived || !statusQueryHint) {
|
|
1583
|
+
throw new Error('process.partial_result 缺少受控状态查询提示');
|
|
1584
|
+
}
|
|
1585
|
+
normalized.statusQueryHint =
|
|
1586
|
+
variables.signingResult === '已提交,等待生效' && variables.applicationResult === '已提交审核,等待上线'
|
|
1587
|
+
? statusQueryHint
|
|
1588
|
+
: '';
|
|
1589
|
+
const onboardingOnly = variables.integrationResult === '不在本轮执行范围';
|
|
1590
|
+
const onboardingReminder = '还需完成支付产品-代码开发,才能在业务系统中实际发起支付。';
|
|
1591
|
+
if (onboardingOnly !== (variables.nextFlowReminder === onboardingReminder)) {
|
|
1592
|
+
throw new Error('process.partial_result 的代码开发结果与后续衔接不一致');
|
|
1593
|
+
}
|
|
1594
|
+
normalized.nextFlowReminderLine = onboardingOnly ? `后续衔接:${normalized.nextFlowReminder}` : '';
|
|
1595
|
+
const hasManualVerificationItems =
|
|
1596
|
+
typeof variables.manualVerificationItems === 'string' &&
|
|
1597
|
+
variables.manualVerificationItems !== '' &&
|
|
1598
|
+
variables.manualVerificationItems !== '无';
|
|
1599
|
+
if (onboardingOnly) {
|
|
1600
|
+
if (variables.sandboxConfigState !== undefined) throw new Error('onboarding_only 禁止传入代码开发沙箱状态');
|
|
1601
|
+
if (hasManualVerificationItems) throw new Error('onboarding_only 禁止传入代码开发人工待验证项');
|
|
1602
|
+
normalized.manualVerificationLine = '';
|
|
1603
|
+
normalized.sandboxRecoveryHint = '';
|
|
1604
|
+
} else if (variables.integrationResult === '已完成') {
|
|
1605
|
+
if (variables.sandboxConfigState !== 'READY') throw new Error('完整代码开发结果必须绑定 READY 沙箱状态');
|
|
1606
|
+
if (hasManualVerificationItems) throw new Error('代码开发已完成时禁止传入人工待验证项');
|
|
1607
|
+
normalized.manualVerificationLine = '';
|
|
1608
|
+
normalized.sandboxRecoveryHint = '';
|
|
1609
|
+
} else if (variables.integrationResult === '部分完成,沙箱配置待完成') {
|
|
1610
|
+
if (hasManualVerificationItems) throw new Error('沙箱待配置结果不通过人工待验证项表达');
|
|
1611
|
+
normalized.sandboxRecoveryHint = {
|
|
1612
|
+
CREATE_PENDING: '沙箱配置待完成:请对 Agent 说:“重新创建并配置沙箱”。',
|
|
1613
|
+
VERIFY_PENDING: '沙箱配置待完成:请对 Agent 说:“重新创建并配置沙箱”。'
|
|
1614
|
+
}[variables.sandboxConfigState];
|
|
1615
|
+
if (!normalized.sandboxRecoveryHint) throw new Error('沙箱待配置结果必须绑定 CREATE_PENDING 或 VERIFY_PENDING');
|
|
1616
|
+
if (variables.remainingActions === '无') throw new Error('沙箱待配置时必须保留剩余待办');
|
|
1617
|
+
normalized.manualVerificationLine = '';
|
|
1618
|
+
} else if (variables.integrationResult === '部分完成,人工待验证') {
|
|
1619
|
+
if (variables.sandboxConfigState !== 'READY') throw new Error('人工待验证结果必须绑定 READY 沙箱状态');
|
|
1620
|
+
if (!hasManualVerificationItems) throw new Error('人工待验证结果必须列出人工待验证项');
|
|
1621
|
+
normalized.manualVerificationLine = `人工待验证:${normalized.manualVerificationItems}`;
|
|
1622
|
+
normalized.sandboxRecoveryHint = '';
|
|
1623
|
+
} else {
|
|
1624
|
+
throw new Error('full_process 进入产品开通收口时只允许已完成、沙箱待配置或人工待验证');
|
|
1625
|
+
}
|
|
1626
|
+
if (variables.productName === 'AI 按量付费' && variables.serviceResult === '无需处理') {
|
|
1627
|
+
throw new Error('AI 按量付费产品开通不能把服务市场标记为无需处理');
|
|
1628
|
+
}
|
|
1629
|
+
if (variables.productName !== 'AI 按量付费' && variables.serviceResult !== '无需处理') {
|
|
1630
|
+
throw new Error('AI 网页应用收款或 AI 移动应用收款的服务市场结果必须为无需处理');
|
|
1631
|
+
}
|
|
1632
|
+
const expectedServiceComplete = variables.productName === 'AI 按量付费'
|
|
1633
|
+
? ['已复用', '创建成功,状态未取得', '修改成功,状态未取得', '已完成'].includes(variables.serviceResult)
|
|
1634
|
+
: variables.serviceResult === '无需处理';
|
|
1635
|
+
const productBranchesComplete =
|
|
1636
|
+
variables.signingResult === '已生效' &&
|
|
1637
|
+
expectedServiceComplete &&
|
|
1638
|
+
variables.applicationResult === '已上线且配置完整';
|
|
1639
|
+
const branchesComplete =
|
|
1640
|
+
productBranchesComplete &&
|
|
1641
|
+
variables.remainingActions === '无' &&
|
|
1642
|
+
!hasManualVerificationItems;
|
|
1643
|
+
normalized.productionReadinessBlock = productionReadinessBlock(variables.productName, {
|
|
1644
|
+
productBranchesComplete,
|
|
1645
|
+
integrationResult: variables.integrationResult
|
|
1646
|
+
});
|
|
1647
|
+
const declaresComplete = variables.processResultTitle === '支付产品-产品开通已完成';
|
|
1648
|
+
if (branchesComplete !== declaresComplete) {
|
|
1649
|
+
throw new Error('process.partial_result 的总结果与分支完成条件不一致');
|
|
1650
|
+
}
|
|
1651
|
+
if (!branchesComplete && variables.remainingActions === '无') {
|
|
1652
|
+
throw new Error('process.partial_result 存在未完成分支时必须列出剩余待办');
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
if (messageId === 'service.create.summary') validateServiceCreateSummary(variantName, variables, normalized);
|
|
1656
|
+
if (messageId === 'onboarding.write.confirm') validateWriteConfirmation(variantName, variables, normalized);
|
|
1657
|
+
let rendered = variant.templateLines.join('\n').replace(placeholderPattern, (_, name) => normalized[name]);
|
|
1658
|
+
if (messageId === 'application.operation.result' || messageId === 'process.partial_result' || messageId === 'error.backend' || messageId === 'error.cli') {
|
|
1659
|
+
rendered = normalizeProcessPartialResultSpacing(rendered);
|
|
1660
|
+
}
|
|
1661
|
+
if (message.blocking && allowedRepliesFor(catalog, message).length > 0) {
|
|
1662
|
+
const hintKeys = {
|
|
1663
|
+
confirm: 'confirmInputHint',
|
|
1664
|
+
authorizationComplete: 'authorizationCompletionInputHint',
|
|
1665
|
+
publicKeyConfirmationComplete: 'publicKeyConfirmationCompletionInputHint',
|
|
1666
|
+
complete: 'completionInputHint'
|
|
1667
|
+
};
|
|
1668
|
+
const hintKey = hintKeys[message.inputHintType] || '';
|
|
1669
|
+
if (!hintKey) throw new Error(`${messageId} 缺少合法 inputHintType`);
|
|
1670
|
+
const hint = catalog.interactionPrompts?.[hintKey];
|
|
1671
|
+
if (!hint) throw new Error(`缺少 ${hintKey}`);
|
|
1672
|
+
rendered = `${rendered}\n\n${hint}`;
|
|
1673
|
+
}
|
|
1674
|
+
if (message.postRenderHintType) {
|
|
1675
|
+
const hintKeys = {
|
|
1676
|
+
planAdjustment: 'planAdjustmentHint',
|
|
1677
|
+
scanReturn: 'scanReturnHint'
|
|
1678
|
+
};
|
|
1679
|
+
const hintKey = hintKeys[message.postRenderHintType];
|
|
1680
|
+
if (!hintKey) throw new Error(`${messageId} 的 postRenderHintType 非法`);
|
|
1681
|
+
const hint = catalog.interactionPrompts?.[hintKey];
|
|
1682
|
+
if (!hint) throw new Error(`缺少 ${hintKey}`);
|
|
1683
|
+
rendered = `${rendered}\n\n${hint}`;
|
|
1684
|
+
}
|
|
1685
|
+
emitMessageMetric(messageId, variantName, variables, telemetryContext);
|
|
1686
|
+
return rendered;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
export function renderMessageWithFallback(messageId, variantName, variables) {
|
|
1690
|
+
const catalog = loadJson(catalogPath);
|
|
1691
|
+
const messages = new Map(catalog.messages.map((entry) => [entry.messageId, entry]));
|
|
1692
|
+
let currentId = messageId;
|
|
1693
|
+
let currentVariant = variantName;
|
|
1694
|
+
let isFallback = false;
|
|
1695
|
+
const visited = new Set();
|
|
1696
|
+
let lastError;
|
|
1697
|
+
while (currentId) {
|
|
1698
|
+
if (visited.has(currentId)) throw new Error(`fallback 存在环: ${currentId}`);
|
|
1699
|
+
visited.add(currentId);
|
|
1700
|
+
try {
|
|
1701
|
+
const message = messages.get(currentId);
|
|
1702
|
+
if (!message) throw new Error(`未知 messageId ${currentId}`);
|
|
1703
|
+
const allowed = new Set((message.variables || []).map((definition) => definition.name));
|
|
1704
|
+
const scopedVariables = isFallback
|
|
1705
|
+
? Object.fromEntries(Object.entries(variables).filter(([key]) => allowed.has(key)))
|
|
1706
|
+
: variables;
|
|
1707
|
+
return renderMessage(currentId, currentVariant, scopedVariables);
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
lastError = error;
|
|
1710
|
+
const fallbackId = messages.get(currentId)?.fallbackMessageId;
|
|
1711
|
+
if (!fallbackId) throw lastError;
|
|
1712
|
+
currentId = fallbackId;
|
|
1713
|
+
currentVariant = 'DEFAULT';
|
|
1714
|
+
isFallback = true;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
throw lastError || new Error('消息无法渲染且没有可执行 fallback');
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
async function readStdin() {
|
|
1721
|
+
let input = '';
|
|
1722
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
1723
|
+
return input.trim() ? JSON.parse(input) : {};
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function inputJsonArg(argv = process.argv) {
|
|
1727
|
+
const index = argv.indexOf('--input-json');
|
|
1728
|
+
if (index < 0) return null;
|
|
1729
|
+
const value = argv[index + 1];
|
|
1730
|
+
if (value === undefined || value.startsWith('--')) throw new Error('参数 --input-json 缺少取值');
|
|
1731
|
+
try {
|
|
1732
|
+
return JSON.parse(value);
|
|
1733
|
+
} catch {
|
|
1734
|
+
throw new Error('--input-json 必须是合法 JSON 对象');
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
export async function runRenderCustomerMessageCli(argv = process.argv) {
|
|
1739
|
+
if (argv.includes('--check-catalog')) {
|
|
1740
|
+
const errors = validateCatalog();
|
|
1741
|
+
if (errors.length) throw new Error(errors.join('\n'));
|
|
1742
|
+
console.log('customer message catalog: OK');
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
if (argv.includes('--validate-authorization-context')) {
|
|
1746
|
+
validateAuthorizationContext(await readStdin());
|
|
1747
|
+
console.log('authorization context: OK');
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
if (argv.includes('--validate-special-license-request')) {
|
|
1751
|
+
validateSpecialLicenseUploadRequest(await readStdin());
|
|
1752
|
+
console.log('special license request: OK');
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (argv.includes('--resolve-special-license-types')) {
|
|
1756
|
+
console.log(JSON.stringify(specialLicenseTypesForUploadRequest(await readStdin())));
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (argv.includes('--validate-signing-special-license-context')) {
|
|
1760
|
+
validateSigningSpecialLicenseContext(await readStdin());
|
|
1761
|
+
console.log('signing special license context: OK');
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (argv.includes('--build-authorization-url')) {
|
|
1765
|
+
console.log(buildAuthorizationUrl(await readStdin()));
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
if (argv.includes('--schema')) {
|
|
1769
|
+
const schemaIndex = argv.indexOf('--schema');
|
|
1770
|
+
const messageId = argv[2] === '--schema' ? argv[schemaIndex + 1] : argv[2];
|
|
1771
|
+
const variantIndex = argv.indexOf('--variant');
|
|
1772
|
+
const variant = variantIndex >= 0 ? argv[variantIndex + 1] : 'DEFAULT';
|
|
1773
|
+
if (!messageId || messageId.startsWith('--')) throw new Error('用法: render_customer_message.mjs --schema <messageId> [--variant NAME]');
|
|
1774
|
+
console.log(JSON.stringify(getMessageSchema(messageId, variant), null, 2));
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
const messageId = argv[2];
|
|
1778
|
+
const variantIndex = argv.indexOf('--variant');
|
|
1779
|
+
const variant = variantIndex >= 0 ? argv[variantIndex + 1] : 'DEFAULT';
|
|
1780
|
+
if (!messageId) throw new Error('用法: render_customer_message.mjs <messageId> [--variant NAME]');
|
|
1781
|
+
assertManagedCliAllowed(messageId);
|
|
1782
|
+
const variables = inputJsonArg(argv) ?? await readStdin();
|
|
1783
|
+
console.log(renderMessageWithFallback(messageId, variant, variables));
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
1787
|
+
runRenderCustomerMessageCli().catch((error) => {
|
|
1788
|
+
console.error('当前步骤的标准消息无法安全生成,已停止后续动作。请按 MESSAGE_RENDER_ERROR 的具体原因修正输入;若消息目录或脚本缺失,再检查 Skill 文件完整性。');
|
|
1789
|
+
console.error(`MESSAGE_RENDER_ERROR:${error.message}`);
|
|
1790
|
+
process.exitCode = 1;
|
|
1791
|
+
});
|
|
1792
|
+
}
|