@alipay/alipay-aipay 1.5.2 → 1.5.4
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 +1 -1
- package/package.json +1 -1
- package/skill/VERSION +1 -1
- package/skill/references/integration/flow.md +2 -2
- package/skill/references/normal/json/customer-messages.json +2 -2
- package/skill/references/normal/json/policy-rules.json +6 -0
- package/skill/references/normal/scripts/onboarding_discovery_runner.mjs +137 -20
- package/skill/references/normal/scripts/onboarding_message_runner.mjs +144 -52
- package/skill/references/normal/scripts/render_customer_message.mjs +36 -3
- package/skill/references/normal/scripts/runtime.mjs +69 -0
- package/skill/references/normal/scripts/telemetry.mjs +11 -0
- package/skill/references/onboarding/flow.md +36 -22
- package/skill/references/onboarding/modules/app-release.md +3 -1
- package/skill/references/onboarding/modules/authorization.md +3 -2
- package/skill/references/onboarding/modules/error-handling.md +10 -4
- package/skill/references/onboarding/modules/product-sign.md +5 -1
- package/skill/references/onboarding/modules/scripts/app.sh +27 -10
- package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +6 -0
- package/skill/references/onboarding/modules/scripts/auth.sh +118 -61
- package/skill/references/onboarding/modules/scripts/error_handler.sh +147 -22
- package/skill/references/onboarding/modules/scripts/service.sh +9 -0
- package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +10 -1
- package/skill/references/onboarding/modules/service-registration.md +1 -1
- package/skill/references/runtime-entry.md +5 -5
package/README.md
CHANGED
package/package.json
CHANGED
package/skill/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.5.
|
|
1
|
+
1.5.4
|
|
@@ -426,7 +426,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message a2m
|
|
|
426
426
|
node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message a2m-execution-guard --project-path "$PROJECT_PATH" --service-url "$SERVICE_URL" --http-method POST --request-body-file "$POST_BODY_FILE" [--prior-receipt "$A2M_EXECUTION_RECEIPT"]
|
|
427
427
|
```
|
|
428
428
|
|
|
429
|
-
只接受唯一 `ALIPAY_AIPAY_INTERNAL:A2M_EXECUTION=<json>`。`decision=RUN` 时保存新 receipt 并继续;`REUSE_PASSED` 复用同一项目和输入的本轮通过证据,不重复联调;`BLOCK_UNCHANGED`
|
|
429
|
+
只接受唯一 `ALIPAY_AIPAY_INTERNAL:A2M_EXECUTION=<json>`。`decision=RUN` 时保存新 receipt 并继续;`REUSE_PASSED` 复用同一项目和输入的本轮通过证据,不重复联调;`BLOCK_UNCHANGED` 表示失败后代码、配置和输入均未变化,禁止重跑,将本轮 A2M 联调记录为失败并继续步骤 7/8;`UNKNOWN_PREVIOUS_EXECUTION` 表示上次命令可能已发出但未登记终态,当前没有登记的结果查询入口,禁止自动重跑或临时探测,固定记录联调结果未知并继续步骤 7/8。项目、服务 URL、方法或 POST body 变化时 runner 才会产生新的 `RUN` receipt。receipt 只保留在当前会话,不写跨会话业务进度。
|
|
430
430
|
|
|
431
431
|
4. 仅 `decision=RUN` 时执行下列固定 runner 命令,输出固定联调说明后立即继续,不等待用户确认;禁止另写开始话术。服务地址和 HTTP 方法只用于后续实际命令,不在本消息中展示:
|
|
432
432
|
|
|
@@ -464,7 +464,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" a2m run --url "$SERVICE
|
|
|
464
464
|
node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" a2m run --url "$SERVICE_URL" --method POST --body "@$POST_BODY_FILE" --buyer-id "$BUYER_2088" --auto-complete --require-payment-validation
|
|
465
465
|
```
|
|
466
466
|
|
|
467
|
-
只传已确认的本机服务 URL、GET/POST、实际沙箱买家 userId 和项目内 POST body 文件;禁止传入自定义收银端点、`Payment-Proof` 或 attestation。只有同一次命令退出码为 0,且输出包含实际资源交付校验通过和敏感产物已清理,才能记录联调通过;204、空响应、空资源、明确验付/履约失败或异常 `Payment-Validation` 均不得通过。命令结束后立即执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message a2m-execution-result --receipt "$A2M_EXECUTION_RECEIPT" --outcome <PASSED|FAILED>` 并保存返回的新 receipt;进程被中断且无法登记时保留原 receipt,由下次 guard 返回 `UNKNOWN_PREVIOUS_EXECUTION
|
|
467
|
+
只传已确认的本机服务 URL、GET/POST、实际沙箱买家 userId 和项目内 POST body 文件;禁止传入自定义收银端点、`Payment-Proof` 或 attestation。只有同一次命令退出码为 0,且输出包含实际资源交付校验通过和敏感产物已清理,才能记录联调通过;204、空响应、空资源、明确验付/履约失败或异常 `Payment-Validation` 均不得通过。命令结束后立即执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message a2m-execution-result --receipt "$A2M_EXECUTION_RECEIPT" --outcome <PASSED|FAILED>` 并保存返回的新 receipt;进程被中断且无法登记时保留原 receipt,由下次 guard 返回 `UNKNOWN_PREVIOUS_EXECUTION`。失败、阻断重复或结果未知都必须进入步骤 7 和最终 checklist 的失败/人工待验证项;只有项目代码、配置或执行输入实际变化并取得新 `RUN` receipt 才能重新联调。
|
|
468
468
|
|
|
469
469
|
服务未就绪时固定使用 `integration.service.not_ready` 输出实际缺口和恢复动作;必须执行下列命令。不得运行测试、登记 `INT.A2M.SERVICE_READY=PASS` 或临场改写近似话术。
|
|
470
470
|
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"trigger": "onboarding.windows_platform",
|
|
44
44
|
"blocking": false,
|
|
45
45
|
"allowedReplies": [],
|
|
46
|
-
"variables": [],
|
|
46
|
+
"variables": [{ "name": "officialUrl", "type": "officialTemporaryUrl", "required": true, "urlKind": "windowsOnboardingRedirect" }],
|
|
47
47
|
"requiredPolicyIds": [],
|
|
48
48
|
"variants": {
|
|
49
|
-
"DEFAULT": { "templateLines": ["当前为 Windows 系统,请您暂时前往支付宝 AI 付站点完成产品开通:", "
|
|
49
|
+
"DEFAULT": { "templateLines": ["当前为 Windows 系统,请您暂时前往支付宝 AI 付站点完成产品开通:", "{{officialUrl}}"] }
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
{
|
|
@@ -174,6 +174,12 @@
|
|
|
174
174
|
"path": "/public-key-confirm",
|
|
175
175
|
"requiredQueryParams": ["keyConfirmToken"],
|
|
176
176
|
"optionalQueryParams": []
|
|
177
|
+
},
|
|
178
|
+
"windowsOnboardingRedirect": {
|
|
179
|
+
"host": "aipay.alipay.com",
|
|
180
|
+
"path": "/",
|
|
181
|
+
"requiredQueryParams": ["from", "reason", "platform_id"],
|
|
182
|
+
"optionalQueryParams": []
|
|
177
183
|
}
|
|
178
184
|
}
|
|
179
185
|
}
|
|
@@ -22,10 +22,11 @@ const productSalesCodes = Object.freeze({
|
|
|
22
22
|
apppay: 'I1080300001000041313'
|
|
23
23
|
});
|
|
24
24
|
const signStatuses = new Set(['NOT_SIGNED', 'SIGN_SUBMITTED', 'SIGNED_EFFECTIVE', 'OTHER_STATUS']);
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const authSignReuseReceiptPrefix = 'AIPAY_SIGN_REUSE_V1.';
|
|
26
|
+
const signFlowByProduct = Object.freeze({
|
|
27
|
+
aipay: Object.freeze({ notSigned: 'FLOW:AI_PAY_NOT_SIGNED', signed: 'FLOW:AI_PAY_SIGNED' }),
|
|
28
|
+
webpay: Object.freeze({ notSigned: 'FLOW:PC_WEB_NOT_SIGNED', signed: 'FLOW:PC_WEB_SIGNED' }),
|
|
29
|
+
apppay: Object.freeze({ notSigned: 'FLOW:APP_NOT_SIGNED', signed: 'FLOW:APP_SIGNED' })
|
|
29
30
|
});
|
|
30
31
|
const serviceFlows = new Set(['FLOW:CREATE_NEW', 'FLOW:SELECT']);
|
|
31
32
|
const applicationFlows = new Set(['FLOW:CREATE_NEW', 'FLOW:SELECT', 'FLOW:PENDING_APPLICATIONS']);
|
|
@@ -69,6 +70,92 @@ function uniqueFlow(lines, allowed) {
|
|
|
69
70
|
return values.length === 1 ? values[0] : '';
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function allowedSignFlowValues(productType) {
|
|
74
|
+
const flows = signFlowByProduct[productType];
|
|
75
|
+
return new Set([flows.notSigned, flows.signed, 'FLOW:OTHER_STATUS']);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeSignFlow(value) {
|
|
79
|
+
if (typeof value !== 'string' || value.length === 0) return '';
|
|
80
|
+
return value.startsWith('FLOW:') ? value : `FLOW:${value}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function legalSignReuseDescription(productType) {
|
|
84
|
+
const flows = signFlowByProduct[productType];
|
|
85
|
+
return [
|
|
86
|
+
`NOT_SIGNED + ${flows.notSigned}`,
|
|
87
|
+
`SIGNED_EFFECTIVE + ${flows.signed}`,
|
|
88
|
+
`SIGN_SUBMITTED + ${flows.signed}`,
|
|
89
|
+
'OTHER_STATUS + FLOW:OTHER_STATUS'
|
|
90
|
+
].join(';');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assertValidSignReuse({ productType, salesCode, signStatus, flow }) {
|
|
94
|
+
if (!productSalesCodes[productType]) {
|
|
95
|
+
throw new Error('签约复用 productType 非法,仅支持 aipay、webpay、apppay');
|
|
96
|
+
}
|
|
97
|
+
if (salesCode !== productSalesCodes[productType]) {
|
|
98
|
+
throw new Error(`签约复用 salesCode 与 productType 不匹配;${productType} 必须使用 ${productSalesCodes[productType]}`);
|
|
99
|
+
}
|
|
100
|
+
const normalizedFlow = normalizeSignFlow(flow);
|
|
101
|
+
if (!signStatuses.has(signStatus) || !allowedSignFlowValues(productType).has(normalizedFlow)) {
|
|
102
|
+
throw new Error(`签约复用状态或 flow 非法;允许组合:${legalSignReuseDescription(productType)}`);
|
|
103
|
+
}
|
|
104
|
+
const flows = signFlowByProduct[productType];
|
|
105
|
+
const validPair =
|
|
106
|
+
(signStatus === 'NOT_SIGNED' && normalizedFlow === flows.notSigned)
|
|
107
|
+
|| ((signStatus === 'SIGNED_EFFECTIVE' || signStatus === 'SIGN_SUBMITTED') && normalizedFlow === flows.signed)
|
|
108
|
+
|| (signStatus === 'OTHER_STATUS' && normalizedFlow === 'FLOW:OTHER_STATUS');
|
|
109
|
+
if (!validPair) {
|
|
110
|
+
throw new Error(`签约复用 status/flow 组合矛盾;允许组合:${legalSignReuseDescription(productType)}`);
|
|
111
|
+
}
|
|
112
|
+
return { signStatus, flow: normalizedFlow };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function decodeBase64UrlJson(value) {
|
|
116
|
+
const normalized = value.replace(/-/gu, '+').replace(/_/gu, '/');
|
|
117
|
+
const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
|
|
118
|
+
return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseSignReuseReceipt(receipt, productType, salesCode) {
|
|
122
|
+
if (typeof receipt !== 'string' || !receipt.startsWith(authSignReuseReceiptPrefix)) {
|
|
123
|
+
throw new Error('签约复用回执格式非法;必须使用 AUTH_SIGN_REUSE_RECEIPT 原值');
|
|
124
|
+
}
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = decodeBase64UrlJson(receipt.slice(authSignReuseReceiptPrefix.length));
|
|
128
|
+
} catch {
|
|
129
|
+
throw new Error('签约复用回执无法解析;必须使用 AUTH_SIGN_REUSE_RECEIPT 原值');
|
|
130
|
+
}
|
|
131
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
132
|
+
throw new Error('签约复用回执内容非法');
|
|
133
|
+
}
|
|
134
|
+
const unknownKeys = Object.keys(parsed).filter((key) => ![
|
|
135
|
+
'schema_ver',
|
|
136
|
+
'product_type',
|
|
137
|
+
'sales_code',
|
|
138
|
+
'sign_status',
|
|
139
|
+
'flow'
|
|
140
|
+
].includes(key));
|
|
141
|
+
if (unknownKeys.length > 0) throw new Error(`签约复用回执包含未知字段:${unknownKeys.join('、')}`);
|
|
142
|
+
if (parsed.schema_ver !== 1) throw new Error('签约复用回执版本非法;当前仅支持 schema_ver=1');
|
|
143
|
+
if (parsed.product_type !== productType) throw new Error('签约复用回执与当前 product-type 不匹配');
|
|
144
|
+
if (parsed.sales_code !== salesCode) throw new Error('签约复用回执与当前 sales-code 不匹配');
|
|
145
|
+
const validated = assertValidSignReuse({
|
|
146
|
+
productType,
|
|
147
|
+
salesCode,
|
|
148
|
+
signStatus: parsed.sign_status,
|
|
149
|
+
flow: parsed.flow
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
query_status: 'SUCCESS',
|
|
153
|
+
source: 'AUTH_REUSE',
|
|
154
|
+
sign_status: validated.signStatus,
|
|
155
|
+
flow: validated.flow
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
72
159
|
function parseArray(value) {
|
|
73
160
|
if (!value) return null;
|
|
74
161
|
try {
|
|
@@ -83,7 +170,7 @@ function runScript(script, args) {
|
|
|
83
170
|
return new Promise((resolve) => {
|
|
84
171
|
const child = spawn('bash', [script, ...args], {
|
|
85
172
|
cwd: onboardingScriptsDir,
|
|
86
|
-
env: process.env,
|
|
173
|
+
env: { ...process.env, ALIPAY_AIPAY_SUPPRESS_AUTH_LOGOUT: '1' },
|
|
87
174
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
88
175
|
});
|
|
89
176
|
let stdout = '';
|
|
@@ -108,19 +195,45 @@ function failedBranch(result, extra = {}) {
|
|
|
108
195
|
function signingBranch(result, productType) {
|
|
109
196
|
const lines = internalLines(result.stderr);
|
|
110
197
|
const signStatus = uniqueValue(lines, 'SIGN_STATUS=');
|
|
111
|
-
const flow = uniqueFlow(lines,
|
|
198
|
+
const flow = uniqueFlow(lines, allowedSignFlowValues(productType));
|
|
112
199
|
if (result.status !== 0 || !signStatuses.has(signStatus) || !flow) return failedBranch(result);
|
|
113
|
-
|
|
200
|
+
try {
|
|
201
|
+
const validated = assertValidSignReuse({
|
|
202
|
+
productType,
|
|
203
|
+
salesCode: productSalesCodes[productType],
|
|
204
|
+
signStatus,
|
|
205
|
+
flow
|
|
206
|
+
});
|
|
207
|
+
return { query_status: 'SUCCESS', source: 'QUERY', sign_status: validated.signStatus, flow: validated.flow };
|
|
208
|
+
} catch {
|
|
209
|
+
return failedBranch(result);
|
|
210
|
+
}
|
|
114
211
|
}
|
|
115
212
|
|
|
116
213
|
function reusedSigningBranch(args, productType) {
|
|
214
|
+
const salesCode = productSalesCodes[productType];
|
|
215
|
+
const receipt = args['reuse-sign-receipt'];
|
|
117
216
|
const signStatus = args['reuse-sign-status'];
|
|
118
217
|
const flow = args['reuse-sign-flow'];
|
|
119
|
-
if (signStatus === undefined && flow === undefined) return null;
|
|
120
|
-
if (
|
|
121
|
-
|
|
218
|
+
if (receipt === undefined && signStatus === undefined && flow === undefined) return null;
|
|
219
|
+
if (receipt !== undefined) {
|
|
220
|
+
const fromReceipt = parseSignReuseReceipt(receipt, productType, salesCode);
|
|
221
|
+
if (signStatus !== undefined || flow !== undefined) {
|
|
222
|
+
if (signStatus === undefined || flow === undefined) {
|
|
223
|
+
throw new Error('签约复用旧双参数必须同时提供 --reuse-sign-status 与 --reuse-sign-flow');
|
|
224
|
+
}
|
|
225
|
+
const fromLegacy = assertValidSignReuse({ productType, salesCode, signStatus, flow });
|
|
226
|
+
if (fromLegacy.signStatus !== fromReceipt.sign_status || fromLegacy.flow !== fromReceipt.flow) {
|
|
227
|
+
throw new Error('签约复用回执与旧双参数冲突,禁止降级复用');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return fromReceipt;
|
|
231
|
+
}
|
|
232
|
+
if (signStatus === undefined || flow === undefined) {
|
|
233
|
+
throw new Error('签约复用旧双参数必须同时提供 --reuse-sign-status 与 --reuse-sign-flow');
|
|
122
234
|
}
|
|
123
|
-
|
|
235
|
+
const validated = assertValidSignReuse({ productType, salesCode, signStatus, flow });
|
|
236
|
+
return { query_status: 'SUCCESS', source: 'AUTH_REUSE', sign_status: validated.signStatus, flow: validated.flow };
|
|
124
237
|
}
|
|
125
238
|
|
|
126
239
|
function serviceBranch(result) {
|
|
@@ -146,7 +259,9 @@ function hasAuthBlocker(result) {
|
|
|
146
259
|
return internalLines(result.stderr).some((line) => [
|
|
147
260
|
'AUTH_FLOW:AUTH_REQUIRED',
|
|
148
261
|
'AUTH_FLOW:SCOPE_MISMATCH',
|
|
149
|
-
'AUTH_FLOW:MCC_MISMATCH'
|
|
262
|
+
'AUTH_FLOW:MCC_MISMATCH',
|
|
263
|
+
'AUTH_FLOW:RETRY_WITH_NETWORK',
|
|
264
|
+
'AUTH_FLOW:LOGOUT_STILL_LOGGED_IN'
|
|
150
265
|
].includes(line));
|
|
151
266
|
}
|
|
152
267
|
|
|
@@ -191,27 +306,29 @@ async function runDiscovery(args) {
|
|
|
191
306
|
if (globalBlocker) envelope.global_blocker = globalBlocker;
|
|
192
307
|
emitEnvelope(envelope);
|
|
193
308
|
|
|
194
|
-
|
|
309
|
+
const customerOutputResults = globalBlocker
|
|
310
|
+
? executedResults.filter(hasAuthBlocker)
|
|
311
|
+
: executedResults;
|
|
312
|
+
const customerOutputs = customerOutputResults
|
|
195
313
|
.map((result) => result.stdout.trim())
|
|
196
314
|
.filter(Boolean);
|
|
197
|
-
|
|
198
|
-
if (globalBlocker)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
customerOutputs.push(renderDiscoverySummary(productType, {
|
|
315
|
+
let uniqueCustomerOutputs = [...new Set(customerOutputs)];
|
|
316
|
+
if (globalBlocker) uniqueCustomerOutputs = uniqueCustomerOutputs.slice(0, 1);
|
|
317
|
+
if (!globalBlocker) {
|
|
318
|
+
uniqueCustomerOutputs.push(renderDiscoverySummary(productType, {
|
|
202
319
|
signStatus: envelope.signing.query_status === 'SUCCESS' ? envelope.signing.sign_status : 'QUERY_FAILED',
|
|
203
320
|
serviceFlow: envelope.service.query_status === 'SUCCESS' ? envelope.service.flow : 'QUERY_FAILED',
|
|
204
321
|
appFlow: envelope.application.query_status === 'SUCCESS' ? envelope.application.flow : 'QUERY_FAILED'
|
|
205
322
|
}));
|
|
206
323
|
}
|
|
207
|
-
if (
|
|
324
|
+
if (uniqueCustomerOutputs.length > 0) process.stdout.write(`${uniqueCustomerOutputs.join('\n\n')}\n`);
|
|
208
325
|
return globalBlocker ? 2 : 0;
|
|
209
326
|
}
|
|
210
327
|
|
|
211
328
|
export async function runOnboardingDiscoveryCli(argv = process.argv) {
|
|
212
329
|
const { command, args } = parseArgs(argv);
|
|
213
330
|
if (command !== 'run') {
|
|
214
|
-
throw new Error('用法: onboarding_discovery_runner.mjs run --product-type <aipay|webpay|apppay> --sales-code <code> [--reuse-sign-status <status> --reuse-sign-flow <flow>]');
|
|
331
|
+
throw new Error('用法: onboarding_discovery_runner.mjs run --product-type <aipay|webpay|apppay> --sales-code <code> [--reuse-sign-receipt <AUTH_SIGN_REUSE_RECEIPT> | --reuse-sign-status <status> --reuse-sign-flow <flow>]');
|
|
215
332
|
}
|
|
216
333
|
process.exitCode = await runDiscovery(args);
|
|
217
334
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import process from 'node:process';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { isWindowsPlatform } from './platform_compat.mjs';
|
|
7
|
-
import { loadMccRecords, renderMessage } from './render_customer_message.mjs';
|
|
8
|
-
import { emitStageMetricAsync, emitTerminalMetric } from './telemetry.mjs';
|
|
8
|
+
import { buildWindowsOnboardingRedirectUrl, loadMccRecords, renderMessage } from './render_customer_message.mjs';
|
|
9
|
+
import { currentRunContext, emitStageMetricAsync, emitTerminalMetric } from './telemetry.mjs';
|
|
9
10
|
|
|
10
11
|
const productTypes = {
|
|
11
12
|
aipay: { variant: 'AIPAY', productName: 'AI 按量付费' },
|
|
@@ -93,7 +94,9 @@ function parseArgs(argv) {
|
|
|
93
94
|
if (!key.startsWith('--')) throw new Error(`未知参数 ${key}`);
|
|
94
95
|
const value = argv[index + 1];
|
|
95
96
|
if (value === undefined || value.startsWith('--')) throw new Error(`参数 ${key} 缺少取值`);
|
|
96
|
-
|
|
97
|
+
const name = key.slice(2);
|
|
98
|
+
if (Object.prototype.hasOwnProperty.call(args, name)) throw new Error(`参数 ${key} 重复`);
|
|
99
|
+
args[name] = value;
|
|
97
100
|
index += 1;
|
|
98
101
|
}
|
|
99
102
|
return { command, args };
|
|
@@ -126,9 +129,14 @@ async function readStdin() {
|
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
function runPlatformEntry() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
if (!isWindowsPlatform()) return '';
|
|
133
|
+
const runContext = currentRunContext();
|
|
134
|
+
if (!runContext || runContext.terminal || !['onboarding_only', 'full_process'].includes(runContext.root_intent)) {
|
|
135
|
+
throw new Error('Windows 产品开通引导缺少有效的当前 Onboarding run context');
|
|
136
|
+
}
|
|
137
|
+
return renderMessage('onboarding.windows.redirect', 'DEFAULT', {
|
|
138
|
+
officialUrl: buildWindowsOnboardingRedirectUrl(runContext.run_id)
|
|
139
|
+
});
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
function optional(args, name) {
|
|
@@ -183,8 +191,34 @@ const mccAliasRules = [
|
|
|
183
191
|
targetTerms: ['电商', '网上商城', '线上商城']
|
|
184
192
|
}
|
|
185
193
|
];
|
|
194
|
+
const agentMccStatuses = new Set(['MATCHED', 'INSUFFICIENT', 'UNSUPPORTED']);
|
|
195
|
+
|
|
196
|
+
function mccCandidateFromRecord(record) {
|
|
197
|
+
return {
|
|
198
|
+
mccCode: record.code,
|
|
199
|
+
mccName: record.name,
|
|
200
|
+
applicableBusinesses: record.applicableBusinesses,
|
|
201
|
+
qualification: record.qualification
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function mccTableSummary(records = loadMccRecords()) {
|
|
206
|
+
const entries = [...records.values()]
|
|
207
|
+
.map((record) => [record.code, record.name, record.applicableBusinesses, record.qualification].join('\t'))
|
|
208
|
+
.sort()
|
|
209
|
+
.join('\n');
|
|
210
|
+
return {
|
|
211
|
+
recordCount: records.size,
|
|
212
|
+
sha256: createHash('sha256').update(entries).digest('hex')
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function directMccCode(query) {
|
|
217
|
+
const raw = query.trim().toUpperCase();
|
|
218
|
+
return /^A\d{4}_B\d{4}$/u.test(raw) ? raw : '';
|
|
219
|
+
}
|
|
186
220
|
|
|
187
|
-
function
|
|
221
|
+
function deterministicMccMatches(query, { limit = 8 } = {}) {
|
|
188
222
|
const normalizedQuery = assertMccQuery(query);
|
|
189
223
|
const scored = [];
|
|
190
224
|
for (const record of loadMccRecords().values()) {
|
|
@@ -201,43 +235,114 @@ function mccSearchResult(query) {
|
|
|
201
235
|
score = Math.max(score, 50);
|
|
202
236
|
}
|
|
203
237
|
}
|
|
204
|
-
if (score > 0) scored.push({ record, score });
|
|
238
|
+
if (score > 0) scored.push({ record, score, strength: score >= 70 ? 'STRONG' : 'WEAK' });
|
|
205
239
|
}
|
|
206
240
|
const strongMatches = scored.filter(({ score }) => score >= 70);
|
|
207
241
|
const ranked = strongMatches.length > 0 ? strongMatches : scored;
|
|
208
|
-
|
|
242
|
+
return ranked
|
|
209
243
|
.sort((left, right) => right.score - left.score || left.record.code.localeCompare(right.record.code))
|
|
210
|
-
.slice(0,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
244
|
+
.slice(0, limit);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function parseAgentMccCodes(args) {
|
|
248
|
+
const raw = optional(args, 'agent-mcc-codes') || '';
|
|
249
|
+
const codes = raw.trim()
|
|
250
|
+
? raw.split(/[,,、\s]+/u).map((code) => code.trim().toUpperCase()).filter(Boolean)
|
|
251
|
+
: [];
|
|
252
|
+
if (codes.length > 3) throw new Error('agent-mcc-codes 最多允许 3 个编码');
|
|
253
|
+
if (new Set(codes).size !== codes.length) throw new Error('agent-mcc-codes 包含重复 MCC 编码');
|
|
254
|
+
return codes;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function agentMccInput(args, query, records) {
|
|
258
|
+
const exactCode = directMccCode(query);
|
|
259
|
+
if (exactCode && records.has(exactCode) && args['agent-mcc-status'] === undefined && args['agent-mcc-codes'] === undefined) {
|
|
260
|
+
return {
|
|
261
|
+
semanticStatus: 'NOT_REQUIRED_DIRECT_CODE',
|
|
262
|
+
codes: [],
|
|
263
|
+
directCode: exactCode
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const semanticStatus = required(args, 'agent-mcc-status');
|
|
268
|
+
if (!agentMccStatuses.has(semanticStatus)) {
|
|
269
|
+
throw new Error('agent-mcc-status 非法,仅支持 MATCHED、INSUFFICIENT、UNSUPPORTED');
|
|
270
|
+
}
|
|
271
|
+
const codes = parseAgentMccCodes(args);
|
|
272
|
+
if (semanticStatus === 'MATCHED' && codes.length === 0) throw new Error('agent-mcc-status=MATCHED 时必须提供 1 到 3 个 agent-mcc-codes');
|
|
273
|
+
if (semanticStatus !== 'MATCHED' && codes.length > 0) throw new Error('agent-mcc-status=INSUFFICIENT|UNSUPPORTED 时不得提供 agent-mcc-codes');
|
|
274
|
+
for (const code of codes) {
|
|
275
|
+
if (!/^A\d{4}_B\d{4}$/u.test(code)) throw new Error(`agent-mcc-codes 包含非法 MCC 编码:${code}`);
|
|
276
|
+
if (!records.has(code)) throw new Error(`agent-mcc-codes 包含当前 MCC 表不存在的编码:${code}`);
|
|
277
|
+
}
|
|
278
|
+
return { semanticStatus, codes, directCode: '' };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function resolveMccResult(query, args) {
|
|
282
|
+
const records = loadMccRecords();
|
|
283
|
+
const agent = agentMccInput(args, query, records);
|
|
284
|
+
if (agent.directCode) {
|
|
285
|
+
const candidate = mccCandidateFromRecord(records.get(agent.directCode));
|
|
286
|
+
return {
|
|
287
|
+
matchType: 'UNIQUE',
|
|
288
|
+
semanticStatus: agent.semanticStatus,
|
|
289
|
+
deterministicCandidateCodes: [agent.directCode],
|
|
290
|
+
deterministicStrongCandidateCodes: [agent.directCode],
|
|
291
|
+
deterministicWeakCandidateCodes: [],
|
|
292
|
+
agentCandidateCodes: [],
|
|
293
|
+
mergedCandidateCodes: [agent.directCode],
|
|
294
|
+
mccTableSummary: mccTableSummary(records),
|
|
295
|
+
candidates: [candidate]
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const deterministicMatches = deterministicMccMatches(query, { limit: 5 });
|
|
300
|
+
const deterministicStrongCodes = deterministicMatches.filter(({ strength }) => strength === 'STRONG').map(({ record }) => record.code);
|
|
301
|
+
const deterministicWeakCodes = deterministicMatches.filter(({ strength }) => strength === 'WEAK').map(({ record }) => record.code);
|
|
302
|
+
const deterministicCodes = deterministicMatches.map(({ record }) => record.code);
|
|
303
|
+
const agentCodes = agent.codes;
|
|
304
|
+
if (agent.semanticStatus !== 'MATCHED') {
|
|
305
|
+
const finalCodes = deterministicStrongCodes.slice(0, 8);
|
|
306
|
+
const candidates = finalCodes.map((code) => mccCandidateFromRecord(records.get(code)));
|
|
307
|
+
return {
|
|
308
|
+
matchType: candidates.length === 1 ? 'UNIQUE' : candidates.length === 0 ? 'NONE' : 'MULTIPLE',
|
|
309
|
+
semanticStatus: agent.semanticStatus,
|
|
310
|
+
deterministicCandidateCodes: deterministicCodes,
|
|
311
|
+
deterministicStrongCandidateCodes: deterministicStrongCodes,
|
|
312
|
+
deterministicWeakCandidateCodes: deterministicWeakCodes,
|
|
313
|
+
agentCandidateCodes: agentCodes,
|
|
314
|
+
mergedCandidateCodes: finalCodes,
|
|
315
|
+
mccTableSummary: mccTableSummary(records),
|
|
316
|
+
candidates
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const deterministicCodeSet = new Set(deterministicCodes);
|
|
320
|
+
const agentCodeSet = new Set(agentCodes);
|
|
321
|
+
const appendUnique = (target, codes) => {
|
|
322
|
+
for (const code of codes) {
|
|
323
|
+
if (!target.includes(code)) target.push(code);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
const mergedCodes = [];
|
|
327
|
+
appendUnique(mergedCodes, deterministicCodes.filter((code) => agentCodeSet.has(code)));
|
|
328
|
+
appendUnique(mergedCodes, deterministicStrongCodes.filter((code) => !agentCodeSet.has(code)));
|
|
329
|
+
appendUnique(mergedCodes, agentCodes.filter((code) => !deterministicCodeSet.has(code)));
|
|
330
|
+
appendUnique(mergedCodes, deterministicWeakCodes.filter((code) => !agentCodeSet.has(code)));
|
|
331
|
+
const finalCodes = mergedCodes.slice(0, 8);
|
|
332
|
+
const candidates = finalCodes.map((code) => mccCandidateFromRecord(records.get(code)));
|
|
217
333
|
return {
|
|
218
334
|
matchType: candidates.length === 1 ? 'UNIQUE' : candidates.length === 0 ? 'NONE' : 'MULTIPLE',
|
|
335
|
+
semanticStatus: agent.semanticStatus,
|
|
336
|
+
deterministicCandidateCodes: deterministicCodes,
|
|
337
|
+
deterministicStrongCandidateCodes: deterministicStrongCodes,
|
|
338
|
+
deterministicWeakCandidateCodes: deterministicWeakCodes,
|
|
339
|
+
agentCandidateCodes: agentCodes,
|
|
340
|
+
mergedCandidateCodes: finalCodes,
|
|
341
|
+
mccTableSummary: mccTableSummary(records),
|
|
219
342
|
candidates
|
|
220
343
|
};
|
|
221
344
|
}
|
|
222
345
|
|
|
223
|
-
async function runMccSearch(args) {
|
|
224
|
-
const productType = args['product-type'];
|
|
225
|
-
if (productType !== undefined) productFor({ ...args, 'product-type': productType });
|
|
226
|
-
emitStageMetricAsync('onboarding.mcc_prepare.start', { productType });
|
|
227
|
-
const query = required(args, 'query');
|
|
228
|
-
return JSON.stringify(mccSearchResult(query));
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function runMccCandidates(args) {
|
|
232
|
-
const productType = required(args, 'product-type');
|
|
233
|
-
productFor({ ...args, 'product-type': productType });
|
|
234
|
-
const result = mccSearchResult(required(args, 'query'));
|
|
235
|
-
if (result.candidates.length === 0) return renderMessage('onboarding.mcc.no_match', 'DEFAULT', {}, { productType });
|
|
236
|
-
return renderMessage('onboarding.mcc.candidates', 'DEFAULT', {
|
|
237
|
-
mccCandidates: result.candidates
|
|
238
|
-
}, { productType });
|
|
239
|
-
}
|
|
240
|
-
|
|
241
346
|
function renderPlanConfirm(productType, mccCandidate) {
|
|
242
347
|
const product = productFor({ 'product-type': productType });
|
|
243
348
|
return renderMessage('onboarding.plan.confirm', product.variant, {
|
|
@@ -257,14 +362,14 @@ async function runPlanConfirm(args) {
|
|
|
257
362
|
});
|
|
258
363
|
}
|
|
259
364
|
|
|
260
|
-
async function
|
|
365
|
+
async function runMccResolvePlan(args) {
|
|
261
366
|
const productType = required(args, 'product-type');
|
|
262
367
|
productFor({ ...args, 'product-type': productType });
|
|
263
368
|
emitStageMetricAsync('onboarding.mcc_prepare.start', { productType });
|
|
264
|
-
const result =
|
|
369
|
+
const result = resolveMccResult(required(args, 'query'), args);
|
|
265
370
|
process.stderr.write(`ALIPAY_AIPAY_INTERNAL:MCC_RESOLUTION=${JSON.stringify(result)}\n`);
|
|
266
371
|
if (result.matchType === 'UNIQUE') {
|
|
267
|
-
return
|
|
372
|
+
return renderPlanConfirm(productType, result.candidates[0]);
|
|
268
373
|
}
|
|
269
374
|
if (result.matchType === 'NONE') {
|
|
270
375
|
return renderMessage('onboarding.mcc.no_match', 'DEFAULT', {}, { productType });
|
|
@@ -561,21 +666,8 @@ export async function runOnboardingMessageCli(argv = process.argv) {
|
|
|
561
666
|
console.log(await runMccClarify(args));
|
|
562
667
|
return;
|
|
563
668
|
}
|
|
564
|
-
if (command === 'mcc-search') {
|
|
565
|
-
console.log(await runMccSearch(args));
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
if (command === 'mcc-candidates') {
|
|
569
|
-
console.log(await runMccCandidates(args));
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
if (command === 'mcc-resolve') {
|
|
573
|
-
const message = await runMccResolve(args);
|
|
574
|
-
if (message) console.log(message);
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
669
|
if (command === 'mcc-resolve-plan') {
|
|
578
|
-
const message = await
|
|
670
|
+
const message = await runMccResolvePlan(args);
|
|
579
671
|
if (message) console.log(message);
|
|
580
672
|
return;
|
|
581
673
|
}
|
|
@@ -632,7 +724,7 @@ export async function runOnboardingMessageCli(argv = process.argv) {
|
|
|
632
724
|
console.log(runApplicationResult(args));
|
|
633
725
|
return;
|
|
634
726
|
}
|
|
635
|
-
throw new Error('用法: onboarding_message_runner.mjs <platform-entry|product-clarify|enterprise-unsupported|mcc-resolve
|
|
727
|
+
throw new Error('用法: onboarding_message_runner.mjs <platform-entry|product-clarify|enterprise-unsupported|mcc-resolve-plan|plan-confirm|mcc-clarify|material-collect|application-candidates|service-candidates|application-recheck|closeout|service-create-summary|service-update-confirm|application-harmony-manual-create|application-reuse-no-key|signing-result|service-result|application-result> ...');
|
|
636
728
|
}
|
|
637
729
|
|
|
638
730
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
@@ -140,6 +140,26 @@ function validateUrl(value, policy, urlKind, expected = {}) {
|
|
|
140
140
|
}
|
|
141
141
|
if (/[\r\n]/.test(parsed.searchParams.get('keyConfirmToken'))) throw new Error('publicKeyConfirmation URL keyConfirmToken 无效');
|
|
142
142
|
}
|
|
143
|
+
if (urlKind === 'windowsOnboardingRedirect') {
|
|
144
|
+
const required = allowed.requiredQueryParams || ['from', 'reason', 'platform_id'];
|
|
145
|
+
const allowedKeys = new Set(required);
|
|
146
|
+
const actualKeys = [...parsed.searchParams.keys()];
|
|
147
|
+
if (actualKeys.some((key) => !allowedKeys.has(key))) throw new Error('windowsOnboardingRedirect URL 包含未允许的查询参数');
|
|
148
|
+
for (const key of required) {
|
|
149
|
+
if (parsed.searchParams.getAll(key).length !== 1 || !parsed.searchParams.get(key)) {
|
|
150
|
+
throw new Error('windowsOnboardingRedirect URL 缺少必要查询参数或参数重复');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (parsed.searchParams.get('from') !== 'alipay-aipay-skill') {
|
|
154
|
+
throw new Error('windowsOnboardingRedirect URL from 无效');
|
|
155
|
+
}
|
|
156
|
+
if (parsed.searchParams.get('reason') !== 'alipay-aipay-skill-onboarding-not-support-windows') {
|
|
157
|
+
throw new Error('windowsOnboardingRedirect URL reason 无效');
|
|
158
|
+
}
|
|
159
|
+
if (/[\u0000-\u001f\u007f,^=]/u.test(parsed.searchParams.get('platform_id'))) {
|
|
160
|
+
throw new Error('windowsOnboardingRedirect URL platform_id 无效');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
143
163
|
return parsed;
|
|
144
164
|
}
|
|
145
165
|
|
|
@@ -191,6 +211,19 @@ export function buildAuthorizationUrl(input) {
|
|
|
191
211
|
return value;
|
|
192
212
|
}
|
|
193
213
|
|
|
214
|
+
export function buildWindowsOnboardingRedirectUrl(platformId) {
|
|
215
|
+
if (typeof platformId !== 'string' || !platformId || /[\u0000-\u001f\u007f,^=]/u.test(platformId)) {
|
|
216
|
+
throw new Error('Windows 产品开通引导缺少合法的当前流程 run_id');
|
|
217
|
+
}
|
|
218
|
+
const url = new URL('https://aipay.alipay.com/');
|
|
219
|
+
url.searchParams.set('from', 'alipay-aipay-skill');
|
|
220
|
+
url.searchParams.set('reason', 'alipay-aipay-skill-onboarding-not-support-windows');
|
|
221
|
+
url.searchParams.set('platform_id', platformId);
|
|
222
|
+
const value = url.toString();
|
|
223
|
+
validateUrl(value, loadJson(policyPath), 'windowsOnboardingRedirect');
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
|
|
194
227
|
function validateValue(definition, value, policy) {
|
|
195
228
|
if (value === undefined || value === null || value === '') {
|
|
196
229
|
if (definition.required) throw new Error(`缺少必填变量 ${definition.name}`);
|
|
@@ -586,11 +619,11 @@ const managedEntrypoints = {
|
|
|
586
619
|
reason: '产品已明确但用户尚未提供 MCC/经营描述时由 runner 绑定产品名称,禁止手写经营类目补问;检索无匹配时不得回到本消息。'
|
|
587
620
|
},
|
|
588
621
|
'onboarding.mcc.no_match': {
|
|
589
|
-
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-
|
|
622
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-resolve-plan --query <business-description> --product-type <aipay|webpay|apppay> --agent-mcc-status <MATCHED|INSUFFICIENT|UNSUPPORTED> [--agent-mcc-codes <Axxxx_Bxxxx,...>]',
|
|
590
623
|
reason: '用户已经提供经营描述但当前 MCC 表无确定匹配时,由 runner 输出独立恢复路径,禁止重新渲染初始 mcc-clarify 形成循环。'
|
|
591
624
|
},
|
|
592
625
|
'onboarding.mcc.candidates': {
|
|
593
|
-
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-
|
|
626
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-resolve-plan --query <business-description> --product-type <aipay|webpay|apppay> --agent-mcc-status <MATCHED|INSUFFICIENT|UNSUPPORTED> [--agent-mcc-codes <Axxxx_Bxxxx,...>]',
|
|
594
627
|
reason: '产品已明确且 MCC 存在多个相似候选时由 runner 绑定本地 MCC 表生成候选事实,禁止手写候选表。'
|
|
595
628
|
},
|
|
596
629
|
'onboarding.discovery.summary': {
|
|
@@ -598,7 +631,7 @@ const managedEntrypoints = {
|
|
|
598
631
|
reason: '该 runner 执行本轮适用查询、校验原始脚本事实、输出唯一 DISCOVERY_RESULT envelope,并在同一次 stdout 生成摘要;禁止单独重放摘要。'
|
|
599
632
|
},
|
|
600
633
|
'onboarding.plan.confirm': {
|
|
601
|
-
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-resolve-plan --query <经营描述|MCC
|
|
634
|
+
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-resolve-plan --query <经营描述|MCC编码> --product-type <aipay|webpay|apppay> [--agent-mcc-status <MATCHED|INSUFFICIENT|UNSUPPORTED> --agent-mcc-codes <Axxxx_Bxxxx,...>],或已持有唯一 MCC 事实时使用同一入口执行 onboarding-message plan-confirm --product-type <...> --mcc-code <...> --mcc-name <...> --mcc-qualification <...>',
|
|
602
635
|
reason: '方案展示必须由 runner 绑定本轮唯一 MCC 事实和产品变体生成,避免 Agent 用 jq/printf/管道手写 renderer 输入。'
|
|
603
636
|
},
|
|
604
637
|
'materials.category.collect': {
|