@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.
@@ -14,7 +14,7 @@
14
14
  # init: AUTH_FLOW:SKIP | AUTH_FLOW:READY_TO_OPEN | AUTH_FLOW:SCOPE_MISMATCH | AUTH_FLOW:MCC_MISMATCH | AUTH_FLOW:RETRY_WITH_NETWORK | AUTH_FLOW:FAILED
15
15
  # wait: AUTH_FLOW:AUTH_SUCCESS | AUTH_FLOW:PENDING | AUTH_FLOW:EXPIRED | AUTH_FLOW:SCOPE_MISMATCH | AUTH_FLOW:MCC_MISMATCH | AUTH_FLOW:CONTEXT_MISMATCH | AUTH_FLOW:RETRY_WITH_NETWORK | AUTH_FLOW:FAILED
16
16
  # confirm: AUTH_FLOW:AUTH_SUCCESS | AUTH_FLOW:PENDING | AUTH_FLOW:EXPIRED | AUTH_FLOW:SCOPE_MISMATCH | AUTH_FLOW:MCC_MISMATCH | AUTH_FLOW:CONTEXT_MISMATCH | AUTH_FLOW:RETRY_WITH_NETWORK | AUTH_FLOW:FAILED
17
- # mismatch: logout 可确认成功后调用 auth init 重新生成授权链接;上下文冲突时输出 AUTH_FLOW:CONTEXT_MISMATCH 且不 logout;无法确认 CLI 结果时输出 AUTH_FLOW:RETRY_WITH_NETWORK
17
+ # mismatch: logout 后置条件可确认后调用 auth init 重新生成授权链接;上下文冲突时输出 AUTH_FLOW:CONTEXT_MISMATCH 且不 logout;无法确认 CLI 结果时输出 AUTH_FLOW:RETRY_WITH_NETWORK;仍处于登录态时输出 AUTH_FLOW:LOGOUT_STILL_LOGGED_IN
18
18
  # stdout: 仅登记对客正文;AUTH_FLOW/状态/查询复用标记写入 stderr
19
19
  #=============================================================================
20
20
 
@@ -37,6 +37,28 @@ require_command node || exit 1
37
37
  require_command mktemp || exit 1
38
38
  umask 077
39
39
 
40
+ auth_product_type_from_sales_code() {
41
+ case "$1" in
42
+ "I1080300001000160457") printf 'aipay' ;;
43
+ "I1080300001000041203") printf 'webpay' ;;
44
+ "I1080300001000041313") printf 'apppay' ;;
45
+ *) return 1 ;;
46
+ esac
47
+ }
48
+
49
+ create_auth_sign_reuse_receipt() {
50
+ local sign_status="$1" sign_flow="$2" product_type payload encoded
51
+ product_type=$(auth_product_type_from_sales_code "$SALES_CODE") || return 1
52
+ payload=$(jq -cn \
53
+ --arg productType "$product_type" \
54
+ --arg salesCode "$SALES_CODE" \
55
+ --arg signStatus "$sign_status" \
56
+ --arg flow "$sign_flow" \
57
+ '{schema_ver:1,product_type:$productType,sales_code:$salesCode,sign_status:$signStatus,flow:$flow}') || return 1
58
+ encoded=$(printf '%s' "$payload" | jq -rR '@base64' | tr '+/' '-_' | tr -d '=\n') || return 1
59
+ printf 'AIPAY_SIGN_REUSE_V1.%s' "$encoded"
60
+ }
61
+
40
62
  # ─── 授权状态文件(用于独立执行 confirm/mismatch 时兜底恢复非敏感参数)────
41
63
  AUTH_STATE_OWNER="$(id -u 2>/dev/null || printf '%s' "${USER:-unknown}" | tr -c 'A-Za-z0-9_-' '_')"
42
64
  AUTH_STATE_BASE="${TMPDIR:-/tmp}"
@@ -159,6 +181,14 @@ save_auth_state() {
159
181
  }
160
182
  }
161
183
 
184
+ save_auth_state_after_page_delivery() {
185
+ if save_auth_state; then
186
+ return 0
187
+ fi
188
+ emit_control "AUTH_STATE:SAVE_FAILED"
189
+ return 0
190
+ }
191
+
162
192
  load_auth_state() {
163
193
  if ! validate_auth_state_file; then
164
194
  echo "❌ 缺少授权上下文(请传入 --sales-code/--mcc-code,或先执行 auth.sh init)"
@@ -320,7 +350,7 @@ wait_for_effective_whoami() {
320
350
  local max_attempts=3
321
351
  local interval_seconds=2
322
352
  local attempt=1
323
- local logged_in is_expired
353
+ local logged_in is_expired whoami_error_type
324
354
 
325
355
  AUTH_WHOAMI_RESULT=""
326
356
  if [ "${ALIPAY_AIPAY_TEST_MODE:-0}" = "1" ] && [[ "${AUTH_WHOAMI_RECHECK_INTERVAL_SECONDS:-}" =~ ^[0-9]+$ ]]; then
@@ -332,8 +362,17 @@ wait_for_effective_whoami() {
332
362
  auth_cli_environment_failure "$action"
333
363
  return 1
334
364
  }
365
+ whoami_error_type=$(detect_error "$AUTH_WHOAMI_RESULT")
335
366
  if ! handle_error "$AUTH_WHOAMI_RESULT"; then
336
- emit_control "AUTH_FLOW:FAILED"
367
+ case "$whoami_error_type" in
368
+ MCP_AUTH_ERROR|AUTH_MISMATCH) ;;
369
+ MCP_SERVICE_ERROR|SERVICE_UNSTABLE)
370
+ emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
371
+ ;;
372
+ *)
373
+ emit_control "AUTH_FLOW:FAILED"
374
+ ;;
375
+ esac
337
376
  return 1
338
377
  fi
339
378
 
@@ -394,8 +433,8 @@ validate_current_authorization() {
394
433
  local whoami_result="${1:-}"
395
434
  local logged_in is_expired granted_scope required_scope
396
435
  local scope_match mcc_verify_result mcc_verify_unwrapped mcc_match mcc_raw retry_rc
397
- local mcc_error_type mcc_error_code mcc_success
398
- local mismatch_type sign_status_output
436
+ local mcc_error_type mcc_error_code mcc_success whoami_error_type
437
+ local mismatch_type sign_status_output sign_status_marker sign_flow_marker sign_reuse_receipt
399
438
  local granted_scope_item required_scope_item required_scope_found
400
439
  local -a granted_scope_items required_scope_items
401
440
 
@@ -412,8 +451,17 @@ validate_current_authorization() {
412
451
  }
413
452
  fi
414
453
 
454
+ whoami_error_type=$(detect_error "$whoami_result")
415
455
  if ! handle_error "$whoami_result"; then
416
- emit_control "AUTH_FLOW:FAILED"
456
+ case "$whoami_error_type" in
457
+ MCP_AUTH_ERROR|AUTH_MISMATCH) ;;
458
+ MCP_SERVICE_ERROR|SERVICE_UNSTABLE)
459
+ emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
460
+ ;;
461
+ *)
462
+ emit_control "AUTH_FLOW:FAILED"
463
+ ;;
464
+ esac
417
465
  return 1
418
466
  fi
419
467
 
@@ -461,12 +509,12 @@ validate_current_authorization() {
461
509
  retry_rc=$?
462
510
  if [ "$retry_rc" -ne 0 ]; then
463
511
  echo "❌ 授权 MCC 校验因网络或服务异常在自动重试两次后仍未恢复"
464
- emit_control "AUTH_FLOW:FAILED"
512
+ emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
465
513
  return 1
466
514
  fi
467
515
  mcc_verify_result=$(extract_json_payload "$mcc_raw") || {
468
516
  echo "❌ alipay-cli mcp call ar-query.queryArInfosBySalesProd 未返回合法 JSON"
469
- emit_control "AUTH_FLOW:FAILED"
517
+ emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
470
518
  return 1
471
519
  }
472
520
 
@@ -494,6 +542,11 @@ validate_current_authorization() {
494
542
  if [ "$mcc_match" = "true" ] && sign_status_output=$(emit_sign_status_from_business "$mcc_verify_unwrapped" "$SALES_CODE" 2>/dev/null); then
495
543
  printf '%s\n' "$sign_status_output" \
496
544
  | while IFS= read -r marker; do emit_control "$marker"; done
545
+ sign_status_marker=$(printf '%s\n' "$sign_status_output" | awk -F= '/^SIGN_STATUS=/{print $2; exit}')
546
+ sign_flow_marker=$(printf '%s\n' "$sign_status_output" | awk '/^FLOW:/{print; exit}')
547
+ if [ -n "$sign_status_marker" ] && [ -n "$sign_flow_marker" ] && sign_reuse_receipt=$(create_auth_sign_reuse_receipt "$sign_status_marker" "$sign_flow_marker"); then
548
+ emit_control "AUTH_SIGN_REUSE_RECEIPT=${sign_reuse_receipt}"
549
+ fi
497
550
  emit_control "AUTH_SIGN_QUERY_REUSABLE=true"
498
551
  fi
499
552
  fi
@@ -576,13 +629,17 @@ auth_init() {
576
629
  # 后端查询没有权威 MCC 字段,旧状态变化时不能把现有登录误判为当前上下文已授权。
577
630
  DEVICE_CODE=""
578
631
  BROWSER_URL=""
579
- save_auth_state || return 1
632
+ if ! save_auth_state; then
633
+ emit_control "AUTH_STATE:SAVE_FAILED"
634
+ fi
580
635
  emit_control "AUTH_FLOW:${AUTH_INIT_CONTEXT_MISMATCH_TYPE}"
581
636
  return 1
582
637
  fi
583
638
  validate_current_authorization "$CHECK_RESULT" || return 1
584
639
  DEVICE_CODE=""
585
- save_auth_state || return 1
640
+ if ! save_auth_state; then
641
+ emit_control "AUTH_STATE:SAVE_FAILED"
642
+ fi
586
643
  emit_control "✅ 当前登录及授权范围有效,可继续后续流程"
587
644
  emit_control "AUTH_FLOW:SKIP"
588
645
  return 0
@@ -602,7 +659,9 @@ auth_init() {
602
659
  if [ "$LOGIN_STATUS" = "already_logged_in" ]; then
603
660
  validate_current_authorization || return 1
604
661
  DEVICE_CODE=""
605
- save_auth_state || return 1
662
+ if ! save_auth_state; then
663
+ emit_control "AUTH_STATE:SAVE_FAILED"
664
+ fi
606
665
  emit_control "✅ 当前登录及授权范围有效,可继续后续流程"
607
666
  emit_control "AUTH_FLOW:SKIP"
608
667
  return 0
@@ -630,10 +689,7 @@ auth_init() {
630
689
  return 1
631
690
  }
632
691
 
633
- # Step 6: 保存参数到状态文件(供 confirm/mismatch 使用)
634
- save_auth_state || return 1
635
-
636
- # Step 7: 有效期转换(对非数字做防护)
692
+ # Step 6: 有效期转换(对非数字做防护)
637
693
  if [[ "$EXPIRES_IN" =~ ^[0-9]+$ ]] && [ "$EXPIRES_IN" -ge 60 ]; then
638
694
  EXPIRES_MIN=$((EXPIRES_IN / 60))
639
695
  EXPIRES_DISPLAY="${EXPIRES_MIN} 分钟"
@@ -643,7 +699,7 @@ auth_init() {
643
699
  EXPIRES_DISPLAY="10 分钟"
644
700
  fi
645
701
 
646
- # Step 8: 由统一目录渲染授权信息;独立 open 动作在消息发出后打开。
702
+ # Step 7: 由统一目录渲染授权信息;独立 open 动作在消息发出后打开。
647
703
  DISPLAY_CODE="${VERIFICATION_CODE:-(请查看支付宝授权页面)}"
648
704
  MESSAGE_INPUT=$(printf '%s\n' "$PRODUCT_NAME" "$DEVICE_CODE" "$MCC_CODE" "$MCC_NAME" "$DISPLAY_CODE" "$EXPIRES_DISPLAY" "$BROWSER_URL" | jq -Rs '
649
705
  split("\n") as $v | {productName:$v[0],deviceCode:$v[1],mccCode:$v[2],mccName:$v[3],verificationCode:$v[4],expiresDisplay:$v[5],officialUrl:$v[6]}
@@ -653,6 +709,8 @@ auth_init() {
653
709
  emit_control "AUTH_FLOW:FAILED"
654
710
  exit 1
655
711
  fi
712
+ # Step 8: 页面 stdout 已经安全送达时,状态文件只用于 opener 便利;保存失败不得生成第二个授权页或截断 wait。
713
+ save_auth_state_after_page_delivery
656
714
  echo ""
657
715
  emit_control "AUTH_FLOW:READY_TO_OPEN"
658
716
  }
@@ -661,12 +719,11 @@ auth_open() {
661
719
  local saved_device saved_url validate_input open_status open_helper
662
720
 
663
721
  restore_auth_context_or_state "$@" || exit 1
664
- saved_device=$(jq -r '.deviceCode // ""' "$AUTH_STATE_FILE")
665
- saved_url=$(jq -r '.authorizationUrl // ""' "$AUTH_STATE_FILE")
722
+ saved_device=$(jq -r '.deviceCode // ""' "$AUTH_STATE_FILE" 2>/dev/null || printf '')
723
+ saved_url=$(jq -r '.authorizationUrl // ""' "$AUTH_STATE_FILE" 2>/dev/null || printf '')
666
724
  if [ -z "$saved_device" ] || [ -z "$saved_url" ]; then
667
- echo "❌ 缺少待打开的授权页面上下文,请重新执行 auth.sh init --defer-open"
668
- emit_control "AUTH_FLOW:FAILED"
669
- return 1
725
+ emit_control "AUTH_OPEN:LINK_ONLY"
726
+ return 0
670
727
  fi
671
728
 
672
729
  validate_input=$(jq -n \
@@ -693,6 +750,7 @@ auth_open() {
693
750
  # ─── auth confirm: 授权确认 + scope 校验 + MCC 授权有效性校验 ────────────────
694
751
  auth_confirm() {
695
752
  AUTH_CONFIRM_RESULT=""
753
+ local complete_error_type
696
754
  # 优先使用显式传入的非敏感上下文;未传时再从状态文件兜底恢复。
697
755
  restore_auth_context_or_state "$@" || exit 1
698
756
 
@@ -740,12 +798,41 @@ auth_confirm() {
740
798
  elif [ "$OUTER_SUCCESS" = "true" ] || [ "$DATA_STATUS" = "logged_in" ] || [ "$DATA_STATUS" = "already_logged_in" ] || [ "$DATA_STATUS" = "completed" ]; then
741
799
  :
742
800
  else
743
- if handle_error "$LOGIN_COMPLETE_RESULT"; then
744
- echo "❌ 授权确认失败:返回结果未包含可识别的完成状态"
801
+ complete_error_type=$(detect_error "$LOGIN_COMPLETE_RESULT")
802
+ if ! handle_error "$LOGIN_COMPLETE_RESULT"; then
803
+ cleanup_auth_state
804
+ case "$complete_error_type" in
805
+ MCP_AUTH_ERROR|AUTH_MISMATCH) ;;
806
+ MCP_SERVICE_ERROR|SERVICE_UNSTABLE)
807
+ emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
808
+ ;;
809
+ *)
810
+ emit_control "AUTH_FLOW:FAILED"
811
+ ;;
812
+ esac
813
+ return 1
814
+ fi
815
+ wait_for_effective_whoami "授权完成确认状态复核"
816
+ whoami_rc=$?
817
+ if [ "$whoami_rc" -eq 2 ]; then
818
+ AUTH_CONFIRM_RESULT="PENDING"
819
+ if [ "${AUTH_POLLING:-false}" != "true" ]; then
820
+ if ! printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.pending --variant DEFAULT; then
821
+ emit_control "AUTH_FLOW:FAILED"
822
+ return 1
823
+ fi
824
+ emit_control "AUTH_FLOW:PENDING"
825
+ fi
826
+ return 0
827
+ elif [ "$whoami_rc" -ne 0 ]; then
828
+ return 1
745
829
  fi
830
+ validate_current_authorization "$AUTH_WHOAMI_RESULT" || return 1
746
831
  cleanup_auth_state
747
- emit_control "AUTH_FLOW:FAILED"
748
- return 1
832
+ AUTH_CONFIRM_RESULT="AUTH_SUCCESS"
833
+ emit_control "✅ 授权确认成功(本机 CLI 登录态、scope 与 MCC 授权有效性均已通过)"
834
+ emit_control "AUTH_FLOW:AUTH_SUCCESS"
835
+ return 0
749
836
  fi
750
837
 
751
838
  # Step 3: 复用 init 的 scope + MCC 授权有效性校验
@@ -817,8 +904,6 @@ auth_wait() {
817
904
 
818
905
  # ─── auth mismatch: 授权范围不满足 → logout → 重新授权 ──────────────────────
819
906
  auth_mismatch() {
820
- local logout_result logout_error_type
821
-
822
907
  if [ "${1:-}" = "--defer-open" ]; then
823
908
  shift
824
909
  else
@@ -847,16 +932,21 @@ auth_mismatch() {
847
932
  emit_control "AUTH_FLOW:FAILED"
848
933
  return 1
849
934
  fi
850
- logout_result=$(run_json_command "alipay-cli logout --json" alipay-cli logout --json) || {
851
- auth_cli_environment_failure "退出当前登录"
852
- return 1
853
- }
854
- logout_error_type=$(detect_error "$logout_result")
855
- if [ "$logout_error_type" != "SUCCESS" ]; then
856
- echo "❌ 退出当前登录失败,已停止重新授权"
857
- emit_control "AUTH_FLOW:FAILED"
858
- return 1
859
- fi
935
+ confirm_logout_postcondition
936
+ case "$ALIPAY_LOGOUT_POSTCONDITION" in
937
+ COMMAND_CONFIRMED_SUCCESS|POSTCONDITION_LOGGED_OUT)
938
+ ;;
939
+ STILL_LOGGED_IN)
940
+ render_logout_still_logged_in || return 1
941
+ emit_control "AUTH_FLOW:LOGOUT_STILL_LOGGED_IN"
942
+ return 1
943
+ ;;
944
+ *)
945
+ render_logout_unconfirmed || return 1
946
+ emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
947
+ return 1
948
+ ;;
949
+ esac
860
950
 
861
951
  # Step 3: 调用 auth init 执行登录 + 构建授权链接 + 输出唯一授权消息。
862
952
  # DEV_TOOL_NAME 已 export,子进程可继承。
@@ -383,31 +383,119 @@ detect_error() {
383
383
  echo "SUCCESS"
384
384
  }
385
385
 
386
- # ─── MCP 认证错误处理 ──────────────────────────────────────────────────────
387
- handle_mcp_auth_error() {
388
- local LOGOUT_RESULT LOGOUT_EXIT_CODE LOGOUT_ERROR_TYPE LOGOUT_ERROR_MSG
389
- local LOGOUT_ANALYSIS LOGOUT_PARSE_STATUS
386
+ extract_captured_json() {
387
+ local RAW="$1"
388
+ local ANALYSIS STATUS
389
+ ANALYSIS=$(extract_unique_json "$RAW" 2>/dev/null || echo '{"status":"none"}')
390
+ STATUS=$(echo "$ANALYSIS" | jq -r '.status // "none"' 2>/dev/null)
391
+ [ "$STATUS" = "ok" ] || return 1
392
+ echo "$ANALYSIS" | jq -ce '.value | select(type == "object")' 2>/dev/null
393
+ }
390
394
 
391
- emit_control "AUTH_FLOW:AUTH_REQUIRED"
392
- render_error_message error.mcp.auth START '{}' || return 1
393
- LOGOUT_RESULT=$(PLATFORM=${DEV_TOOL_NAME:-unknown} PLATFORM_ID="${PLATFORM_ID:-}" alipay-cli logout --json 2>&1)
394
- LOGOUT_EXIT_CODE=$?
395
- LOGOUT_ERROR_TYPE=$(detect_error "$LOGOUT_RESULT")
396
- LOGOUT_ANALYSIS=$(extract_unique_json "$LOGOUT_RESULT" 2>/dev/null || echo '{"status":"none"}')
397
- LOGOUT_PARSE_STATUS=$(echo "$LOGOUT_ANALYSIS" | jq -r '.status // "none"' 2>/dev/null)
398
-
399
- if [ "$LOGOUT_EXIT_CODE" -ne 0 ] || [ "$LOGOUT_ERROR_TYPE" != "SUCCESS" ]; then
400
- if [ "$LOGOUT_PARSE_STATUS" != "ok" ] || [ "$LOGOUT_ERROR_TYPE" = "MCP_SERVICE_ERROR" ] || [ "$LOGOUT_ERROR_TYPE" = "SERVICE_UNSTABLE" ]; then
401
- render_error_message error.mcp.auth LOGOUT_UNCONFIRMED '{}' || return 1
402
- else
403
- LOGOUT_ERROR_MSG=$(sanitize_customer_error_text "$(extract_error_message "$LOGOUT_RESULT")")
404
- MESSAGE_INPUT=$(jq -cn --arg errorMessage "$LOGOUT_ERROR_MSG" '{errorMessage:$errorMessage}')
405
- render_error_message error.mcp.auth LOGOUT_FAILED "$MESSAGE_INPUT" || return 1
395
+ run_alipay_cli_json_capture() {
396
+ local stderr_file stdout stderr_output combined_output
397
+
398
+ ALIPAY_CLI_JSON_CAPTURE=""
399
+ ALIPAY_CLI_JSON_EXIT_CODE=0
400
+ stderr_file="$(mktemp "${TMPDIR:-/tmp}/alipay_cli_capture_stderr.XXXXXX")" || return 1
401
+ stdout=$(PLATFORM="${DEV_TOOL_NAME:-unknown}" PLATFORM_ID="${PLATFORM_ID:-}" PRODUCT="${PRODUCT:-}" "$@" 2>"$stderr_file")
402
+ ALIPAY_CLI_JSON_EXIT_CODE=$?
403
+ stderr_output=$(cat "$stderr_file")
404
+ rm -f "$stderr_file"
405
+
406
+ if ALIPAY_CLI_JSON_CAPTURE=$(extract_captured_json "$stdout"); then
407
+ return 0
408
+ fi
409
+ if ALIPAY_CLI_JSON_CAPTURE=$(extract_captured_json "$stderr_output"); then
410
+ return 0
411
+ fi
412
+ combined_output=$(printf '%s\n%s' "$stdout" "$stderr_output")
413
+ if ALIPAY_CLI_JSON_CAPTURE=$(extract_captured_json "$combined_output"); then
414
+ return 0
415
+ fi
416
+
417
+ ALIPAY_CLI_JSON_CAPTURE=""
418
+ return 1
419
+ }
420
+
421
+ confirm_logout_postcondition() {
422
+ local logout_error_type whoami_error_type whoami_has_state logged_in is_expired
423
+
424
+ ALIPAY_LOGOUT_POSTCONDITION=""
425
+ ALIPAY_LOGOUT_ERROR_MESSAGE=""
426
+
427
+ if run_alipay_cli_json_capture alipay-cli logout --json; then
428
+ logout_error_type=$(detect_error "$ALIPAY_CLI_JSON_CAPTURE")
429
+ if [ "$ALIPAY_CLI_JSON_EXIT_CODE" -eq 0 ] && [ "$logout_error_type" = "SUCCESS" ]; then
430
+ ALIPAY_LOGOUT_POSTCONDITION="COMMAND_CONFIRMED_SUCCESS"
431
+ return 0
406
432
  fi
433
+ ALIPAY_LOGOUT_ERROR_MESSAGE=$(sanitize_customer_error_text "$(extract_error_message "$ALIPAY_CLI_JSON_CAPTURE")")
434
+ else
435
+ ALIPAY_LOGOUT_ERROR_MESSAGE="退出命令未返回可唯一解析结果"
436
+ fi
437
+
438
+ if ! run_alipay_cli_json_capture alipay-cli whoami --json; then
439
+ ALIPAY_LOGOUT_POSTCONDITION="UNCONFIRMED"
407
440
  return 1
408
441
  fi
409
442
 
410
- render_error_message error.mcp.auth LOGOUT_SUCCESS '{}' || return 1
443
+ whoami_has_state=$(echo "$ALIPAY_CLI_JSON_CAPTURE" | jq -r '
444
+ if (.data | type) == "object" and ((.data | has("logged_in")) or (.data | has("is_expired"))) then "true" else "false" end
445
+ ' 2>/dev/null)
446
+ if [ "$whoami_has_state" != "true" ]; then
447
+ ALIPAY_LOGOUT_POSTCONDITION="UNCONFIRMED"
448
+ return 1
449
+ fi
450
+ logged_in=$(echo "$ALIPAY_CLI_JSON_CAPTURE" | jq -r '.data.logged_in // false' 2>/dev/null)
451
+ is_expired=$(echo "$ALIPAY_CLI_JSON_CAPTURE" | jq -r '.data.is_expired // false' 2>/dev/null)
452
+ if [ "$logged_in" = "true" ] && [ "$is_expired" != "true" ]; then
453
+ ALIPAY_LOGOUT_POSTCONDITION="STILL_LOGGED_IN"
454
+ return 1
455
+ fi
456
+ whoami_error_type=$(detect_error "$ALIPAY_CLI_JSON_CAPTURE")
457
+ if [ "$ALIPAY_CLI_JSON_EXIT_CODE" -ne 0 ] || { [ "$whoami_error_type" != "SUCCESS" ] && [ "$logged_in" = "true" ] && [ "$is_expired" != "true" ]; }; then
458
+ ALIPAY_LOGOUT_POSTCONDITION="UNCONFIRMED"
459
+ return 1
460
+ fi
461
+
462
+ ALIPAY_LOGOUT_POSTCONDITION="POSTCONDITION_LOGGED_OUT"
463
+ return 0
464
+ }
465
+
466
+ render_logout_unconfirmed() {
467
+ render_error_message error.mcp.auth LOGOUT_UNCONFIRMED '{}'
468
+ }
469
+
470
+ render_logout_still_logged_in() {
471
+ local message input
472
+ message="${ALIPAY_LOGOUT_ERROR_MESSAGE:-退出命令未能使当前登录态失效}"
473
+ input=$(jq -cn --arg errorMessage "$message" '{errorMessage:$errorMessage}')
474
+ render_error_message error.mcp.auth LOGOUT_FAILED "$input"
475
+ }
476
+
477
+ # ─── MCP 认证错误处理 ──────────────────────────────────────────────────────
478
+ handle_mcp_auth_error() {
479
+ render_error_message error.mcp.auth START '{}' || return 1
480
+ if [ "${ALIPAY_AIPAY_SUPPRESS_AUTH_LOGOUT:-}" = "1" ]; then
481
+ emit_control "AUTH_FLOW:AUTH_REQUIRED"
482
+ return 1
483
+ fi
484
+ confirm_logout_postcondition
485
+ case "$ALIPAY_LOGOUT_POSTCONDITION" in
486
+ COMMAND_CONFIRMED_SUCCESS|POSTCONDITION_LOGGED_OUT)
487
+ render_error_message error.mcp.auth LOGOUT_SUCCESS '{}' || return 1
488
+ emit_control "AUTH_FLOW:AUTH_REQUIRED"
489
+ ;;
490
+ STILL_LOGGED_IN)
491
+ render_logout_still_logged_in || return 1
492
+ emit_control "AUTH_FLOW:LOGOUT_STILL_LOGGED_IN"
493
+ ;;
494
+ *)
495
+ render_logout_unconfirmed || return 1
496
+ emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
497
+ ;;
498
+ esac
411
499
  }
412
500
 
413
501
  # ─── MCP 服务不可用处理 ────────────────────────────────────────────────────
@@ -351,6 +351,7 @@ service_save() {
351
351
  if [ "$RETRY_RC" -eq 75 ]; then
352
352
  if [ -z "$SERVICE_ID" ]; then
353
353
  echo "❌ 服务创建响应不明;现有查询无法把同名候选唯一归属于本次创建,结果标记为 UNKNOWN,禁止自动重复创建"
354
+ emit_control "SERVICE_SAVE_RESULT=UNKNOWN"
354
355
  exit 1
355
356
  fi
356
357
  if fetch_service_list; then
@@ -365,11 +366,13 @@ service_save() {
365
366
  if [ "$MATCH_COUNT" -eq 1 ]; then
366
367
  emit_control "✅ 服务修改已由列表查询核验成功"
367
368
  emit_control "📋 服务ID: $SERVICE_ID"
369
+ emit_control "SERVICE_SAVE_RESULT=SUCCESS"
368
370
  emit_control_field "SERVICE_ID" "$SERVICE_ID" || return 1
369
371
  return 0
370
372
  fi
371
373
  fi
372
374
  echo "❌ 服务修改响应不明且现有列表无法排除传播延迟,结果标记为 UNKNOWN,禁止自动重复修改"
375
+ emit_control "SERVICE_SAVE_RESULT=UNKNOWN"
373
376
  exit 1
374
377
  elif [ "$RETRY_RC" -ne 0 ]; then
375
378
  if [ -z "$SERVICE_ID" ]; then
@@ -377,11 +380,13 @@ service_save() {
377
380
  else
378
381
  echo "❌ 服务修改因明确未发送的网络异常在自动重试两次后仍未恢复"
379
382
  fi
383
+ emit_control "SERVICE_SAVE_RESULT=FAILED"
380
384
  exit 1
381
385
  fi
382
386
 
383
387
  # 错误检测
384
388
  if ! handle_error "$RESULT"; then
389
+ emit_control "SERVICE_SAVE_RESULT=FAILED"
385
390
  exit 1
386
391
  fi
387
392
 
@@ -418,10 +423,12 @@ service_save() {
418
423
 
419
424
  if ! validate_control_value "$NEW_SERVICE_ID"; then
420
425
  echo "❌ 服务创建返回成功,但未解析到 serviceId,禁止继续后续流程"
426
+ emit_control "SERVICE_SAVE_RESULT=UNKNOWN"
421
427
  exit 1
422
428
  fi
423
429
  emit_control "✅ 服务${OPERATION}成功"
424
430
  emit_control "📋 服务ID: $NEW_SERVICE_ID"
431
+ emit_control "SERVICE_SAVE_RESULT=SUCCESS"
425
432
  emit_control_field "SERVICE_ID" "$NEW_SERVICE_ID" || exit 1
426
433
  else
427
434
  OPERATION="创建"
@@ -437,8 +444,10 @@ service_save() {
437
444
  ' 2>/dev/null)
438
445
  ERROR_MSG=$(sanitize_customer_error_text "$ERROR_MSG")
439
446
  echo "❌ 服务${OPERATION}失败: ${BUSINESS_CODE} / ${ERROR_MSG}"
447
+ emit_control "SERVICE_SAVE_RESULT=FAILED"
440
448
  else
441
449
  echo "❌ 服务${OPERATION}返回结构异常,无法确认操作结果"
450
+ emit_control "SERVICE_SAVE_RESULT=UNKNOWN"
442
451
  fi
443
452
  exit 1
444
453
  fi
@@ -33,6 +33,7 @@ case "$PRODUCT_TYPE" in
33
33
  *) echo "❌ 截图上传仅支持 webpay|apppay"; exit 1 ;;
34
34
  esac
35
35
  init_alipay_cli_product "$PRODUCT_TYPE" || exit 1
36
+ export ALIPAY_AIPAY_SUPPRESS_AUTH_LOGOUT=1
36
37
 
37
38
  HOME_IMG="${1:-}"
38
39
  SHOP_IMG="${2:-}"
@@ -83,7 +84,7 @@ wait
83
84
  # 解析图片引用值(兼容 file upload 返回 fileKey 或 URL 字段)
84
85
  parse_image_ref() {
85
86
  local RAW=$(cat "$1")
86
- if ! handle_error "$RAW" >/dev/null; then
87
+ if [ "$(detect_error "$RAW")" != "SUCCESS" ]; then
87
88
  return 1
88
89
  fi
89
90
  # alipay-cli file upload 可能返回 MCP 信封,先解包
@@ -120,7 +121,15 @@ PAY_REF=$(parse_image_ref "$TMP_PAY")
120
121
  # 校验上传结果(含认证错误检测)
121
122
  check_upload_error() {
122
123
  local RAW=$(cat "$1")
124
+ local ERROR_TYPE
125
+ ERROR_TYPE=$(detect_error "$RAW")
126
+ if [ "$ERROR_TYPE" = "MCP_AUTH_ERROR" ] && [ "${UPLOAD_AUTH_ERROR_SEEN:-false}" = "true" ]; then
127
+ return 1
128
+ fi
123
129
  if ! handle_error "$RAW"; then
130
+ if [ "$ERROR_TYPE" = "MCP_AUTH_ERROR" ]; then
131
+ UPLOAD_AUTH_ERROR_SEEN=true
132
+ fi
124
133
  return 1
125
134
  fi
126
135
  return 0
@@ -29,7 +29,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" service validate --name
29
29
  - 修改:必须传当前候选完整 `serviceId` 和全部五项资料;保存前使用 `onboarding.write.confirm/SERVICE_UPDATE_ONLY` 绑定当前登录会话、脱敏主体、产品、动作、目标与参数,并等待当前摘要后的明确肯定回复;`1` 只是兼容快捷输入。
30
30
  - 任何目标、参数、主体或会话变化都使旧修改确认失效。创建不得进入修改确认。
31
31
 
32
- 保存写操作调用 `a2a-pay-service.saveBazaarServiceForMcp`。明确 `NOT_SENT` 才能自动重试;`MAYBE_SENT` 时用列表按 `serviceId` 和完整五项资料核验,不能靠同名服务猜测。创建结果未解析到实际 `serviceId` 时禁止宣称完成。
32
+ 保存写操作调用 `a2a-pay-service.saveBazaarServiceForMcp`。明确 `NOT_SENT` 才能自动重试;`MAYBE_SENT` 时用列表按 `serviceId` 和完整五项资料核验,不能靠同名服务猜测。创建结果未解析到实际 `serviceId` 时禁止宣称完成。脚本必须输出 `SERVICE_SAVE_RESULT=SUCCESS|FAILED|UNKNOWN`;`UNKNOWN` 只表示禁止自动重复保存并进入服务分支待办,不清除其他分支事实。
33
33
 
34
34
  成功结果只用登记 renderer:
35
35
 
@@ -41,20 +41,20 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" render <messageId> --va
41
41
  | 意图 | 判定 | 入口 | 后续按需加载 |
42
42
  |---|---|---|---|
43
43
  | `integration_only` | 只要求代码开发、支付集成或集成排障,未同时要求产品开通 | 执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" flow integration entry` | 当前阶段、产品和语言的契约、示例与脚本 |
44
- | `onboarding_only` | 只要求签约、入驻或产品开通,未同时要求代码开发 | 执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" flow onboarding entry` | 当前阶段的授权、查询或写分支模块 |
44
+ | `onboarding_only` | 只要求签约、入驻或产品开通,未同时要求代码开发 | 执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-bootstrap --root-intent onboarding_only` | 当前阶段的授权、查询或写分支模块 |
45
45
  | `full_process` | 明确要求完整接入、一站式,或同一请求同时要求代码开发和产品开通 | `normal/full-process-routing.md` | 先 Integration,满足衔接条件后 Onboarding |
46
46
 
47
- `onboarding_only` 或 `full_process` 衔接 Onboarding 时先执行 flow 登记的 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry`:Windows 只发送登记引导并停止 Skill 内产品开通;非 Windows stdout 为空并继续。门禁早于产品/MCC、方案和环境检查,不手写或重复。
47
+ `onboarding_only` 正常入口使用 `onboarding-bootstrap --root-intent onboarding_only`,该组合动作只串行执行 `telemetry start-run`、`onboarding-message platform-entry` 和 `flow onboarding entry` 三个既有无业务写入动作;Windows 只发送登记引导并停止 Skill 内产品开通,非 Windows 继续返回 entry 执行契约。执行该组合动作后不得再单独重复执行这三条原子命令。`full_process` 衔接 Onboarding 时已存在同一根流程 run,不使用带 `start-run` 的 bootstrap;仍先执行 flow 登记的 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry`,再按需加载 Onboarding entry,避免重复 `run.intent_detected`。
48
48
 
49
- 识别根意图后立即执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry start-run --root-intent <integration_only|onboarding_only|full_process>`;产品已唯一确定时追加 `--product-type <aipay|webpay|apppay>`。命令创建/复用当前流程 `run_id`,并在遥测开启时记录 `run.intent_detected`:同一意图和产品继续复用,`UNKNOWN` 补全产品仍复用同一 `run_id`;明确换产品、换根意图或既有 run 已终态才重建。第三方平台项目 ID 仍优先固定为 `run_id`,不得被显式 run ID 或本地生成值覆盖。业务 stage 只由登记 runner/script 异步记录。
49
+ 识别根意图后立即执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry start-run --root-intent <integration_only|onboarding_only|full_process>`;产品已唯一确定时追加 `--product-type <aipay|webpay|apppay>`。唯一例外是 `onboarding_only` 已按上表执行 `onboarding-bootstrap --root-intent onboarding_only` 时,bootstrap 已包含同一次 `start-run`,不得重复执行本条 telemetry。命令创建/复用当前流程 `run_id`,并在遥测开启时记录 `run.intent_detected`:同一意图和产品继续复用,`UNKNOWN` 补全产品仍复用同一 `run_id`;明确换产品、换根意图或既有 run 已终态才重建。第三方平台项目 ID 仍优先固定为 `run_id`,不得被显式 run ID 或本地生成值覆盖。业务 stage 只由登记 runner/script 异步记录。
50
50
 
51
51
  遥测默认开启,`ALIPAY_AIPAY_TELEMETRY=0|false|off|no` 可关闭 `METRIC_EVENT`、outbox、上传和 flush;私有流程 run context 仍用于 `PLATFORM_ID` 和终态生命周期。落盘/网络/上传/flush 失败静默,不改变业务、消息或确认,stdout 为空且 stderr 内部行不对客。`integration_only` 由 checklist 终态结束 run,其他意图由 Onboarding closeout 结束。脚本始终以当前流程 `run_id` 为 `PLATFORM_ID`,Agent 不手写环境前缀或改 MCP payload;维护诊断、outbox、上传和 flush 细节只在 `normal/telemetry.md` 按需读取。
52
52
 
53
- 三种入口均只在无法从已有产品名称或业务描述可靠确定唯一产品时执行 flow 登记的 `product-clarify` runner;不得推定或手写澄清。产品明确但 MCC 尚无检索输入时使用 `mcc-clarify`;已有经营描述后统一使用 `mcc-resolve-plan`,不得拆回 `mcc-search + mcc-candidates` 或为方案展示再拼 renderer 输入。
53
+ 三种入口均只在无法从已有产品名称或业务描述可靠确定唯一产品时执行 flow 登记的 `product-clarify` runner;不得推定或手写澄清。产品明确但 MCC 尚无检索输入时使用 `mcc-clarify`;已有经营描述后先按 Onboarding flow 读取一次当前 `modules/mcc-reference.md` 做 Agent 语义候选,再统一调用 `mcc-resolve-plan` 由 runner 复核、合并和渲染;用户直接提供当前表内完整 MCC 编码时可跳过 Agent 泛化并由 runner 精确校验。不得拆回 `mcc-search + mcc-candidates`、先旧检索失败后临时猜测,或为方案展示再拼 renderer 输入。
54
54
 
55
55
  代码开发或完整接入缺项目位置/技术栈时,执行 `integration.context.required` 或 `integration.project_path.required` 标准消息。先区分新建与复用;`PROJECT_INPUT` 保留原话,明确新建项目/网站/商城/应用等产物时必须 `resolve-project --intent new`,不得因当前目录有项目改为复用。未知现有项目先轻量定位,所有路径再由 `resolve-project` 解析;补齐前不初始化、装依赖或改文件。
56
56
 
57
- 每轮代码开发只渲染一次非阻塞 `integration.start.confirm`,两种意图复用同一次检查器事实并覆盖 Integration 步骤 1 至步骤8:代码开发校验;`full_process` 在方案展示前已完成步骤 1,方案送达且 receipt 有效后进入步骤 3。Onboarding 只保留一次非阻塞方案展示;服务修改仍保留绑定当前摘要的阻塞确认。签约材料完整后直接提交,服务创建摘要后直接创建,新建应用公钥校验成功后直接提审。
57
+ 每轮代码开发只渲染一次非阻塞 `integration.start.confirm`,两种意图复用同一次检查器事实并覆盖 Integration 步骤 1 至步骤8:代码开发校验;`full_process` 在方案展示前已完成步骤 1,方案送达且 receipt 有效后进入步骤 3。Onboarding 只保留一次非阻塞方案展示;方案送达且 MCC 唯一事实有效后,正常路径执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-execute-ready --product-type "$PRODUCT_TYPE"`,该组合动作只串行加载 `flow onboarding execute` 并执行既有 `env check`,不跨用户输入、授权页或任何写操作;执行后不得再重复单独加载 execute phase 或重复环境检查。服务修改仍保留绑定当前摘要的阻塞确认。签约材料完整后直接提交,服务创建摘要后直接创建,新建应用公钥校验成功后直接提审。
58
58
 
59
59
  扫码页非阻塞:授权最多 12 次/间隔 5 秒,公钥最多 20 次/间隔 2 秒,成功即推进;耗尽后经统一回复判定只查一次,不重启轮询。
60
60