@alipay/alipay-aipay 1.5.8-beta2 → 1.5.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.5.8-beta2",
3
+ "version": "1.5.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.5.8-beta2
1
+ 1.5.8-beta4
@@ -167,8 +167,9 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message win
167
167
  - `READY`:退出码为 0;唯一带 `ALIPAY_AIPAY_INTERNAL:` 前缀的 `SANDBOX_ENSURE_ACTION=CREATED|VERIFIED`、`SANDBOX_CONFIG_PATH=<同一目标项目绝对路径>/.alipay-sandbox.json` 和 `FLOW:SANDBOX_CONFIG_READY` 内部事实。
168
168
  - `CREATE_PENDING`:退出码为 0;脚本 stdout 已渲染唯一 `sandbox.configuration.pending/CREATE` 正文;唯一带 `ALIPAY_AIPAY_INTERNAL:` 前缀的 `SANDBOX_PENDING_PATH=<同一目标项目绝对路径>/.alipay-sandbox.json` 和 `FLOW:SANDBOX_CONFIG_PENDING_CREATE` 内部事实;不得出现 READY 标记。
169
169
  - `VERIFY_PENDING`:退出码为 0;脚本 stdout 已渲染唯一 `sandbox.configuration.pending/VERIFY` 正文;唯一带 `ALIPAY_AIPAY_INTERNAL:` 前缀的 `SANDBOX_PENDING_PATH=<同一目标项目绝对路径>/.alipay-sandbox.json` 和 `FLOW:SANDBOX_CONFIG_PENDING_VERIFY` 内部事实;不得出现 READY 标记。
170
+ - `LOCAL_FS_RECOVERY`:仅创建分支、退出码 `77`、stdout 为空,且唯一包含 `SANDBOX_LOCAL_FS_PERMISSION_MODE=CREATE` 与 `CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION`。这证明 `createAnonymousSandbox` 调用次数为 0;宿主支持时允许同一 `sandbox ensure` 命令访问两个 CLI 私有状态目录后原样重试一次。拒绝、不支持或再次命中时不再申请、不执行原始 MCP,直接记录 `sandboxConfigState=CREATE_PENDING` 并继续步骤 2,不新增第二份对客消息或用户确认。
170
171
  8. 所有 `ALIPAY_AIPAY_INTERNAL:` 行只用于流程转移;即使宿主合并 stdout/stderr,也必须整行剔除后再判断对客正文。命中 `READY` 时剔除后 stdout 为空,不发送“环境检查完成”“沙箱已创建/已复核”等消息,记录 `sandboxConfigState=READY` 并直接进入步骤 2;命中待配置终态时,脚本 stdout 已包含唯一对客正文,Agent 原文发送且不得转述,分别记录 `sandboxConfigState=CREATE_PENDING|VERIFY_PENDING` 后直接进入步骤 2。两类终态都禁止追加 `jq`、`cat`、`grep`、`ls`、`stat` 或临时脚本;待配置状态还禁止读取、引用或复制当前无效/不存在配置中的任何字段。
171
- 9. 命令非 0、终态标记缺失/重复/冲突、路径不一致或输出含 `SANDBOX_ERROR` 时属于脚本执行协议失败,仍停留在步骤 1;不得把这种执行异常降级为待配置,也不得临场改写 jq、重放原始 MCP 命令或自行检查敏感配置。Windows 配置检查未通过仍按 Windows 登记分支处理。项目内自建 JSON、Agent 自报或补充诊断结果都不能替代上述终态。
172
+ 9. 除上项精确的 `LOCAL_FS_RECOVERY` 外,命令非 0、终态标记缺失/重复/冲突、路径不一致或输出含 `SANDBOX_ERROR` 时属于脚本执行协议失败,仍停留在步骤 1;不得把这种执行异常降级为待配置,也不得临场改写 jq、重放原始 MCP 命令或自行检查敏感配置。CLI 已执行后才出现本地状态权限证据时,创建请求属于 `MAYBE_SENT`,脚本不得返回恢复 marker 或重放,`ensure` 直接使用既有 `CREATE_PENDING` 正文和终态继续代码开发。Windows 配置检查未通过仍按 Windows 登记分支处理。项目内自建 JSON、Agent 自报或补充诊断结果都不能替代上述终态。
172
173
  10. 准备阶段不得渲染 `sandbox.environment.summary` 或 `sandbox.environment.reminder`,不得展示沙箱买家密码,也不得输出“沙箱测试通过”。只有 `sandboxConfigState=READY` 且代码开发完成并通过配置后置校验后,才按步骤 5 的摘要展示规则对客展示沙箱环境配置。
173
174
 
174
175
  ### 步骤 2:产品决策
@@ -96,6 +96,13 @@ if [ "$MODE" = "ensure" ] || [ "$MODE" = "reverify" ]; then
96
96
  while true; do
97
97
  ensure_output=$(bash "$0" "$ENSURE_MODE" "$PROJECT_PATH" "$LANGUAGE" 2>&1)
98
98
  ensure_rc=$?
99
+ if [ "$ensure_rc" -eq "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE" ] &&
100
+ [ "$(printf '%s\n' "$ensure_output" | grep -Fxc 'ALIPAY_AIPAY_INTERNAL:SANDBOX_LOCAL_FS_PERMISSION_MODE=CREATE')" -eq 1 ] &&
101
+ [ "$(printf '%s\n' "$ensure_output" | grep -Fxc 'ALIPAY_AIPAY_INTERNAL:CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION')" -eq 1 ]; then
102
+ emit_control "SANDBOX_LOCAL_FS_PERMISSION_MODE=CREATE"
103
+ emit_control "CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION"
104
+ exit "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE"
105
+ fi
99
106
  if [ "$ensure_rc" -eq 0 ] &&
100
107
  [ "$(printf '%s\n' "$ensure_output" | grep -Fxc "ALIPAY_AIPAY_INTERNAL:SANDBOX_CONFIG_PATH=$CONFIG_PATH")" -eq 1 ] &&
101
108
  [ "$(printf '%s\n' "$ensure_output" | grep -Fxc 'ALIPAY_AIPAY_INTERNAL:FLOW:SANDBOX_CONFIG_READY')" -eq 1 ] &&
@@ -230,10 +237,19 @@ if [ "$MODE" = "create" ]; then
230
237
  ensure_gitignore_protection || { echo "SANDBOX_ERROR:无法为沙箱配置建立 Git 忽略保护" >&2; exit 1; }
231
238
  PLATFORM=$(bash "$DETECTOR") || PLATFORM="unknown"
232
239
  init_platform_id
240
+ if ! require_alipay_cli_local_state_access; then
241
+ emit_control "SANDBOX_LOCAL_FS_PERMISSION_MODE=CREATE"
242
+ emit_control "CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION"
243
+ exit "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE"
244
+ fi
233
245
  attempt=1
234
246
  while true; do
235
247
  raw=$(env -u PRODUCT PLATFORM="$PLATFORM" PLATFORM_ID="${PLATFORM_ID:-}" alipay-cli mcp call alipay-anonymous-sandbox.createAnonymousSandbox --data '{"request":{"appType":"PUBLICAPP"}}' --json 2>&1)
236
248
  rc=$?
249
+ if alipay_cli_has_local_state_permission_error "$raw"; then
250
+ echo "SANDBOX_ERROR:快速沙箱创建结果不明,禁止自动重复创建" >&2
251
+ exit 1
252
+ fi
237
253
  [ "$rc" -eq 0 ] && break
238
254
  if printf '%s' "$raw" | grep -qiE '版本过旧|version.*old'; then break; fi
239
255
  if [ "$attempt" -lt 3 ] && printf '%s' "$raw" | grep -qiE 'MCP_SERVICE_ERROR|SERVICE_UNSTABLE|timeout|timed out|connection|network|ECONN|socket|temporar'; then
@@ -255,6 +271,10 @@ if [ "$MODE" = "create" ]; then
255
271
  sleep 2
256
272
  raw=$(env -u PRODUCT PLATFORM="$PLATFORM" PLATFORM_ID="${PLATFORM_ID:-}" alipay-cli mcp call alipay-anonymous-sandbox.createAnonymousSandbox --data '{"request":{"appType":"PUBLICAPP"}}' --json 2>&1)
257
273
  rc=$?
274
+ if alipay_cli_has_local_state_permission_error "$raw"; then
275
+ echo "SANDBOX_ERROR:快速沙箱创建结果不明,禁止自动重复创建" >&2
276
+ exit 1
277
+ fi
258
278
  [ "$rc" -eq 0 ] || { echo "SANDBOX_ERROR:证书密钥临时错误重试后 CLI 调用失败" >&2; exit 1; }
259
279
  outer=$(extract_json_payload "$raw") || { echo "SANDBOX_ERROR:重试输出中未找到唯一合法 JSON 对象" >&2; exit 1; }
260
280
  text=$(echo "$outer" | jq -r '.result.content[0].text // .content[0].text // empty')
@@ -39,6 +39,9 @@ which alipay-cli 2>/dev/null && alipay-cli version
39
39
  - 若命令已经因网络失败,重试时必须复用同一条命令并申请可联网权限;不要把该失败解释为用户网络故障。
40
40
  - `auth.sh init|confirm|mismatch` 返回 `AUTH_FLOW:RETRY_WITH_NETWORK`:当前环境未取得可确认 CLI 结果;取联网权限后重试同一完整命令,不得解释为业务或 login/logout 失败,也不得改为用户手工操作。
41
41
  - `auth.sh init|wait|confirm|mismatch` 返回 `AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION`:仅缺 CLI 状态目录权限。允许同一 runtime 命令访问 `$HOME/.alipay-cli/credentials`、`${XDG_DATA_HOME:-$HOME/.local/share}/alipay-cli/logs` 后原样重试一次;仅供 CLI 使用,Agent 禁止读取、列举、打印或复制已有内容,不改变其他脚本边界。
42
+ - 签约、服务、应用、截图上传及匿名沙箱创建等受控 runtime 动作同样属于“联网 + CLI 私有状态读写”命令。脚本在参数和上下文校验后、首次 `alipay-cli` 调用前,对上述两个目录执行私有随机文件的创建、固定内容写入、同目录 rename 和删除探测;同一 Shell 进程后续分页、轮询或写后核验不重复探测,不读取、列举或改动任何已有凭据和日志。
43
+ - 非 auth 动作在 CLI 调用前探测失败时只输出 `CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION`,本次 CLI 调用次数为 0;允许当前同一 runtime 命令访问两个目录后原样重试一次。discovery 将其收口为唯一 `DISCOVERY_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION`。拒绝、不支持或重试仍命中同一 marker 时停止申请,按当前 flow 的既有分支失败或待配置边界继续,不新增业务确认或普通对客文案。
44
+ - CLI 已执行后才出现本地状态权限证据时,只读动作仍可按上述 marker 原样重试一次;写动作一律视为 `MAYBE_SENT`,只走现有查询核验或 `UNKNOWN`,不得输出本地权限重试 marker、不得重放原写请求。stdout 成功 JSON 与 stderr 本地权限强证据冲突时仍按本规则阻断。
42
45
  - `app.sh reuse|verify-key|verify-key-and-audit|audit` 已取得 RSA2 支付宝公钥但 `${XDG_CONFIG_HOME:-$HOME/.config}` 探测失败时,返回 `APP_FLOW:RETRY_WITH_LOCAL_CONFIG_PERMISSION` 和 `ALIPAY_PUBLIC_KEY_EXPORT_STATUS=RETRY_WITH_LOCAL_CONFIG_PERMISSION`。仅允许同一 runtime 命令访问该目录后原样重试一次,用于写 `<appId>-alipayPublicKey.keytext`;Agent 禁止检查已有文件。仍失败按目录文案收口。
43
46
  - 本地纯解析、文件检查、`bash -n`、`jq` 校验等不联网命令仍可在默认沙箱内执行。
44
47
 
@@ -106,6 +106,14 @@ require_alipay_cli_local_state_access() {
106
106
  ALIPAY_CLI_LOCAL_STATE_ACCESS_CONFIRMED=true
107
107
  }
108
108
 
109
+ require_alipay_cli_local_state_access_or_emit() {
110
+ if require_alipay_cli_local_state_access; then
111
+ return 0
112
+ fi
113
+ emit_control "CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION"
114
+ return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE"
115
+ }
116
+
109
117
  alipay_aipay_config_dir() {
110
118
  local config_home="${XDG_CONFIG_HOME:-}"
111
119
  if [ -z "$config_home" ]; then
@@ -259,13 +259,14 @@ function hasAuthBlocker(result) {
259
259
  return internalLines(result.stderr).some((line) => [
260
260
  'AUTH_FLOW:AUTH_REQUIRED',
261
261
  'AUTH_FLOW:SCOPE_MISMATCH',
262
- 'AUTH_FLOW:MCC_MISMATCH',
263
- 'AUTH_FLOW:RETRY_WITH_NETWORK',
264
- 'AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION',
265
- 'AUTH_FLOW:LOGOUT_STILL_LOGGED_IN'
262
+ 'AUTH_FLOW:MCC_MISMATCH'
266
263
  ].includes(line));
267
264
  }
268
265
 
266
+ function hasLocalFsRecovery(result) {
267
+ return internalLines(result.stderr).includes('CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION');
268
+ }
269
+
269
270
  function hasNetworkRecovery(result) {
270
271
  return internalLines(result.stderr).some((line) =>
271
272
  line.startsWith('NETWORK_RETRY_EXHAUSTED=') && line.endsWith(':RETRY_WITH_NETWORK'));
@@ -300,7 +301,8 @@ async function runDiscovery(args) {
300
301
 
301
302
  const executedResults = [signingResult, serviceResult, applicationResult].filter(Boolean);
302
303
  const globalBlocker = executedResults.some(hasAuthBlocker) ? 'AUTHORIZATION_INVALID' : '';
303
- const retryWithNetwork = !globalBlocker && executedResults.some(hasNetworkRecovery);
304
+ const retryWithLocalFs = !globalBlocker && executedResults.some(hasLocalFsRecovery);
305
+ const retryWithNetwork = !globalBlocker && !retryWithLocalFs && executedResults.some(hasNetworkRecovery);
304
306
  const envelope = {
305
307
  schema_ver: 1,
306
308
  product_type: productType,
@@ -312,19 +314,23 @@ async function runDiscovery(args) {
312
314
  };
313
315
  if (globalBlocker) envelope.global_blocker = globalBlocker;
314
316
  emitEnvelope(envelope);
315
- if (retryWithNetwork) {
317
+ if (retryWithLocalFs) {
318
+ process.stderr.write(`${internalPrefix}DISCOVERY_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION\n`);
319
+ } else if (retryWithNetwork) {
316
320
  process.stderr.write(`${internalPrefix}DISCOVERY_FLOW:RETRY_WITH_NETWORK\n`);
317
321
  }
318
322
 
319
- const customerOutputResults = globalBlocker
320
- ? executedResults.filter(hasAuthBlocker)
321
- : executedResults;
323
+ const customerOutputResults = retryWithLocalFs
324
+ ? []
325
+ : globalBlocker
326
+ ? executedResults.filter(hasAuthBlocker)
327
+ : executedResults;
322
328
  const customerOutputs = customerOutputResults
323
329
  .map((result) => result.stdout.trim())
324
330
  .filter(Boolean);
325
331
  let uniqueCustomerOutputs = [...new Set(customerOutputs)];
326
332
  if (globalBlocker) uniqueCustomerOutputs = uniqueCustomerOutputs.slice(0, 1);
327
- if (!globalBlocker) {
333
+ if (!globalBlocker && !retryWithLocalFs) {
328
334
  uniqueCustomerOutputs.push(renderDiscoverySummary(productType, {
329
335
  signStatus: envelope.signing.query_status === 'SUCCESS' ? envelope.signing.sign_status : 'QUERY_FAILED',
330
336
  serviceFlow: envelope.service.query_status === 'SUCCESS' ? envelope.service.flow : 'QUERY_FAILED',
@@ -332,7 +338,7 @@ async function runDiscovery(args) {
332
338
  }));
333
339
  }
334
340
  if (uniqueCustomerOutputs.length > 0) process.stdout.write(`${uniqueCustomerOutputs.join('\n\n')}\n`);
335
- return globalBlocker || retryWithNetwork ? 2 : 0;
341
+ return globalBlocker || retryWithLocalFs || retryWithNetwork ? 2 : 0;
336
342
  }
337
343
 
338
344
  export async function runOnboardingDiscoveryCli(argv = process.argv) {
@@ -203,6 +203,7 @@ Step 1 的产品/MCC 唯一事实校验和方案送达、Step 2 的环境 READY
203
203
  ├─ AUTH_FLOW:READY_TO_OPEN → 输出非阻塞授权页,执行第 2 项
204
204
  │ └─ 若同时存在 AUTH_STATE:SAVE_FAILED:授权页 stdout 已送达,后续 open 仅可降级为 AUTH_OPEN:LINK_ONLY;wait/confirm 仍使用本条命令显式上下文继续,禁止重新生成授权页
205
205
  ├─ 底层 init 命中 SCOPE_MISMATCH / MCC_MISMATCH → runtime 在同一命令内自动调用既有第 4 项,只透传 mismatch 的最终结果;唯一 AUTH_AUTO_MISMATCH 仅供内部诊断,不是终态
206
+ ├─ AUTH_FLOW:AUTH_REQUIRED → 当前失效会话已按脚本后置条件处理;原样重新执行第 1 项一次,生成当前上下文的新授权页;再次命中则停止循环
206
207
  ├─ AUTH_FLOW:RETRY_WITH_NETWORK → 取得联网权限后重试同一完整命令
207
208
  ├─ AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION → 取得 CLI 私有状态目录权限后原样重试本命令一次
208
209
  ├─ AUTH_FLOW:LOGOUT_STILL_LOGGED_IN → 自动 mismatch 的 logout 后置条件未成立,按第 4 项同名分支停止
@@ -216,6 +217,7 @@ Step 1 的产品/MCC 唯一事实校验和方案送达、Step 2 的环境 READY
216
217
  ├─ AUTH_FLOW:PENDING → 输出 auth.pending,等待用户明确表示扫码已完成
217
218
  ├─ AUTH_FLOW:EXPIRED → 重新执行第 1 项生成授权链接
218
219
  ├─ AUTH_FLOW:CONTEXT_MISMATCH → 当前动作未执行 login --complete/logout;使用当前已确认的完整参数重新执行第 1 项
220
+ ├─ AUTH_FLOW:AUTH_REQUIRED → 当前授权会话已失效,旧页面不再作为恢复依据;原样重新执行第 1 项一次,再次命中则停止循环
219
221
  ├─ AUTH_FLOW:RETRY_WITH_NETWORK → 取得联网权限后重试同一完整命令
220
222
  ├─ AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION → 保留当前授权页短时状态,取得 CLI 私有状态目录权限后原样重试当前 wait 命令一次
221
223
  ├─ AUTH_FLOW:FAILED → 停止并按错误输出恢复,禁止进入 Step 3.1
@@ -253,9 +255,9 @@ Step 1 的产品/MCC 唯一事实校验和方案送达、Step 2 的环境 READY
253
255
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-discovery run --product-type "$PRODUCT_TYPE" --sales-code "$SALES_CODE" [--reuse-sign-receipt "$AUTH_SIGN_REUSE_RECEIPT"]
254
256
  ```
255
257
 
256
- 只接受唯一 `ALIPAY_AIPAY_INTERNAL:DISCOVERY_RESULT=<json>`。runner stdout 保留底层脚本已有的受控错误正文,并在没有全局授权阻断时追加原 `onboarding.discovery.summary` 标准正文;必须原文送达,不得手写、压缩或再次调用 `discovery-summary`。envelope 中每个分支只允许 `SUCCESS|QUERY_FAILED|NOT_APPLICABLE`:成功分支携带原脚本的确定状态和候选,失败不得伪装为空列表;存在 `global_blocker=AUTHORIZATION_INVALID` 时停止查询组,按 Step 3 重新授权,不进入 Step 4。不存在授权阻断且底层重试耗尽具有 DNS、连接、timeout、socket/TLS、`fetch failed`、`network_error`、`网络连接失败` 或连接前沙箱禁网强证据时,runner 额外且至多输出一个 `ALIPAY_AIPAY_INTERNAL:DISCOVERY_FLOW:RETRY_WITH_NETWORK`;支付宝侧明确服务异常、业务错误、裸 `permission denied` 或结构异常不得触发该 marker。
258
+ 只接受唯一 `ALIPAY_AIPAY_INTERNAL:DISCOVERY_RESULT=<json>`。runner stdout 保留底层脚本已有的受控错误正文,并在没有全局授权阻断或本地权限恢复时追加原 `onboarding.discovery.summary` 标准正文;必须原文送达,不得手写、压缩或再次调用 `discovery-summary`。envelope 中每个分支只允许 `SUCCESS|QUERY_FAILED|NOT_APPLICABLE`:成功分支携带原脚本的确定状态和候选,失败不得伪装为空列表;存在 `global_blocker=AUTHORIZATION_INVALID` 时停止查询组,按 Step 3 重新授权,不进入 Step 4。不存在授权阻断且任一只读子查询命中 CLI 私有状态 preflight/调用后权限证据时,runner 只输出一个 `ALIPAY_AIPAY_INTERNAL:DISCOVERY_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION`,不输出摘要或网络恢复 marker;否则底层重试耗尽具有 DNS、连接、timeout、socket/TLS、`fetch failed`、`network_error`、`网络连接失败` 或连接前沙箱禁网强证据时,runner 额外且至多输出一个 `ALIPAY_AIPAY_INTERNAL:DISCOVERY_FLOW:RETRY_WITH_NETWORK`。支付宝侧明确服务异常、业务错误、裸 `permission denied` 或结构异常不得触发这两个恢复 marker。
257
259
 
258
- 命中 `DISCOVERY_FLOW:RETRY_WITH_NETWORK` 时先暂存本次 stdout,不对客发送;宿主支持时申请联网权限,并以完全相同的产品、salesCode 和签约复用回执原样重试同一条 discovery 命令一次。重试成功只送达重试后的 stdout 并继续;权限被拒绝、宿主不支持时送达首次 stdout 一次;重试仍返回同一 marker 时只送达重试后的 stdout 一次并停止再次申请。该恢复不新增对客文案、确认点或 closeout 枚举。`AUTHORIZATION_INVALID` 优先,禁止同时进入网络恢复。
260
+ 命中 `DISCOVERY_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION` 时 stdout 必须为空;宿主支持时只允许当前同一 discovery 命令访问两个 CLI 私有状态目录并原样重试一次。拒绝、不支持或再次命中时停止申请,按最新 envelope 保留失败分支,不渲染额外恢复文案。命中 `DISCOVERY_FLOW:RETRY_WITH_NETWORK` 时先暂存本次 stdout,不对客发送;宿主支持时申请联网权限,并以完全相同的产品、salesCode 和签约复用回执原样重试同一条 discovery 命令一次。重试成功只送达重试后的 stdout 并继续;权限被拒绝、宿主不支持时送达首次 stdout 一次;重试仍返回同一 marker 时只送达重试后的 stdout 一次并停止再次申请。两类恢复都不新增对客文案、确认点或 closeout 枚举。优先级固定为 `AUTHORIZATION_INVALID`、本地权限、网络权限,禁止同时进入两个恢复分支。
259
261
 
260
262
  不存在全局授权阻断时,执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" flow onboarding branch-summary` 加载 Step 4 和三个分支共用的 Step 5 约束;共用区块只加载一次,不得整份读取后续 flow。
261
263
 
@@ -273,6 +275,7 @@ Step 5 直接使用对应 phase 的完整命令;仅出现脚本异常、需要
273
275
 
274
276
  - MCP 认证失败或产品/scope/MCC 授权不匹配:discovery runner 子查询只上报认证失效事实,不在并发子进程内 logout;立即停止查询组,不再处理剩余查询结果。授权不匹配时调用 Step 3 的 `auth.sh mismatch`,由该命令统一 logout 并重新授权。重新授权后仅恢复尚未执行的查询,保留重新授权前已经成功取得且主体未变化的结果。若无法确认仍为同一账号主体,则重新执行全部适用查询。
275
277
  - 强网络证据且无授权阻断:只按上文 `DISCOVERY_FLOW:RETRY_WITH_NETWORK` 契约申请权限并原样重试 discovery 一次;连接前沙箱明确禁网不在子脚本内浪费两次固定间隔重试。远端服务异常仍使用既有分支失败和收口,不误报为本地联网权限。
278
+ - CLI 私有状态权限且无授权阻断:只按上文 `DISCOVERY_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION` 契约恢复同一只读查询组一次;不得改走 auth mismatch、不得解释为网络或支付宝侧查询失败。
276
279
  - 单个业务失败、响应结构错误或 MCP 服务失败,但当前会话仍有效:把该分支标记为查询失败,继续其他不依赖该结果的只读查询,最后一次性输出“已取得结果 + 失败分支”摘要。不得把失败视为空列表;应用查询失败时禁止应用复用/创建,服务查询失败时禁止服务复用/新建/修改。
277
280
  - 签约查询失败:只阻断签约分支;可继续应用/服务查询、候选展示、资源决策及其独立资料采集和写操作。不得推断签约状态,不得采集签约截图或签约所需 APP 名称,`signStatus` 和签约 `materialStatus` 保持未设置。签约查询恢复成功后,只有状态为 `NOT_SIGNED` 才进入签约材料类别。应用或服务查询失败也只阻断对应分支;认证失败、授权不匹配或无法确认登录主体一致时仍是全局阻断。
278
281
 
@@ -311,6 +314,8 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mate
311
314
 
312
315
  按“签约 -> 服务 -> 应用”默认顺序,一次只处理一个实际适用且可推进的类别;顺序不构成分支依赖。认证、授权或主体变化时全局停止并重新校验;单分支失败只登记该分支恢复动作,继续其他独立分支,不清除已验证材料或成功事实。全部适用分支已处理,或失败、未知及外部待办均已登记后,才执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" flow onboarding closeout`;仍有可推进分支时不得提前收口。
313
316
 
317
+ 本阶段的签约、服务、应用和截图 runtime 命令在 CLI 调用前返回唯一 `CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION` 时,当前业务调用次数为 0;允许同一命令访问 CLI 私有状态目录后原样重试一次,不重收材料、不重开页面、不新增确认。拒绝、不支持或再次命中时停止申请并按当前分支既有失败边界收口。CLI 已执行后出现同类证据时,读操作仍可进入该恢复;签约提交、服务保存、应用创建/公钥页/提审和截图上传等写操作只走既有核验或 `UNKNOWN`,禁止因权限 marker 重放。
318
+
314
319
  轮到某类别且即将执行其业务动作时,先执行对应 loader:签约用 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" flow onboarding signing --product-type "$PRODUCT_TYPE"`,仅按量付费服务用 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" flow onboarding service --product-type "$PRODUCT_TYPE"`,应用用 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" flow onboarding application --product-type "$PRODUCT_TYPE"`。loader 自动记录该分支 stage;未轮到或不适用的分支不得加载、不得记录。
315
320
 
316
321
  <!-- ALIPAY_AIPAY_FLOW:ONBOARDING_BRANCH_SUMMARY:END -->
@@ -22,6 +22,7 @@
22
22
  - `AUTH_FLOW:PENDING`:有限轮询耗尽或单次恢复查询尚未得到有效本机登录态;不等于失败,也不证明用户未扫码。
23
23
  - `AUTH_FLOW:EXPIRED`:当前页面实际返回过期,旧页面和旧回复失效,重新执行 flow 登记的 init。
24
24
  - `AUTH_FLOW:SCOPE_MISMATCH|MCC_MISMATCH`:只能走 `auth.sh mismatch`。统一 runtime 的 `auth init` 在底层 init 唯一返回 mismatch 且 stdout 为空时,会在同一命令内自动调用既有 `auth.sh mismatch`,抑制中间 mismatch 终态并只透传最终结果;`wait|confirm` 返回 mismatch 时仍按 flow 显式调用 mismatch。logout 后置条件未成立前不得重授权;仍登录返回 `AUTH_FLOW:LOGOUT_STILL_LOGGED_IN`。
25
+ - `AUTH_FLOW:AUTH_REQUIRED`:`whoami` 或已认证资源校验已证明当前 CLI 会话失效,且脚本已完成既有 logout 后置条件处理;按 flow 原样重新执行当前上下文的 `auth init` 一次,旧授权页不再作为恢复依据,再次命中时停止循环。
25
26
  - `AUTH_FLOW:RETRY_WITH_NETWORK`:当前 Agent 环境无法确认结果,申请联网权限后原样重试同一命令;不得解释为用户本机登录失败。
26
27
  - `AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION`:授权状态命令 preflight 失败,或实际输出命中 CLI 私有状态写失败证据。可恢复阶段 stdout 为空;仅允许当前受控 runtime 命令访问 CLI 私有状态目录后原样重试一次;`wait|confirm` 保留短时授权状态,不重建授权页。Agent 禁止读取、列举、打印或复制已有凭据与日志;拒绝、不支持或重试仍命中同一 marker 时才输出最小失败说明。
27
28
 
@@ -6,24 +6,25 @@
6
6
 
7
7
  | 内部分类 | 已验证触发 | 恢复边界 |
8
8
  | --- | --- | --- |
9
- | `MCP_AUTH_ERROR` | HTTP 401、`Authorization is empty`、非法认证信息 | 停止查询组;logout 后置条件成立后重授权。仍登录则阻断;不可确认或无网络时只重试同一动作,不解释为用户本机或支付宝业务失败 |
9
+ | `MCP_AUTH_ERROR` | 非 JSON 传输错误中的 HTTP 401/认证强证据;JSON 顶层或登记 error/response 层中的数字/字符串 `401`、`unauthorized`、`Authorization is empty`、非法认证信息、`用户未登录`、`not_logged_in` 等明确登录失效证据;已认证资源动作顶层或 `data.logged_in=false` | 停止查询组;logout 后置条件成立后重授权。仍登录则阻断;不可确认或无网络时只重试同一动作,不解释为用户本机或支付宝业务失败。成功候选、说明或请求示例中的相同文本不构成认证错误 |
10
10
  | `AUTH_MISMATCH` | `mccCode is not auth`、`salesProductCodes is not auth`、`scope is not auth` | 停止当前动作,只进 `auth.sh mismatch`;logout 后置条件未成立前禁重授权 |
11
11
  | `MCP_SERVICE_ERROR` / `SERVICE_UNSTABLE` | 已识别连接、网络或服务异常 | 按下方有限重试规则处理;耗尽后记录受影响分支,依赖允许时继续其他独立分支,最终统一收口 |
12
12
  | `DISCOVERY_FLOW:RETRY_WITH_NETWORK` | discovery 子查询重试耗尽,且原始输出包含 DNS、连接、timeout、socket/TLS、`fetch failed`、`network_error`、`网络连接失败` 或连接前沙箱禁网强证据 | 只在无授权阻断时输出一次;暂存首次 stdout,申请联网权限后原样重试同一 discovery 命令一次。拒绝、不支持或再次命中时停止申请,只送达最新一次既有 stdout |
13
- | `CLI_LOCAL_FS_PERMISSION` | 授权状态命令 preflight 失败;或原始 stdout+stderr 命中私有状态写失败强证据,或私有状态路径与权限错误组合 | 仅用于 `auth.sh init|wait|confirm|mismatch` 的 `login/login --complete/logout` 链路;可恢复阶段 stdout 为空,只输出唯一 `AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION`,取得 CLI 私有状态目录权限后原样重试一次,不改产品、scope、MCC 或授权页 |
13
+ | `DISCOVERY_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION` | discovery 任一子查询在 CLI 调用前探测失败,或只读调用后命中 CLI 私有状态权限证据 | 授权阻断优先;本次不渲染 discovery 摘要,取得两个 CLI 私有状态目录权限后原样重试同一 discovery 命令一次,不改产品、salesCode 或签约复用回执 |
14
+ | `CLI_LOCAL_FS_PERMISSION` | 受控 CLI 动作 preflight 失败;或原始 stdout+stderr 命中私有状态写失败强证据,或私有状态路径与权限错误组合 | auth 使用唯一 `AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION`;其他未发送或只读动作使用唯一 `CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION`。取得目录权限后原样重试一次;写动作已执行则保持 `MAYBE_SENT` 并只核验/标记 `UNKNOWN`,不得重放 |
14
15
  | `APP_LOCAL_CONFIG_PERMISSION` | 已取 RSA2 支付宝公钥但 config 探针失败 | stdout 空;输出两个本地权限 marker,取权后原样重试当前 app 命令一次,不重开公钥页 |
15
16
  | `OPEN_ERROR_KIND` | URL 已交付但 opener 失败 | 保留 URL 兜底;诊断不对客,不改写为业务、网络或授权失败 |
16
17
  | `ERROR:<code>` | 当前响应中存在可识别业务错误 | 展示登记且脱敏的业务错误,修正业务条件前不重试 |
17
18
  | `CLI_ERROR:<message>` | CLI 失败、异常结构或结果不可唯一解析 | 隐藏原始输出,停止并按登记 CLI 错误恢复,不猜成功或空结果 |
18
19
  | `SUCCESS` | 唯一合法成功结构 | 才允许进入当前脚本的业务字段解析 |
19
20
 
20
- 发现阶段的单分支失败不得伪装为空列表,也不得回滚其他已成功分支;认证失效或授权不匹配属于查询组全局阻断,重新授权后恢复尚未执行的查询,主体一致性无法确认时重新查询全部适用状态。
21
+ 发现阶段的单分支失败不得伪装为空列表,也不得回滚其他已成功分支;认证失效或授权不匹配属于查询组全局阻断,重新授权后恢复尚未执行的查询,主体一致性无法确认时重新查询全部适用状态。`logged_in=false` 只在已认证的签约、服务、应用和上传等资源动作中属于认证失效;`auth.sh whoami` 的成功未登录结果仍是正常登录入口,失败响应即使携带该字段也不能证明已退出。discovery 的恢复优先级固定为真实认证/授权阻断、本地 CLI 状态权限、网络权限、普通分支失败;`AUTH_FLOW:RETRY_WITH_NETWORK|RETRY_WITH_LOCAL_FS_PERMISSION|LOGOUT_STILL_LOGGED_IN` 不是 discovery 子查询的认证失效证据,不得映射为 `AUTHORIZATION_INVALID`。
21
22
 
22
23
  discovery runner 和截图上传这类并发/分组动作不得让每个子进程各自 logout。子脚本在分组环境下只上报认证失效事实;由 flow 登记的唯一授权恢复入口执行一次 logout 后置条件确认。三张截图同时认证失败时只允许产生一个全局认证恢复 marker。
23
24
 
24
25
  ## 解包与业务错误
25
26
 
26
- `unwrap_mcp` 兼容纯业务 JSON、唯一 MCP `content[0].text` 信封和混合日志。授权状态命令捕获先合并 stdout/stderr 检查 `写入临时文件失败|删除凭据文件失败|can't rename log file` 强证据,或 `poll_state.enc.tmp|session.enc|.alipay-cli/credentials|alipay-cli/logs` 与 `permission denied|operation not permitted|EACCES|EPERM` 组合;命中时不得按 `network_error` 处理,裸后端 `permission denied` 不算本地权限。其他 CLI 捕获再执行唯一性解析:单个 JSON 加普通日志可接受,相同 JSON 重复可去重,多个不同 JSON 或成功/失败冲突不得选择任一路,也不得回显原始响应。每次 MCP 调用先把唯一原始结果交给 `handle_error`,返回成功后才调用 `unwrap_mcp` 和当前脚本的业务解析;不得统一假定成功字段位于 `.success`。
27
+ `unwrap_mcp` 兼容纯业务 JSON、唯一 MCP `content[0].text` 信封和混合日志。所有受控 CLI 捕获先合并 stdout/stderr 检查 `写入临时文件失败|删除凭据文件失败|can't rename log file` 强证据,或 `poll_state.enc.tmp|session.enc|.alipay-cli/credentials|alipay-cli/logs` 与 `permission denied|operation not permitted|EACCES|EPERM` 组合;命中时不得按成功 JSON 或 `network_error` 处理,裸后端 `permission denied` 不算本地权限。其他 CLI 捕获再执行唯一性解析:单个 JSON 加普通日志可接受,相同 JSON 重复可去重,多个不同 JSON 或成功/失败冲突不得选择任一路,也不得回显原始响应。每次 MCP 调用先把唯一原始结果交给 `handle_error`,返回成功后才调用 `unwrap_mcp` 和当前脚本的业务解析;不得统一假定成功字段位于 `.success`。认证分类只扫描真实响应错误层,禁止递归扫描成功候选、业务说明或请求示例中的错误样例。
27
28
 
28
29
  每次 logout 失败后只执行一次 `whoami` 后置核验;logout 明确成功时无需额外核验,最多尝试 3 次 logout。`whoami` 后置核验必须先满足唯一 JSON、退出码为 0 且 `detect_error=SUCCESS`,再读取类型正确的 `logged_in/is_expired`;业务失败响应即使携带 `logged_in=false` 也不能证明已退出。logout 网络/服务异常、冲突输出或结果无法确认时,即使 `whoami` 仍显示已登录,也只能转 `AUTH_FLOW:RETRY_WITH_NETWORK` 并重试同一动作。logout 是可由 `whoami` 验证最终状态的幂等恢复动作:明确业务失败且仍登录时,脚本内部使用“初次 + 2 次”固定预算、间隔 3 秒重试完全相同的 logout;任一次确认已退出即继续,全部耗尽且仍登录才转 `AUTH_FLOW:LOGOUT_STILL_LOGGED_IN`。所有失败分支都不得执行 login。每个 auth 动作只允许产生一个最终 `AUTH_FLOW:*`,具体恢复 marker 已由错误处理器输出时,调用方不得追加 `AUTH_FLOW:FAILED`。
29
30
 
@@ -314,17 +314,21 @@ query_security_key() {
314
314
  REQUEST_JSON=$(jq -n --arg appId "$APP_ID" '{request:{appId:$appId}}')
315
315
  fi
316
316
 
317
+ require_alipay_cli_local_state_access_or_emit || return "$?"
317
318
  export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
318
319
  run_network_retry RESULT read application_security_key -- alipay-cli mcp call apprelease.queryApplicationSecurityKey \
319
320
  -d "$REQUEST_JSON" \
320
321
  --json
321
322
  RETRY_RC=$?
323
+ if [ "$RETRY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
324
+ return "$RETRY_RC"
325
+ fi
322
326
  if [ "$RETRY_RC" -ne 0 ]; then
323
327
  echo "❌ 应用安全密钥查询因网络或服务异常在自动重试两次后仍未恢复"
324
328
  return 1
325
329
  fi
326
330
 
327
- if ! handle_error "$RESULT"; then
331
+ if ! handle_error "$RESULT" "MCP_AUTHENTICATED_CALL"; then
328
332
  return 1
329
333
  fi
330
334
 
@@ -343,17 +347,21 @@ query_application_info() {
343
347
  local APP_ID="$1"
344
348
 
345
349
  REQUEST_JSON=$(jq -n --arg appId "$APP_ID" '{request:{appId:$appId}}')
350
+ require_alipay_cli_local_state_access_or_emit || return "$?"
346
351
  export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
347
352
  run_network_retry RESULT read application_info -- alipay-cli mcp call apprelease.queryApplicationInfo \
348
353
  -d "$REQUEST_JSON" \
349
354
  --json
350
355
  RETRY_RC=$?
356
+ if [ "$RETRY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
357
+ return "$RETRY_RC"
358
+ fi
351
359
  if [ "$RETRY_RC" -ne 0 ]; then
352
360
  echo "❌ 应用信息查询因网络或服务异常在自动重试两次后仍未恢复"
353
361
  return 1
354
362
  fi
355
363
 
356
- if ! handle_error "$RESULT"; then
364
+ if ! handle_error "$RESULT" "MCP_AUTHENTICATED_CALL"; then
357
365
  return 1
358
366
  fi
359
367
 
@@ -500,7 +508,7 @@ ensure_rsa2_application_key_ready() {
500
508
  local PUBLIC_KEY="${2:-}"
501
509
  local ATTEMPT=1
502
510
  local MAX_ATTEMPTS=20
503
- local INTERVAL_SECONDS=2
511
+ local INTERVAL_SECONDS=2 QUERY_RC
504
512
  KEY_VERIFY_RESULT=""
505
513
 
506
514
  if [ "${ALIPAY_AIPAY_RECOVERY_QUERY_ONCE:-0}" = "1" ]; then
@@ -512,7 +520,13 @@ ensure_rsa2_application_key_ready() {
512
520
  fi
513
521
 
514
522
  while [ "$ATTEMPT" -le "$MAX_ATTEMPTS" ]; do
515
- if ! query_security_key "$APP_ID" "$PUBLIC_KEY"; then
523
+ query_security_key "$APP_ID" "$PUBLIC_KEY"
524
+ QUERY_RC=$?
525
+ if [ "$QUERY_RC" -ne 0 ]; then
526
+ if [ "$QUERY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
527
+ KEY_VERIFY_RESULT="LOCAL_FS_PERMISSION"
528
+ return "$QUERY_RC"
529
+ fi
516
530
  KEY_VERIFY_RESULT="QUERY_FAILED"
517
531
  return 1
518
532
  fi
@@ -563,8 +577,11 @@ export_alipay_public_key_from_security_business() {
563
577
 
564
578
  verify_key_and_export_alipay_public_key() {
565
579
  local APP_ID="$1"
580
+ local QUERY_RC
566
581
 
567
- query_security_key "$APP_ID" || return 1
582
+ query_security_key "$APP_ID"
583
+ QUERY_RC=$?
584
+ [ "$QUERY_RC" -eq 0 ] || return "$QUERY_RC"
568
585
  export_alipay_public_key_from_security_business "$APP_ID"
569
586
  }
570
587
 
@@ -578,11 +595,15 @@ app_list() {
578
595
  init_app_cli_product || exit 1
579
596
  REQUEST_JSON=$(jq -n --arg applicationType "$APP_APPLICATION_TYPE" '{request:{appTypes:[$applicationType]}}')
580
597
 
598
+ require_alipay_cli_local_state_access_or_emit || exit "$?"
581
599
  export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
582
600
  run_network_retry RESULT read application_list -- alipay-cli mcp call apprelease.queryApplicationList \
583
601
  -d "$REQUEST_JSON" \
584
602
  --json
585
603
  RETRY_RC=$?
604
+ if [ "$RETRY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
605
+ exit "$RETRY_RC"
606
+ fi
586
607
  if [ "$RETRY_RC" -ne 0 ]; then
587
608
  echo "❌ 应用列表查询因网络或服务异常在自动重试两次后仍未恢复"
588
609
  emit_control "FLOW:ERROR"
@@ -590,7 +611,7 @@ app_list() {
590
611
  fi
591
612
 
592
613
  # 错误检测
593
- if ! handle_error "$RESULT"; then
614
+ if ! handle_error "$RESULT" "MCP_AUTHENTICATED_CALL"; then
594
615
  exit 1
595
616
  fi
596
617
 
@@ -685,6 +706,7 @@ app_create() {
685
706
  exit 1
686
707
  }
687
708
 
709
+ require_alipay_cli_local_state_access_or_emit || exit "$?"
688
710
  export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
689
711
  run_network_retry RESULT write application_create -- alipay-cli mcp call apprelease.createApplication \
690
712
  -d "$REQUEST_JSON" \
@@ -694,6 +716,8 @@ app_create() {
694
716
  echo "❌ 应用创建响应不明;现有应用列表无法把同类型新应用唯一归属于本次创建,结果标记为 UNKNOWN,禁止自动重复创建"
695
717
  emit_control "APPLICATION_CREATE_RESULT=UNKNOWN"
696
718
  exit 1
719
+ elif [ "$RETRY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
720
+ exit "$RETRY_RC"
697
721
  elif [ "$RETRY_RC" -ne 0 ]; then
698
722
  echo "❌ 应用创建因明确未发送的网络异常在自动重试两次后仍未恢复"
699
723
  emit_control "APPLICATION_CREATE_RESULT=FAILED"
@@ -701,7 +725,7 @@ app_create() {
701
725
  fi
702
726
 
703
727
  # 错误检测
704
- if ! handle_error "$RESULT"; then
728
+ if ! handle_error "$RESULT" "MCP_AUTHENTICATED_CALL"; then
705
729
  emit_control "APPLICATION_CREATE_RESULT=FAILED"
706
730
  exit 1
707
731
  fi
@@ -766,6 +790,7 @@ app_key() {
766
790
  --arg publicKey "$PUBLIC_KEY" \
767
791
  '{request:{appId:$appId,signType:"RSA2",publicKey:$publicKey}}')
768
792
 
793
+ require_alipay_cli_local_state_access_or_emit || exit "$?"
769
794
  export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
770
795
  run_network_retry RESULT write application_key_confirm_page -- alipay-cli mcp call apprelease.createKeyConfirmPage \
771
796
  -d "$REQUEST_JSON" \
@@ -775,6 +800,8 @@ app_key() {
775
800
  echo "❌ 公钥确认页生成响应不明,结果标记为 UNKNOWN;不得自动生成第二个页面"
776
801
  emit_control "KEY_PAGE_RESULT=UNKNOWN"
777
802
  exit 1
803
+ elif [ "$RETRY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
804
+ exit "$RETRY_RC"
778
805
  elif [ "$RETRY_RC" -ne 0 ]; then
779
806
  echo "❌ 公钥确认页因明确未发送的网络异常在自动重试两次后仍未恢复"
780
807
  emit_control "KEY_PAGE_RESULT=FAILED"
@@ -782,7 +809,7 @@ app_key() {
782
809
  fi
783
810
 
784
811
  # 错误检测
785
- if ! handle_error "$RESULT"; then
812
+ if ! handle_error "$RESULT" "MCP_AUTHENTICATED_CALL"; then
786
813
  emit_control "KEY_PAGE_RESULT=FAILED"
787
814
  exit 1
788
815
  fi
@@ -872,13 +899,14 @@ submit_application_audit() {
872
899
  local APP_ID="$1"
873
900
  REQUEST_JSON=$(jq -n --arg appId "$APP_ID" '{request:{appId:$appId}}')
874
901
 
902
+ require_alipay_cli_local_state_access_or_emit || exit "$?"
875
903
  export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
876
904
  run_network_retry RESULT write application_audit -- alipay-cli mcp call apprelease.submitApplicationAudit \
877
905
  -d "$REQUEST_JSON" \
878
906
  --json
879
907
  RETRY_RC=$?
880
908
  if [ "$RETRY_RC" -eq 75 ]; then
881
- if query_application_info "$APP_ID"; then
909
+ if ALIPAY_AIPAY_SUPPRESS_LOCAL_FS_RETRY_MARKER=1 query_application_info "$APP_ID"; then
882
910
  ACTUAL_STATUS=$(extract_application_status)
883
911
  case "$ACTUAL_STATUS" in
884
912
  AUDITING|ON_LINE)
@@ -893,6 +921,8 @@ submit_application_audit() {
893
921
  echo "❌ 应用提审响应不明且现有状态无法排除传播延迟,结果标记为 UNKNOWN,禁止自动重复提审"
894
922
  emit_control "APPLICATION_AUDIT_RESULT=UNKNOWN"
895
923
  exit 1
924
+ elif [ "$RETRY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
925
+ exit "$RETRY_RC"
896
926
  elif [ "$RETRY_RC" -ne 0 ]; then
897
927
  echo "❌ 应用提审因明确未发送的网络异常在自动重试两次后仍未恢复"
898
928
  emit_control "APPLICATION_AUDIT_RESULT=FAILED"
@@ -900,7 +930,7 @@ submit_application_audit() {
900
930
  fi
901
931
 
902
932
  # 错误检测
903
- if ! handle_error "$RESULT"; then
933
+ if ! handle_error "$RESULT" "MCP_AUTHENTICATED_CALL"; then
904
934
  emit_control "APPLICATION_AUDIT_RESULT=FAILED"
905
935
  exit 1
906
936
  fi
@@ -962,7 +992,9 @@ app_verify_key() {
962
992
  fi
963
993
  validate_control_value "$APP_ID" || { echo "❌ appId 包含非法控制字符"; exit 1; }
964
994
 
965
- if ensure_rsa2_application_key_ready "$APP_ID" "$PUBLIC_KEY"; then
995
+ ensure_rsa2_application_key_ready "$APP_ID" "$PUBLIC_KEY"
996
+ VERIFY_RC=$?
997
+ if [ "$VERIFY_RC" -eq 0 ]; then
966
998
  export_alipay_public_key_from_security_business "$APP_ID"
967
999
  EXPORT_RC=$?
968
1000
  [ "$EXPORT_RC" -eq 0 ] || exit "$EXPORT_RC"
@@ -970,7 +1002,9 @@ app_verify_key() {
970
1002
  emit_control_field "APP_ID" "$APP_ID" || exit 1
971
1003
  emit_control "FLOW:KEY_CONFIRMED"
972
1004
  else
973
- if [ "$KEY_VERIFY_RESULT" = "PENDING" ]; then
1005
+ if [ "$KEY_VERIFY_RESULT" = "LOCAL_FS_PERMISSION" ]; then
1006
+ exit "$VERIFY_RC"
1007
+ elif [ "$KEY_VERIFY_RESULT" = "PENDING" ]; then
974
1008
  if ! render_key_pending; then
975
1009
  emit_control "FLOW:ERROR"
976
1010
  exit 1
@@ -997,8 +1031,12 @@ app_verify_key_and_audit() {
997
1031
  [ -n "$APP_ID" ] || { echo "❌ 请提供 appId"; exit 1; }
998
1032
  validate_control_value "$APP_ID" || { echo "❌ appId 包含非法控制字符"; exit 1; }
999
1033
 
1000
- if ! ensure_rsa2_application_key_ready "$APP_ID" "$PUBLIC_KEY"; then
1001
- if [ "$KEY_VERIFY_RESULT" = "PENDING" ]; then
1034
+ ensure_rsa2_application_key_ready "$APP_ID" "$PUBLIC_KEY"
1035
+ VERIFY_RC=$?
1036
+ if [ "$VERIFY_RC" -ne 0 ]; then
1037
+ if [ "$KEY_VERIFY_RESULT" = "LOCAL_FS_PERMISSION" ]; then
1038
+ exit "$VERIFY_RC"
1039
+ elif [ "$KEY_VERIFY_RESULT" = "PENDING" ]; then
1002
1040
  if ! render_key_pending; then
1003
1041
  emit_control "FLOW:ERROR"
1004
1042
  exit 1
@@ -1034,7 +1072,9 @@ app_reuse() {
1034
1072
  fi
1035
1073
  validate_control_value "$APP_ID" || { echo "❌ appId 包含非法控制字符"; exit 1; }
1036
1074
 
1037
- if query_application_info "$APP_ID"; then
1075
+ query_application_info "$APP_ID"
1076
+ QUERY_RC=$?
1077
+ if [ "$QUERY_RC" -eq 0 ]; then
1038
1078
  APP_STATUS=$(extract_application_status)
1039
1079
  if [ "$APP_STATUS" != "ON_LINE" ]; then
1040
1080
  echo "❌ 仅允许复用 ON_LINE(已上线)应用,当前应用状态: ${APP_STATUS:-未知}"
@@ -1042,11 +1082,16 @@ app_reuse() {
1042
1082
  exit 1
1043
1083
  fi
1044
1084
  else
1085
+ if [ "$QUERY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
1086
+ exit "$QUERY_RC"
1087
+ fi
1045
1088
  emit_control "FLOW:ERROR"
1046
1089
  exit 1
1047
1090
  fi
1048
1091
 
1049
- if query_security_key "$APP_ID"; then
1092
+ query_security_key "$APP_ID"
1093
+ QUERY_RC=$?
1094
+ if [ "$QUERY_RC" -eq 0 ]; then
1050
1095
  RSA2_KEY_INFO=$(extract_rsa2_application_key_info)
1051
1096
  RSA2_ALIPAY_KEY_INFO=$(extract_rsa2_alipay_key_info)
1052
1097
  ALIPAY_PUBLIC_KEY=$(extract_alipay_public_key "$RSA2_ALIPAY_KEY_INFO")
@@ -1067,6 +1112,9 @@ app_reuse() {
1067
1112
  exit 1
1068
1113
  fi
1069
1114
  else
1115
+ if [ "$QUERY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
1116
+ exit "$QUERY_RC"
1117
+ fi
1070
1118
  emit_control "FLOW:ERROR"
1071
1119
  exit 1
1072
1120
  fi
@@ -213,12 +213,13 @@ else
213
213
  exit 1
214
214
  fi
215
215
 
216
+ require_alipay_cli_local_state_access_or_emit || exit "$?"
216
217
  export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
217
218
  run_network_retry RESULT write sign_apply -- alipay-cli mcp call ar-sign.apply \
218
219
  -d "$REQUEST_JSON" --json
219
220
  RETRY_RC=$?
220
221
  if [ "$RETRY_RC" -eq 75 ]; then
221
- RECONCILE_OUTPUT=$(bash "${SCRIPT_DIR}/query_sign_status.sh" --sales-code "$SALES_CODE" --product-type "$PRODUCT_TYPE" 2>&1 || true)
222
+ RECONCILE_OUTPUT=$(ALIPAY_AIPAY_SUPPRESS_LOCAL_FS_RETRY_MARKER=1 bash "${SCRIPT_DIR}/query_sign_status.sh" --sales-code "$SALES_CODE" --product-type "$PRODUCT_TYPE" 2>&1 || true)
222
223
  RECONCILED_STATUS=$(printf '%s\n' "$RECONCILE_OUTPUT" | sed -n 's/^ALIPAY_AIPAY_INTERNAL:SIGN_STATUS=//p' | tail -1)
223
224
  if [ "$RECONCILED_STATUS" = "SIGN_SUBMITTED" ] || [ "$RECONCILED_STATUS" = "SIGNED_EFFECTIVE" ]; then
224
225
  emit_control "SIGN_APPLY_RESULT=SUCCESS"
@@ -232,6 +233,8 @@ if [ "$RETRY_RC" -eq 75 ]; then
232
233
  echo "❌ 签约提交响应不明且现有查询无法排除传播延迟,结果标记为 UNKNOWN,禁止自动重复提交"
233
234
  emit_control "SIGN_APPLY_RESULT=UNKNOWN"
234
235
  exit 1
236
+ elif [ "$RETRY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
237
+ exit "$RETRY_RC"
235
238
  elif [ "$RETRY_RC" -ne 0 ]; then
236
239
  echo "❌ 签约提交因明确未发送的网络异常在自动重试两次后仍未恢复"
237
240
  emit_control "SIGN_APPLY_RESULT=FAILED"
@@ -239,7 +242,7 @@ elif [ "$RETRY_RC" -ne 0 ]; then
239
242
  fi
240
243
 
241
244
  # 错误检测(先于业务解析,handle_error 内部会自动解包信封)
242
- if ! handle_error "$RESULT"; then
245
+ if ! handle_error "$RESULT" "MCP_AUTHENTICATED_CALL"; then
243
246
  emit_control "SIGN_APPLY_RESULT=FAILED"
244
247
  exit 1
245
248
  fi
@@ -513,10 +513,14 @@ validate_current_authorization() {
513
513
  mcc_match="true"
514
514
  if [ "$scope_match" = "true" ]; then
515
515
  export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
516
- run_network_retry mcc_raw read authorization_mcc_check -- alipay-cli mcp call ar-query.queryArInfosBySalesProd \
516
+ ALIPAY_AIPAY_SUPPRESS_LOCAL_FS_RETRY_MARKER=1 run_network_retry mcc_raw read authorization_mcc_check -- alipay-cli mcp call ar-query.queryArInfosBySalesProd \
517
517
  -d "{\"request\":{\"salesProductCodes\":[\"${SALES_CODE}\"]},\"ctx\":{}}" \
518
518
  --json
519
519
  retry_rc=$?
520
+ if [ "$retry_rc" -eq "$AUTH_LOCAL_STATE_PERMISSION_EXIT_CODE" ]; then
521
+ auth_local_state_permission_failure
522
+ return 1
523
+ fi
520
524
  if [ "$retry_rc" -ne 0 ]; then
521
525
  echo "❌ 授权 MCC 校验因网络或服务异常在自动重试两次后仍未恢复"
522
526
  emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
@@ -529,9 +533,12 @@ validate_current_authorization() {
529
533
  }
530
534
 
531
535
  mcc_verify_unwrapped=$(unwrap_mcp "$mcc_verify_result")
532
- mcc_error_type=$(detect_error "$mcc_verify_result")
536
+ mcc_error_type=$(detect_error "$mcc_verify_result" "MCP_AUTHENTICATED_CALL")
533
537
  if [ "$mcc_error_type" = "AUTH_MISMATCH" ]; then
534
538
  mcc_match="false"
539
+ elif [ "$mcc_error_type" = "MCP_AUTH_ERROR" ]; then
540
+ handle_error "$mcc_verify_result" "MCP_AUTHENTICATED_CALL" || true
541
+ return 1
535
542
  elif [ "$mcc_error_type" != "SUCCESS" ]; then
536
543
  echo "❌ MCC 校验失败"
537
544
  emit_control "AUTH_FLOW:FAILED"
@@ -179,10 +179,20 @@ extract_first_named_field() {
179
179
  extract_error_object() {
180
180
  echo "$1" | jq -c '
181
181
  [
182
- .. | objects |
182
+ .,
183
+ (.error? | objects),
184
+ (.data? | objects),
185
+ (.data.error? | objects),
186
+ (.response? | objects),
187
+ (.data.response? | objects),
188
+ (.errorContext.errorStack[]? | objects)
189
+ |
183
190
  select(
184
191
  ((.errorCode? | type) == "string" and .errorCode != "") or
185
- ((.errorCode? | type) == "object" and ((.errorCode.code? // "") != ""))
192
+ ((.errorCode? | type) == "number") or
193
+ ((.errorCode? | type) == "object" and
194
+ (((.errorCode.code? // "") | tostring) != "" or
195
+ (((.errorCode.status? // "") | tostring) != "")))
186
196
  )
187
197
  ][0] // empty
188
198
  ' 2>/dev/null
@@ -298,8 +308,15 @@ extract_error_object_field() {
298
308
  extract_success_state() {
299
309
  local JSON="$1"
300
310
  echo "$JSON" | jq -r '
301
- if any(.. | objects; .success? == false) then "false"
302
- elif any(.. | objects; .success? == true) then "true"
311
+ if .success? == false or
312
+ ((.data? | type) == "object" and .data.success? == false) or
313
+ ((.response? | type) == "object" and .response.success? == false) or
314
+ ((.data.response? | type) == "object" and .data.response.success? == false) then "false"
315
+ elif .success? == true or
316
+ ((.data? | type) == "object" and .data.success? == true) or
317
+ ((.response? | type) == "object" and .response.success? == true) or
318
+ ((.data.response? | type) == "object" and .data.response.success? == true) or
319
+ (((.code? // "") | tostring) == "10000") then "true"
303
320
  else "null"
304
321
  end
305
322
  ' 2>/dev/null
@@ -322,9 +339,11 @@ extract_checked_errors() {
322
339
 
323
340
  # ─── 统一错误检测 ──────────────────────────────────────────────────────────
324
341
  # 参数: $1 - CLI 执行结果文本(原始输出,含信封或不含)
342
+ # $2 - 可选调用上下文;MCP_AUTHENTICATED_CALL 允许识别资源响应中的明确未登录状态
325
343
  # 返回: MCP_AUTH_ERROR | MCP_SERVICE_ERROR | AUTH_MISMATCH | SERVICE_UNSTABLE | CLI_LOCAL_FS_PERMISSION | ERROR:xxx | CLI_ERROR:xxx | SUCCESS
326
344
  detect_error() {
327
345
  local CLI_RESULT="$1"
346
+ local ERROR_CONTEXT="${2:-}"
328
347
 
329
348
  # 空输入视为异常,不应被当作 SUCCESS
330
349
  if [ -z "$CLI_RESULT" ]; then
@@ -340,26 +359,108 @@ detect_error() {
340
359
  # 先解包 MCP 信封,再进行错误检测
341
360
  local UNWRAPPED=$(unwrap_mcp "$CLI_RESULT")
342
361
 
343
- # 1. 优先检测 MCP 认证错误
344
- if echo "$UNWRAPPED" | grep -qiE "HTTP 401|Authorization is empty|非法的认证信息"; then
362
+ # 1. 优先检测 MCP 认证错误。非 JSON 只接受传输层强证据;JSON 只检查
363
+ # 顶层响应和登记的 error/response 层,不递归扫描成功候选或示例数据。
364
+ if ! echo "$UNWRAPPED" | jq -e . >/dev/null 2>&1; then
365
+ if echo "$UNWRAPPED" | grep -qiE \
366
+ "HTTP[[:space:]]*401|Authorization is empty|非法的?认证信息|用户未登录|not[ _-]+logged[ _-]+in|(authorization|authentication|login)[-[:space:]_:=]+(is[-[:space:]_:=]+)?required|(access|auth|authentication|authorization)[ _-]+token[-[:space:]_:=]+(is[-[:space:]_:=]+)?(missing|expired)|(^|[^[:alnum:]_])(missing|expired)[-[:space:]_:=]+(access|auth|authentication|authorization)[ _-]+token([^[:alnum:]_]|$)|(^|[^[:alnum:]_])token[-[:space:]_:=]+(is[-[:space:]_:=]+)?(missing|expired)([^[:alnum:]_]|$)"; then
367
+ echo "MCP_AUTH_ERROR"
368
+ return
369
+ fi
370
+ elif echo "$UNWRAPPED" | jq -e --arg context "$ERROR_CONTEXT" '
371
+ def failure_shaped:
372
+ type == "object" and (
373
+ .success? == false or
374
+ (.error? != null) or
375
+ (.errorCode? != null) or
376
+ ((.errorMessage? | type) == "string" and .errorMessage != "") or
377
+ ((.errorMsg? | type) == "string" and .errorMsg != "") or
378
+ (((.code? // "") | tostring) as $code |
379
+ $code != "" and $code != "0" and $code != "10000")
380
+ );
381
+ def auth_code:
382
+ if type != "string" and type != "number" then false
383
+ else
384
+ (tostring | ascii_downcase | gsub("^[[:space:]]+|[[:space:]]+$"; "")) as $code |
385
+ ([
386
+ "401",
387
+ "unauthorized",
388
+ "not_logged_in",
389
+ "login_required",
390
+ "unauthenticated",
391
+ "authorization_required",
392
+ "auth_required",
393
+ "access_token_missing",
394
+ "access_token_expired",
395
+ "auth_token_missing",
396
+ "auth_token_expired"
397
+ ] | index($code) != null)
398
+ end;
399
+ def auth_message:
400
+ type == "string" and test(
401
+ "HTTP[[:space:]]*401|Authorization is empty|非法的?认证信息|用户未登录|not[ _-]+logged[ _-]+in|(authorization|authentication|login)[-[:space:]_:=]+(is[-[:space:]_:=]+)?required|(access|auth|authentication|authorization)[ _-]+token[-[:space:]_:=]+(is[-[:space:]_:=]+)?(missing|expired)|(^|[^[:alnum:]_])(missing|expired)[-[:space:]_:=]+(access|auth|authentication|authorization)[ _-]+token([^[:alnum:]_]|$)|(^|[^[:alnum:]_])token[-[:space:]_:=]+(is[-[:space:]_:=]+)?(missing|expired)([^[:alnum:]_]|$)";
402
+ "i"
403
+ );
404
+ . as $root |
405
+ ([
406
+ $root,
407
+ ($root.data? | objects | select(failure_shaped)),
408
+ ($root.response? | objects | select(failure_shaped)),
409
+ ($root.data.response? | objects | select(failure_shaped))
410
+ ]) as $response_nodes |
411
+ ([
412
+ ($root.error? | objects),
413
+ ($root.errorCode? | objects),
414
+ ($root.data.error? | objects),
415
+ ($root.data.errorCode? | objects),
416
+ ($root.response.error? | objects),
417
+ ($root.data.response.error? | objects),
418
+ ($root.errorContext.errorStack[]? | objects)
419
+ ]) as $error_nodes |
420
+ (($response_nodes + $error_nodes) | any(.[];
421
+ [
422
+ .code?, .errorCode?, .status?, .reason?,
423
+ (.errorCode? | objects | (.code?, .status?, .reason?))
424
+ ] | any(.[]; auth_code)
425
+ )) or
426
+ ($response_nodes | any(.[];
427
+ failure_shaped and
428
+ ([.message?, .errorMessage?, .errorMsg?, .msg?, .subMsg?] | any(.[]; auth_message))
429
+ )) or
430
+ ($error_nodes | any(.[];
431
+ [.message?, .errorMessage?, .errorMsg?, .msg?, .subMsg?] | any(.[]; auth_message)
432
+ )) or
433
+ ($context == "MCP_AUTHENTICATED_CALL" and (
434
+ (($root.logged_in? | type) == "boolean" and $root.logged_in == false) or
435
+ (($root.data? | type) == "object" and
436
+ ($root.data.logged_in? | type) == "boolean" and $root.data.logged_in == false) or
437
+ (($root.data? | type) == "object" and
438
+ ([$root.data.code?, $root.data.errorCode?, $root.data.status?, $root.data.reason?] |
439
+ any(.[]; auth_code)))
440
+ ))
441
+ ' >/dev/null 2>&1; then
345
442
  echo "MCP_AUTH_ERROR"
346
443
  return
347
444
  fi
348
445
 
349
- # 2. MCP 服务不稳定(后端返回的服务异常)
350
- if echo "$UNWRAPPED" | grep -qiE "MCP.*服务.*不稳定|服务暂时不可用"; then
446
+ local SUCCESS_STATE
447
+ SUCCESS_STATE=$(extract_success_state "$UNWRAPPED")
448
+
449
+ # 2. MCP 服务不稳定(后端返回的服务异常)。已登记成功响应中的
450
+ # 候选描述和示例不参与错误关键词分类。
451
+ if [ "$SUCCESS_STATE" != "true" ] && echo "$UNWRAPPED" | grep -qiE "MCP.*服务.*不稳定|服务暂时不可用"; then
351
452
  echo "SERVICE_UNSTABLE"
352
453
  return
353
454
  fi
354
455
 
355
456
  # 3. MCP 调用失败(网络/连接错误)
356
- if echo "$UNWRAPPED" | grep -qiE "MCP 调用失败|connection refused|timeout|network[ _]error|网络连接失败"; then
457
+ if [ "$SUCCESS_STATE" != "true" ] && echo "$UNWRAPPED" | grep -qiE "MCP 调用失败|connection refused|timeout|network[ _]error|网络连接失败"; then
357
458
  echo "MCP_SERVICE_ERROR"
358
459
  return
359
460
  fi
360
461
 
361
462
  # 4. 授权信息不匹配(MCC/产品/scope 未授权)
362
- if echo "$UNWRAPPED" | grep -qiE "mccCode.*is not auth|salesProductCodes.*is not auth|scope.*is not auth"; then
463
+ if [ "$SUCCESS_STATE" != "true" ] && echo "$UNWRAPPED" | grep -qiE "mccCode.*is not auth|salesProductCodes.*is not auth|scope.*is not auth"; then
363
464
  echo "AUTH_MISMATCH"
364
465
  return
365
466
  fi
@@ -377,9 +478,8 @@ detect_error() {
377
478
  fi
378
479
 
379
480
  # 6. CLI 命令本身的错误(success: false,从解包后的 JSON 提取)
380
- # 注意: MCP 业务响应可能嵌套在 data/response 下,因此递归检测 success=false。
381
- local SUCCESS=$(extract_success_state "$UNWRAPPED")
382
- if [ "$SUCCESS" = "false" ]; then
481
+ # 注意: MCP 业务响应可能嵌套在登记的 data/response 层。
482
+ if [ "$SUCCESS_STATE" = "false" ]; then
383
483
  local ERROR_MSG=$(extract_error_message "$UNWRAPPED")
384
484
  echo "CLI_ERROR:$ERROR_MSG"
385
485
  return
@@ -647,10 +747,12 @@ handle_service_unstable() {
647
747
 
648
748
  # ─── 统一错误处理入口 ──────────────────────────────────────────────────────
649
749
  # 参数: $1 - CLI 执行结果文本(原始输出,含信封或不含)
750
+ # $2 - 可选调用上下文,透传给 detect_error
650
751
  # 返回: 0=成功, 1=当前动作失败(由主流程判断恢复、阻断或继续独立分支)
651
752
  handle_error() {
652
753
  local CLI_RESULT="$1"
653
- local ERROR_TYPE=$(detect_error "$CLI_RESULT")
754
+ local ERROR_CONTEXT="${2:-}"
755
+ local ERROR_TYPE=$(detect_error "$CLI_RESULT" "$ERROR_CONTEXT")
654
756
  # 解包一次供 CLI_ERROR 分支使用(其他分支由各自的 handler 自己解包)
655
757
  local _UNWRAPPED=$(unwrap_mcp "$CLI_RESULT")
656
758
 
@@ -40,7 +40,7 @@ run_network_retry() {
40
40
 
41
41
  local max_retries="${NETWORK_RETRY_MAX_RETRIES:-2}"
42
42
  local delay_seconds="${NETWORK_RETRY_DELAY_SECONDS:-3}"
43
- local attempt=0 stdout_file stderr_file stdout stderr rc classification transient dispatch
43
+ local attempt=0 stdout_file stderr_file stdout stderr combined result_payload rc classification transient dispatch
44
44
  stdout_file=$(mktemp "${TMPDIR:-/tmp}/alipay-aipay-stdout.XXXXXX") || return 1
45
45
  stderr_file=$(mktemp "${TMPDIR:-/tmp}/alipay-aipay-stderr.XXXXXX") || { rm -f "$stdout_file"; return 1; }
46
46
  chmod 600 "$stdout_file" "$stderr_file" 2>/dev/null || true
@@ -54,10 +54,26 @@ run_network_retry() {
54
54
  rc=$?
55
55
  stdout=$(cat "$stdout_file")
56
56
  stderr=$(cat "$stderr_file")
57
+ combined=$(printf '%s\n%s' "$stdout" "$stderr")
57
58
  classification=""
58
59
  transient=false
59
60
  dispatch="MAYBE_SENT"
60
61
 
62
+ if type alipay_cli_has_local_state_permission_error >/dev/null 2>&1 &&
63
+ alipay_cli_has_local_state_permission_error "$combined"; then
64
+ NETWORK_RETRY_CLASS="CLI_LOCAL_FS_PERMISSION"
65
+ NETWORK_DISPATCH_STATE="MAYBE_SENT"
66
+ NETWORK_ATTEMPTS="$attempt"
67
+ printf -v "$result_var" '%s' "$stdout"
68
+ if [ "$operation" = "read" ]; then
69
+ if [ "${ALIPAY_AIPAY_SUPPRESS_LOCAL_FS_RETRY_MARKER:-0}" != "1" ]; then
70
+ emit_control "CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION"
71
+ fi
72
+ return "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}"
73
+ fi
74
+ return 75
75
+ fi
76
+
61
77
  local stdout_is_business_json=false unwrapped_stdout=""
62
78
  if [ -n "$stdout" ]; then
63
79
  classification=$(detect_error "$stdout" 2>/dev/null || true)
@@ -91,7 +107,11 @@ run_network_retry() {
91
107
  NETWORK_RETRY_CLASS="$classification"
92
108
  NETWORK_DISPATCH_STATE="$dispatch"
93
109
  NETWORK_ATTEMPTS="$attempt"
94
- printf -v "$result_var" '%s' "$stdout"
110
+ result_payload="$stdout"
111
+ if [ "$stdout_is_business_json" != true ] && [ "$rc" -ne 0 ] && [ -n "$stderr" ]; then
112
+ result_payload="$stderr"
113
+ fi
114
+ printf -v "$result_var" '%s' "$result_payload"
95
115
 
96
116
  if [ "$transient" != true ]; then
97
117
  return 0
@@ -101,13 +121,13 @@ run_network_retry() {
101
121
  return 75
102
122
  fi
103
123
 
104
- if network_retry_is_explicit_network_restriction "$(printf '%s\n%s' "$stdout" "$stderr")"; then
105
- network_retry_emit_exhausted "$action_name" "$classification" "$dispatch" "$attempt" "$(printf '%s\n%s' "$stdout" "$stderr")"
124
+ if network_retry_is_explicit_network_restriction "$combined"; then
125
+ network_retry_emit_exhausted "$action_name" "$classification" "$dispatch" "$attempt" "$combined"
106
126
  return 76
107
127
  fi
108
128
 
109
129
  if [ "$attempt" -gt "$max_retries" ]; then
110
- network_retry_emit_exhausted "$action_name" "$classification" "$dispatch" "$attempt" "$(printf '%s\n%s' "$stdout" "$stderr")"
130
+ network_retry_emit_exhausted "$action_name" "$classification" "$dispatch" "$attempt" "$combined"
111
131
  return 76
112
132
  fi
113
133
  sleep "$delay_seconds"
@@ -74,18 +74,22 @@ fi
74
74
  init_alipay_cli_product "$EXPECTED_PRODUCT_TYPE" || exit 1
75
75
 
76
76
  # 查询签约状态
77
+ require_alipay_cli_local_state_access_or_emit || exit "$?"
77
78
  export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
78
79
  run_network_retry RESULT read query_sign_status -- alipay-cli mcp call ar-query.queryArInfosBySalesProd \
79
80
  -d "{\"request\":{\"salesProductCodes\":[\"${SALES_CODE}\"]},\"ctx\":{}}" \
80
81
  --json
81
82
  RETRY_RC=$?
83
+ if [ "$RETRY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
84
+ exit "$RETRY_RC"
85
+ fi
82
86
  if [ "$RETRY_RC" -ne 0 ]; then
83
87
  echo "❌ 签约状态查询因网络或服务异常在自动重试两次后仍未恢复"
84
88
  exit 1
85
89
  fi
86
90
 
87
91
  # 错误检测
88
- if ! handle_error "$RESULT"; then
92
+ if ! handle_error "$RESULT" "MCP_AUTHENTICATED_CALL"; then
89
93
  exit 1
90
94
  fi
91
95
 
@@ -97,17 +97,21 @@ fetch_service_page() {
97
97
  --argjson offset "$OFFSET" \
98
98
  '{request:{limit:$limit,offset:$offset}}')
99
99
 
100
+ require_alipay_cli_local_state_access_or_emit || return "$?"
100
101
  export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
101
102
  run_network_retry RESULT read service_list_page -- alipay-cli mcp call a2a-pay-service.discoverBazaarServicesForMcp \
102
103
  -d "$REQUEST_JSON" \
103
104
  --json
104
105
  RETRY_RC=$?
106
+ if [ "$RETRY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
107
+ return "$RETRY_RC"
108
+ fi
105
109
  if [ "$RETRY_RC" -ne 0 ]; then
106
110
  echo "❌ 服务列表当前分页因网络或服务异常在自动重试两次后仍未恢复"
107
111
  return 1
108
112
  fi
109
113
 
110
- if ! handle_error "$RESULT"; then
114
+ if ! handle_error "$RESULT" "MCP_AUTHENTICATED_CALL"; then
111
115
  return 1
112
116
  fi
113
117
 
@@ -176,8 +180,10 @@ fetch_service_list() {
176
180
  SERVICE_LIST='[]'
177
181
 
178
182
  while true; do
179
- if ! fetch_service_page "$OFFSET" "$LIMIT"; then
180
- return 1
183
+ fetch_service_page "$OFFSET" "$LIMIT"
184
+ FETCH_RC=$?
185
+ if [ "$FETCH_RC" -ne 0 ]; then
186
+ return "$FETCH_RC"
181
187
  fi
182
188
 
183
189
  if [ "$PAGE_FORMAT" = "legacy" ]; then
@@ -234,8 +240,10 @@ fetch_service_list() {
234
240
  }
235
241
 
236
242
  service_list() {
237
- if ! fetch_service_list; then
238
- exit 1
243
+ fetch_service_list
244
+ FETCH_RC=$?
245
+ if [ "$FETCH_RC" -ne 0 ]; then
246
+ exit "$FETCH_RC"
239
247
  fi
240
248
 
241
249
  SERVICE_CANDIDATES=$(echo "$SERVICE_LIST" | jq -c '[.[] | {
@@ -313,8 +321,10 @@ service_save() {
313
321
 
314
322
  if [ -z "$SERVICE_ID" ]; then
315
323
  # 创建新服务(不传 serviceId)
316
- if ! fetch_service_list; then
317
- exit 1
324
+ fetch_service_list
325
+ FETCH_RC=$?
326
+ if [ "$FETCH_RC" -ne 0 ]; then
327
+ exit "$FETCH_RC"
318
328
  fi
319
329
  if [ "$SERVICE_COUNT" -ge "$SERVICE_CREATE_LIMIT" ]; then
320
330
  echo "❌ 已达到服务数量上限(${SERVICE_CREATE_LIMIT}个),无法创建新服务"
@@ -343,6 +353,7 @@ service_save() {
343
353
 
344
354
  fi
345
355
 
356
+ require_alipay_cli_local_state_access_or_emit || exit "$?"
346
357
  export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
347
358
  run_network_retry RESULT write service_save -- alipay-cli mcp call a2a-pay-service.saveBazaarServiceForMcp \
348
359
  -d "$REQUEST_JSON" \
@@ -354,7 +365,7 @@ service_save() {
354
365
  emit_control "SERVICE_SAVE_RESULT=UNKNOWN"
355
366
  exit 1
356
367
  fi
357
- if fetch_service_list; then
368
+ if ALIPAY_AIPAY_SUPPRESS_LOCAL_FS_RETRY_MARKER=1 fetch_service_list; then
358
369
  MATCH_COUNT=$(echo "$SERVICE_LIST" | jq --arg id "$SERVICE_ID" --arg name "$SERVICE_NAME" --arg desc "$SERVICE_DESC" --arg url "$RESOURCE_URL" --arg pricing "$PRICING" --arg schema "$SCHEMA_URL" '
359
370
  [.[] | select(
360
371
  .serviceId == $id and .serviceName == $name and .serviceDesc == $desc and
@@ -374,6 +385,8 @@ service_save() {
374
385
  echo "❌ 服务修改响应不明且现有列表无法排除传播延迟,结果标记为 UNKNOWN,禁止自动重复修改"
375
386
  emit_control "SERVICE_SAVE_RESULT=UNKNOWN"
376
387
  exit 1
388
+ elif [ "$RETRY_RC" -eq "${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
389
+ exit "$RETRY_RC"
377
390
  elif [ "$RETRY_RC" -ne 0 ]; then
378
391
  if [ -z "$SERVICE_ID" ]; then
379
392
  echo "❌ 服务创建因明确未发送的网络异常在自动重试两次后仍未恢复"
@@ -385,7 +398,7 @@ service_save() {
385
398
  fi
386
399
 
387
400
  # 错误检测
388
- if ! handle_error "$RESULT"; then
401
+ if ! handle_error "$RESULT" "MCP_AUTHENTICATED_CALL"; then
389
402
  emit_control "SERVICE_SAVE_RESULT=FAILED"
390
403
  exit 1
391
404
  fi
@@ -62,29 +62,55 @@ if [ ! -f "$PAY_IMG" ]; then
62
62
  exit 1
63
63
  fi
64
64
 
65
+ require_alipay_cli_local_state_access_or_emit || exit "$?"
66
+
65
67
  # 使用 mktemp 创建并发安全的临时文件。模板需以 XXXXXX 结尾以兼容 macOS/BSD mktemp。
66
68
  TMP_BASE="${TMPDIR:-/tmp}"
67
69
  TMP_BASE="${TMP_BASE%/}"
68
- TMP_HOME=$(mktemp "${TMP_BASE}/upload_home.XXXXXX") || { echo "❌ 创建临时文件失败"; exit 1; }
69
- TMP_SHOP=$(mktemp "${TMP_BASE}/upload_shop.XXXXXX") || { echo "❌ 创建临时文件失败"; exit 1; }
70
- TMP_PAY=$(mktemp "${TMP_BASE}/upload_pay.XXXXXX") || { echo "❌ 创建临时文件失败"; exit 1; }
71
-
72
- # 清理临时文件
70
+ TMP_HOME=""
71
+ TMP_SHOP=""
72
+ TMP_PAY=""
73
+ TMP_HOME_ERR=""
74
+ TMP_SHOP_ERR=""
75
+ TMP_PAY_ERR=""
76
+
77
+ # 清理临时文件,包括只创建了部分文件时的失败分支。
73
78
  cleanup_upload_tmp() {
74
- rm -f "$TMP_HOME" "$TMP_SHOP" "$TMP_PAY"
79
+ local tmp_file
80
+ for tmp_file in "$TMP_HOME" "$TMP_SHOP" "$TMP_PAY" "$TMP_HOME_ERR" "$TMP_SHOP_ERR" "$TMP_PAY_ERR"; do
81
+ [ -z "$tmp_file" ] || rm -f "$tmp_file"
82
+ done
75
83
  }
76
84
  trap cleanup_upload_tmp EXIT
77
85
 
86
+ TMP_HOME=$(mktemp "${TMP_BASE}/upload_home.XXXXXX") || { echo "❌ 创建临时文件失败"; exit 1; }
87
+ TMP_SHOP=$(mktemp "${TMP_BASE}/upload_shop.XXXXXX") || { echo "❌ 创建临时文件失败"; exit 1; }
88
+ TMP_PAY=$(mktemp "${TMP_BASE}/upload_pay.XXXXXX") || { echo "❌ 创建临时文件失败"; exit 1; }
89
+ TMP_HOME_ERR=$(mktemp "${TMP_BASE}/upload_home_err.XXXXXX") || { echo "❌ 创建临时文件失败"; exit 1; }
90
+ TMP_SHOP_ERR=$(mktemp "${TMP_BASE}/upload_shop_err.XXXXXX") || { echo "❌ 创建临时文件失败"; exit 1; }
91
+ TMP_PAY_ERR=$(mktemp "${TMP_BASE}/upload_pay_err.XXXXXX") || { echo "❌ 创建临时文件失败"; exit 1; }
92
+
78
93
  # 并行上传 3 张截图。PLATFORM_ID 与 PRODUCT 均由公共上下文初始化。
79
- PLATFORM="${DEV_TOOL_NAME:-unknown}" PLATFORM_ID="${PLATFORM_ID:-}" PRODUCT="${PRODUCT:-}" alipay-cli file upload "$HOME_IMG" -s payMerchantcodeSkill --json 2>/dev/null > "$TMP_HOME" &
80
- PLATFORM="${DEV_TOOL_NAME:-unknown}" PLATFORM_ID="${PLATFORM_ID:-}" PRODUCT="${PRODUCT:-}" alipay-cli file upload "$SHOP_IMG" -s payMerchantcodeSkill --json 2>/dev/null > "$TMP_SHOP" &
81
- PLATFORM="${DEV_TOOL_NAME:-unknown}" PLATFORM_ID="${PLATFORM_ID:-}" PRODUCT="${PRODUCT:-}" alipay-cli file upload "$PAY_IMG" -s payMerchantcodeSkill --json 2>/dev/null > "$TMP_PAY" &
82
- wait
94
+ PLATFORM="${DEV_TOOL_NAME:-unknown}" PLATFORM_ID="${PLATFORM_ID:-}" PRODUCT="${PRODUCT:-}" alipay-cli file upload "$HOME_IMG" -s payMerchantcodeSkill --json >"$TMP_HOME" 2>"$TMP_HOME_ERR" &
95
+ PID_HOME=$!
96
+ PLATFORM="${DEV_TOOL_NAME:-unknown}" PLATFORM_ID="${PLATFORM_ID:-}" PRODUCT="${PRODUCT:-}" alipay-cli file upload "$SHOP_IMG" -s payMerchantcodeSkill --json >"$TMP_SHOP" 2>"$TMP_SHOP_ERR" &
97
+ PID_SHOP=$!
98
+ PLATFORM="${DEV_TOOL_NAME:-unknown}" PLATFORM_ID="${PLATFORM_ID:-}" PRODUCT="${PRODUCT:-}" alipay-cli file upload "$PAY_IMG" -s payMerchantcodeSkill --json >"$TMP_PAY" 2>"$TMP_PAY_ERR" &
99
+ PID_PAY=$!
100
+ wait "$PID_HOME"; HOME_UPLOAD_RC=$?
101
+ wait "$PID_SHOP"; SHOP_UPLOAD_RC=$?
102
+ wait "$PID_PAY"; PAY_UPLOAD_RC=$?
103
+
104
+ upload_combined_output() {
105
+ local stdout_file="$1" stderr_file="$2"
106
+ printf '%s\n%s' "$(cat "$stdout_file")" "$(cat "$stderr_file")"
107
+ }
83
108
 
84
109
  # 解析图片引用值(兼容 file upload 返回 fileKey 或 URL 字段)
85
110
  parse_image_ref() {
86
- local RAW=$(cat "$1")
87
- if [ "$(detect_error "$RAW")" != "SUCCESS" ]; then
111
+ local RAW
112
+ RAW=$(upload_combined_output "$1" "$2")
113
+ if [ "$(detect_error "$RAW" "MCP_AUTHENTICATED_CALL")" != "SUCCESS" ]; then
88
114
  return 1
89
115
  fi
90
116
  # alipay-cli file upload 可能返回 MCP 信封,先解包
@@ -114,19 +140,24 @@ parse_image_ref() {
114
140
  ' 2>/dev/null
115
141
  }
116
142
 
117
- HOME_REF=$(parse_image_ref "$TMP_HOME")
118
- SHOP_REF=$(parse_image_ref "$TMP_SHOP")
119
- PAY_REF=$(parse_image_ref "$TMP_PAY")
143
+ HOME_REF=$(parse_image_ref "$TMP_HOME" "$TMP_HOME_ERR")
144
+ SHOP_REF=$(parse_image_ref "$TMP_SHOP" "$TMP_SHOP_ERR")
145
+ PAY_REF=$(parse_image_ref "$TMP_PAY" "$TMP_PAY_ERR")
120
146
 
121
147
  # 校验上传结果(含认证错误检测)
122
148
  check_upload_error() {
123
- local RAW=$(cat "$1")
149
+ local RAW
124
150
  local ERROR_TYPE
125
- ERROR_TYPE=$(detect_error "$RAW")
151
+ RAW=$(upload_combined_output "$1" "$2")
152
+ ERROR_TYPE=$(detect_error "$RAW" "MCP_AUTHENTICATED_CALL")
153
+ if [ "$ERROR_TYPE" = "CLI_LOCAL_FS_PERMISSION" ]; then
154
+ UPLOAD_LOCAL_FS_AFTER_DISPATCH=true
155
+ return 1
156
+ fi
126
157
  if [ "$ERROR_TYPE" = "MCP_AUTH_ERROR" ] && [ "${UPLOAD_AUTH_ERROR_SEEN:-false}" = "true" ]; then
127
158
  return 1
128
159
  fi
129
- if ! handle_error "$RAW"; then
160
+ if ! handle_error "$RAW" "MCP_AUTHENTICATED_CALL"; then
130
161
  if [ "$ERROR_TYPE" = "MCP_AUTH_ERROR" ]; then
131
162
  UPLOAD_AUTH_ERROR_SEEN=true
132
163
  fi
@@ -136,22 +167,23 @@ check_upload_error() {
136
167
  }
137
168
 
138
169
  ERRORS=""
170
+ UPLOAD_LOCAL_FS_AFTER_DISPATCH=false
139
171
  if [ -z "$HOME_REF" ]; then
140
- if ! check_upload_error "$TMP_HOME"; then
172
+ if ! check_upload_error "$TMP_HOME" "$TMP_HOME_ERR"; then
141
173
  ERRORS="$ERRORS - 首页截图上传失败(认证或服务异常)"$'\n'
142
174
  else
143
175
  ERRORS="$ERRORS - 首页截图上传后未返回可用于签约的图片引用值"$'\n'
144
176
  fi
145
177
  fi
146
178
  if [ -z "$SHOP_REF" ]; then
147
- if ! check_upload_error "$TMP_SHOP"; then
179
+ if ! check_upload_error "$TMP_SHOP" "$TMP_SHOP_ERR"; then
148
180
  ERRORS="$ERRORS - 商品页截图上传失败(认证或服务异常)"$'\n'
149
181
  else
150
182
  ERRORS="$ERRORS - 商品页截图上传后未返回可用于签约的图片引用值"$'\n'
151
183
  fi
152
184
  fi
153
185
  if [ -z "$PAY_REF" ]; then
154
- if ! check_upload_error "$TMP_PAY"; then
186
+ if ! check_upload_error "$TMP_PAY" "$TMP_PAY_ERR"; then
155
187
  ERRORS="$ERRORS - 支付页截图上传失败(认证或服务异常)"$'\n'
156
188
  else
157
189
  ERRORS="$ERRORS - 支付页截图上传后未返回可用于签约的图片引用值"$'\n'
@@ -159,6 +191,9 @@ if [ -z "$PAY_REF" ]; then
159
191
  fi
160
192
 
161
193
  if [ -n "$ERRORS" ]; then
194
+ if [ "$UPLOAD_LOCAL_FS_AFTER_DISPATCH" = true ]; then
195
+ emit_control "SCREENSHOT_UPLOAD_RESULT=UNKNOWN"
196
+ fi
162
197
  echo "❌ 截图上传失败:"
163
198
  printf "%s" "$ERRORS"
164
199
  exit 1