@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  # 支付宝 AI 付 Skill
2
2
 
3
- 支持按量付费、网站支付、APP 支付的代码开发与产品开通。
3
+ 支持 AI 按量付费、AI 网页应用收款、AI 移动应用收款的代码开发与产品开通。
4
4
 
5
5
  支付宝 AI 付站点:https://aipay.alipay.com/?from=alipay-aipay-skill
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alipay/alipay-aipay",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "支付宝 AI 付 Skill 安装包 - 安装 Agent Skill,并检测/后台准备 alipay-cli;jq 可通过 --with-jq 尝试准备",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/VERSION CHANGED
@@ -1 +1 @@
1
- 1.5.2
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` 表示失败后代码、配置和输入均未变化,禁止重跑;`UNKNOWN_PREVIOUS_EXECUTION` 表示上次命令可能已发出但未登记终态,禁止自动重跑,先核对实际结果。项目、服务 URL、方法或 POST body 变化时 runner 才会产生新的 `RUN` receipt。receipt 只保留在当前会话,不写跨会话业务进度。
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`。失败后只有项目代码、配置或执行输入实际变化并取得新 `RUN` receipt 才能重新联调。
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 signFlows = Object.freeze({
26
- aipay: new Set(['FLOW:AI_PAY_NOT_SIGNED', 'FLOW:AI_PAY_SIGNED']),
27
- webpay: new Set(['FLOW:PC_WEB_NOT_SIGNED', 'FLOW:PC_WEB_SIGNED']),
28
- apppay: new Set(['FLOW:APP_NOT_SIGNED', 'FLOW:APP_SIGNED'])
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, signFlows[productType]);
198
+ const flow = uniqueFlow(lines, allowedSignFlowValues(productType));
112
199
  if (result.status !== 0 || !signStatuses.has(signStatus) || !flow) return failedBranch(result);
113
- return { query_status: 'SUCCESS', source: 'QUERY', sign_status: signStatus, flow };
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 (!signStatuses.has(signStatus) || !signFlows[productType].has(flow)) {
121
- throw new Error('授权签约复用标记不完整或与当前产品不匹配');
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
- return { query_status: 'SUCCESS', source: 'AUTH_REUSE', sign_status: signStatus, flow };
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
- args[key.slice(2)] = value;
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 mccSearchResult(query) {
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
- const candidates = ranked
237
+ return ranked
209
238
  .sort((left, right) => right.score - left.score || left.record.code.localeCompare(right.record.code))
210
- .slice(0, 8)
211
- .map(({ record }) => ({
212
- mccCode: record.code,
213
- mccName: record.name,
214
- applicableBusinesses: record.applicableBusinesses,
215
- qualification: record.qualification
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 runMccResolve(args, { renderPlan = false } = {}) {
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 = mccSearchResult(required(args, 'query'));
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 renderPlan ? renderPlanConfirm(productType, result.candidates[0]) : '';
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 runMccResolve(args, { renderPlan: true });
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|mcc-resolve-plan|plan-confirm|mcc-search|mcc-clarify|mcc-candidates|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> ...');
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-candidates --query <business-description> --product-type <aipay|webpay|apppay>',
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-candidates --query <business-description> --product-type <aipay|webpay|apppay>',
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编码|MCC名称> --product-type <aipay|webpay|apppay>,或已持有唯一 MCC 事实时使用同一入口执行 onboarding-message plan-confirm --product-type <...> --mcc-code <...> --mcc-name <...> --mcc-qualification <...>',
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);