@alipay/alipay-aipay 1.6.8-beta2 → 1.6.8-beta4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alipay/alipay-aipay",
3
- "version": "1.6.8-beta2",
3
+ "version": "1.6.8-beta4",
4
4
  "description": "支付宝 AI 付接入 Skill 安装包 - 优先安装 Agent Skill,并在 macOS/Linux 后台准备用户级 CLI 工具",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/VERSION CHANGED
@@ -1 +1 @@
1
- 1.6.8-beta2
1
+ 1.6.8-beta4
@@ -348,6 +348,41 @@ function recordsForArgs(args) {
348
348
  return dynamicMccRecordsFromCatalog(catalog);
349
349
  }
350
350
 
351
+ function sortedMccRecords(records) {
352
+ return [...records.values()].sort((left, right) => left.code.localeCompare(right.code));
353
+ }
354
+
355
+ function mccCatalogForAgent(records) {
356
+ return sortedMccRecords(records).map((record, index) => ({
357
+ ref: `mcc-${index + 1}`,
358
+ mccName: record.name,
359
+ mccDescription: record.applicableBusinesses
360
+ }));
361
+ }
362
+
363
+ function parseAgentCandidateRefs(args, records) {
364
+ const raw = optional(args, 'agent-candidate-refs-json');
365
+ if (raw === undefined) return { codes: [] };
366
+ let parsed;
367
+ try {
368
+ parsed = JSON.parse(raw);
369
+ } catch {
370
+ throw new Error('agent-candidate-refs-json 必须是合法 JSON 字符串数组');
371
+ }
372
+ if (!Array.isArray(parsed) || parsed.length > 8 || parsed.some((ref) => typeof ref !== 'string')) {
373
+ throw new Error('agent-candidate-refs-json 必须是最多 8 项的 JSON 字符串数组');
374
+ }
375
+ const refs = new Map(sortedMccRecords(records).map((record, index) => [`mcc-${index + 1}`, record.code]));
376
+ const codes = [];
377
+ for (const ref of parsed) {
378
+ if (!refs.has(ref) || codes.includes(refs.get(ref))) {
379
+ throw new Error('agent-candidate-refs-json 只能引用本轮 queryMcc 返回的唯一 MCC 目录引用');
380
+ }
381
+ codes.push(refs.get(ref));
382
+ }
383
+ return { codes };
384
+ }
385
+
351
386
  function mccTableSummary(records) {
352
387
  const entries = [...records.values()]
353
388
  .map((record) => [
@@ -555,7 +590,8 @@ function deterministicMccMatches(query, records, { limit = 8, semanticTerms = []
555
590
  score: originalEvidence.score + ((semanticEvidence?.score || 0) * 0.7),
556
591
  exactName: originalEvidence.exactName,
557
592
  originalEligible: originalEvidence.eligible,
558
- semanticEligible: Boolean(semanticEvidence)
593
+ semanticEligible: Boolean(semanticEvidence),
594
+ semanticExactName: Boolean(semanticEvidence?.exactName)
559
595
  });
560
596
  }
561
597
 
@@ -583,13 +619,21 @@ function createMccSelectionReceipt(productType, query, result, mccRunContext = c
583
619
  return `${mccSelectionReceiptPrefix}${codes}.${createHash('sha256').update(binding).digest('base64url')}`;
584
620
  }
585
621
 
586
- function resolveMccSelection(productType, query, selectedInput, receipt, records, semanticTerms, mccRunContext) {
622
+ function resolveMccSelection(productType, query, selectedInput, receipt, records, semanticTerms, agentCandidateCodes, mccRunContext) {
587
623
  const candidateCodes = mccSelectionReceiptCodes(productType, query, receipt, records, semanticTerms, mccRunContext);
588
624
  if (!candidateCodes) {
589
625
  const semanticStatus = typeof receipt === 'string' && receipt.startsWith(mccSelectionReceiptPrefix)
590
626
  ? 'SELECTION_RECEIPT_STALE'
591
627
  : 'SELECTION_RECEIPT_INVALID';
592
- const refreshed = resolveMccResultFromRecords(query, productType, records, semanticTerms, semanticStatus, mccRunContext);
628
+ const refreshed = resolveMccResultFromRecords(
629
+ query,
630
+ productType,
631
+ records,
632
+ semanticTerms,
633
+ semanticStatus,
634
+ mccRunContext,
635
+ agentCandidateCodes
636
+ );
593
637
  return {
594
638
  ...refreshed,
595
639
  matchType: refreshed.matchType === 'NONE' ? 'NONE' : 'MULTIPLE',
@@ -646,7 +690,15 @@ function mccSelectionReceiptCodes(productType, query, receipt, records, semantic
646
690
  return candidateCodes;
647
691
  }
648
692
 
649
- function resolveMccResultFromRecords(query, productType, records, semanticTerms = [], semanticStatus, mccRunContext = captureMccRunContext()) {
693
+ function resolveMccResultFromRecords(
694
+ query,
695
+ productType,
696
+ records,
697
+ semanticTerms = [],
698
+ semanticStatus,
699
+ mccRunContext = captureMccRunContext(),
700
+ agentCandidateCodes = []
701
+ ) {
650
702
  const directCode = directMccCode(query, records);
651
703
  if (directCode.isCode) {
652
704
  if (directCode.matches.length === 1) {
@@ -659,6 +711,7 @@ function resolveMccResultFromRecords(query, productType, records, semanticTerms
659
711
  deterministicStrongCandidateCodes: [code],
660
712
  deterministicWeakCandidateCodes: [],
661
713
  semanticCandidateCodes: [],
714
+ agentCandidateCodes: [],
662
715
  semanticTerms,
663
716
  mergedCandidateCodes: [code],
664
717
  mccTableSummary: mccTableSummary(records),
@@ -673,6 +726,7 @@ function resolveMccResultFromRecords(query, productType, records, semanticTerms
673
726
  deterministicStrongCandidateCodes: finalCodes,
674
727
  deterministicWeakCandidateCodes: [],
675
728
  semanticCandidateCodes: [],
729
+ agentCandidateCodes: [],
676
730
  semanticTerms,
677
731
  mergedCandidateCodes: finalCodes,
678
732
  mccTableSummary: mccTableSummary(records),
@@ -683,19 +737,40 @@ function resolveMccResultFromRecords(query, productType, records, semanticTerms
683
737
  const matches = deterministicMccMatches(query, records, { limit: 8, semanticTerms });
684
738
  const deterministicCodes = matches.filter(({ originalEligible }) => originalEligible).map(({ record }) => record.code);
685
739
  const semanticCodes = matches.filter(({ semanticEligible }) => semanticEligible).map(({ record }) => record.code);
686
- const finalCodes = matches.map(({ record }) => record.code).slice(0, 8);
740
+ const hasExactSemanticName = matches.some(({ semanticExactName }) => semanticExactName);
741
+ const localCodes = matches.map(({ record }) => record.code);
742
+ const mergedCodes = [...agentCandidateCodes, ...localCodes]
743
+ .filter((code, index, all) => all.indexOf(code) === index);
744
+ const finalCodes = mergedCodes.slice(0, 8);
687
745
  const candidates = finalCodes.map((code) => mccCandidateFromRecord(records.get(code), productType, mccRunContext));
746
+ const needsAgentReview = agentCandidateCodes.length === 0
747
+ && (
748
+ matches.length === 0
749
+ || deterministicCodes.length !== 1
750
+ || (semanticTerms.length > 0 && deterministicCodes.length === 0 && !hasExactSemanticName)
751
+ );
688
752
  return {
689
753
  matchType: candidates.length === 1 ? 'UNIQUE' : candidates.length === 0 ? 'NONE' : 'MULTIPLE',
690
- semanticStatus: semanticStatus || (semanticTerms.length > 0 ? 'AGENT_SEMANTIC_TERMS_USED' : 'DYNAMIC_CATALOG'),
754
+ semanticStatus: semanticStatus
755
+ || (agentCandidateCodes.length > 0 ? 'AGENT_CATALOG_REVIEW_USED' : semanticTerms.length > 0 ? 'AGENT_SEMANTIC_TERMS_USED' : 'DYNAMIC_CATALOG'),
691
756
  deterministicCandidateCodes: deterministicCodes,
692
757
  deterministicStrongCandidateCodes: deterministicCodes,
693
758
  deterministicWeakCandidateCodes: [],
694
759
  semanticCandidateCodes: semanticCodes,
760
+ agentCandidateCodes,
695
761
  semanticTerms,
696
762
  mergedCandidateCodes: finalCodes,
697
763
  mccTableSummary: mccTableSummary(records),
698
- candidates
764
+ candidates,
765
+ ...(needsAgentReview ? {
766
+ agentReview: {
767
+ required: true,
768
+ attempt: 1,
769
+ maxAttempts: 1,
770
+ reason: 'LOW_CONFIDENCE_LOCAL_MATCH',
771
+ catalog: mccCatalogForAgent(records)
772
+ }
773
+ } : {})
699
774
  };
700
775
  }
701
776
 
@@ -759,9 +834,10 @@ async function runMccResolvePlan(args) {
759
834
  throw new Error('mcc-resolve-plan 不再接受 --agent-mcc-status 或 --agent-mcc-codes;MCC 匹配只使用本轮动态 catalog');
760
835
  }
761
836
  const records = recordsForArgs(args);
837
+ const agentReview = parseAgentCandidateRefs(args, records);
762
838
  let result = selected === undefined
763
- ? resolveMccResultFromRecords(query, productType, records, semanticTerms, undefined, mccRunContext)
764
- : resolveMccSelection(productType, query, selected, selectionReceipt, records, semanticTerms, mccRunContext);
839
+ ? resolveMccResultFromRecords(query, productType, records, semanticTerms, undefined, mccRunContext, agentReview.codes)
840
+ : resolveMccSelection(productType, query, selected, selectionReceipt, records, semanticTerms, agentReview.codes, mccRunContext);
765
841
  if (selectionState === 'STALE') {
766
842
  result = {
767
843
  ...result,
@@ -802,8 +878,10 @@ async function runMccMatchCodes(args) {
802
878
  if (args['agent-mcc-status'] !== undefined || args['agent-mcc-codes'] !== undefined) {
803
879
  throw new Error('mcc-match-codes 不接受 --agent-mcc-status 或 --agent-mcc-codes;MCC 匹配只使用本轮动态 catalog');
804
880
  }
881
+ const records = recordsForArgs(args);
882
+ const agentReview = parseAgentCandidateRefs(args, records);
805
883
  const mccRunContext = captureMccRunContext();
806
- const result = resolveMccResultFromRecords(query, productType, recordsForArgs(args), semanticTerms, undefined, mccRunContext);
884
+ const result = resolveMccResultFromRecords(query, productType, records, semanticTerms, undefined, mccRunContext, agentReview.codes);
807
885
  const { candidates: _internalCandidates, ...controlResult } = result;
808
886
  process.stderr.write(`ALIPAY_AIPAY_INTERNAL:MCC_RESOLUTION=${JSON.stringify(controlResult)}\n`);
809
887
  return '';
@@ -13,7 +13,7 @@
13
13
 
14
14
  步骤顺序固定为方案展示后自动授权,再执行适用只读查询,最后处理独立分支。查询失败只阻断对应分支,不当作空列表;其他成功分支可继续。
15
15
 
16
- 方案展示前只按本轮 `mcc.sh resolve-plan` 结果陈述 MCC 名称、描述和特殊资质。Agent 可从用户原始经营描述提炼有依据的行业概念和同义表达,但不得提供或猜测 MCC 编码;runner 只在当前产品动态目录内检索并校验候选。`ar-support.queryMcc`、按需 `mccsearchservice.batchQueryMcc` 及候选快照的完整执行和失效规则见步骤 2;无查询依据的结论禁止推断。
16
+ 方案展示前只按本轮 `mcc.sh resolve-plan` 结果陈述 MCC 名称、描述和特殊资质。Agent 可从用户原始经营描述提炼有依据的行业概念和同义表达;本地词面结果不明确时,runner 会把同一次 `queryMcc` 返回的当前产品 MCC 名称和描述以一次性目录复核 marker 交给 Agent,Agent 只能返回目录引用,runner 再映射并校验为当前动态目录中的候选。`ar-support.queryMcc`、按需 `mccsearchservice.batchQueryMcc` 及候选快照的完整执行和失效规则见步骤 2;无查询依据的结论禁止推断。
17
17
 
18
18
  ---
19
19
 
@@ -103,17 +103,17 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message prod
103
103
 
104
104
  产品明确但无经营描述/MCC:执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-clarify --product-type "$PRODUCT_TYPE" --root-intent "$ROOT_INTENT"`。该命令必须先创建或复用正式 telemetry run 并记录 `clarify_intent`;缺少或非法 `ROOT_INTENT` 时停止,不得继续 MCC 或授权。
105
105
 
106
- 已有经营描述、二级类目名称或编码时,以原话作为 `MCC_SEARCH_QUERY` 进入唯一 MCC 解析/方案入口。非精确编码时,Agent 应先理解原话;遇到行业俗称、场景表达或目录不一定直接出现的用词时,在首次调用中一并传入 0 到 12 个有原话依据的行业概念、经营对象或同义表达,形成 `MCC_SEMANTIC_TERMS_JSON`;只传概念、不传 MCC 编码,不补原话无依据的行业。没有可靠扩展时省略,不追加用户确认。runner 在完整动态类目内做 NFKC、泛词剥离、标点边界内二/三元词组召回和动态稀有度排序;原始精确编码/完整名称决定性优先,其余原话与 Agent 辅助概念联合评分,语义项按 `0.7` 权重参与,名称证据高于描述,其他候选最多 8 项。评分只用于真实目录召回排序;禁止业务词到 MCC 的硬编码点映射、Agent 全表判断、远程语义或额外网络调用。
106
+ 已有经营描述、二级类目名称或编码时,以原话作为 `MCC_SEARCH_QUERY` 进入唯一 MCC 解析/方案入口。非精确编码时,Agent 应先理解原话;遇到行业俗称、场景表达或目录不一定直接出现的用词时,在首次调用中一并传入 0 到 12 个有原话依据的行业概念、经营对象或同义表达,形成 `MCC_SEMANTIC_TERMS_JSON`;只传概念,不补原话无依据的行业。没有可靠扩展时省略,不追加用户确认。runner 在完整动态类目内做 NFKC、泛词剥离、标点边界内二/三元词组召回和动态稀有度排序;原始精确编码/完整名称决定性优先,其余原话与 Agent 辅助概念联合评分,语义项按 `0.7` 权重参与,名称证据高于描述,其他候选最多 8 项。若本地结果为 `NONE`、候选冲突或仅由低置信语义词产生,runner 只发出一次 `MCC_AGENT_REVIEW_REQUIRED`,其中包含当前动态目录的稳定引用、名称和描述;Agent 必须基于该目录返回最多 8 个引用,并在同一原始 query 下用 `--agent-candidate-refs-json` 重跑,runner 只接受本轮目录中的引用。目录复核不新增 MCP 或网络调用,也不替代本地校验;禁止业务词到 MCC 的硬编码点映射、自由提交 MCC code、远程语义或额外网络调用。
107
107
 
108
108
  同一 `run + 用户输入 + 原始 MCC_SEARCH_QUERY + 产品` 的语义扩充最多重试 2 次。首次未提供语义词且结果为 `NONE`,或已提供语义词但结果仍为 `NONE` 且仍有预算时,`mcc.sh` 只输出唯一内部 `MCC_SEMANTIC_RETRY_REQUIRED`,stdout 为空;Agent 必须保留原始 query、复用同一 `USER_INPUT_ID`,根据原话补充此前未使用且有依据的概念后原样重跑本命令,不向用户展示中间无匹配。后续重试提交的语义词必须保留此前语义词并至少新增一个此前未使用的词;未新增或替换原语义词时不消耗额度,直接进入既有无匹配分支。`MCC_SEMANTIC_RETRY_REQUIRED` 的 `attempt` 从 1 到 2,达到 2 后不再产生该 marker;仍为 `NONE` 时输出下方既有无匹配文案并停止。精确 MCC 编码不存在不进入语义重试;网络/CLI 失败仍只走各自既有恢复分支。语义重试计数只保存在当前 run 的私有缓存中,不随 MCC 树缓存 TTL 过期,读写异常按已耗尽处理,禁止通过更换 query、重置参数或重复调用绕过上限。
109
109
 
110
110
  `queryMcc` 树和多候选快照使用默认 1800 秒、本轮 run 私有缓存;无有效 run 不缓存。快照绑定产品、salesCode、原查询、规范化语义词、选择回执、候选树事实及按需取得的资格详情;有效命中原样复用,失效才重查。runner 只接受最多 12 项、单项 2 到 40 字符、总计不超过 240 字符的 JSON 字符串数组并拒绝 MCC 编码;Agent 参数错误时基于原话更正或省略,不要求用户重述。两种 MCP 请求体不变;`batchQueryMcc` 最多一次,只传树内命中且 `needSpecialQual=true` 的候选编码。
111
111
 
112
112
  ```bash
113
- node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" mcc resolve-plan --query "$MCC_SEARCH_QUERY" --product-type "$PRODUCT_TYPE" --root-intent "$ROOT_INTENT" [--semantic-terms-json "$MCC_SEMANTIC_TERMS_JSON"] --input-id "$USER_INPUT_ID" [--telemetry-run-id "$RUN_ID" --telemetry-history-count "$HISTORY_COUNT"]
113
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" mcc resolve-plan --query "$MCC_SEARCH_QUERY" --product-type "$PRODUCT_TYPE" --root-intent "$ROOT_INTENT" [--semantic-terms-json "$MCC_SEMANTIC_TERMS_JSON"] [--agent-candidate-refs-json "$MCC_AGENT_CANDIDATE_REFS_JSON"] --input-id "$USER_INPUT_ID" [--telemetry-run-id "$RUN_ID" --telemetry-history-count "$HISTORY_COUNT"]
114
114
  ```
115
115
 
116
- 只收唯一 `ALIPAY_AIPAY_INTERNAL:MCC_QUERY_RESULT=SUCCESS` 与 `ALIPAY_AIPAY_INTERNAL:MCC_RESOLUTION=<json>`。若同时收到唯一 `ALIPAY_AIPAY_INTERNAL:MCC_SEMANTIC_RETRY_REQUIRED=<json>` 且 stdout 为空,不产生对客消息,按上面的规则在同一工作回合自动重跑;不得把该 marker 或中间状态展示给用户。`UNIQUE` 恰一项,并有唯一 `CUSTOMER_MESSAGE_DELIVERY_REQUIRED=onboarding.plan.confirm`、`MCC_CONTEXT_RECEIPT`;先把 stdout 作为可见进行中消息原文送达,随后同一回合进入 `execute`,不等待回复。`MULTIPLE` 送候选后等待;预算耗尽的 `NONE` 送既有无匹配文案后等待用户重新描述。禁止旧 `mcc-search`/`mcc-candidates`、手写候选或近似追问。
116
+ 只收唯一 `ALIPAY_AIPAY_INTERNAL:MCC_QUERY_RESULT=SUCCESS` 与 `ALIPAY_AIPAY_INTERNAL:MCC_RESOLUTION=<json>`。若同时收到唯一 `ALIPAY_AIPAY_INTERNAL:MCC_AGENT_REVIEW_REQUIRED=<json>` 且 stdout 为空,Agent 读取其中稳定引用、名称和描述,最多返回 8 个当前目录引用后用同一 query、语义词和 `USER_INPUT_ID` 原样重跑一次;不得把该 marker 或中间状态展示给用户。目录复核最多一次,不消耗语义扩充预算;未返回有效引用时沿用本地结果并继续既有 `NONE`/候选分支。若同时收到唯一 `ALIPAY_AIPAY_INTERNAL:MCC_SEMANTIC_RETRY_REQUIRED=<json>` 且 stdout 为空,按上面的规则在同一工作回合自动重跑;不得把该 marker 或中间状态展示给用户。`UNIQUE` 恰一项,并有唯一 `CUSTOMER_MESSAGE_DELIVERY_REQUIRED=onboarding.plan.confirm`、`MCC_CONTEXT_RECEIPT`;先把 stdout 作为可见进行中消息原文送达,随后同一回合进入 `execute`,不等待回复。`MULTIPLE` 送候选后等待;预算耗尽的 `NONE` 送既有无匹配文案后等待用户重新描述。禁止旧 `mcc-search`/`mcc-candidates`、手写候选或近似追问。
117
117
 
118
118
  `MCC_FLOW:RETRY_WITH_NETWORK`:联网后原样重试同一 `mcc resolve-plan`。`CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION` 且 stdout 空:给同命令 CLI 私有状态目录权限后原样重试。失败停 MCC;禁无匹配兜底或进方案/授权/签约。
119
119
 
@@ -125,7 +125,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" mcc resolve-plan --quer
125
125
 
126
126
  `salesCode`、`scope` 和基础材料只取“产品映射”。方案只由 `mcc resolve-plan` 或已有唯一 MCC 事实时的 `onboarding-message plan-confirm --root-intent "$ROOT_INTENT" --mcc-context-receipt ... [--telemetry-run-id "$RUN_ID" --telemetry-history-count "$HISTORY_COUNT"]` 生成,展示产品/scope、MCC、特殊资质、企业账号边界及商家平台链接。已有 active run 时必须传 telemetry 参数;如果该命令首次创建 run,则尚无 marker,省略参数并由同一进程把本次 plan stage 的 history 固定为 0,命令返回后取得后续使用的 `RUN_ID`。`ROOT_INTENT` 取当前路线根意图,只能是 `onboarding_only` 或 `full_process`。特殊资质只来自 `qualificationGroups`:多个 group 是 OR 关系;同一 group 内多个 qualifications 是 AND 关系;`attaType` 为空或 null 的 qualification 只展示 `qualName`,不生成上传项。禁止补充模板外资格或审核规则。
127
127
 
128
- 不得保留“先运行旧字符串搜索,无匹配后再临时让 Agent 猜”的两阶段流程,或重复询问已知信息;这里的语义重试只允许按上一段登记的两次预算执行,且不增加用户确认。语义概念与原话必须在同一次 `resolve-plan` 中提交并统一校验。唯一事实校验通过、方案 marker 唯一且 stdout 已送达后,执行 `onboarding-execute-ready --product-type "$PRODUCT_TYPE"`;缺值/冲突/非唯一继续澄清,产品/MCC 变更重渲染。`execute` loader 只继承 `clarify_intent` 已记录的正式 run 事实,不再单独记录 stage,不代表新增用户回复,禁手工上报;Agent 在收到每条聊天框用户消息时,必须先按 `runtime-entry.md` 的统一前置命令计数,不按是否与 Skill 相关筛选,`summary` 收口后不再计入当前 run,`full_process` 不重置。
128
+ 不得保留“先运行旧字符串搜索,无匹配后再临时让 Agent 猜”的无边界两阶段流程,或重复询问已知信息;目录复核是完整 `queryMcc` 事实上的一次受限 Agent 判断,必须在 `MCC_AGENT_REVIEW_REQUIRED` marker 后立即完成,最多一次且不增加用户确认。语义重试只允许按上一段登记的两次预算执行。语义概念、目录引用与原话必须在同一次 `resolve-plan` 中提交并统一校验。唯一事实校验通过、方案 marker 唯一且 stdout 已送达后,执行 `onboarding-execute-ready --product-type "$PRODUCT_TYPE"`;缺值/冲突/非唯一继续澄清,产品/MCC 变更重渲染。`execute` loader 只继承 `clarify_intent` 已记录的正式 run 事实,不再单独记录 stage,不代表新增用户回复,禁手工上报;Agent 在收到每条聊天框用户消息时,必须先按 `runtime-entry.md` 的统一前置命令计数,不按是否与 Skill 相关筛选,`summary` 收口后不再计入当前 run,`full_process` 不重置。
129
129
 
130
130
  <!-- ALIPAY_AIPAY_FLOW:ONBOARDING_ENTRY:END -->
131
131
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  `queryMcc`、签约查询和签约提交使用 `{"request":{...},"ctx":{}}`;`batchQueryMcc` 使用 `{"request":{"mccCodes":[...]}}`。完整 payload 只在脚本中维护;Agent 不从本模块自行构造 MCP 调用。
15
15
 
16
- `queryMcc` 成功树按同一 run、产品短时缓存,并逐项提供二级 MCC 编码、名称、描述和布尔型 `needSpecialQual`;任一必需字段缺失、为空或类型异常均失败关闭,不从其他字段回退。非精确编码时,Agent 应先理解用户原话,并在行业俗称或场景表达与目录用词可能不同时于首次调用一并提供有原话依据的行业概念或同义表达;runner 拒绝由 Agent 提供 MCC code,并只在当前产品动态树内召回和校验。首次未提供语义词且结果为 `NONE`,或已提供语义词但结果仍为 `NONE` 时,由脚本发出 flow 登记的重试 marker,Agent 按 flow 最多完成两次语义扩充重试;后续重试必须保留此前语义词并至少新增一个此前未使用的词,未新增或替换原语义词时不消耗额度,直接回到既有无匹配分支;耗尽后回到 flow 登记的无匹配文案,要求重新描述经营信息。候选确定后,仅把其中 `needSpecialQual=true` 的编码集中交给一次 `batchQueryMcc` 获取特殊资质项;没有此类候选时不调用。多候选的完整树事实和资格详情按 run、产品、原查询、规范化语义词和选择回执缓存,用户选择时携带同一语义词且有效命中则两种 MCC 方法都不重调。缓存默认 1800 秒、无有效 run 不落盘;过期、损坏、符号链接、跨 run、语义词变化或事实哈希不一致时忽略并从完整当前产品目录重查,不修补缓存事实,不做产品级全量预取。
16
+ `queryMcc` 成功树按同一 run、产品短时缓存,并逐项提供二级 MCC 编码、名称、描述和布尔型 `needSpecialQual`;任一必需字段缺失、为空或类型异常均失败关闭,不从其他字段回退。非精确编码时,Agent 应先理解用户原话,并在行业俗称或场景表达与目录用词可能不同时于首次调用一并提供有原话依据的行业概念或同义表达;本地词面结果为 `NONE`、候选冲突或低置信时,runner 可发出一次目录复核 marker,携带同一次 `queryMcc` 返回的当前产品 MCC 名称、描述和稳定引用;Agent 只返回该 marker 中的目录引用,runner 映射并校验为当前动态目录中的候选,不接受自由提交 MCC code。目录复核不新增 MCP/网络调用,语义扩充仍按 flow 最多完成两次。候选确定后,仅把其中 `needSpecialQual=true` 的编码集中交给一次 `batchQueryMcc` 获取特殊资质项;没有此类候选时不调用。多候选的完整树事实和资格详情按 run、产品、原查询、规范化语义词和选择回执缓存,用户选择时携带同一语义词且有效命中则两种 MCC 方法都不重调。缓存默认 1800 秒、无有效 run 不落盘;过期、损坏、符号链接、跨 run、语义词变化或事实哈希不一致时忽略并从完整当前产品目录重查,不修补缓存事实,不做产品级全量预取。
17
17
 
18
18
  ## 签约状态
19
19
 
@@ -4,7 +4,7 @@
4
4
  # 功能描述: 查询当前产品支持的 MCC 经营类目并托管 MCC 匹配与方案展示
5
5
  # 调用位置: Step 1 方案规划
6
6
  # 用法:
7
- # bash mcc.sh resolve-plan --query <经营描述|MCC编码> --product-type <aipay|webpay|apppay> --root-intent <onboarding_only|full_process> [--semantic-terms-json <JSON数组>] [--select <MCC编码> --selection-receipt <receipt>] [--input-id <USER_INPUT_ID>]
7
+ # bash mcc.sh resolve-plan --query <经营描述|MCC编码> --product-type <aipay|webpay|apppay> --root-intent <onboarding_only|full_process> [--semantic-terms-json <JSON数组>] [--agent-candidate-refs-json <JSON数组>] [--select <MCC编码> --selection-receipt <receipt>] [--input-id <USER_INPUT_ID>]
8
8
  #=============================================================================
9
9
 
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -323,6 +323,92 @@ mcc_semantic_retry_file() {
323
323
  "$(mcc_cache_component "$sales_code")"
324
324
  }
325
325
 
326
+ mcc_agent_review_file() {
327
+ local product_type="$1" sales_code="$2" root scope
328
+ root="$(mcc_cache_root)" || return 1
329
+ scope="$(mcc_cache_scope_id)" || return 1
330
+ if [ -e "$root" ] && { [ ! -d "$root" ] || [ -L "$root" ]; }; then
331
+ return 1
332
+ fi
333
+ mkdir -p "$root" 2>/dev/null || return 1
334
+ chmod 700 "$root" 2>/dev/null || true
335
+ printf '%s/%s.%s.%s.agent-reviews.json' \
336
+ "$root" \
337
+ "$(mcc_cache_component "$scope")" \
338
+ "$(mcc_cache_component "$product_type")" \
339
+ "$(mcc_cache_component "$sales_code")"
340
+ }
341
+
342
+ mcc_agent_review_issued() {
343
+ local product_type="$1" sales_code="$2" input_id="$3" file
344
+ file="$(mcc_agent_review_file "$product_type" "$sales_code")" || return 1
345
+ [ -f "$file" ] && [ ! -L "$file" ] || return 1
346
+ jq -e \
347
+ --arg inputId "$input_id" \
348
+ '.schemaVersion == 1
349
+ and ((.entries // null) | type == "array")
350
+ and all(.entries[];
351
+ type == "object"
352
+ and ((.query // null) | type == "string")
353
+ and ((.inputId // null) | type == "string")
354
+ )
355
+ and any(.entries[]; .inputId == $inputId)' \
356
+ "$file" >/dev/null 2>&1
357
+ }
358
+
359
+ mark_mcc_agent_review_issued() {
360
+ local product_type="$1" sales_code="$2" query="$3" input_id="$4"
361
+ local file dir lock tmp
362
+ file="$(mcc_agent_review_file "$product_type" "$sales_code")" || return 1
363
+ dir="${file%/*}"
364
+ lock="${file}.lock"
365
+ if ! mkdir "$lock" 2>/dev/null; then
366
+ return 1
367
+ fi
368
+ if [ -e "$file" ] && { [ ! -f "$file" ] || [ -L "$file" ]; }; then
369
+ rmdir "$lock" 2>/dev/null || true
370
+ return 1
371
+ fi
372
+ tmp="$(mktemp "$dir/.mcc-agent-reviews.XXXXXX" 2>/dev/null)" || {
373
+ rmdir "$lock" 2>/dev/null || true
374
+ return 1
375
+ }
376
+ chmod 600 "$tmp" 2>/dev/null || true
377
+ if [ -f "$file" ]; then
378
+ if ! jq -ce \
379
+ --arg query "$query" \
380
+ --arg inputId "$input_id" '
381
+ if .schemaVersion != 1 or ((.entries // null) | type != "array") then
382
+ error("invalid MCC agent review state")
383
+ elif any(.entries[]; .query == $query and .inputId == $inputId) then
384
+ .
385
+ else
386
+ .entries += [{query: $query, inputId: $inputId}]
387
+ end
388
+ ' "$file" >"$tmp" 2>/dev/null; then
389
+ rm -f "$tmp" 2>/dev/null || true
390
+ rmdir "$lock" 2>/dev/null || true
391
+ return 1
392
+ fi
393
+ else
394
+ if ! jq -cn \
395
+ --arg query "$query" \
396
+ --arg inputId "$input_id" \
397
+ '{schemaVersion: 1, entries: [{query: $query, inputId: $inputId}]}' \
398
+ >"$tmp" 2>/dev/null; then
399
+ rm -f "$tmp" 2>/dev/null || true
400
+ rmdir "$lock" 2>/dev/null || true
401
+ return 1
402
+ fi
403
+ fi
404
+ if ! mv "$tmp" "$file" 2>/dev/null; then
405
+ rm -f "$tmp" 2>/dev/null || true
406
+ rmdir "$lock" 2>/dev/null || true
407
+ return 1
408
+ fi
409
+ rmdir "$lock" 2>/dev/null || true
410
+ }
411
+
326
412
  claim_mcc_semantic_retry() {
327
413
  local product_type="$1" sales_code="$2" query="$3" input_id="$4" semantic_terms_json="$5"
328
414
  local file dir lock entry original_query retry_count next_retry_count now tmp lock_acquired=false
@@ -654,6 +740,15 @@ query_mcc_detail_chunk() {
654
740
  printf '%s' "$business"
655
741
  }
656
742
 
743
+ mcc_match_error_kind() {
744
+ case "$1" in
745
+ *agent-candidate-refs-json*) printf 'INVALID_AGENT_CANDIDATE_REFS' ;;
746
+ *semantic-terms-json*) printf 'INVALID_SEMANTIC_TERMS' ;;
747
+ *mcc-catalog-json*) printf 'INVALID_DYNAMIC_CATALOG' ;;
748
+ *) printf 'MATCHER_RUNTIME_ERROR' ;;
749
+ esac
750
+ }
751
+
657
752
  normalize_mcc_catalog() {
658
753
  local rows_json="$1" details_json="$2"
659
754
  jq -cn --argjson rows "$rows_json" --argjson details "$details_json" '
@@ -712,18 +807,34 @@ normalize_mcc_catalog() {
712
807
  }
713
808
 
714
809
  run_mcc_match_codes() {
715
- local query="$1" product_type="$2" root_intent="$3" catalog="$4" semantic_terms_json="$5" runner_out runner_err runner_rc line resolution=""
810
+ local query="$1" product_type="$2" root_intent="$3" catalog="$4" semantic_terms_json="$5" agent_candidate_refs_json="${6:-}" runner_out runner_err runner_rc line resolution=""
811
+ local runner_error error_kind
716
812
  runner_out=$(mktemp "${TMPDIR:-/tmp}/mcc_match_out.XXXXXX") || { echo "❌ 创建临时文件失败"; exit 1; }
717
813
  runner_err=$(mktemp "${TMPDIR:-/tmp}/mcc_match_err.XXXXXX") || { rm -f "$runner_out"; echo "❌ 创建临时文件失败"; exit 1; }
718
- "${NODE_BINARY:-node}" "$ONBOARDING_RUNNER" mcc-match-codes \
814
+ local node_args=(
815
+ "$ONBOARDING_RUNNER" mcc-match-codes
719
816
  --query "$query" \
720
817
  --product-type "$product_type" \
721
818
  --root-intent "$root_intent" \
722
819
  --mcc-catalog-json "$catalog" \
723
- --semantic-terms-json "$semantic_terms_json" >"$runner_out" 2>"$runner_err"
820
+ --semantic-terms-json "$semantic_terms_json"
821
+ )
822
+ if [ -n "$agent_candidate_refs_json" ]; then
823
+ node_args+=(--agent-candidate-refs-json "$agent_candidate_refs_json")
824
+ fi
825
+ "${NODE_BINARY:-node}" "${node_args[@]}" >"$runner_out" 2>"$runner_err"
724
826
  runner_rc=$?
725
827
  if [ "$runner_rc" -ne 0 ] || [ -s "$runner_out" ]; then
828
+ runner_error="$(sed -n 's/^ONBOARDING_MESSAGE_ERROR://p' "$runner_err" | sed -n '1p')"
829
+ error_kind="$(mcc_match_error_kind "$runner_error")"
830
+ if [ "$error_kind" = "INVALID_AGENT_CANDIDATE_REFS" ] && [ -n "$agent_candidate_refs_json" ]; then
831
+ rm -f "$runner_out" "$runner_err"
832
+ emit_control "MCC_AGENT_CANDIDATE_REFS_IGNORED=INVALID"
833
+ run_mcc_match_codes "$query" "$product_type" "$root_intent" "$catalog" "$semantic_terms_json"
834
+ return $?
835
+ fi
726
836
  rm -f "$runner_out" "$runner_err"
837
+ emit_control "MCC_MATCH_ERROR=$error_kind"
727
838
  mcc_query_failed
728
839
  fi
729
840
  while IFS= read -r line; do
@@ -748,7 +859,7 @@ validate_mcc_selection_cache() {
748
859
  }
749
860
 
750
861
  run_resolve_plan() {
751
- local query="" product_type="" root_intent="" semantic_terms_json='[]' select_value="" selection_receipt="" input_id="" sales_code tree rows tree_catalog resolution match_type semantic_status full_codes candidate_rows qualification_rows details catalog selection_cache="" used_selection_cache=false stale_selection=false resolved_semantic_terms='[]' retry_input_id="" retry_rc=0 next_retry_count=0 retry_reason="" retry_payload=""
862
+ local query="" product_type="" root_intent="" semantic_terms_json='[]' agent_candidate_refs_json="" select_value="" selection_receipt="" input_id="" sales_code tree rows tree_catalog resolution match_type semantic_status full_codes candidate_rows qualification_rows details catalog selection_cache="" used_selection_cache=false stale_selection=false resolved_semantic_terms='[]' retry_rc=0 next_retry_count=0 retry_reason="" retry_payload="" agent_review_required="false" agent_review_payload=""
752
863
  shift
753
864
  while [[ $# -gt 0 ]]; do
754
865
  case "$1" in
@@ -764,6 +875,9 @@ run_resolve_plan() {
764
875
  --semantic-terms-json)
765
876
  [ $# -ge 2 ] || { echo "❌ 缺少参数值: $1"; exit 1; }
766
877
  semantic_terms_json="$2"; shift 2 ;;
878
+ --agent-candidate-refs-json)
879
+ [ $# -ge 2 ] || { echo "❌ 缺少参数值: $1"; exit 1; }
880
+ agent_candidate_refs_json="$2"; shift 2 ;;
767
881
  --select)
768
882
  [ $# -ge 2 ] || { echo "❌ 缺少参数值: $1"; exit 1; }
769
883
  select_value="$2"; shift 2 ;;
@@ -826,17 +940,30 @@ run_resolve_plan() {
826
940
  tree_catalog=$(normalize_mcc_tree_catalog "$rows") || {
827
941
  mcc_query_failed
828
942
  }
829
- capture_or_exit resolution run_mcc_match_codes "$query" "$product_type" "$root_intent" "$tree_catalog" "$semantic_terms_json"
943
+ capture_or_exit resolution run_mcc_match_codes "$query" "$product_type" "$root_intent" "$tree_catalog" "$semantic_terms_json" "$agent_candidate_refs_json"
830
944
  match_type=$(printf '%s' "$resolution" | jq -r '.matchType // ""' 2>/dev/null) || mcc_query_failed
831
945
  semantic_status=$(printf '%s' "$resolution" | jq -r '.semanticStatus // ""' 2>/dev/null) || mcc_query_failed
832
946
  resolved_semantic_terms=$(printf '%s' "$resolution" | jq -c '.semanticTerms // []' 2>/dev/null) || mcc_query_failed
947
+ agent_review_required=$(printf '%s' "$resolution" | jq -r '.agentReview.required // false' 2>/dev/null) || mcc_query_failed
948
+ if [ -n "$input_id" ] && [ -n "$agent_candidate_refs_json" ]; then
949
+ mark_mcc_agent_review_issued "$product_type" "$sales_code" "$query" "$input_id" || true
950
+ fi
951
+ if [ "$agent_review_required" = "true" ] && [ -z "$agent_candidate_refs_json" ] && [ -n "$input_id" ]; then
952
+ agent_review_payload=$(printf '%s' "$resolution" | jq -c '.agentReview' 2>/dev/null) || mcc_query_failed
953
+ if ! mcc_agent_review_issued "$product_type" "$sales_code" "$input_id"; then
954
+ mark_mcc_agent_review_issued "$product_type" "$sales_code" "$query" "$input_id" || true
955
+ emit_control "MCC_QUERY_RESULT=SUCCESS"
956
+ emit_control "MCC_RESOLUTION=$(printf '%s' "$resolution" | jq -c 'del(.agentReview)' 2>/dev/null)"
957
+ emit_control "MCC_AGENT_REVIEW_REQUIRED=$agent_review_payload"
958
+ return 0
959
+ fi
960
+ fi
833
961
  if [ "$match_type" = "NONE" ] \
834
962
  && [ "$semantic_status" != "DYNAMIC_CATALOG_CODE_NOT_FOUND" ] \
835
963
  && [ "$semantic_status" != "DYNAMIC_CATALOG_CODE_AMBIGUOUS" ]; then
836
964
  if [ -n "$input_id" ]; then
837
- retry_input_id="$input_id"
838
965
  next_retry_count="$(claim_mcc_semantic_retry \
839
- "$product_type" "$sales_code" "$query" "$retry_input_id" "$resolved_semantic_terms" 2>/dev/null)"
966
+ "$product_type" "$sales_code" "$query" "$input_id" "$resolved_semantic_terms" 2>/dev/null)"
840
967
  retry_rc=$?
841
968
  else
842
969
  # A retry without a message identity cannot be bound to one user input.
@@ -898,6 +1025,9 @@ run_resolve_plan() {
898
1025
  if [ -n "$input_id" ]; then
899
1026
  node_args+=(--input-id "$input_id")
900
1027
  fi
1028
+ if [ -n "$agent_candidate_refs_json" ]; then
1029
+ node_args+=(--agent-candidate-refs-json "$agent_candidate_refs_json")
1030
+ fi
901
1031
  if [ "$stale_selection" = true ]; then
902
1032
  node_args+=(--selection-state STALE)
903
1033
  elif [ -n "$select_value" ] || [ -n "$selection_receipt" ]; then
@@ -60,7 +60,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" render <messageId> --va
60
60
 
61
61
  每个登记 stage 动作前,Agent 回顾当前可见历史并得到 `HISTORY_COUNT`;只统计本 run `clarify_intent` 后的用户角色消息,无法可靠回顾时使用 `null`。调用能生成 stage 的业务命令时追加 `--telemetry-run-id "$RUN_ID" --telemetry-history-count "$HISTORY_COUNT"`,由统一 gateway 在业务 parser 前剥离并传给唯一 stage 责任方,Agent 不再额外调用 `telemetry stage`。首次在产品/MCC 确认命令内部新建 run 时尚无 marker,该进程使用刚创建的 active run 且本次 history 固定为 0。`c4` 只承载 `user_input_after_clarify_intent`、`history_count`、`root_intent` 和按唯一非 summary event 数计算的 `progress_pct`;正式计数始终取 runtime counter,history 只诊断、不纠偏。
62
62
 
63
- 三种入口均只在无法确定产品时执行 `product-clarify` runner;产品明确但缺 MCC 用 `mcc-clarify`,且必须携带当前 `ROOT_INTENT`,由 runner 先创建或复用正式 telemetry run。已有经营描述、名称或编码时只执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" mcc resolve-plan --query "$MCC_SEARCH_QUERY" --product-type "$PRODUCT_TYPE" --root-intent "$ROOT_INTENT" [--semantic-terms-json "$MCC_SEMANTIC_TERMS_JSON"] --input-id "$USER_INPUT_ID"`。`ROOT_INTENT` 缺失或非法时 runner 必须停止,禁止依赖跨进程环境变量静默推断或在没有正式 run 的情况下继续 MCC 查询。非精确编码时,Agent 在首次调用中最多补充 12 个有原话依据的行业概念、经营对象或同义表达;首次未提供语义词且结果为 `NONE`,或已提供语义词但结果仍为 `NONE` 时,同一 `run + 用户输入 + 原始 query + 产品` 最多再扩充并重试 2 次,复用同一 `USER_INPUT_ID`,每次只增加有原话依据且此前未使用的概念;后续重试提交的语义词必须保留此前语义词并至少新增一个此前未使用的词,未新增或替换原语义词时不消耗额度;达到上限仍为 `NONE` 时必须按既有 no-match 对客消息要求用户重新描述经营信息。只传概念、不传 MCC 编码且不新增确认;禁止通过更换 query、重置参数或重复调用绕过上限,也禁止硬编码映射、全表判断、远程语义或手拼候选。runner 按 Onboarding entry 严格校验当前产品 `queryMcc` 目录、联合排序原话与语义证据,只为命中且 `needSpecialQual=true` 的候选调用一次 `batchQueryMcc`,并复用有效的同 run/产品候选快照。
63
+ 三种入口均只在无法确定产品时执行 `product-clarify` runner;产品明确但缺 MCC 用 `mcc-clarify`,且必须携带当前 `ROOT_INTENT`,由 runner 先创建或复用正式 telemetry run。已有经营描述、名称或编码时只执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" mcc resolve-plan --query "$MCC_SEARCH_QUERY" --product-type "$PRODUCT_TYPE" --root-intent "$ROOT_INTENT" [--semantic-terms-json "$MCC_SEMANTIC_TERMS_JSON"] [--agent-candidate-refs-json "$MCC_AGENT_CANDIDATE_REFS_JSON"] --input-id "$USER_INPUT_ID"`。`ROOT_INTENT` 缺失或非法时 runner 必须停止,禁止依赖跨进程环境变量静默推断或在没有正式 run 的情况下继续 MCC 查询。非精确编码时,Agent 在首次调用中最多补充 12 个有原话依据的行业概念、经营对象或同义表达;本地结果为 `NONE`、候选冲突或低置信时,runner 只发出一次目录复核 marker,携带同一次 `queryMcc` 返回的当前产品 MCC 名称、描述和稳定引用,Agent 返回最多 8 个本轮目录引用后原样重跑;目录复核不新增 MCP/网络调用且不消耗语义重试预算。语义扩充仍受同一 `run + 用户输入 + 原始 query + 产品` 最多 2 次限制,复用同一 `USER_INPUT_ID`,每次只增加有原话依据且此前未使用的概念;达到上限仍为 `NONE` 时必须按既有 no-match 对客消息要求用户重新描述经营信息。禁止通过更换 query、重置参数或重复调用绕过上限,也禁止硬编码映射、自由提交 MCC code、远程语义或手拼候选。runner 按 Onboarding entry 严格校验当前产品 `queryMcc` 目录、联合排序原话、语义证据和目录引用,只为命中且 `needSpecialQual=true` 的候选调用一次 `batchQueryMcc`,并复用有效的同 run/产品候选快照。
64
64
 
65
65
  代码开发或完整接入缺项目位置/服务端语言时,使用 `integration.context.required` 或 `integration.project_path.required` 标准消息;框架缺失不触发补问。先区分新建/复用,`PROJECT_INPUT` 保留原话。Agent 确定两类项目的真实 `PROJECT_PATH`,以 `resolve-project --project-path "$PROJECT_PATH" --intent <existing|new>` 交 runner 校验;`PROJECT_INPUT` 不得代替路径。默认新建也先选安全路径并显式传入。现有项目位置未知时先定位候选;路径经 `resolve-project` 后再用 `framework-resolve`。补齐前不初始化、装依赖或改文件。
66
66