@alipay/alipay-aipay 1.5.2 → 1.5.3
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/scripts/onboarding_discovery_runner.mjs +128 -13
- package/skill/references/normal/scripts/onboarding_message_runner.mjs +132 -47
- package/skill/references/normal/scripts/render_customer_message.mjs +3 -3
- package/skill/references/normal/scripts/runtime.mjs +69 -0
- package/skill/references/onboarding/flow.md +33 -21
- 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 +6 -2
- 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 +127 -37
- package/skill/references/onboarding/modules/scripts/error_handler.sh +108 -20
- 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.3
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -211,7 +326,7 @@ async function runDiscovery(args) {
|
|
|
211
326
|
export async function runOnboardingDiscoveryCli(argv = process.argv) {
|
|
212
327
|
const { command, args } = parseArgs(argv);
|
|
213
328
|
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>]');
|
|
329
|
+
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
330
|
}
|
|
216
331
|
process.exitCode = await runDiscovery(args);
|
|
217
332
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
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';
|
|
@@ -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 };
|
|
@@ -183,8 +186,34 @@ const mccAliasRules = [
|
|
|
183
186
|
targetTerms: ['电商', '网上商城', '线上商城']
|
|
184
187
|
}
|
|
185
188
|
];
|
|
189
|
+
const agentMccStatuses = new Set(['MATCHED', 'INSUFFICIENT', 'UNSUPPORTED']);
|
|
186
190
|
|
|
187
|
-
function
|
|
191
|
+
function mccCandidateFromRecord(record) {
|
|
192
|
+
return {
|
|
193
|
+
mccCode: record.code,
|
|
194
|
+
mccName: record.name,
|
|
195
|
+
applicableBusinesses: record.applicableBusinesses,
|
|
196
|
+
qualification: record.qualification
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function mccTableSummary(records = loadMccRecords()) {
|
|
201
|
+
const entries = [...records.values()]
|
|
202
|
+
.map((record) => [record.code, record.name, record.applicableBusinesses, record.qualification].join('\t'))
|
|
203
|
+
.sort()
|
|
204
|
+
.join('\n');
|
|
205
|
+
return {
|
|
206
|
+
recordCount: records.size,
|
|
207
|
+
sha256: createHash('sha256').update(entries).digest('hex')
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function directMccCode(query) {
|
|
212
|
+
const raw = query.trim().toUpperCase();
|
|
213
|
+
return /^A\d{4}_B\d{4}$/u.test(raw) ? raw : '';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function deterministicMccMatches(query, { limit = 8 } = {}) {
|
|
188
217
|
const normalizedQuery = assertMccQuery(query);
|
|
189
218
|
const scored = [];
|
|
190
219
|
for (const record of loadMccRecords().values()) {
|
|
@@ -201,43 +230,112 @@ function mccSearchResult(query) {
|
|
|
201
230
|
score = Math.max(score, 50);
|
|
202
231
|
}
|
|
203
232
|
}
|
|
204
|
-
if (score > 0) scored.push({ record, score });
|
|
233
|
+
if (score > 0) scored.push({ record, score, strength: score >= 70 ? 'STRONG' : 'WEAK' });
|
|
205
234
|
}
|
|
206
235
|
const strongMatches = scored.filter(({ score }) => score >= 70);
|
|
207
236
|
const ranked = strongMatches.length > 0 ? strongMatches : scored;
|
|
208
|
-
|
|
237
|
+
return ranked
|
|
209
238
|
.sort((left, right) => right.score - left.score || left.record.code.localeCompare(right.record.code))
|
|
210
|
-
.slice(0,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
239
|
+
.slice(0, limit);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function parseAgentMccCodes(args) {
|
|
243
|
+
const raw = optional(args, 'agent-mcc-codes') || '';
|
|
244
|
+
const codes = raw.trim()
|
|
245
|
+
? raw.split(/[,,、\s]+/u).map((code) => code.trim().toUpperCase()).filter(Boolean)
|
|
246
|
+
: [];
|
|
247
|
+
if (codes.length > 3) throw new Error('agent-mcc-codes 最多允许 3 个编码');
|
|
248
|
+
if (new Set(codes).size !== codes.length) throw new Error('agent-mcc-codes 包含重复 MCC 编码');
|
|
249
|
+
return codes;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function agentMccInput(args, query, records) {
|
|
253
|
+
const exactCode = directMccCode(query);
|
|
254
|
+
if (exactCode && records.has(exactCode) && args['agent-mcc-status'] === undefined && args['agent-mcc-codes'] === undefined) {
|
|
255
|
+
return {
|
|
256
|
+
semanticStatus: 'NOT_REQUIRED_DIRECT_CODE',
|
|
257
|
+
codes: [],
|
|
258
|
+
directCode: exactCode
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const semanticStatus = required(args, 'agent-mcc-status');
|
|
263
|
+
if (!agentMccStatuses.has(semanticStatus)) {
|
|
264
|
+
throw new Error('agent-mcc-status 非法,仅支持 MATCHED、INSUFFICIENT、UNSUPPORTED');
|
|
265
|
+
}
|
|
266
|
+
const codes = parseAgentMccCodes(args);
|
|
267
|
+
if (semanticStatus === 'MATCHED' && codes.length === 0) throw new Error('agent-mcc-status=MATCHED 时必须提供 1 到 3 个 agent-mcc-codes');
|
|
268
|
+
if (semanticStatus !== 'MATCHED' && codes.length > 0) throw new Error('agent-mcc-status=INSUFFICIENT|UNSUPPORTED 时不得提供 agent-mcc-codes');
|
|
269
|
+
for (const code of codes) {
|
|
270
|
+
if (!/^A\d{4}_B\d{4}$/u.test(code)) throw new Error(`agent-mcc-codes 包含非法 MCC 编码:${code}`);
|
|
271
|
+
if (!records.has(code)) throw new Error(`agent-mcc-codes 包含当前 MCC 表不存在的编码:${code}`);
|
|
272
|
+
}
|
|
273
|
+
return { semanticStatus, codes, directCode: '' };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function resolveMccResult(query, args) {
|
|
277
|
+
const records = loadMccRecords();
|
|
278
|
+
const agent = agentMccInput(args, query, records);
|
|
279
|
+
if (agent.directCode) {
|
|
280
|
+
const candidate = mccCandidateFromRecord(records.get(agent.directCode));
|
|
281
|
+
return {
|
|
282
|
+
matchType: 'UNIQUE',
|
|
283
|
+
semanticStatus: agent.semanticStatus,
|
|
284
|
+
deterministicCandidateCodes: [agent.directCode],
|
|
285
|
+
deterministicStrongCandidateCodes: [agent.directCode],
|
|
286
|
+
deterministicWeakCandidateCodes: [],
|
|
287
|
+
agentCandidateCodes: [],
|
|
288
|
+
mergedCandidateCodes: [agent.directCode],
|
|
289
|
+
mccTableSummary: mccTableSummary(records),
|
|
290
|
+
candidates: [candidate]
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const deterministicMatches = deterministicMccMatches(query, { limit: 5 });
|
|
295
|
+
const deterministicStrongCodes = deterministicMatches.filter(({ strength }) => strength === 'STRONG').map(({ record }) => record.code);
|
|
296
|
+
const deterministicWeakCodes = deterministicMatches.filter(({ strength }) => strength === 'WEAK').map(({ record }) => record.code);
|
|
297
|
+
const deterministicCodes = deterministicMatches.map(({ record }) => record.code);
|
|
298
|
+
const agentCodes = agent.codes;
|
|
299
|
+
if (agent.semanticStatus !== 'MATCHED') {
|
|
300
|
+
return {
|
|
301
|
+
matchType: 'NONE',
|
|
302
|
+
semanticStatus: agent.semanticStatus,
|
|
303
|
+
deterministicCandidateCodes: deterministicCodes,
|
|
304
|
+
deterministicStrongCandidateCodes: deterministicStrongCodes,
|
|
305
|
+
deterministicWeakCandidateCodes: deterministicWeakCodes,
|
|
306
|
+
agentCandidateCodes: agentCodes,
|
|
307
|
+
mergedCandidateCodes: [],
|
|
308
|
+
mccTableSummary: mccTableSummary(records),
|
|
309
|
+
candidates: []
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const deterministicCodeSet = new Set(deterministicCodes);
|
|
313
|
+
const agentCodeSet = new Set(agentCodes);
|
|
314
|
+
const appendUnique = (target, codes) => {
|
|
315
|
+
for (const code of codes) {
|
|
316
|
+
if (!target.includes(code)) target.push(code);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const mergedCodes = [];
|
|
320
|
+
appendUnique(mergedCodes, deterministicCodes.filter((code) => agentCodeSet.has(code)));
|
|
321
|
+
appendUnique(mergedCodes, deterministicStrongCodes.filter((code) => !agentCodeSet.has(code)));
|
|
322
|
+
appendUnique(mergedCodes, agentCodes.filter((code) => !deterministicCodeSet.has(code)));
|
|
323
|
+
appendUnique(mergedCodes, deterministicWeakCodes.filter((code) => !agentCodeSet.has(code)));
|
|
324
|
+
const finalCodes = mergedCodes.slice(0, 8);
|
|
325
|
+
const candidates = finalCodes.map((code) => mccCandidateFromRecord(records.get(code)));
|
|
217
326
|
return {
|
|
218
327
|
matchType: candidates.length === 1 ? 'UNIQUE' : candidates.length === 0 ? 'NONE' : 'MULTIPLE',
|
|
328
|
+
semanticStatus: agent.semanticStatus,
|
|
329
|
+
deterministicCandidateCodes: deterministicCodes,
|
|
330
|
+
deterministicStrongCandidateCodes: deterministicStrongCodes,
|
|
331
|
+
deterministicWeakCandidateCodes: deterministicWeakCodes,
|
|
332
|
+
agentCandidateCodes: agentCodes,
|
|
333
|
+
mergedCandidateCodes: finalCodes,
|
|
334
|
+
mccTableSummary: mccTableSummary(records),
|
|
219
335
|
candidates
|
|
220
336
|
};
|
|
221
337
|
}
|
|
222
338
|
|
|
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
339
|
function renderPlanConfirm(productType, mccCandidate) {
|
|
242
340
|
const product = productFor({ 'product-type': productType });
|
|
243
341
|
return renderMessage('onboarding.plan.confirm', product.variant, {
|
|
@@ -257,14 +355,14 @@ async function runPlanConfirm(args) {
|
|
|
257
355
|
});
|
|
258
356
|
}
|
|
259
357
|
|
|
260
|
-
async function
|
|
358
|
+
async function runMccResolvePlan(args) {
|
|
261
359
|
const productType = required(args, 'product-type');
|
|
262
360
|
productFor({ ...args, 'product-type': productType });
|
|
263
361
|
emitStageMetricAsync('onboarding.mcc_prepare.start', { productType });
|
|
264
|
-
const result =
|
|
362
|
+
const result = resolveMccResult(required(args, 'query'), args);
|
|
265
363
|
process.stderr.write(`ALIPAY_AIPAY_INTERNAL:MCC_RESOLUTION=${JSON.stringify(result)}\n`);
|
|
266
364
|
if (result.matchType === 'UNIQUE') {
|
|
267
|
-
return
|
|
365
|
+
return renderPlanConfirm(productType, result.candidates[0]);
|
|
268
366
|
}
|
|
269
367
|
if (result.matchType === 'NONE') {
|
|
270
368
|
return renderMessage('onboarding.mcc.no_match', 'DEFAULT', {}, { productType });
|
|
@@ -561,21 +659,8 @@ export async function runOnboardingMessageCli(argv = process.argv) {
|
|
|
561
659
|
console.log(await runMccClarify(args));
|
|
562
660
|
return;
|
|
563
661
|
}
|
|
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
662
|
if (command === 'mcc-resolve-plan') {
|
|
578
|
-
const message = await
|
|
663
|
+
const message = await runMccResolvePlan(args);
|
|
579
664
|
if (message) console.log(message);
|
|
580
665
|
return;
|
|
581
666
|
}
|
|
@@ -632,7 +717,7 @@ export async function runOnboardingMessageCli(argv = process.argv) {
|
|
|
632
717
|
console.log(runApplicationResult(args));
|
|
633
718
|
return;
|
|
634
719
|
}
|
|
635
|
-
throw new Error('用法: onboarding_message_runner.mjs <platform-entry|product-clarify|enterprise-unsupported|mcc-resolve
|
|
720
|
+
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
721
|
}
|
|
637
722
|
|
|
638
723
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
@@ -586,11 +586,11 @@ const managedEntrypoints = {
|
|
|
586
586
|
reason: '产品已明确但用户尚未提供 MCC/经营描述时由 runner 绑定产品名称,禁止手写经营类目补问;检索无匹配时不得回到本消息。'
|
|
587
587
|
},
|
|
588
588
|
'onboarding.mcc.no_match': {
|
|
589
|
-
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-
|
|
589
|
+
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
590
|
reason: '用户已经提供经营描述但当前 MCC 表无确定匹配时,由 runner 输出独立恢复路径,禁止重新渲染初始 mcc-clarify 形成循环。'
|
|
591
591
|
},
|
|
592
592
|
'onboarding.mcc.candidates': {
|
|
593
|
-
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-
|
|
593
|
+
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
594
|
reason: '产品已明确且 MCC 存在多个相似候选时由 runner 绑定本地 MCC 表生成候选事实,禁止手写候选表。'
|
|
595
595
|
},
|
|
596
596
|
'onboarding.discovery.summary': {
|
|
@@ -598,7 +598,7 @@ const managedEntrypoints = {
|
|
|
598
598
|
reason: '该 runner 执行本轮适用查询、校验原始脚本事实、输出唯一 DISCOVERY_RESULT envelope,并在同一次 stdout 生成摘要;禁止单独重放摘要。'
|
|
599
599
|
},
|
|
600
600
|
'onboarding.plan.confirm': {
|
|
601
|
-
command: 'node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-resolve-plan --query <经营描述|MCC
|
|
601
|
+
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
602
|
reason: '方案展示必须由 runner 绑定本轮唯一 MCC 事实和产品变体生成,避免 Agent 用 jq/printf/管道手写 renderer 输入。'
|
|
603
603
|
},
|
|
604
604
|
'materials.category.collect': {
|
|
@@ -36,6 +36,8 @@ const nodeEntrypoints = Object.freeze({
|
|
|
36
36
|
|
|
37
37
|
export const runtimeDomains = Object.freeze([
|
|
38
38
|
'self-update',
|
|
39
|
+
'onboarding-bootstrap',
|
|
40
|
+
'onboarding-execute-ready',
|
|
39
41
|
...Object.keys(nodeEntrypoints),
|
|
40
42
|
'env',
|
|
41
43
|
...Object.keys(shellEntrypoints),
|
|
@@ -84,7 +86,74 @@ function runtimeError(domain, error) {
|
|
|
84
86
|
process.stderr.write(`${prefixes[domain] || 'ALIPAY_AIPAY_INTERNAL:RUNTIME_ERROR='}${message}\n`);
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
function parseRuntimeArgs(args) {
|
|
90
|
+
const parsed = {};
|
|
91
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
92
|
+
const token = args[index];
|
|
93
|
+
if (!token.startsWith('--')) throw new Error(`未知参数 ${token}`);
|
|
94
|
+
const value = args[index + 1];
|
|
95
|
+
if (value === undefined || value.startsWith('--')) throw new Error(`参数 ${token} 缺少取值`);
|
|
96
|
+
const name = token.slice(2);
|
|
97
|
+
if (Object.prototype.hasOwnProperty.call(parsed, name)) throw new Error(`参数 ${token} 重复`);
|
|
98
|
+
parsed[name] = value;
|
|
99
|
+
index += 1;
|
|
100
|
+
}
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function requiredOption(options, name) {
|
|
105
|
+
const value = options[name];
|
|
106
|
+
if (typeof value !== 'string' || value.length === 0) throw new Error(`缺少参数 --${name}`);
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assertOnlyOptions(options, allowedNames) {
|
|
111
|
+
const allowed = new Set(allowedNames);
|
|
112
|
+
const unknown = Object.keys(options).filter((name) => !allowed.has(name));
|
|
113
|
+
if (unknown.length > 0) throw new Error(`未知参数 --${unknown[0]}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function assertEnumOption(options, name, allowedValues, { required = true } = {}) {
|
|
117
|
+
if (!Object.prototype.hasOwnProperty.call(options, name)) {
|
|
118
|
+
if (required) throw new Error(`缺少参数 --${name}`);
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
const value = requiredOption(options, name);
|
|
122
|
+
if (!allowedValues.includes(value)) {
|
|
123
|
+
throw new Error(`参数 --${name} 非法,仅支持 ${allowedValues.join('、')}`);
|
|
124
|
+
}
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function runOnboardingBootstrap(args) {
|
|
129
|
+
const options = parseRuntimeArgs(args);
|
|
130
|
+
assertOnlyOptions(options, ['root-intent', 'product-type']);
|
|
131
|
+
const rootIntent = assertEnumOption(options, 'root-intent', ['onboarding_only']);
|
|
132
|
+
const productType = assertEnumOption(options, 'product-type', ['aipay', 'webpay', 'apppay'], { required: false });
|
|
133
|
+
const telemetryArgs = ['start-run', '--root-intent', rootIntent];
|
|
134
|
+
if (productType) telemetryArgs.push('--product-type', productType);
|
|
135
|
+
const { runTelemetryCli } = await import('./telemetry.mjs');
|
|
136
|
+
await runTelemetryCli(cliArgv(telemetryArgs));
|
|
137
|
+
const { runOnboardingMessageCli } = await import('./onboarding_message_runner.mjs');
|
|
138
|
+
await runOnboardingMessageCli(cliArgv(['platform-entry']));
|
|
139
|
+
const { isWindowsPlatform } = await import('./platform_compat.mjs');
|
|
140
|
+
if (isWindowsPlatform()) return;
|
|
141
|
+
const { runFlowCli } = await import('./flow_loader.mjs');
|
|
142
|
+
runFlowCli(cliArgv(['onboarding', 'entry']));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function runOnboardingExecuteReady(args) {
|
|
146
|
+
const options = parseRuntimeArgs(args);
|
|
147
|
+
assertOnlyOptions(options, ['product-type']);
|
|
148
|
+
const productType = assertEnumOption(options, 'product-type', ['aipay', 'webpay', 'apppay']);
|
|
149
|
+
const { runFlowCli } = await import('./flow_loader.mjs');
|
|
150
|
+
runFlowCli(cliArgv(['onboarding', 'execute', '--product-type', productType]));
|
|
151
|
+
return dispatch('env', ['check', '--product-type', productType]);
|
|
152
|
+
}
|
|
153
|
+
|
|
87
154
|
async function dispatch(domain, args) {
|
|
155
|
+
if (domain === 'onboarding-bootstrap') return runOnboardingBootstrap(args);
|
|
156
|
+
if (domain === 'onboarding-execute-ready') return runOnboardingExecuteReady(args);
|
|
88
157
|
if (shellEntrypoints[domain]) {
|
|
89
158
|
const [script, cwd] = shellPath(domain);
|
|
90
159
|
return runShell(script, cwd, args);
|