@alipay/alipay-aipay 1.2.2 → 1.4.9
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 +5 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +13 -12
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +17 -9
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +5 -5
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/utils/config.d.ts +5 -3
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +34 -12
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/file.d.ts +8 -0
- package/dist/utils/file.d.ts.map +1 -1
- package/dist/utils/file.js +161 -9
- package/dist/utils/file.js.map +1 -1
- package/dist/utils/jq.d.ts.map +1 -1
- package/dist/utils/jq.js +8 -0
- package/dist/utils/jq.js.map +1 -1
- package/package.json +6 -5
- package/skill/SKILL.md +15 -169
- package/skill/VERSION +1 -1
- package/skill/references/integration/flow.md +321 -292
- package/skill/references/integration/modules/alipay-sdk-reminder.md +5 -3
- package/skill/references/integration/modules/checklist.md +41 -27
- package/skill/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +199 -0
- package/skill/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 +8 -4
- package/skill/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 +6 -2
- package/skill/references/integration/modules/code-examples/csharp/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.cs +3 -1
- package/skill/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +184 -0
- package/skill/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 +4 -1
- package/skill/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 +5 -2
- package/skill/references/integration/modules/code-examples/java/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemoController.java +3 -1
- package/skill/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +135 -0
- package/skill/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 +16 -10
- package/skill/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 +10 -4
- package/skill/references/integration/modules/code-examples/nodejs/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.js +2 -1
- package/skill/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +157 -0
- package/skill/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 +4 -1
- package/skill/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 +5 -2
- package/skill/references/integration/modules/code-examples/php/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.php +2 -1
- package/skill/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +128 -0
- package/skill/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 +4 -1
- package/skill/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 +5 -1
- package/skill/references/integration/modules/code-examples/python/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.py +4 -2
- package/skill/references/integration/modules/interface-contract/aipay-interface-contract.md +53 -0
- package/skill/references/integration/modules/interface-contract/apppay-interface-contract.md +45 -0
- package/skill/references/integration/modules/interface-contract/common-trade-contract.md +42 -0
- package/skill/references/integration/modules/interface-contract/notify-url-contract.md +68 -0
- package/skill/references/integration/modules/interface-contract/webpay-interface-contract.md +44 -0
- package/skill/references/integration/modules/interface-guide.md +37 -72
- package/skill/references/integration/modules/product-decision.md +10 -27
- package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +62 -94
- package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +43 -40
- package/skill/references/integration/modules/sandbox/sandbox-setup-guide.md +93 -114
- package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +357 -147
- package/skill/references/integration/modules/scripts/sandbox_config.sh +299 -0
- package/skill/references/integration/modules/troubleshooting-flow.md +28 -0
- package/skill/references/normal/alipay-cli-env.md +15 -5
- package/skill/references/normal/full-process-routing.md +80 -0
- package/skill/references/normal/json/customer-messages.json +937 -0
- package/skill/references/normal/json/policy-rules.json +179 -0
- package/skill/references/normal/rejection-guide.md +11 -6
- package/skill/references/normal/scripts/alipay_cli_env.mjs +29 -0
- package/skill/references/normal/scripts/alipay_cli_env.sh +21 -0
- package/skill/references/normal/scripts/common.sh +87 -0
- package/skill/references/normal/scripts/detect_dev_tool.sh +5 -1
- package/skill/references/normal/scripts/integration_message_runner.mjs +801 -0
- package/skill/references/normal/scripts/onboarding_message_runner.mjs +436 -0
- package/skill/references/normal/scripts/open_official_url.sh +82 -0
- package/skill/references/normal/scripts/platform_compat.mjs +39 -0
- package/skill/references/normal/scripts/project_route_inspector.sh +132 -0
- package/skill/references/normal/scripts/python_script_runner.mjs +56 -0
- package/skill/references/normal/scripts/render_customer_message.mjs +1263 -0
- package/skill/references/normal/scripts/self_update.mjs +160 -0
- package/skill/references/normal/scripts/telemetry.mjs +1278 -0
- package/skill/references/normal/self-update.md +18 -51
- package/skill/references/onboarding/flow.md +381 -283
- package/skill/references/onboarding/modules/app-release.md +132 -179
- package/skill/references/onboarding/modules/authorization.md +53 -64
- package/skill/references/onboarding/modules/error-handling.md +31 -34
- package/skill/references/onboarding/modules/product-sign.md +62 -66
- package/skill/references/onboarding/modules/scripts/app.sh +397 -158
- package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +38 -22
- package/skill/references/onboarding/modules/scripts/auth.sh +479 -167
- package/skill/references/onboarding/modules/scripts/download_key_tool.sh +157 -0
- package/skill/references/onboarding/modules/scripts/error_handler.sh +100 -130
- package/skill/references/onboarding/modules/scripts/network_retry.sh +96 -0
- package/skill/references/onboarding/modules/scripts/query_sign_status.sh +17 -70
- package/skill/references/onboarding/modules/scripts/service.sh +153 -89
- package/skill/references/onboarding/modules/scripts/sign_status_parser.sh +58 -0
- package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +16 -14
- package/skill/references/onboarding/modules/service-registration.md +67 -72
- package/skill/references/runtime-entry.md +62 -0
|
@@ -0,0 +1,1263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { emitMessageMetric } from './telemetry.mjs';
|
|
8
|
+
|
|
9
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const normalDir = path.resolve(scriptDir, '..');
|
|
11
|
+
const jsonDir = path.join(normalDir, 'json');
|
|
12
|
+
const catalogPath = path.join(jsonDir, 'customer-messages.json');
|
|
13
|
+
const policyPath = path.join(jsonDir, 'policy-rules.json');
|
|
14
|
+
const mccReferencePath = path.join(normalDir, '../onboarding/modules/mcc-reference.md');
|
|
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
|
+
export function loadMccRecords() {
|
|
55
|
+
const records = new Map();
|
|
56
|
+
let primaryCode = '';
|
|
57
|
+
for (const line of fs.readFileSync(mccReferencePath, 'utf8').split(/\r?\n/)) {
|
|
58
|
+
const heading = line.match(/一级类目 code:\s*\*\*(A\d{4})\*\*/);
|
|
59
|
+
if (heading) {
|
|
60
|
+
primaryCode = heading[1];
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!primaryCode || !/^\|.*\|$/.test(line)) continue;
|
|
64
|
+
const cells = line.split('|').slice(1, -1).map((cell) => cell.trim());
|
|
65
|
+
if (cells.length < 4 || !/^B\d{4}$/.test(cells[1])) continue;
|
|
66
|
+
const code = `${primaryCode}_${cells[1]}`;
|
|
67
|
+
records.set(code, { code, name: cells[0], applicableBusinesses: cells[2], qualification: cells[3] });
|
|
68
|
+
}
|
|
69
|
+
return records;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function authorizationProduct(policy, predicate) {
|
|
73
|
+
const products = policy.authorizationProducts || [];
|
|
74
|
+
return products.find(predicate);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateUrl(value, policy, urlKind, expected = {}) {
|
|
78
|
+
if (/[\u0000-\u0020\u007f`()\[\]<>|]/.test(value)) {
|
|
79
|
+
throw new Error('officialTemporaryUrl 包含不允许的 Markdown 边界');
|
|
80
|
+
}
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = new URL(value);
|
|
84
|
+
} catch {
|
|
85
|
+
throw new Error('officialTemporaryUrl 不是合法 URL');
|
|
86
|
+
}
|
|
87
|
+
const allowed = policy.temporaryUrlAllowlist[urlKind];
|
|
88
|
+
if (!allowed || parsed.protocol !== 'https:' || parsed.hostname !== allowed.host || parsed.pathname !== allowed.path || parsed.username || parsed.password || parsed.port || parsed.hash) {
|
|
89
|
+
throw new Error(`officialTemporaryUrl 不符合 ${urlKind} allowlist`);
|
|
90
|
+
}
|
|
91
|
+
if (urlKind === 'authorization') {
|
|
92
|
+
const required = allowed.requiredQueryParams || ['deviceCode', 'productCode', 'mccCode'];
|
|
93
|
+
const optional = allowed.optionalQueryParams || [];
|
|
94
|
+
const allowedKeys = new Set([...required, ...optional]);
|
|
95
|
+
const actualKeys = [...parsed.searchParams.keys()];
|
|
96
|
+
if (actualKeys.some((key) => !allowedKeys.has(key))) throw new Error('authorization URL 包含未允许的查询参数');
|
|
97
|
+
for (const key of required) {
|
|
98
|
+
if (parsed.searchParams.getAll(key).length !== 1 || !parsed.searchParams.get(key)) {
|
|
99
|
+
throw new Error('authorization URL 缺少必要查询参数或参数重复');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const key of optional) {
|
|
103
|
+
if (parsed.searchParams.getAll(key).length > 1) throw new Error('authorization URL 可选参数重复');
|
|
104
|
+
}
|
|
105
|
+
const productCode = parsed.searchParams.get('productCode');
|
|
106
|
+
if (!authorizationProduct(policy, (product) => product.salesCode === productCode)) {
|
|
107
|
+
throw new Error('authorization URL productCode 不是受支持产品');
|
|
108
|
+
}
|
|
109
|
+
if (!/^A\d{4}_B\d{4}$/.test(parsed.searchParams.get('mccCode'))) {
|
|
110
|
+
throw new Error('authorization URL mccCode 格式无效');
|
|
111
|
+
}
|
|
112
|
+
for (const [key, expectedValue] of Object.entries(expected)) {
|
|
113
|
+
if (expectedValue !== undefined && parsed.searchParams.get(key) !== expectedValue) {
|
|
114
|
+
throw new Error(`authorization URL ${key} 与当前授权上下文不一致`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (urlKind === 'publicKeyConfirmation') {
|
|
119
|
+
const required = allowed.requiredQueryParams || ['keyConfirmToken'];
|
|
120
|
+
const optional = allowed.optionalQueryParams || [];
|
|
121
|
+
const allowedKeys = new Set([...required, ...optional]);
|
|
122
|
+
const actualKeys = [...parsed.searchParams.keys()];
|
|
123
|
+
if (actualKeys.some((key) => !allowedKeys.has(key))) throw new Error('publicKeyConfirmation URL 包含未允许的查询参数');
|
|
124
|
+
for (const key of required) {
|
|
125
|
+
if (parsed.searchParams.getAll(key).length !== 1 || !parsed.searchParams.get(key)) {
|
|
126
|
+
throw new Error('publicKeyConfirmation URL 缺少必要查询参数或参数重复');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const key of optional) {
|
|
130
|
+
if (parsed.searchParams.getAll(key).length > 1) throw new Error('publicKeyConfirmation URL 可选参数重复');
|
|
131
|
+
}
|
|
132
|
+
if (/[\r\n]/.test(parsed.searchParams.get('keyConfirmToken'))) throw new Error('publicKeyConfirmation URL keyConfirmToken 无效');
|
|
133
|
+
}
|
|
134
|
+
return parsed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function validateAuthorizationContext(input) {
|
|
138
|
+
const policy = loadJson(policyPath);
|
|
139
|
+
const required = ['productName', 'salesCode', 'scope', 'mccCode', 'mccName'];
|
|
140
|
+
for (const key of required) {
|
|
141
|
+
if (typeof input[key] !== 'string' || !input[key]) throw new Error(`授权上下文缺少 ${key}`);
|
|
142
|
+
}
|
|
143
|
+
const product = authorizationProduct(policy, (entry) => entry.salesCode === input.salesCode);
|
|
144
|
+
if (!product || product.productName !== input.productName || product.scope !== input.scope) {
|
|
145
|
+
throw new Error('产品名称、salesCode 和 scope 不属于同一固定产品映射');
|
|
146
|
+
}
|
|
147
|
+
const record = loadMccRecords().get(input.mccCode);
|
|
148
|
+
if (!record || record.name !== input.mccName) throw new Error('MCC 名称和编码不匹配当前参考表');
|
|
149
|
+
if (input.deviceCode !== undefined && (typeof input.deviceCode !== 'string' || !input.deviceCode || /[\r\n]/.test(input.deviceCode))) {
|
|
150
|
+
throw new Error('本次授权 deviceCode 无效');
|
|
151
|
+
}
|
|
152
|
+
if (input.officialUrl !== undefined) {
|
|
153
|
+
if (typeof input.deviceCode !== 'string' || !input.deviceCode || /[\r\n]/.test(input.deviceCode)) {
|
|
154
|
+
throw new Error('本次授权 deviceCode 无效');
|
|
155
|
+
}
|
|
156
|
+
if (typeof input.officialUrl !== 'string' || !input.officialUrl) throw new Error('缺少本次授权 URL');
|
|
157
|
+
validateUrl(input.officialUrl, policy, 'authorization', {
|
|
158
|
+
deviceCode: input.deviceCode,
|
|
159
|
+
productCode: input.salesCode,
|
|
160
|
+
mccCode: input.mccCode,
|
|
161
|
+
...(input.platform ? { platform: input.platform } : {})
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return { product, record };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function buildAuthorizationUrl(input) {
|
|
168
|
+
validateAuthorizationContext(input);
|
|
169
|
+
if (typeof input.deviceCode !== 'string' || !input.deviceCode || /[\r\n]/.test(input.deviceCode)) {
|
|
170
|
+
throw new Error('本次授权 deviceCode 无效');
|
|
171
|
+
}
|
|
172
|
+
if (input.platform !== undefined && (typeof input.platform !== 'string' || /[\r\n]/.test(input.platform))) {
|
|
173
|
+
throw new Error('授权 platform 无效');
|
|
174
|
+
}
|
|
175
|
+
const url = new URL('https://aipay.alipay.com/cli-auth');
|
|
176
|
+
url.searchParams.set('deviceCode', input.deviceCode);
|
|
177
|
+
url.searchParams.set('productCode', input.salesCode);
|
|
178
|
+
url.searchParams.set('mccCode', input.mccCode);
|
|
179
|
+
if (input.platform && input.platform !== 'unknown') url.searchParams.set('platform', input.platform);
|
|
180
|
+
const value = url.toString();
|
|
181
|
+
validateAuthorizationContext({ ...input, officialUrl: value });
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function validateValue(definition, value, policy) {
|
|
186
|
+
if (value === undefined || value === null || value === '') {
|
|
187
|
+
if (definition.required) throw new Error(`缺少必填变量 ${definition.name}`);
|
|
188
|
+
if (definition.type === 'applicationCandidates' || definition.type === 'serviceCandidates' || definition.type === 'customerErrorDetails') return [];
|
|
189
|
+
return '不在本轮范围';
|
|
190
|
+
}
|
|
191
|
+
if (definition.type === 'applicationCandidates' || definition.type === 'serviceCandidates' || definition.type === 'mccCandidates' || definition.type === 'customerErrorDetails') {
|
|
192
|
+
if (!Array.isArray(value)) throw new Error(`变量 ${definition.name} 必须是候选数组`);
|
|
193
|
+
if (definition.type === 'customerErrorDetails') {
|
|
194
|
+
if (value.length > 100) throw new Error(`变量 ${definition.name} 错误详情数量超过安全上限`);
|
|
195
|
+
return value.map((item) => {
|
|
196
|
+
if (typeof item !== 'string' || !item || item.length > 1000 || /[\r\n]/.test(item)
|
|
197
|
+
|| item.includes('{{') || item.includes('}}') || /<INTERNAL_/i.test(item) || sensitiveValuePattern.test(item)) {
|
|
198
|
+
throw new Error(`变量 ${definition.name} 包含不安全的错误详情`);
|
|
199
|
+
}
|
|
200
|
+
return escapeMarkdownText(item);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
if (typeof value !== 'string') throw new Error(`变量 ${definition.name} 必须是字符串`);
|
|
206
|
+
if (value.includes('{{') || value.includes('}}') || /<INTERNAL_/i.test(value)) {
|
|
207
|
+
throw new Error(`变量 ${definition.name} 包含禁止内容`);
|
|
208
|
+
}
|
|
209
|
+
if (definition.type !== 'sandboxCredential' && sensitiveValuePattern.test(value)) {
|
|
210
|
+
throw new Error(`变量 ${definition.name} 包含可复用敏感内容`);
|
|
211
|
+
}
|
|
212
|
+
switch (definition.type) {
|
|
213
|
+
case 'enum':
|
|
214
|
+
if (!definition.values.includes(value)) {
|
|
215
|
+
throw new Error(`变量 ${definition.name} 不在允许枚举中;允许值:${definition.values.join('、')}`);
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
case 'enumList': {
|
|
219
|
+
const items = value.split('、');
|
|
220
|
+
if (items.some((item) => !item || !definition.values.includes(item)) || new Set(items).size !== items.length) {
|
|
221
|
+
throw new Error(`变量 ${definition.name} 包含未允许或重复的列表项;允许值:${definition.values.join('、')}`);
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case 'mccCode':
|
|
226
|
+
if (!/^A\d{4}_B\d{4}$/.test(value)) throw new Error(`变量 ${definition.name} 不是合法 MCC 编码`);
|
|
227
|
+
break;
|
|
228
|
+
case 'officialTemporaryUrl':
|
|
229
|
+
validateUrl(value, policy, definition.urlKind);
|
|
230
|
+
break;
|
|
231
|
+
case 'runtimeUrl': {
|
|
232
|
+
if (value.length > 4096 || /[\u0000-\u0020\u007f`()\[\]<>|]/.test(value)) {
|
|
233
|
+
throw new Error(`变量 ${definition.name} 必须是不含空白、控制字符或 Markdown 边界的 URL`);
|
|
234
|
+
}
|
|
235
|
+
let parsed;
|
|
236
|
+
try { parsed = new URL(value); } catch { throw new Error(`变量 ${definition.name} 不是合法 URL`); }
|
|
237
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error(`变量 ${definition.name} 只允许 HTTP(S) URL`);
|
|
238
|
+
if (parsed.username || parsed.password) throw new Error(`变量 ${definition.name} 禁止 URL 用户信息`);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case 'sandboxCredential':
|
|
242
|
+
if (value.length > 256 || /[\r\n]/.test(value)) throw new Error(`变量 ${definition.name} 不是合法沙箱凭据`);
|
|
243
|
+
break;
|
|
244
|
+
case 'sandboxTableRows':
|
|
245
|
+
if (value.length > 4096 || !value.split('\n').every((line) => /^\| [^|]+ \| [^|]+ \|$/.test(line))) {
|
|
246
|
+
throw new Error(`变量 ${definition.name} 不是合法沙箱摘要行`);
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
case 'existingPath':
|
|
250
|
+
if (value.length > 4096 || /[\u0000-\u001f\u007f`|]/.test(value) || !path.isAbsolute(value) || !fs.existsSync(value)) {
|
|
251
|
+
throw new Error(`变量 ${definition.name} 必须是安全且实际存在的绝对路径`);
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
case 'path':
|
|
255
|
+
if (value.length > 4096 || /[\u0000-\u001f\u007f`|]/.test(value) || !path.isAbsolute(value)) {
|
|
256
|
+
throw new Error(`变量 ${definition.name} 必须是安全绝对路径`);
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
case 'singleLine':
|
|
260
|
+
if (value.length > 1000 || /[\u0000-\u001f\u007f|`]/.test(value) || /https?:\/\//i.test(value)) {
|
|
261
|
+
throw new Error(`变量 ${definition.name} 必须是不含控制字符、Markdown 代码边界或 URL 的安全单行文本`);
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
case 'command':
|
|
265
|
+
if (value.length > 2000 || /[\u0000-\u001f\u007f`]/.test(value)) {
|
|
266
|
+
throw new Error(`变量 ${definition.name} 必须是安全单行命令`);
|
|
267
|
+
}
|
|
268
|
+
break;
|
|
269
|
+
case 'customerErrorText':
|
|
270
|
+
if (value.length > 1000 || /[\r\n]/.test(value)) throw new Error(`变量 ${definition.name} 不是安全单行错误文本`);
|
|
271
|
+
value = escapeMarkdownText(value);
|
|
272
|
+
break;
|
|
273
|
+
case 'string':
|
|
274
|
+
if (value.length > 12000 || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)) {
|
|
275
|
+
throw new Error(`变量 ${definition.name} 包含不允许的控制字符或内容过长`);
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
default:
|
|
279
|
+
throw new Error(`变量 ${definition.name} 使用未知类型 ${definition.type}`);
|
|
280
|
+
}
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function escapeMarkdownText(value) {
|
|
285
|
+
return value
|
|
286
|
+
.replace(/([\\`\[\]<>])/g, '\\$1')
|
|
287
|
+
.replace(/\|/g, '\\|');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function candidateText(value, fallback) {
|
|
291
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
292
|
+
if (!['string', 'number', 'boolean'].includes(typeof value)) throw new Error('候选字段必须是可显示的标量');
|
|
293
|
+
const text = String(value);
|
|
294
|
+
if (text.length > 4096 || text.includes('{{') || text.includes('}}') || /<INTERNAL_/i.test(text) || sensitiveValuePattern.test(text)) {
|
|
295
|
+
throw new Error('候选字段包含禁止内容或过长');
|
|
296
|
+
}
|
|
297
|
+
return escapeMarkdownText(text.replace(/[\u0000-\u001f\u007f]/g, ' '));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function validateCandidateKeys(candidate, allowedKeys) {
|
|
301
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) throw new Error('候选项必须是 JSON 对象');
|
|
302
|
+
for (const key of Object.keys(candidate)) {
|
|
303
|
+
if (!allowedKeys.has(key)) throw new Error(`候选项包含未知字段 ${key}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function applicationCandidateTable(variantName, candidates) {
|
|
308
|
+
const allowedKeys = new Set(['appId', 'appName', 'status']);
|
|
309
|
+
for (const candidate of candidates) validateCandidateKeys(candidate, allowedKeys);
|
|
310
|
+
const online = candidates.filter((candidate) => candidate.status === 'ON_LINE');
|
|
311
|
+
const pending = candidates.filter((candidate) => candidate.status !== 'ON_LINE');
|
|
312
|
+
if (variantName === 'EMPTY' && candidates.length !== 0) throw new Error('应用空列表消息与候选事实不一致');
|
|
313
|
+
if (variantName === 'PENDING_ONLY' && (pending.length === 0 || online.length !== 0)) throw new Error('应用未上线列表消息与候选事实不一致');
|
|
314
|
+
if (variantName === 'ONLINE_WITH_OPTIONAL_PENDING' && online.length === 0) throw new Error('应用上线列表消息缺少可复用候选');
|
|
315
|
+
for (const candidate of online) {
|
|
316
|
+
if (typeof candidate.appId !== 'string' || !candidate.appId || /[\u0000-\u001f\u007f]/.test(candidate.appId)) {
|
|
317
|
+
throw new Error('可复用应用候选 appId 缺失或包含控制字符');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (variantName === 'EMPTY') return '';
|
|
321
|
+
const table = (rows, onlineTable) => [
|
|
322
|
+
onlineTable ? '| 应用ID | 应用名称 | 状态 |' : '| 应用ID | 应用名称 | 实际状态 |',
|
|
323
|
+
onlineTable ? '|--------|----------|------|' : '|--------|----------|----------|',
|
|
324
|
+
...rows.map((candidate) => `| ${candidateText(candidate.appId, '未知')} | ${candidateText(candidate.appName, '未知')} | ${onlineTable ? '已上线' : candidateText(candidate.status, '未知')} |`)
|
|
325
|
+
];
|
|
326
|
+
if (variantName === 'PENDING_ONLY') return table(pending, false).join('\n');
|
|
327
|
+
const lines = table(online, true);
|
|
328
|
+
if (pending.length > 0) {
|
|
329
|
+
lines.push('', '另有以下尚未上线的同类型应用,当前不可复用:', '', ...table(pending, false), '', '新建前请结合以上状态避免重复创建。');
|
|
330
|
+
}
|
|
331
|
+
return lines.join('\n');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function serviceCandidateTable(variantName, candidates) {
|
|
335
|
+
const allowedKeys = new Set(['serviceId', 'serviceName', 'serviceDesc', 'pricing', 'serviceStatus', 'status', 'resourceUrl']);
|
|
336
|
+
for (const candidate of candidates) {
|
|
337
|
+
validateCandidateKeys(candidate, allowedKeys);
|
|
338
|
+
if (typeof candidate.serviceId !== 'string' || !candidate.serviceId || /[\u0000-\u001f\u007f]/.test(candidate.serviceId)) {
|
|
339
|
+
throw new Error('服务候选 serviceId 缺失或包含控制字符');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (variantName === 'EMPTY' && candidates.length !== 0) throw new Error('服务空列表消息与候选事实不一致');
|
|
343
|
+
if (variantName === 'DEFAULT' && candidates.length === 0) throw new Error('服务候选消息缺少候选事实');
|
|
344
|
+
if (variantName === 'EMPTY') return '';
|
|
345
|
+
const statusDisplay = (candidate) => {
|
|
346
|
+
const status = candidate.serviceStatus ?? candidate.status ?? '未知';
|
|
347
|
+
return { ACTIVE: '已上线', PENDING: '审核中', REJECTED: '审核拒绝' }[status] || candidateText(status, '未知');
|
|
348
|
+
};
|
|
349
|
+
const pricingDisplay = (candidate) => {
|
|
350
|
+
if (candidate.pricing === undefined || candidate.pricing === null || candidate.pricing === '' || candidate.pricing === '未知') {
|
|
351
|
+
return '未知';
|
|
352
|
+
}
|
|
353
|
+
return `${candidateText(candidate.pricing, '未知')}元/次`;
|
|
354
|
+
};
|
|
355
|
+
return [
|
|
356
|
+
'| 服务ID | 服务名称 | 描述 | 价格 | 实际状态 | 服务地址 |',
|
|
357
|
+
'|--------|----------|------|------|----------|----------|',
|
|
358
|
+
...candidates.map((candidate) => `| ${candidateText(candidate.serviceId, '未知')} | ${candidateText(candidate.serviceName, '未知')} | ${candidateText(candidate.serviceDesc, '无描述')} | ${pricingDisplay(candidate)} | ${statusDisplay(candidate)} | ${candidateText(candidate.resourceUrl, '未知')} |`)
|
|
359
|
+
].join('\n');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function mccCandidateTable(variantName, candidates) {
|
|
363
|
+
const allowedKeys = new Set(['mccCode', 'mccName', 'applicableBusinesses', 'qualification']);
|
|
364
|
+
for (const candidate of candidates) {
|
|
365
|
+
validateCandidateKeys(candidate, allowedKeys);
|
|
366
|
+
if (typeof candidate.mccCode !== 'string' || !/^A\d{4}_B\d{4}$/.test(candidate.mccCode)) {
|
|
367
|
+
throw new Error('MCC 候选 mccCode 缺失或格式无效');
|
|
368
|
+
}
|
|
369
|
+
const record = loadMccRecords().get(candidate.mccCode);
|
|
370
|
+
if (!record || record.name !== candidate.mccName || record.applicableBusinesses !== candidate.applicableBusinesses || record.qualification !== candidate.qualification) {
|
|
371
|
+
throw new Error('MCC 候选与当前参考表不一致');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (variantName !== 'DEFAULT') throw new Error(`onboarding.mcc.candidates 不支持 variant ${variantName}`);
|
|
375
|
+
if (candidates.length === 0 || candidates.length > 8) throw new Error('MCC 候选数量必须在 1 到 8 之间');
|
|
376
|
+
return [
|
|
377
|
+
'| 序号 | MCC 二级类目 | 适用商家 | 特殊资质 |',
|
|
378
|
+
'|---|---|---|---|',
|
|
379
|
+
...candidates.map((candidate, index) => `| ${index + 1} | ${candidateText(candidate.mccName, '未知')}(\`${candidate.mccCode}\`) | ${candidateText(candidate.applicableBusinesses, '未知')} | ${candidate.qualification === '-' ? '当前类目参考表未列出' : candidateText(candidate.qualification, '未知')} |`)
|
|
380
|
+
].join('\n');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function hasValue(variables, name) {
|
|
384
|
+
return typeof variables[name] === 'string' && variables[name].length > 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function normalizeServicePricing(value) {
|
|
388
|
+
if (/[\r\n\t]/.test(value)) {
|
|
389
|
+
throw new Error('服务单价与 service.sh 校验规则不一致;请使用数字金额,模板会追加“元/次”单位');
|
|
390
|
+
}
|
|
391
|
+
const match = value.match(/^ *([0-9]+(?:\.[0-9]+)?) *(?:元\/次)? *$/u);
|
|
392
|
+
if (!match || Number(match[1]) < 0.01) {
|
|
393
|
+
throw new Error('服务单价与 service.sh 校验规则不一致;请使用数字金额,模板会追加“元/次”单位');
|
|
394
|
+
}
|
|
395
|
+
return match[1];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function validateServicePayload(variables, normalized) {
|
|
399
|
+
normalized.servicePricing = normalizeServicePricing(variables.servicePricing);
|
|
400
|
+
let schema;
|
|
401
|
+
try { schema = JSON.parse(variables.serviceSchema); } catch { throw new Error('服务请求示例不是合法 JSON'); }
|
|
402
|
+
normalized.serviceSchema = JSON.stringify(schema);
|
|
403
|
+
if (normalized.serviceSchema.includes('```')) throw new Error('服务请求示例包含 Markdown 代码围栏');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function validateServiceCreateSummary(variantName, variables, normalized) {
|
|
407
|
+
if (variantName !== 'DEFAULT') throw new Error(`service.create.summary 不支持 variant ${variantName}`);
|
|
408
|
+
for (const name of ['serviceName', 'serviceDescription', 'serviceUrl', 'servicePricing', 'serviceSchema']) {
|
|
409
|
+
if (!hasValue(variables, name)) throw new Error(`服务创建摘要缺少 ${name}`);
|
|
410
|
+
}
|
|
411
|
+
validateServicePayload(variables, normalized);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function validateIntegrationStart(variantName, variables) {
|
|
415
|
+
const expectedProducts = { AIPAY: '按量付费', WEBPAY: '网站支付', APPPAY: 'APP 支付' };
|
|
416
|
+
if (expectedProducts[variantName] !== variables.productName) {
|
|
417
|
+
throw new Error('integration.start.confirm variant 与产品不匹配');
|
|
418
|
+
}
|
|
419
|
+
if (variables.nextFlow !== 'integration') {
|
|
420
|
+
throw new Error('integration.start.confirm 只能进入支付产品-代码开发');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function validateWriteConfirmation(variantName, variables, normalized) {
|
|
425
|
+
const common = ['sessionSummary', 'subjectSummary', 'productName', 'actionTypes'];
|
|
426
|
+
const service = ['serviceId', 'serviceName', 'serviceDescription', 'serviceUrl', 'servicePricing', 'serviceSchema'];
|
|
427
|
+
const definitions = {
|
|
428
|
+
SERVICE_UPDATE_ONLY: { actions: '服务修改', product: '按量付费', required: ['serviceId', 'serviceName', 'serviceDescription', 'serviceUrl', 'servicePricing', 'serviceSchema'], allowed: [...common, ...service] }
|
|
429
|
+
};
|
|
430
|
+
const rule = definitions[variantName];
|
|
431
|
+
if (!rule) throw new Error(`onboarding.write.confirm 不支持 variant ${variantName}`);
|
|
432
|
+
if (variables.actionTypes !== rule.actions) throw new Error('写确认 variant 与 actionTypes 不匹配');
|
|
433
|
+
if (rule.product && variables.productName !== rule.product) throw new Error('服务写确认只适用于按量付费');
|
|
434
|
+
for (const name of rule.required) {
|
|
435
|
+
if (!hasValue(variables, name)) throw new Error(`写确认缺少 ${name}`);
|
|
436
|
+
}
|
|
437
|
+
const allowed = new Set(rule.allowed);
|
|
438
|
+
for (const [name, value] of Object.entries(variables)) {
|
|
439
|
+
if (value !== undefined && value !== null && value !== '' && !allowed.has(name)) {
|
|
440
|
+
throw new Error(`写确认包含当前动作不适用的变量 ${name}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
validateServicePayload(variables, normalized);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function productionSpecificLines(productName) {
|
|
448
|
+
if (productName === '按量付费') {
|
|
449
|
+
return [
|
|
450
|
+
'3. 按量付费 `serviceId`:把沙箱 `api_mock_service_id` 替换为真实 `serviceId`,`serviceId` 获取方式是:登录支付宝 AI 付站点(https://aipay.alipay.com/?from=alipay-aipay-skill) → 控制台 → 服务管理 → 复制审核已通过的目标服务 ID。',
|
|
451
|
+
'4. 按量付费商户标识:`seller_id`/`seller_unique_id` 填收款商户 PID/2088,PID 获取方式是:登录支付宝商家平台(https://b.alipay.com/page/portal/home) → 点击头像 → 复制账户名下方 2088 开头的商家 ID;`seller_app_id` 填生产 `appId`。',
|
|
452
|
+
'5. 按量付费验收:A2M 不使用 `notify_url`;上线前用生产应用私钥重新签名 402 账单,并验证 `Payment-Proof`、金额/订单/资源一致、`trade_no` 防重复履约和 `alipay.aipay.agent.fulfillment.confirm` 异步回执。'
|
|
453
|
+
];
|
|
454
|
+
}
|
|
455
|
+
if (productName === 'APP 支付') {
|
|
456
|
+
return [
|
|
457
|
+
'3. APP 支付 `notify_url`:生成生产 `orderStr` 时传入商家已部署的公网 HTTPS `notify_url`;未传不会触发异步通知。',
|
|
458
|
+
'4. APP 支付验收:`notify_url` 完成验签,校验 `app_id`/`out_trade_no`/`total_amount`/`trade_status`,幂等处理并返回 `success`;支付结果以异步通知或 `alipay.trade.query` 补偿查询为准。客户端 SDK 使用服务端 `orderStr` 调起支付,客户端结果只做展示,不能作为付款成功依据。'
|
|
459
|
+
];
|
|
460
|
+
}
|
|
461
|
+
return [
|
|
462
|
+
'3. 网站支付 `notify_url`:生产下单时传入商家已部署的公网 HTTPS `notify_url`;该地址是商家后端通知地址,不是支付宝生成的 URL。',
|
|
463
|
+
'4. 网站支付验收:`notify_url` 完成验签,校验 `app_id`/`out_trade_no`/`total_amount`/`trade_status`,幂等处理并返回 `success`;支付结果以异步通知或 `alipay.trade.query` 补偿查询为准,同步回跳或前台页面不能作为付款成功依据。',
|
|
464
|
+
'5. 网站支付收款限制:如需解除网站支付的收款限制,请前往 [支付宝商家平台 - 签约管理](https://b.alipay.com/page/ar-center/ar-manage) 补全网站支付信息。'
|
|
465
|
+
];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function productionConfigSourceLines() {
|
|
469
|
+
return [
|
|
470
|
+
'1. 确认上线条件:前往支付宝 AI 付站点(https://aipay.alipay.com/?from=alipay-aipay-skill)查看产品开通情况。如本次流程新建应用,进入一站式接入 → 选择产品 → 密钥配置,确认应用“已上线”。',
|
|
471
|
+
'2. 替换到项目:把生产环境的应用配置写入业务系统生产环境变量、受保护配置文件或密钥管理系统,确保 `appId`、应用公钥、应用私钥、支付宝公钥同属一套生产应用并修改生产网关为 `https://openapi.alipay.com/gateway.do`。'
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function productionReadinessBlock(productName, context) {
|
|
476
|
+
const statusLines = [];
|
|
477
|
+
if (!context.productBranchesComplete) {
|
|
478
|
+
statusLines.push('- 还不能正式收款,产品签约、服务市场或应用发布仍有上表待办。');
|
|
479
|
+
}
|
|
480
|
+
if (context.integrationResult === '不在本轮执行范围') {
|
|
481
|
+
statusLines.push('- 本轮只完成产品开通流程收口,不代表业务系统已经具备收款代码;先完成支付产品-代码开发、项目部署和生产参数验收。');
|
|
482
|
+
} else if (context.integrationResult === '部分完成,沙箱配置待完成') {
|
|
483
|
+
statusLines.push('- 代码开发仍有沙箱配置缺口;可继续产品开通流程,但不能进入真实生产收款。');
|
|
484
|
+
} else if (context.integrationResult === '部分完成,人工待验证') {
|
|
485
|
+
statusLines.push('- 代码开发还有人工待验证项;完成后才能判断是否可进入真实生产上线。');
|
|
486
|
+
}
|
|
487
|
+
if (statusLines.length > 0) {
|
|
488
|
+
return [
|
|
489
|
+
'【当前状态】',
|
|
490
|
+
...statusLines,
|
|
491
|
+
'',
|
|
492
|
+
'【上线前有以下关键点】',
|
|
493
|
+
...productionConfigSourceLines(),
|
|
494
|
+
...productionSpecificLines(productName),
|
|
495
|
+
'',
|
|
496
|
+
'【提示】',
|
|
497
|
+
'- 如找不到配置替换位置,可以试试对 Agent 说:“我要上线了,帮我找一下项目里支付宝生产配置应该改哪些地方”。让 Agent 定位文件/变量名,但不要把生产应用私钥、账号密码、支付凭证或未脱敏通知内容直接发给 Agent。',
|
|
498
|
+
'',
|
|
499
|
+
'【其他】',
|
|
500
|
+
'- 生产首单、退款或对账验收由商家在生产环境自行安排。'
|
|
501
|
+
].join('\n');
|
|
502
|
+
}
|
|
503
|
+
return [
|
|
504
|
+
'【当前状态】',
|
|
505
|
+
'- 代码开发和产品开通流程已完成,但当前 Agent 没有替你切生产配置,也不会默认发起真实生产交易。',
|
|
506
|
+
'',
|
|
507
|
+
'【上线前有以下关键点】',
|
|
508
|
+
...productionConfigSourceLines(),
|
|
509
|
+
...productionSpecificLines(productName),
|
|
510
|
+
'',
|
|
511
|
+
'【提示】',
|
|
512
|
+
'- 如找不到配置替换位置,可以试试对 Agent 说:“我要上线了,帮我找一下项目里支付宝生产配置应该改哪些地方”。让 Agent 定位文件/变量名,但不要把生产应用私钥、账号密码、支付凭证或未脱敏通知内容直接发给 Agent。',
|
|
513
|
+
'',
|
|
514
|
+
'【其他】',
|
|
515
|
+
'- 生产首单、退款或对账验收由商家在生产环境自行安排。'
|
|
516
|
+
].join('\n');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function normalizeProcessPartialResultSpacing(rendered) {
|
|
520
|
+
const lines = [];
|
|
521
|
+
let previousBlank = false;
|
|
522
|
+
for (const rawLine of rendered.split('\n')) {
|
|
523
|
+
const line = rawLine.trimEnd();
|
|
524
|
+
const blank = line === '';
|
|
525
|
+
if (blank && previousBlank) continue;
|
|
526
|
+
lines.push(line);
|
|
527
|
+
previousBlank = blank;
|
|
528
|
+
}
|
|
529
|
+
return lines.join('\n').trimEnd();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function validateVariantInputShape(messageId, variantName, variables) {
|
|
533
|
+
if (messageId === 'onboarding.discovery.summary') {
|
|
534
|
+
if (variantName === 'AIPAY') {
|
|
535
|
+
if (!hasValue(variables, 'serviceStatus')) {
|
|
536
|
+
throw new Error('onboarding.discovery.summary/AIPAY 缺少 serviceStatus 字段;允许变量:signingStatus、serviceStatus、applicationStatus');
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (Object.prototype.hasOwnProperty.call(variables, 'serviceStatus')) {
|
|
541
|
+
throw new Error(`onboarding.discovery.summary/${variantName} 禁止传入 serviceStatus 字段;网站支付和 APP 支付的服务市场由模板固定为“无需处理”,请从 JSON 删除 serviceStatus 字段;允许变量:signingStatus、applicationStatus`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const managedEntrypoints = {
|
|
547
|
+
'onboarding.windows.redirect': {
|
|
548
|
+
command: 'node ../normal/scripts/onboarding_message_runner.mjs platform-entry',
|
|
549
|
+
reason: 'Windows 产品开通入口必须由 runner 根据真实运行平台确定;非 Windows 不得输出该消息。'
|
|
550
|
+
},
|
|
551
|
+
'integration.start.confirm': {
|
|
552
|
+
command: 'node ../normal/scripts/integration_message_runner.mjs start-confirm --product-type <aipay|webpay|apppay> --project-path <absPath> --project-selection <CURRENT_PROJECT|OTHER_PROJECT|PREPARED_NEW_PROJECT> --language <language> --framework <framework>',
|
|
553
|
+
reason: '代码开发启动确认必须绑定 runner 同一次项目扫描的项目来源、代码状态和其他支付产品,禁止手写项目扫描事实。'
|
|
554
|
+
},
|
|
555
|
+
'integration.checklist.result': {
|
|
556
|
+
command: 'node ../normal/scripts/integration_message_runner.mjs checklist-result --variant <AIPAY|WEBPAY|APPPAY>',
|
|
557
|
+
reason: '最终 checklist 结论必须由 runner 统一接收最终证据并调用 renderer,同一次代码开发只输出一次。'
|
|
558
|
+
},
|
|
559
|
+
'integration.project_candidates.select': {
|
|
560
|
+
command: 'node ../normal/scripts/integration_message_runner.mjs locate-projects --base-path <absDir> --search-input <current|relativeSubdir|absDir> --format message',
|
|
561
|
+
reason: '项目候选目录必须由 runner 轻量定位项目标记后统一输出,禁止手写候选列表或扫描整个大目录。'
|
|
562
|
+
},
|
|
563
|
+
'onboarding.mcc.clarify': {
|
|
564
|
+
command: 'node ../normal/scripts/onboarding_message_runner.mjs mcc-clarify --product-type <aipay|webpay|apppay>',
|
|
565
|
+
reason: '产品已明确但用户尚未提供 MCC/经营描述时由 runner 绑定产品名称,禁止手写经营类目补问;检索无匹配时不得回到本消息。'
|
|
566
|
+
},
|
|
567
|
+
'onboarding.mcc.no_match': {
|
|
568
|
+
command: 'node ../normal/scripts/onboarding_message_runner.mjs mcc-candidates --query <business-description> --product-type <aipay|webpay|apppay>',
|
|
569
|
+
reason: '用户已经提供经营描述但当前 MCC 表无确定匹配时,由 runner 输出独立恢复路径,禁止重新渲染初始 mcc-clarify 形成循环。'
|
|
570
|
+
},
|
|
571
|
+
'onboarding.mcc.candidates': {
|
|
572
|
+
command: 'node ../normal/scripts/onboarding_message_runner.mjs mcc-candidates --query <business-description> --product-type <aipay|webpay|apppay>',
|
|
573
|
+
reason: '产品已明确且 MCC 存在多个相似候选时由 runner 绑定本地 MCC 表生成候选事实,禁止手写候选表。'
|
|
574
|
+
},
|
|
575
|
+
'onboarding.discovery.summary': {
|
|
576
|
+
command: 'node ../normal/scripts/onboarding_message_runner.mjs discovery-summary --product-type <aipay|webpay|apppay>',
|
|
577
|
+
reason: '运行时只传脚本原始 signStatus/serviceFlow/appFlow,由 runner 映射枚举并处理网站支付/APP 支付禁用 serviceStatus 的变体规则。'
|
|
578
|
+
},
|
|
579
|
+
'materials.category.collect': {
|
|
580
|
+
command: 'node ../normal/scripts/onboarding_message_runner.mjs material-collect --category <signing|service|application> --state <INITIAL|PARTIAL|INVALID|APP_MOBILE_INITIAL> --product-type <aipay|webpay|apppay>',
|
|
581
|
+
reason: '运行时禁止手写 categoryName/currentStatus/missingFields;runner 会归一化“签约资料”“网站支付签约截图”“mobilePlatform”等常见别名后再调用 renderer。'
|
|
582
|
+
},
|
|
583
|
+
'application.candidates.facts': {
|
|
584
|
+
command: 'node ../normal/scripts/onboarding_message_runner.mjs application-candidates --application-type <WEBAPP|MOBILEAPP>',
|
|
585
|
+
reason: '应用候选事实和适用选择提示必须由 runner 接收结构化候选数组并统一生成,禁止脚本或 Agent 拼接 Markdown 行。'
|
|
586
|
+
},
|
|
587
|
+
'application.candidate.select': {
|
|
588
|
+
command: 'node ../normal/scripts/onboarding_message_runner.mjs application-candidates --application-type <WEBAPP|MOBILEAPP>',
|
|
589
|
+
reason: '应用候选选择必须由 runner 绑定同一份结构化候选快照生成,禁止脚本或 Agent 单独调用。'
|
|
590
|
+
},
|
|
591
|
+
'service.candidates.facts': {
|
|
592
|
+
command: 'node ../normal/scripts/onboarding_message_runner.mjs service-candidates',
|
|
593
|
+
reason: '服务候选事实和适用选择提示必须由 runner 接收结构化候选数组并统一生成,禁止脚本或 Agent 拼接 Markdown 行。'
|
|
594
|
+
},
|
|
595
|
+
'service.candidate.select': {
|
|
596
|
+
command: 'node ../normal/scripts/onboarding_message_runner.mjs service-candidates',
|
|
597
|
+
reason: '服务候选选择必须由 runner 绑定同一份结构化候选快照生成,禁止脚本或 Agent 单独调用。'
|
|
598
|
+
},
|
|
599
|
+
'process.partial_result': {
|
|
600
|
+
command: 'node ../normal/scripts/onboarding_message_runner.mjs closeout',
|
|
601
|
+
reason: '产品开通 Step 6 最终收口必须由 runner 统一合并代码开发状态、产品开通分支状态、最近应用分支结果和上线前关键点;禁止 Agent 手写、缩写或删减网站支付收款限制引导。'
|
|
602
|
+
},
|
|
603
|
+
'auth.page': {
|
|
604
|
+
command: 'bash modules/scripts/auth.sh init --defer-open --scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>,页面 stdout 独立送达后执行 bash modules/scripts/auth.sh open ...',
|
|
605
|
+
reason: '授权页必须由 auth.sh 绑定本次 CLI deviceCode、固定产品/MCC 上下文和 URL 白名单;受控 opener 必须在页面消息独立送达后执行,后续 auth.pending 不得合并到同一条对客回复。',
|
|
606
|
+
allowedCallers: ['auth.sh']
|
|
607
|
+
},
|
|
608
|
+
'auth.pending': {
|
|
609
|
+
command: 'bash modules/scripts/auth.sh wait|confirm [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]',
|
|
610
|
+
reason: '授权待完成状态只能来自 auth.sh 有限轮询耗尽或用户输入 1 后的单次实际查询。',
|
|
611
|
+
allowedCallers: ['auth.sh']
|
|
612
|
+
},
|
|
613
|
+
'auth.expired': {
|
|
614
|
+
command: 'bash modules/scripts/auth.sh confirm [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]',
|
|
615
|
+
reason: '授权过期状态只能来自本轮 auth.sh confirm 对 CLI 授权状态的实际查询。',
|
|
616
|
+
allowedCallers: ['auth.sh']
|
|
617
|
+
},
|
|
618
|
+
'auth.mismatch': {
|
|
619
|
+
command: 'bash modules/scripts/auth.sh mismatch --defer-open [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>],页面 stdout 独立送达后执行 bash modules/scripts/auth.sh open ...',
|
|
620
|
+
reason: 'scope 或 MCC 不匹配必须由 auth.sh 执行 logout 与重新授权链路,不能只输出提示。',
|
|
621
|
+
allowedCallers: ['auth.sh']
|
|
622
|
+
},
|
|
623
|
+
'error.mcp.auth': {
|
|
624
|
+
command: '由 onboarding modules/scripts/error_handler.sh 根据实际认证错误和 logout 结果托管输出',
|
|
625
|
+
reason: 'MCP 认证恢复文案必须绑定 error_handler.sh 的实际分类与 logout 结果。',
|
|
626
|
+
allowedCallers: ['error_handler.sh']
|
|
627
|
+
},
|
|
628
|
+
'error.mcp.service': {
|
|
629
|
+
command: '由 onboarding modules/scripts/error_handler.sh 根据实际服务错误托管输出',
|
|
630
|
+
reason: 'MCP 服务异常文案必须绑定 error_handler.sh 的实际错误分类和脱敏详情。',
|
|
631
|
+
allowedCallers: ['error_handler.sh']
|
|
632
|
+
},
|
|
633
|
+
'error.backend': {
|
|
634
|
+
command: '由 onboarding modules/scripts/error_handler.sh 根据实际后端错误托管输出',
|
|
635
|
+
reason: '业务错误文案必须只使用 error_handler.sh 已提取并脱敏的错误事实。',
|
|
636
|
+
allowedCallers: ['error_handler.sh']
|
|
637
|
+
},
|
|
638
|
+
'error.service.unstable': {
|
|
639
|
+
command: '由 onboarding modules/scripts/error_handler.sh 根据实际服务错误托管输出',
|
|
640
|
+
reason: '服务不稳定文案必须来自 error_handler.sh 的实际错误分类。',
|
|
641
|
+
allowedCallers: ['error_handler.sh']
|
|
642
|
+
},
|
|
643
|
+
'error.cli': {
|
|
644
|
+
command: '由 onboarding modules/scripts/error_handler.sh 根据实际 CLI 错误托管输出',
|
|
645
|
+
reason: 'CLI 错误文案必须只使用 error_handler.sh 已提取并脱敏的错误事实。',
|
|
646
|
+
allowedCallers: ['error_handler.sh']
|
|
647
|
+
},
|
|
648
|
+
'application.key.page': {
|
|
649
|
+
command: 'bash modules/scripts/app.sh key --defer-open <appId> <publicKey>,页面 stdout 独立送达后执行 bash modules/scripts/app.sh open-key-page <appId>',
|
|
650
|
+
reason: '公钥确认页必须由 app.sh 调用 createKeyConfirmPage 后校验官方 URL;受控 opener 必须在页面消息独立送达后执行,后续 application.key.pending 不得合并到同一条对客回复。',
|
|
651
|
+
allowedCallers: ['app.sh']
|
|
652
|
+
},
|
|
653
|
+
'application.key.pending': {
|
|
654
|
+
command: 'bash modules/scripts/app.sh verify-key <appId> [publicKey] 或 verify-key-and-audit <appId> [publicKey]',
|
|
655
|
+
reason: '公钥确认超时提示只能来自 app.sh 完成有限次数的实际公钥状态查询后。',
|
|
656
|
+
allowedCallers: ['app.sh']
|
|
657
|
+
},
|
|
658
|
+
'key_tool.download.result': {
|
|
659
|
+
command: 'bash modules/scripts/download_key_tool.sh [downloadDir]',
|
|
660
|
+
reason: '密钥工具下载成功消息必须来自下载脚本的实际下载路径、跳转校验和安装包校验结果。',
|
|
661
|
+
allowedCallers: ['download_key_tool.sh']
|
|
662
|
+
},
|
|
663
|
+
'key_tool.download.fallback': {
|
|
664
|
+
command: 'bash modules/scripts/download_key_tool.sh [downloadDir]',
|
|
665
|
+
reason: '密钥工具下载失败或不支持时必须由下载脚本统一给出官方手动下载兜底。',
|
|
666
|
+
allowedCallers: ['download_key_tool.sh']
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
function publicManagedEntrypoint(messageId) {
|
|
671
|
+
const managed = managedEntrypoints[messageId];
|
|
672
|
+
if (!managed) return null;
|
|
673
|
+
return { command: managed.command, reason: managed.reason };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function addManagedEntrypointHint(messageId, error) {
|
|
677
|
+
const managed = managedEntrypoints[messageId];
|
|
678
|
+
if (!managed) return error;
|
|
679
|
+
return new Error(`${error.message};该消息运行时应使用托管入口:${managed.command};${managed.reason}`);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function assertManagedCliAllowed(messageId) {
|
|
683
|
+
const managed = managedEntrypoints[messageId];
|
|
684
|
+
if (!managed) return;
|
|
685
|
+
const caller = process.env.ALIPAY_AIPAY_RENDERER_MANAGED_CALLER || '';
|
|
686
|
+
if (managed.allowedCallers?.includes(caller)) return;
|
|
687
|
+
throw new Error(`${messageId} 由托管入口输出,禁止直接调用 renderer;请使用托管入口:${managed.command};${managed.reason}`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export function getMessageSchema(messageId, variantName = 'DEFAULT') {
|
|
691
|
+
const catalog = loadJson(catalogPath);
|
|
692
|
+
const message = catalog.messages.find((entry) => entry.messageId === messageId);
|
|
693
|
+
if (!message) throw new Error(`未知 messageId ${messageId}`);
|
|
694
|
+
if (message.owner !== 'catalog_owned') throw new Error(`${messageId} 由脚本直接输出,没有 renderer 输入 schema`);
|
|
695
|
+
const acceptsRuntimeChecklistVariant =
|
|
696
|
+
messageId === 'integration.checklist.result' &&
|
|
697
|
+
['AIPAY', 'WEBPAY', 'APPPAY'].includes(variantName);
|
|
698
|
+
if (!acceptsRuntimeChecklistVariant && !message.variants?.[variantName]) {
|
|
699
|
+
throw new Error(`${messageId} 不支持 variant ${variantName}`);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
let inputVariables = (message.variables || [])
|
|
703
|
+
.filter((definition) => !definition.derived)
|
|
704
|
+
.map(cloneInputVariable);
|
|
705
|
+
const variantRules = [];
|
|
706
|
+
|
|
707
|
+
if (messageId === 'onboarding.discovery.summary') {
|
|
708
|
+
if (variantName === 'AIPAY') {
|
|
709
|
+
setVariableRule(inputVariables, 'serviceStatus', { required: true });
|
|
710
|
+
variantRules.push('AIPAY 必须传入 serviceStatus。');
|
|
711
|
+
} else if (['WEBPAY', 'APPPAY'].includes(variantName)) {
|
|
712
|
+
inputVariables = inputVariables.filter((variable) => variable.name !== 'serviceStatus');
|
|
713
|
+
variantRules.push('WEBPAY/APPPAY 禁止传入 serviceStatus;服务市场由模板固定为“无需处理”。');
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (messageId === 'materials.category.collect') {
|
|
718
|
+
const expectedStatus = {
|
|
719
|
+
INITIAL: '待补充',
|
|
720
|
+
APP_MOBILE_INITIAL: '待补充',
|
|
721
|
+
PARTIAL: '部分已提供,待补充',
|
|
722
|
+
INVALID: '校验失败,需更正'
|
|
723
|
+
}[variantName];
|
|
724
|
+
if (!expectedStatus) throw new Error(`${messageId} 不支持 variant ${variantName}`);
|
|
725
|
+
setVariableRule(inputVariables, 'currentStatus', { values: [expectedStatus] });
|
|
726
|
+
variantRules.push(`${variantName} 的 currentStatus 固定为“${expectedStatus}”。`);
|
|
727
|
+
if (variantName === 'APP_MOBILE_INITIAL') {
|
|
728
|
+
setVariableRule(inputVariables, 'categoryName', { values: ['应用资料'] });
|
|
729
|
+
setVariableRule(inputVariables, 'missingFields', { values: ['应用平台和对应资料'] });
|
|
730
|
+
variantRules.push('APP_MOBILE_INITIAL 只允许 categoryName=应用资料 且 missingFields=应用平台和对应资料。');
|
|
731
|
+
} else {
|
|
732
|
+
const missingFields = inputVariables.find((variable) => variable.name === 'missingFields');
|
|
733
|
+
if (missingFields?.values) {
|
|
734
|
+
missingFields.values = missingFields.values.filter((value) => value !== '应用平台和对应资料');
|
|
735
|
+
}
|
|
736
|
+
variantRules.push('应用平台首次收集必须使用 APP_MOBILE_INITIAL,不得在其他变体传入“应用平台和对应资料”。');
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (messageId === 'service.create.summary' || messageId === 'onboarding.write.confirm') {
|
|
741
|
+
setVariableRule(inputVariables, 'servicePricing', {
|
|
742
|
+
format: '纯数字金额,例如 0.08;模板负责追加“元/次”,不要传入单位。'
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (messageId === 'onboarding.write.confirm' && variantName === 'SERVICE_UPDATE_ONLY') {
|
|
747
|
+
for (const requiredName of ['serviceId', 'serviceName', 'serviceDescription', 'serviceUrl', 'servicePricing', 'serviceSchema']) {
|
|
748
|
+
setVariableRule(inputVariables, requiredName, { required: true });
|
|
749
|
+
}
|
|
750
|
+
setVariableRule(inputVariables, 'productName', { values: ['按量付费'] });
|
|
751
|
+
setVariableRule(inputVariables, 'actionTypes', { values: ['服务修改'] });
|
|
752
|
+
variantRules.push('SERVICE_UPDATE_ONLY 只用于按量付费服务修改;签约提交、服务创建、应用创建和应用提审不得进入该确认消息。');
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return {
|
|
756
|
+
schemaVersion: 1,
|
|
757
|
+
messageId,
|
|
758
|
+
variant: variantName,
|
|
759
|
+
owner: message.owner,
|
|
760
|
+
blocking: Boolean(message.blocking),
|
|
761
|
+
replyMode: message.replyMode || 'exact',
|
|
762
|
+
allowedReplies: message.allowedReplies || [],
|
|
763
|
+
inputHintType: message.inputHintType || null,
|
|
764
|
+
postRenderHintType: message.postRenderHintType || null,
|
|
765
|
+
managedEntrypoint: publicManagedEntrypoint(messageId),
|
|
766
|
+
inputVariables,
|
|
767
|
+
variantRules
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export function validateCatalog() {
|
|
772
|
+
const catalog = loadJson(catalogPath);
|
|
773
|
+
const policy = loadJson(policyPath);
|
|
774
|
+
const errors = [];
|
|
775
|
+
const messages = new Map();
|
|
776
|
+
const policyIds = new Set(policy.rules.map((rule) => rule.policyId));
|
|
777
|
+
const officialUrls = new Set(Object.values(policy.officialUrls));
|
|
778
|
+
|
|
779
|
+
const inputHints = catalog.interactionPrompts || {};
|
|
780
|
+
if (typeof inputHints.confirmInputHint !== 'string' || !inputHints.confirmInputHint) {
|
|
781
|
+
errors.push('缺少统一确认输入提示');
|
|
782
|
+
}
|
|
783
|
+
if (typeof inputHints.authorizationCompletionInputHint !== 'string' || !inputHints.authorizationCompletionInputHint) {
|
|
784
|
+
errors.push('缺少统一授权完成输入提示');
|
|
785
|
+
}
|
|
786
|
+
if (typeof inputHints.publicKeyConfirmationCompletionInputHint !== 'string' || !inputHints.publicKeyConfirmationCompletionInputHint) {
|
|
787
|
+
errors.push('缺少统一应用公钥确认完成输入提示');
|
|
788
|
+
}
|
|
789
|
+
if (typeof inputHints.completionInputHint !== 'string' || !inputHints.completionInputHint) {
|
|
790
|
+
errors.push('缺少统一完成输入提示');
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const productNames = new Set();
|
|
794
|
+
const salesCodes = new Set();
|
|
795
|
+
for (const product of policy.authorizationProducts || []) {
|
|
796
|
+
if (!product.productName || !product.salesCode || !product.scope) errors.push('authorizationProducts 存在不完整映射');
|
|
797
|
+
if (productNames.has(product.productName)) errors.push(`authorizationProducts 重复 productName: ${product.productName}`);
|
|
798
|
+
if (salesCodes.has(product.salesCode)) errors.push(`authorizationProducts 重复 salesCode: ${product.salesCode}`);
|
|
799
|
+
productNames.add(product.productName);
|
|
800
|
+
salesCodes.add(product.salesCode);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
for (const message of catalog.messages) {
|
|
804
|
+
if (messages.has(message.messageId)) errors.push(`重复 messageId: ${message.messageId}`);
|
|
805
|
+
messages.set(message.messageId, message);
|
|
806
|
+
if (!['catalog_owned', 'script_owned'].includes(message.owner)) errors.push(`${message.messageId}: owner 无效`);
|
|
807
|
+
if (message.blocking && JSON.stringify(message.allowedReplies) === '["1"]' && !['confirm', 'authorizationComplete', 'publicKeyConfirmationComplete', 'complete'].includes(message.inputHintType)) {
|
|
808
|
+
errors.push(`${message.messageId}: 单一确认消息必须声明合法 inputHintType`);
|
|
809
|
+
}
|
|
810
|
+
if (message.postRenderHintType !== undefined && message.postRenderHintType !== 'scanReturn') {
|
|
811
|
+
errors.push(`${message.messageId}: postRenderHintType 无效`);
|
|
812
|
+
}
|
|
813
|
+
if (message.postRenderHintType === 'scanReturn' && (message.blocking || JSON.stringify(message.allowedReplies || []) !== '[]')) {
|
|
814
|
+
errors.push(`${message.messageId}: scanReturn 只允许用于非阻塞且无回复枚举的消息`);
|
|
815
|
+
}
|
|
816
|
+
for (const id of message.requiredPolicyIds || []) {
|
|
817
|
+
if (!policyIds.has(id)) errors.push(`${message.messageId}: 未知 policyId ${id}`);
|
|
818
|
+
}
|
|
819
|
+
const variableNames = new Set((message.variables || []).map((variable) => variable.name));
|
|
820
|
+
for (const variable of message.variables || []) {
|
|
821
|
+
if (!allowedVariableTypes.has(variable.type)) errors.push(`${message.messageId}: 未知变量类型 ${variable.type}`);
|
|
822
|
+
if (variable.derived && variable.required) errors.push(`${message.messageId}: 派生变量不得声明为必填输入`);
|
|
823
|
+
if (variable.derived && (!Array.isArray(variable.values) || variable.values.length !== 1)) {
|
|
824
|
+
errors.push(`${message.messageId}: 派生变量必须声明唯一目录值`);
|
|
825
|
+
}
|
|
826
|
+
if (sensitiveNamePattern.test(variable.name) && !['officialTemporaryUrl', 'sandboxCredential'].includes(variable.type)) {
|
|
827
|
+
errors.push(`${message.messageId}: 禁止的敏感变量名 ${variable.name}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (message.replyMode && !['exact', 'freeform', 'runtime_enum'].includes(message.replyMode)) errors.push(`${message.messageId}: replyMode 无效`);
|
|
831
|
+
if (message.owner === 'script_owned') {
|
|
832
|
+
if (!message.scriptSource || !message.snapshotId || message.variants) {
|
|
833
|
+
errors.push(`${message.messageId}: script_owned 必须只有 scriptSource/snapshotId`);
|
|
834
|
+
}
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
if (!message.variants || Object.keys(message.variants).length === 0) {
|
|
838
|
+
errors.push(`${message.messageId}: catalog_owned 缺少 variants`);
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
for (const [variantName, variant] of Object.entries(message.variants)) {
|
|
842
|
+
if (!Array.isArray(variant.templateLines)) errors.push(`${message.messageId}/${variantName}: templateLines 必须是数组`);
|
|
843
|
+
const template = (variant.templateLines || []).join('\n');
|
|
844
|
+
for (const match of template.matchAll(placeholderPattern)) {
|
|
845
|
+
if (!variableNames.has(match[1])) errors.push(`${message.messageId}/${variantName}: 未声明变量 ${match[1]}`);
|
|
846
|
+
}
|
|
847
|
+
for (const match of template.matchAll(/https:\/\/[^\s)`),。;:]+/g)) {
|
|
848
|
+
if (!officialUrls.has(match[0])) errors.push(`${message.messageId}/${variantName}: 静态 URL 未登记 ${match[0]}`);
|
|
849
|
+
}
|
|
850
|
+
if (/<INTERNAL_/i.test(template)) errors.push(`${message.messageId}/${variantName}: 模板包含内部标签`);
|
|
851
|
+
if (message.blocking && JSON.stringify(message.allowedReplies) === '["1"]' && /(?:回复|输入)\s*`?1`?/.test(template)) {
|
|
852
|
+
errors.push(`${message.messageId}/${variantName}: 单一确认输入提示必须由 renderer 统一追加`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
for (const message of catalog.messages) {
|
|
858
|
+
if (message.fallbackMessageId && !messages.has(message.fallbackMessageId)) {
|
|
859
|
+
errors.push(`${message.messageId}: fallback 不存在 ${message.fallbackMessageId}`);
|
|
860
|
+
}
|
|
861
|
+
const seen = new Set([message.messageId]);
|
|
862
|
+
let current = message;
|
|
863
|
+
while (current?.fallbackMessageId) {
|
|
864
|
+
if (seen.has(current.fallbackMessageId)) {
|
|
865
|
+
errors.push(`${message.messageId}: fallback 存在环`);
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
seen.add(current.fallbackMessageId);
|
|
869
|
+
current = messages.get(current.fallbackMessageId);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return errors;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export function renderMessage(messageId, variantName, variables, telemetryContext = {}) {
|
|
876
|
+
const catalog = loadJson(catalogPath);
|
|
877
|
+
const policy = loadJson(policyPath);
|
|
878
|
+
const message = catalog.messages.find((entry) => entry.messageId === messageId);
|
|
879
|
+
if (!message) throw new Error(`未知 messageId ${messageId}`);
|
|
880
|
+
if (message.owner !== 'catalog_owned') throw new Error(`${messageId} 由脚本直接输出,禁止 renderer 复制`);
|
|
881
|
+
const catalogVariantName = messageId === 'integration.checklist.result'
|
|
882
|
+
? `${variantName}_${variables.overallResult === '通过' ? 'PASSED' : 'INCOMPLETE'}`
|
|
883
|
+
: variantName;
|
|
884
|
+
const variant = message.variants[catalogVariantName];
|
|
885
|
+
if (!variant) throw new Error(`${messageId} 不支持 variant ${variantName}`);
|
|
886
|
+
const definitions = new Map(message.variables.map((definition) => [definition.name, definition]));
|
|
887
|
+
const normalized = {};
|
|
888
|
+
try {
|
|
889
|
+
for (const key of Object.keys(variables)) {
|
|
890
|
+
if (!definitions.has(key)) throw new Error(`出现未声明变量 ${key};允许变量:${allowedVariableNames(definitions)}`);
|
|
891
|
+
if (definitions.get(key).derived) throw new Error(`派生变量 ${key} 只能由 renderer 生成`);
|
|
892
|
+
}
|
|
893
|
+
validateVariantInputShape(messageId, variantName, variables);
|
|
894
|
+
for (const definition of message.variables) {
|
|
895
|
+
normalized[definition.name] = definition.derived ? '' : validateValue(definition, variables[definition.name], policy);
|
|
896
|
+
}
|
|
897
|
+
} catch (error) {
|
|
898
|
+
throw addManagedEntrypointHint(messageId, error);
|
|
899
|
+
}
|
|
900
|
+
if (messageId === 'application.candidates.facts') {
|
|
901
|
+
normalized.candidateTable = applicationCandidateTable(variantName, variables.applicationCandidates);
|
|
902
|
+
}
|
|
903
|
+
if (messageId === 'service.candidates.facts') {
|
|
904
|
+
normalized.candidateTable = serviceCandidateTable(variantName, variables.serviceCandidates);
|
|
905
|
+
}
|
|
906
|
+
if (messageId === 'onboarding.mcc.candidates') {
|
|
907
|
+
normalized.candidateTable = mccCandidateTable(variantName, variables.mccCandidates);
|
|
908
|
+
}
|
|
909
|
+
if (messageId === 'error.backend') {
|
|
910
|
+
normalized.errorSceneLine = variables.errorScene || variables.errorSpecific
|
|
911
|
+
? `📋 错误场景:${variables.errorScene ? normalized.errorScene : '未知'} / ${variables.errorSpecific ? normalized.errorSpecific : '未知'}`
|
|
912
|
+
: '';
|
|
913
|
+
normalized.checkedErrorsBlock = normalized.checkedErrors.length > 0
|
|
914
|
+
? ['📋 详细校验错误:', ...normalized.checkedErrors.map((detail) => ` - ${detail}`)].join('\n')
|
|
915
|
+
: '';
|
|
916
|
+
normalized.bizTipsLine = variables.bizTips ? `💡 提示:${normalized.bizTips}` : '';
|
|
917
|
+
normalized.appMaxLine = variables.appMax === 'YES'
|
|
918
|
+
? '📌 处理建议:当前主体下应用数量已达到上限,无法继续新建应用。请优先复用已有上线应用;如必须新建,请前往支付宝开放平台处理应用配额或联系支付宝技术支持。'
|
|
919
|
+
: '';
|
|
920
|
+
}
|
|
921
|
+
if (messageId === 'error.cli') {
|
|
922
|
+
normalized.bizTipsLine = variables.bizTips ? `💡 ${normalized.bizTips}` : '';
|
|
923
|
+
normalized.checkedErrorsBlock = normalized.checkedErrors.length > 0
|
|
924
|
+
? ['📋 详细校验错误:', ...normalized.checkedErrors.map((detail) => ` - ${detail}`)].join('\n')
|
|
925
|
+
: '';
|
|
926
|
+
}
|
|
927
|
+
const hasMccInput =
|
|
928
|
+
variables.mccCode !== undefined ||
|
|
929
|
+
variables.mccName !== undefined ||
|
|
930
|
+
variables.mccQualification !== undefined;
|
|
931
|
+
if (hasMccInput) {
|
|
932
|
+
if (!definitions.has('mccCode') || !definitions.has('mccName')) throw new Error(`${messageId} 的 MCC 变量声明不完整`);
|
|
933
|
+
const record = loadMccRecords().get(variables.mccCode);
|
|
934
|
+
if (!record || record.name !== variables.mccName) throw new Error('MCC 名称和编码不匹配当前参考表');
|
|
935
|
+
normalized.mccName = record.name;
|
|
936
|
+
normalized.mccCode = variables.mccCode;
|
|
937
|
+
if (definitions.has('mccQualification') && variables.mccQualification !== undefined) {
|
|
938
|
+
if (record.qualification !== variables.mccQualification) throw new Error('MCC 特殊资质不匹配当前参考表');
|
|
939
|
+
normalized.mccQualification = record.qualification === '-' ? '当前类目参考表未列出' : record.qualification;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (messageId === 'auth.page') {
|
|
943
|
+
const product = authorizationProduct(policy, (entry) => entry.productName === variables.productName);
|
|
944
|
+
if (!product) throw new Error('授权消息产品不在固定映射中');
|
|
945
|
+
validateAuthorizationContext({
|
|
946
|
+
productName: variables.productName,
|
|
947
|
+
salesCode: product.salesCode,
|
|
948
|
+
scope: product.scope,
|
|
949
|
+
mccCode: variables.mccCode,
|
|
950
|
+
mccName: variables.mccName,
|
|
951
|
+
deviceCode: variables.deviceCode,
|
|
952
|
+
officialUrl: variables.officialUrl
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
if (messageId === 'integration.start.confirm') {
|
|
956
|
+
validateIntegrationStart(variantName, variables);
|
|
957
|
+
normalized.integrationStatus = {
|
|
958
|
+
TARGET_PARTIAL: '已发现目标产品相关代码,需要继续核验或补全',
|
|
959
|
+
OTHER_PRODUCT_ONLY: '未发现目标产品代码,已检测到其他支付能力',
|
|
960
|
+
NO_PAYMENT: '未发现支付代码'
|
|
961
|
+
}[variables.integrationStatus];
|
|
962
|
+
}
|
|
963
|
+
if (messageId === 'integration.checklist.result') {
|
|
964
|
+
const sandboxPending = variables.sandboxConfigState !== 'READY';
|
|
965
|
+
const hasBlockingDefect = variables.blockingDefectState === 'PRESENT';
|
|
966
|
+
if (variantName === 'AIPAY' && !variables.sandboxResult) throw new Error('按量付费 checklist 缺少沙箱联调结果');
|
|
967
|
+
if (variantName !== 'AIPAY' && variables.sandboxResult) throw new Error('非按量付费 checklist 禁止传入按量付费沙箱结果');
|
|
968
|
+
if (variantName === 'WEBPAY' && !variables.webpayExperienceResult) throw new Error('网站支付 checklist 缺少沙箱体验实际结果');
|
|
969
|
+
if (variantName !== 'WEBPAY' && variables.webpayExperienceResult) throw new Error('非网站支付 checklist 禁止传入网站支付沙箱体验结果');
|
|
970
|
+
const hasFailedItems = variables.failedItems !== '无';
|
|
971
|
+
const hasManualItems = variables.manualItems !== '无';
|
|
972
|
+
const manualOnly = !hasFailedItems && hasManualItems;
|
|
973
|
+
const expectedReminder = variables.executionMode === '仅代码开发'
|
|
974
|
+
? '正式上线前还需完成支付产品-产品开通。'
|
|
975
|
+
: sandboxPending && variables.overallResult === '部分通过' && !hasBlockingDefect
|
|
976
|
+
? '沙箱配置尚未完成,但不阻塞本轮继续支付产品-产品开通;完成配置后再进行沙箱测试。'
|
|
977
|
+
: !sandboxPending && variables.overallResult === '部分通过' && !hasBlockingDefect && manualOnly
|
|
978
|
+
? '代码开发自动校验已完成,仍有人工待验证项;本轮继续产品开通。'
|
|
979
|
+
: '代码开发校验通过后将自动进入支付产品-产品开通;未通过时停留在代码开发。';
|
|
980
|
+
if (variables.nextFlowReminder !== expectedReminder) throw new Error('代码开发执行模式与后续衔接不一致');
|
|
981
|
+
if (variables.overallResult === '通过' && (hasFailedItems || hasManualItems)) {
|
|
982
|
+
throw new Error('代码开发结果为通过时不得存在未通过项或人工待验证项');
|
|
983
|
+
}
|
|
984
|
+
if (variables.overallResult === '部分通过' && !hasFailedItems && !hasManualItems) {
|
|
985
|
+
throw new Error('代码开发结果为部分通过时必须存在未通过项或人工待验证项');
|
|
986
|
+
}
|
|
987
|
+
if (variables.overallResult === '未通过' && !hasFailedItems) {
|
|
988
|
+
throw new Error('代码开发结果为未通过时必须列出未通过项');
|
|
989
|
+
}
|
|
990
|
+
if (variables.overallResult === '通过' && hasBlockingDefect) {
|
|
991
|
+
throw new Error('代码开发通过时不得存在其他阻塞缺口');
|
|
992
|
+
}
|
|
993
|
+
if (variables.overallResult !== '通过' && !sandboxPending && !hasBlockingDefect && !manualOnly) {
|
|
994
|
+
throw new Error('沙箱已就绪且没有阻塞缺口时,非通过结果只能来自人工待验证项');
|
|
995
|
+
}
|
|
996
|
+
if (variables.overallResult === '未通过' && !hasBlockingDefect) {
|
|
997
|
+
throw new Error('代码开发未通过时必须登记其他阻塞缺口');
|
|
998
|
+
}
|
|
999
|
+
if (sandboxPending && variables.overallResult === '通过') {
|
|
1000
|
+
throw new Error('沙箱配置未完成时不得标记代码开发通过');
|
|
1001
|
+
}
|
|
1002
|
+
if (sandboxPending && !hasFailedItems) {
|
|
1003
|
+
throw new Error('沙箱配置未完成时必须列入未通过或部分通过项');
|
|
1004
|
+
}
|
|
1005
|
+
if (sandboxPending && variantName === 'AIPAY' && variables.sandboxResult !== '按量付费沙箱服务端联调未通过') {
|
|
1006
|
+
throw new Error('沙箱配置未完成时按量付费联调不得标记通过');
|
|
1007
|
+
}
|
|
1008
|
+
if (sandboxPending && variantName === 'WEBPAY' && variables.webpayExperienceResult !== '未提供浏览器付款入口或完整操作指引,需先补齐') {
|
|
1009
|
+
throw new Error('沙箱配置未完成时网站支付不得标记已交付付款体验');
|
|
1010
|
+
}
|
|
1011
|
+
if (variantName === 'AIPAY' && variables.overallResult === '通过' && variables.sandboxResult !== '按量付费沙箱服务端联调通过') {
|
|
1012
|
+
throw new Error('按量付费代码开发通过必须有沙箱服务端联调通过结论');
|
|
1013
|
+
}
|
|
1014
|
+
if (variantName === 'WEBPAY' && variables.overallResult === '通过' && variables.webpayExperienceResult === '未提供浏览器付款入口或完整操作指引,需先补齐') {
|
|
1015
|
+
throw new Error('网站支付未交付付款入口时不得标记代码开发通过');
|
|
1016
|
+
}
|
|
1017
|
+
if (manualOnly) {
|
|
1018
|
+
normalized.failedItems = `人工待验证项未完成:${normalized.manualItems}`;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (messageId === 'materials.category.collect') {
|
|
1022
|
+
const expectedStatus = {
|
|
1023
|
+
INITIAL: '待补充',
|
|
1024
|
+
APP_MOBILE_INITIAL: '待补充',
|
|
1025
|
+
PARTIAL: '部分已提供,待补充',
|
|
1026
|
+
INVALID: '校验失败,需更正'
|
|
1027
|
+
}[variantName];
|
|
1028
|
+
if (!expectedStatus || variables.currentStatus !== expectedStatus) {
|
|
1029
|
+
throw new Error('materials.category.collect variant 与材料状态不匹配');
|
|
1030
|
+
}
|
|
1031
|
+
const isMobileInitial = variantName === 'APP_MOBILE_INITIAL';
|
|
1032
|
+
if (isMobileInitial && (variables.categoryName !== '应用资料' || variables.missingFields !== '应用平台和对应资料')) {
|
|
1033
|
+
throw new Error('APP_MOBILE_INITIAL 只允许收集应用平台和对应资料');
|
|
1034
|
+
}
|
|
1035
|
+
if (!isMobileInitial && variables.missingFields.split('、').includes('应用平台和对应资料')) {
|
|
1036
|
+
throw new Error('应用平台首次收集必须使用 APP_MOBILE_INITIAL');
|
|
1037
|
+
}
|
|
1038
|
+
normalized.paymentPageHint = variables.missingFields.split('、').includes('支付页截图')
|
|
1039
|
+
? definitions.get('paymentPageHint')?.values?.[0] || ''
|
|
1040
|
+
: '';
|
|
1041
|
+
}
|
|
1042
|
+
if (messageId === 'process.partial_result') {
|
|
1043
|
+
const statusQueryHintDefinition = definitions.get('statusQueryHint');
|
|
1044
|
+
const statusQueryHint = statusQueryHintDefinition?.values?.[0];
|
|
1045
|
+
if (!statusQueryHintDefinition?.derived || !statusQueryHint) {
|
|
1046
|
+
throw new Error('process.partial_result 缺少受控状态查询提示');
|
|
1047
|
+
}
|
|
1048
|
+
normalized.statusQueryHint =
|
|
1049
|
+
variables.signingResult === '已提交,等待生效' && variables.applicationResult === '已提交审核,等待上线'
|
|
1050
|
+
? statusQueryHint
|
|
1051
|
+
: '';
|
|
1052
|
+
const onboardingOnly = variables.integrationResult === '不在本轮执行范围';
|
|
1053
|
+
const onboardingReminder = '还需完成支付产品-代码开发,才能在业务系统中实际发起支付。';
|
|
1054
|
+
if (onboardingOnly !== (variables.nextFlowReminder === onboardingReminder)) {
|
|
1055
|
+
throw new Error('process.partial_result 的代码开发结果与后续衔接不一致');
|
|
1056
|
+
}
|
|
1057
|
+
normalized.nextFlowReminderLine = onboardingOnly ? `后续衔接:${normalized.nextFlowReminder}` : '';
|
|
1058
|
+
const hasManualVerificationItems =
|
|
1059
|
+
typeof variables.manualVerificationItems === 'string' &&
|
|
1060
|
+
variables.manualVerificationItems !== '' &&
|
|
1061
|
+
variables.manualVerificationItems !== '无';
|
|
1062
|
+
const applicationOperationFields = [
|
|
1063
|
+
'applicationOperationAppId',
|
|
1064
|
+
'applicationOperationActualStatus',
|
|
1065
|
+
'applicationOperationNextAction'
|
|
1066
|
+
];
|
|
1067
|
+
const providedApplicationOperationFields = applicationOperationFields.filter((name) =>
|
|
1068
|
+
typeof variables[name] === 'string' && variables[name] !== ''
|
|
1069
|
+
);
|
|
1070
|
+
if (providedApplicationOperationFields.length !== 0 && providedApplicationOperationFields.length !== applicationOperationFields.length) {
|
|
1071
|
+
throw new Error('应用分支操作结果必须同时传入 appId、实际状态和下一步');
|
|
1072
|
+
}
|
|
1073
|
+
if (providedApplicationOperationFields.length === applicationOperationFields.length) {
|
|
1074
|
+
if (onboardingOnly) {
|
|
1075
|
+
throw new Error('onboarding_only 不合并应用操作结果;请按应用模块原规则输出');
|
|
1076
|
+
}
|
|
1077
|
+
if (variables.applicationOperationActualStatus !== variables.applicationResult) {
|
|
1078
|
+
throw new Error('应用分支操作实际状态与应用发布结果不一致');
|
|
1079
|
+
}
|
|
1080
|
+
if (variables.applicationResult === '已上线且配置完整') {
|
|
1081
|
+
normalized.applicationOperationBlock = '';
|
|
1082
|
+
} else if ([
|
|
1083
|
+
'已创建,待设置公钥',
|
|
1084
|
+
'公钥待确认',
|
|
1085
|
+
'已提交审核,等待上线',
|
|
1086
|
+
'需人工配置',
|
|
1087
|
+
'失败',
|
|
1088
|
+
'结果未知'
|
|
1089
|
+
].includes(variables.applicationResult)) {
|
|
1090
|
+
normalized.applicationOperationBlock = [
|
|
1091
|
+
'应用分支结果:',
|
|
1092
|
+
`- 应用 \`${normalized.applicationOperationAppId}\` 的实际状态:${normalized.applicationOperationActualStatus}。`,
|
|
1093
|
+
`- 应用分支下一步:${normalized.applicationOperationNextAction}`,
|
|
1094
|
+
'- 待审核、待设置公钥或待人工配置时不会表述为应用发布完成。'
|
|
1095
|
+
].join('\n');
|
|
1096
|
+
} else {
|
|
1097
|
+
throw new Error('当前应用分支结果不允许合并应用操作结果');
|
|
1098
|
+
}
|
|
1099
|
+
} else {
|
|
1100
|
+
normalized.applicationOperationBlock = '';
|
|
1101
|
+
}
|
|
1102
|
+
if (onboardingOnly) {
|
|
1103
|
+
if (variables.sandboxConfigState !== undefined) throw new Error('onboarding_only 禁止传入代码开发沙箱状态');
|
|
1104
|
+
if (hasManualVerificationItems) throw new Error('onboarding_only 禁止传入代码开发人工待验证项');
|
|
1105
|
+
normalized.manualVerificationLine = '';
|
|
1106
|
+
normalized.sandboxRecoveryHint = '';
|
|
1107
|
+
} else if (variables.integrationResult === '已完成') {
|
|
1108
|
+
if (variables.sandboxConfigState !== 'READY') throw new Error('完整代码开发结果必须绑定 READY 沙箱状态');
|
|
1109
|
+
if (hasManualVerificationItems) throw new Error('代码开发已完成时禁止传入人工待验证项');
|
|
1110
|
+
normalized.manualVerificationLine = '';
|
|
1111
|
+
normalized.sandboxRecoveryHint = '';
|
|
1112
|
+
} else if (variables.integrationResult === '部分完成,沙箱配置待完成') {
|
|
1113
|
+
if (hasManualVerificationItems) throw new Error('沙箱待配置结果不通过人工待验证项表达');
|
|
1114
|
+
normalized.sandboxRecoveryHint = {
|
|
1115
|
+
CREATE_PENDING: '沙箱配置待完成:请对 Agent 说:“重新创建并配置沙箱”。',
|
|
1116
|
+
VERIFY_PENDING: '沙箱配置待完成:请对 Agent 说:“重新创建并配置沙箱”。'
|
|
1117
|
+
}[variables.sandboxConfigState];
|
|
1118
|
+
if (!normalized.sandboxRecoveryHint) throw new Error('沙箱待配置结果必须绑定 CREATE_PENDING 或 VERIFY_PENDING');
|
|
1119
|
+
if (variables.remainingActions === '无') throw new Error('沙箱待配置时必须保留剩余待办');
|
|
1120
|
+
normalized.manualVerificationLine = '';
|
|
1121
|
+
} else if (variables.integrationResult === '部分完成,人工待验证') {
|
|
1122
|
+
if (variables.sandboxConfigState !== 'READY') throw new Error('人工待验证结果必须绑定 READY 沙箱状态');
|
|
1123
|
+
if (!hasManualVerificationItems) throw new Error('人工待验证结果必须列出人工待验证项');
|
|
1124
|
+
normalized.manualVerificationLine = `人工待验证:${normalized.manualVerificationItems}`;
|
|
1125
|
+
normalized.sandboxRecoveryHint = '';
|
|
1126
|
+
} else {
|
|
1127
|
+
throw new Error('full_process 进入产品开通收口时只允许已完成、沙箱待配置或人工待验证');
|
|
1128
|
+
}
|
|
1129
|
+
if (variables.productName === '按量付费' && variables.serviceResult === '无需处理') {
|
|
1130
|
+
throw new Error('按量付费产品开通不能把服务市场标记为无需处理');
|
|
1131
|
+
}
|
|
1132
|
+
if (variables.productName !== '按量付费' && variables.serviceResult !== '无需处理') {
|
|
1133
|
+
throw new Error('网站支付或 APP 支付的服务市场结果必须为无需处理');
|
|
1134
|
+
}
|
|
1135
|
+
const expectedServiceComplete = variables.productName === '按量付费'
|
|
1136
|
+
? ['已复用', '创建成功,状态未取得', '修改成功,状态未取得', '已完成'].includes(variables.serviceResult)
|
|
1137
|
+
: variables.serviceResult === '无需处理';
|
|
1138
|
+
const productBranchesComplete =
|
|
1139
|
+
variables.signingResult === '已生效' &&
|
|
1140
|
+
expectedServiceComplete &&
|
|
1141
|
+
variables.applicationResult === '已上线且配置完整';
|
|
1142
|
+
const branchesComplete =
|
|
1143
|
+
productBranchesComplete &&
|
|
1144
|
+
variables.remainingActions === '无' &&
|
|
1145
|
+
!hasManualVerificationItems;
|
|
1146
|
+
normalized.productionReadinessBlock = productionReadinessBlock(variables.productName, {
|
|
1147
|
+
productBranchesComplete,
|
|
1148
|
+
integrationResult: variables.integrationResult
|
|
1149
|
+
});
|
|
1150
|
+
const declaresComplete = variables.processResultTitle === '支付产品-产品开通已完成';
|
|
1151
|
+
if (branchesComplete !== declaresComplete) {
|
|
1152
|
+
throw new Error('process.partial_result 的总结果与分支完成条件不一致');
|
|
1153
|
+
}
|
|
1154
|
+
if (!branchesComplete && variables.remainingActions === '无') {
|
|
1155
|
+
throw new Error('process.partial_result 存在未完成分支时必须列出剩余待办');
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (messageId === 'service.create.summary') validateServiceCreateSummary(variantName, variables, normalized);
|
|
1159
|
+
if (messageId === 'onboarding.write.confirm') validateWriteConfirmation(variantName, variables, normalized);
|
|
1160
|
+
let rendered = variant.templateLines.join('\n').replace(placeholderPattern, (_, name) => normalized[name]);
|
|
1161
|
+
if (messageId === 'process.partial_result' || messageId === 'error.backend' || messageId === 'error.cli') {
|
|
1162
|
+
rendered = normalizeProcessPartialResultSpacing(rendered);
|
|
1163
|
+
}
|
|
1164
|
+
if (message.blocking && JSON.stringify(message.allowedReplies) === '["1"]') {
|
|
1165
|
+
const hintKeys = {
|
|
1166
|
+
confirm: 'confirmInputHint',
|
|
1167
|
+
authorizationComplete: 'authorizationCompletionInputHint',
|
|
1168
|
+
publicKeyConfirmationComplete: 'publicKeyConfirmationCompletionInputHint',
|
|
1169
|
+
complete: 'completionInputHint'
|
|
1170
|
+
};
|
|
1171
|
+
const hintKey = hintKeys[message.inputHintType] || '';
|
|
1172
|
+
if (!hintKey) throw new Error(`${messageId} 缺少合法 inputHintType`);
|
|
1173
|
+
const hint = catalog.interactionPrompts?.[hintKey];
|
|
1174
|
+
if (!hint) throw new Error(`缺少 ${hintKey}`);
|
|
1175
|
+
rendered = `${rendered}\n\n${hint}`;
|
|
1176
|
+
}
|
|
1177
|
+
if (message.postRenderHintType === 'scanReturn') {
|
|
1178
|
+
const hint = catalog.interactionPrompts?.scanReturnHint;
|
|
1179
|
+
if (!hint) throw new Error('缺少 scanReturnHint');
|
|
1180
|
+
rendered = `${rendered}\n\n${hint}`;
|
|
1181
|
+
}
|
|
1182
|
+
emitMessageMetric(messageId, variantName, variables, telemetryContext);
|
|
1183
|
+
return rendered;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
export function renderMessageWithFallback(messageId, variantName, variables) {
|
|
1187
|
+
const catalog = loadJson(catalogPath);
|
|
1188
|
+
const messages = new Map(catalog.messages.map((entry) => [entry.messageId, entry]));
|
|
1189
|
+
let currentId = messageId;
|
|
1190
|
+
let currentVariant = variantName;
|
|
1191
|
+
let isFallback = false;
|
|
1192
|
+
const visited = new Set();
|
|
1193
|
+
let lastError;
|
|
1194
|
+
while (currentId) {
|
|
1195
|
+
if (visited.has(currentId)) throw new Error(`fallback 存在环: ${currentId}`);
|
|
1196
|
+
visited.add(currentId);
|
|
1197
|
+
try {
|
|
1198
|
+
const message = messages.get(currentId);
|
|
1199
|
+
if (!message) throw new Error(`未知 messageId ${currentId}`);
|
|
1200
|
+
const allowed = new Set((message.variables || []).map((definition) => definition.name));
|
|
1201
|
+
const scopedVariables = isFallback
|
|
1202
|
+
? Object.fromEntries(Object.entries(variables).filter(([key]) => allowed.has(key)))
|
|
1203
|
+
: variables;
|
|
1204
|
+
return renderMessage(currentId, currentVariant, scopedVariables);
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
lastError = error;
|
|
1207
|
+
const fallbackId = messages.get(currentId)?.fallbackMessageId;
|
|
1208
|
+
if (!fallbackId) throw lastError;
|
|
1209
|
+
currentId = fallbackId;
|
|
1210
|
+
currentVariant = 'DEFAULT';
|
|
1211
|
+
isFallback = true;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
throw lastError || new Error('消息无法渲染且没有可执行 fallback');
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
async function readStdin() {
|
|
1218
|
+
let input = '';
|
|
1219
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
1220
|
+
return input.trim() ? JSON.parse(input) : {};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
async function main() {
|
|
1224
|
+
if (process.argv.includes('--check-catalog')) {
|
|
1225
|
+
const errors = validateCatalog();
|
|
1226
|
+
if (errors.length) throw new Error(errors.join('\n'));
|
|
1227
|
+
console.log('customer message catalog: OK');
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
if (process.argv.includes('--validate-authorization-context')) {
|
|
1231
|
+
validateAuthorizationContext(await readStdin());
|
|
1232
|
+
console.log('authorization context: OK');
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (process.argv.includes('--build-authorization-url')) {
|
|
1236
|
+
console.log(buildAuthorizationUrl(await readStdin()));
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (process.argv.includes('--schema')) {
|
|
1240
|
+
const schemaIndex = process.argv.indexOf('--schema');
|
|
1241
|
+
const messageId = process.argv[2] === '--schema' ? process.argv[schemaIndex + 1] : process.argv[2];
|
|
1242
|
+
const variantIndex = process.argv.indexOf('--variant');
|
|
1243
|
+
const variant = variantIndex >= 0 ? process.argv[variantIndex + 1] : 'DEFAULT';
|
|
1244
|
+
if (!messageId || messageId.startsWith('--')) throw new Error('用法: render_customer_message.mjs --schema <messageId> [--variant NAME]');
|
|
1245
|
+
console.log(JSON.stringify(getMessageSchema(messageId, variant), null, 2));
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
const messageId = process.argv[2];
|
|
1249
|
+
const variantIndex = process.argv.indexOf('--variant');
|
|
1250
|
+
const variant = variantIndex >= 0 ? process.argv[variantIndex + 1] : 'DEFAULT';
|
|
1251
|
+
if (!messageId) throw new Error('用法: render_customer_message.mjs <messageId> [--variant NAME]');
|
|
1252
|
+
assertManagedCliAllowed(messageId);
|
|
1253
|
+
const variables = await readStdin();
|
|
1254
|
+
console.log(renderMessageWithFallback(messageId, variant, variables));
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
1258
|
+
main().catch((error) => {
|
|
1259
|
+
console.error('当前步骤的标准消息无法安全生成,已停止后续动作。请按 MESSAGE_RENDER_ERROR 的具体原因修正输入;若消息目录或脚本缺失,再检查 Skill 文件完整性。');
|
|
1260
|
+
console.error(`MESSAGE_RENDER_ERROR:${error.message}`);
|
|
1261
|
+
process.exitCode = 1;
|
|
1262
|
+
});
|
|
1263
|
+
}
|