@alipay/alipay-aipay 1.2.2 → 1.5.0

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.
Files changed (104) hide show
  1. package/README.md +5 -0
  2. package/dist/commands/install.d.ts.map +1 -1
  3. package/dist/commands/install.js +13 -12
  4. package/dist/commands/install.js.map +1 -1
  5. package/dist/commands/uninstall.d.ts.map +1 -1
  6. package/dist/commands/uninstall.js +17 -9
  7. package/dist/commands/uninstall.js.map +1 -1
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +5 -5
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/index.js +4 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/utils/config.d.ts +5 -3
  14. package/dist/utils/config.d.ts.map +1 -1
  15. package/dist/utils/config.js +34 -12
  16. package/dist/utils/config.js.map +1 -1
  17. package/dist/utils/file.d.ts +8 -0
  18. package/dist/utils/file.d.ts.map +1 -1
  19. package/dist/utils/file.js +161 -9
  20. package/dist/utils/file.js.map +1 -1
  21. package/dist/utils/jq.d.ts.map +1 -1
  22. package/dist/utils/jq.js +8 -0
  23. package/dist/utils/jq.js.map +1 -1
  24. package/package.json +9 -5
  25. package/skill/SKILL.md +15 -169
  26. package/skill/VERSION +1 -1
  27. package/skill/references/integration/flow.md +388 -291
  28. package/skill/references/integration/modules/alipay-sdk-reminder.md +5 -3
  29. package/skill/references/integration/modules/checklist.md +54 -32
  30. package/skill/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +199 -0
  31. package/skill/references/integration/modules/code-examples/csharp/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +8 -4
  32. package/skill/references/integration/modules/code-examples/csharp/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +6 -2
  33. package/skill/references/integration/modules/code-examples/csharp/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.cs +3 -1
  34. package/skill/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +184 -0
  35. package/skill/references/integration/modules/code-examples/java/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +4 -1
  36. package/skill/references/integration/modules/code-examples/java/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +5 -2
  37. package/skill/references/integration/modules/code-examples/java/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemoController.java +3 -1
  38. package/skill/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +135 -0
  39. package/skill/references/integration/modules/code-examples/nodejs/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +16 -10
  40. package/skill/references/integration/modules/code-examples/nodejs/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +10 -4
  41. package/skill/references/integration/modules/code-examples/nodejs/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.js +2 -1
  42. package/skill/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +157 -0
  43. package/skill/references/integration/modules/code-examples/php/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +4 -1
  44. package/skill/references/integration/modules/code-examples/php/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +5 -2
  45. package/skill/references/integration/modules/code-examples/php/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.php +2 -1
  46. package/skill/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +128 -0
  47. package/skill/references/integration/modules/code-examples/python/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +4 -1
  48. package/skill/references/integration/modules/code-examples/python/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +5 -1
  49. package/skill/references/integration/modules/code-examples/python/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.py +4 -2
  50. package/skill/references/integration/modules/interface-contract/aipay-interface-contract.md +52 -0
  51. package/skill/references/integration/modules/interface-contract/apppay-interface-contract.md +45 -0
  52. package/skill/references/integration/modules/interface-contract/common-trade-contract.md +42 -0
  53. package/skill/references/integration/modules/interface-contract/notify-url-contract.md +68 -0
  54. package/skill/references/integration/modules/interface-contract/webpay-interface-contract.md +44 -0
  55. package/skill/references/integration/modules/interface-guide.md +38 -71
  56. package/skill/references/integration/modules/product-decision.md +18 -35
  57. package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +62 -94
  58. package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +43 -40
  59. package/skill/references/integration/modules/sandbox/sandbox-setup-guide.md +93 -114
  60. package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +358 -148
  61. package/skill/references/integration/modules/scripts/sandbox_config.sh +299 -0
  62. package/skill/references/integration/modules/troubleshooting-flow.md +28 -0
  63. package/skill/references/normal/alipay-cli-env.md +15 -5
  64. package/skill/references/normal/full-process-routing.md +82 -0
  65. package/skill/references/normal/json/customer-messages.json +942 -0
  66. package/skill/references/normal/json/policy-rules.json +179 -0
  67. package/skill/references/normal/rejection-guide.md +12 -7
  68. package/skill/references/normal/scripts/alipay_cli_env.mjs +33 -0
  69. package/skill/references/normal/scripts/alipay_cli_env.sh +55 -0
  70. package/skill/references/normal/scripts/common.sh +151 -0
  71. package/skill/references/normal/scripts/detect_dev_tool.sh +5 -1
  72. package/skill/references/normal/scripts/flow_loader.mjs +241 -0
  73. package/skill/references/normal/scripts/integration_context_runner.mjs +166 -0
  74. package/skill/references/normal/scripts/integration_message_runner.mjs +1023 -0
  75. package/skill/references/normal/scripts/interaction_reply.mjs +133 -0
  76. package/skill/references/normal/scripts/onboarding_discovery_runner.mjs +224 -0
  77. package/skill/references/normal/scripts/onboarding_message_runner.mjs +451 -0
  78. package/skill/references/normal/scripts/onboarding_recovery_runner.mjs +96 -0
  79. package/skill/references/normal/scripts/open_official_url.sh +82 -0
  80. package/skill/references/normal/scripts/platform_compat.mjs +39 -0
  81. package/skill/references/normal/scripts/project_route_inspector.sh +132 -0
  82. package/skill/references/normal/scripts/python_script_runner.mjs +56 -0
  83. package/skill/references/normal/scripts/render_customer_message.mjs +1288 -0
  84. package/skill/references/normal/scripts/self_update.mjs +160 -0
  85. package/skill/references/normal/scripts/telemetry.mjs +1327 -0
  86. package/skill/references/normal/self-update.md +18 -51
  87. package/skill/references/normal/telemetry.md +26 -0
  88. package/skill/references/onboarding/flow.md +421 -309
  89. package/skill/references/onboarding/modules/app-release.md +44 -438
  90. package/skill/references/onboarding/modules/authorization.md +25 -281
  91. package/skill/references/onboarding/modules/error-handling.md +31 -237
  92. package/skill/references/onboarding/modules/product-sign.md +25 -294
  93. package/skill/references/onboarding/modules/scripts/app.sh +445 -169
  94. package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +47 -28
  95. package/skill/references/onboarding/modules/scripts/auth.sh +484 -167
  96. package/skill/references/onboarding/modules/scripts/download_key_tool.sh +157 -0
  97. package/skill/references/onboarding/modules/scripts/error_handler.sh +101 -130
  98. package/skill/references/onboarding/modules/scripts/network_retry.sh +96 -0
  99. package/skill/references/onboarding/modules/scripts/query_sign_status.sh +20 -71
  100. package/skill/references/onboarding/modules/scripts/service.sh +154 -89
  101. package/skill/references/onboarding/modules/scripts/sign_status_parser.sh +58 -0
  102. package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +34 -20
  103. package/skill/references/onboarding/modules/service-registration.md +20 -317
  104. package/skill/references/runtime-entry.md +63 -0
@@ -3,15 +3,19 @@
3
3
  # 脚本名称: auth.sh
4
4
  # 功能描述: 登录授权全流程 - 检查状态 + 执行登录 + 授权确认 + scope/MCC 校验 + 重新授权
5
5
  # 调用位置: Step 3 登录授权
6
- # 调用前置: 脚本通过 error_handler.sh 间接初始化 DEV_TOOL_NAME;必要时可用 DEV_TOOL_NAME 环境变量覆盖
6
+ # 调用前置: 脚本通过 error_handler.sh 间接初始化 DEV_TOOL_NAME/PLATFORM_ID;必要时可用 DEV_TOOL_NAME 环境变量覆盖
7
7
  # 用法:
8
- # auth init --scope <scope> --sales-code <code> --mcc-code <code> [--product-name <name> --mcc-name <name>]
8
+ # auth init --defer-open --scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>
9
+ # auth open [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]
9
10
  # auth confirm [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]
10
- # auth mismatch [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]
11
+ # auth wait [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]
12
+ # auth mismatch --defer-open [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]
11
13
  # 返回值:
12
- # init: AUTH_FLOW:SKIP | AUTH_FLOW:READY | AUTH_FLOW:SCOPE_MISMATCH | AUTH_FLOW:MCC_MISMATCH | AUTH_FLOW:FAILED
13
- # confirm: AUTH_FLOW:AUTH_SUCCESS | AUTH_FLOW:PENDING | AUTH_FLOW:EXPIRED | AUTH_FLOW:SCOPE_MISMATCH | AUTH_FLOW:MCC_MISMATCH | AUTH_FLOW:FAILED
14
- # mismatch: 执行一次 logout 后调用 auth init 重新生成授权链接(不输出 FLOW 标记,由 auth init 输出 AUTH_FLOW:READY)
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
+ # 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
+ # 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
18
+ # stdout: 仅登记对客正文;AUTH_FLOW/状态/查询复用标记写入 stderr
15
19
  #=============================================================================
16
20
 
17
21
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -23,22 +27,113 @@ else
23
27
  echo "❌ 缺少错误处理脚本: ${SCRIPT_DIR}/error_handler.sh"
24
28
  exit 1
25
29
  fi
30
+ source "${SCRIPT_DIR}/network_retry.sh"
31
+ source "${SCRIPT_DIR}/sign_status_parser.sh"
32
+ RENDERER="${SCRIPT_DIR}/../../../normal/scripts/render_customer_message.mjs"
26
33
 
27
34
  require_command jq || exit 1
28
35
  require_command alipay-cli || exit 1
36
+ require_command node || exit 1
29
37
  require_command mktemp || exit 1
38
+ umask 077
30
39
 
31
40
  # ─── 授权状态文件(用于独立执行 confirm/mismatch 时兜底恢复非敏感参数)────
32
41
  AUTH_STATE_OWNER="$(id -u 2>/dev/null || printf '%s' "${USER:-unknown}" | tr -c 'A-Za-z0-9_-' '_')"
33
- AUTH_STATE_DIR="${TMPDIR:-/tmp}"
34
- AUTH_STATE_DIR="${AUTH_STATE_DIR%/}"
35
- AUTH_STATE_FILE="${ALIPAY_AUTH_STATE_FILE:-${AUTH_STATE_DIR}/.alipay_auth_state_${AUTH_STATE_OWNER}.json}"
42
+ AUTH_STATE_BASE="${TMPDIR:-/tmp}"
43
+ AUTH_STATE_BASE="${AUTH_STATE_BASE%/}"
44
+ if [ -n "${ALIPAY_AUTH_STATE_FILE:-}" ]; then
45
+ AUTH_STATE_FILE="$ALIPAY_AUTH_STATE_FILE"
46
+ AUTH_STATE_DIR="$(dirname "$AUTH_STATE_FILE")"
47
+ AUTH_STATE_DIR_MANAGED=false
48
+ else
49
+ AUTH_STATE_DIR="${AUTH_STATE_BASE}/alipay-aipay-auth-${AUTH_STATE_OWNER}"
50
+ AUTH_STATE_FILE="${AUTH_STATE_DIR}/state.json"
51
+ AUTH_STATE_DIR_MANAGED=true
52
+ fi
53
+
54
+ secure_mode() {
55
+ local target="$1" expected="$2" mode
56
+ if mode=$(stat -f '%Lp' "$target" 2>/dev/null); then
57
+ :
58
+ elif mode=$(stat -c '%a' "$target" 2>/dev/null); then
59
+ :
60
+ else
61
+ return 1
62
+ fi
63
+ [ "$mode" = "$expected" ]
64
+ }
65
+
66
+ prepare_auth_state_dir() {
67
+ if [ "$AUTH_STATE_DIR_MANAGED" = true ]; then
68
+ if [ -L "$AUTH_STATE_DIR" ] || { [ -e "$AUTH_STATE_DIR" ] && [ ! -d "$AUTH_STATE_DIR" ]; }; then
69
+ echo "❌ 授权临时目录不安全,已停止" >&2
70
+ return 1
71
+ fi
72
+ mkdir -p "$AUTH_STATE_DIR" || return 1
73
+ chmod 700 "$AUTH_STATE_DIR" 2>/dev/null || return 1
74
+ secure_mode "$AUTH_STATE_DIR" 700 || return 1
75
+ else
76
+ [ -d "$AUTH_STATE_DIR" ] && [ ! -L "$AUTH_STATE_DIR" ] || {
77
+ echo "❌ 指定的授权状态目录不可用或不安全,已停止" >&2
78
+ return 1
79
+ }
80
+ fi
81
+ }
82
+
83
+ validate_auth_state_file() {
84
+ [ -f "$AUTH_STATE_FILE" ] && [ ! -L "$AUTH_STATE_FILE" ] || return 1
85
+ chmod 600 "$AUTH_STATE_FILE" 2>/dev/null || return 1
86
+ secure_mode "$AUTH_STATE_FILE" 600
87
+ }
36
88
 
37
89
  cleanup_auth_state() {
38
90
  rm -f "$AUTH_STATE_FILE"
39
91
  }
40
92
 
93
+ reset_auth_state_for_init() {
94
+ local field current saved
95
+ AUTH_INIT_CONTEXT_MISMATCH_TYPE=""
96
+ [ -e "$AUTH_STATE_FILE" ] || [ -L "$AUTH_STATE_FILE" ] || return 0
97
+ if ! prepare_auth_state_dir || ! validate_auth_state_file; then
98
+ echo "❌ 授权状态文件不可用或不安全,已停止"
99
+ emit_control "AUTH_FLOW:FAILED"
100
+ return 1
101
+ fi
102
+
103
+ # 新上下文通过机器校验后才读取旧状态;只记录差异类型,不复用旧 deviceCode。
104
+ if jq -e 'type == "object"' "$AUTH_STATE_FILE" >/dev/null 2>&1; then
105
+ for field in salesCode scope productName mccCode mccName; do
106
+ case "$field" in
107
+ salesCode) current="$SALES_CODE" ;;
108
+ scope) current="$SCOPE" ;;
109
+ productName) current="$PRODUCT_NAME" ;;
110
+ mccCode) current="$MCC_CODE" ;;
111
+ mccName) current="$MCC_NAME" ;;
112
+ esac
113
+ saved=$(jq -r --arg field "$field" '.[$field] // ""' "$AUTH_STATE_FILE")
114
+ if [ "$current" != "$saved" ]; then
115
+ case "$field" in
116
+ salesCode|scope|productName) AUTH_INIT_CONTEXT_MISMATCH_TYPE="SCOPE_MISMATCH" ;;
117
+ *) [ -n "$AUTH_INIT_CONTEXT_MISMATCH_TYPE" ] || AUTH_INIT_CONTEXT_MISMATCH_TYPE="MCC_MISMATCH" ;;
118
+ esac
119
+ fi
120
+ done
121
+ else
122
+ # 无法证明旧状态属于当前产品上下文时,已有登录也必须重新授权。
123
+ AUTH_INIT_CONTEXT_MISMATCH_TYPE="SCOPE_MISMATCH"
124
+ fi
125
+ cleanup_auth_state
126
+ }
127
+
41
128
  save_auth_state() {
129
+ local temp_state
130
+ prepare_auth_state_dir || return 1
131
+ if [ -L "$AUTH_STATE_FILE" ] || { [ -e "$AUTH_STATE_FILE" ] && [ ! -f "$AUTH_STATE_FILE" ]; }; then
132
+ echo "❌ 授权状态路径不是安全的普通文件,已停止" >&2
133
+ return 1
134
+ fi
135
+ temp_state=$(mktemp "${AUTH_STATE_DIR}/.auth-state.XXXXXX") || return 1
136
+ chmod 600 "$temp_state" 2>/dev/null || { rm -f "$temp_state"; return 1; }
42
137
  jq -n \
43
138
  --arg salesCode "$SALES_CODE" \
44
139
  --arg mccCode "$MCC_CODE" \
@@ -46,19 +141,26 @@ save_auth_state() {
46
141
  --arg productName "${PRODUCT_NAME:-}" \
47
142
  --arg mccName "${MCC_NAME:-}" \
48
143
  --arg deviceCode "${DEVICE_CODE:-}" \
144
+ --arg authorizationUrl "${BROWSER_URL:-}" \
49
145
  '{
50
146
  salesCode: $salesCode,
51
147
  mccCode: $mccCode,
52
148
  scope: $scope,
53
149
  productName: $productName,
54
150
  mccName: $mccName,
55
- deviceCode: $deviceCode
56
- }' > "$AUTH_STATE_FILE"
57
- chmod 600 "$AUTH_STATE_FILE" 2>/dev/null || true
151
+ deviceCode: $deviceCode,
152
+ authorizationUrl: $authorizationUrl
153
+ }' > "$temp_state" || { rm -f "$temp_state"; return 1; }
154
+ mv "$temp_state" "$AUTH_STATE_FILE" || { rm -f "$temp_state"; return 1; }
155
+ validate_auth_state_file || {
156
+ cleanup_auth_state
157
+ echo "❌ 无法确认授权状态文件权限为 0600,已停止" >&2
158
+ return 1
159
+ }
58
160
  }
59
161
 
60
162
  load_auth_state() {
61
- if [ ! -f "$AUTH_STATE_FILE" ]; then
163
+ if ! validate_auth_state_file; then
62
164
  echo "❌ 缺少授权上下文(请传入 --sales-code/--mcc-code,或先执行 auth.sh init)"
63
165
  return 1
64
166
  fi
@@ -75,6 +177,33 @@ load_auth_state() {
75
177
  [ -n "${MCC_NAME:-}" ] || MCC_NAME=$(jq -r '.mccName // ""' "$AUTH_STATE_FILE")
76
178
  }
77
179
 
180
+ verify_context_against_auth_state() {
181
+ local field current saved
182
+ [ -e "$AUTH_STATE_FILE" ] || [ -L "$AUTH_STATE_FILE" ] || return 0
183
+ if ! validate_auth_state_file; then
184
+ echo "❌ 授权状态文件不可用或不安全,已停止"
185
+ return 1
186
+ fi
187
+ if ! jq -e . "$AUTH_STATE_FILE" >/dev/null 2>&1; then
188
+ echo "❌ 授权状态文件不是合法 JSON: $AUTH_STATE_FILE"
189
+ return 1
190
+ fi
191
+ for field in salesCode mccCode scope productName mccName; do
192
+ case "$field" in
193
+ salesCode) current="$SALES_CODE" ;;
194
+ mccCode) current="$MCC_CODE" ;;
195
+ scope) current="$SCOPE" ;;
196
+ productName) current="$PRODUCT_NAME" ;;
197
+ mccName) current="$MCC_NAME" ;;
198
+ esac
199
+ saved=$(jq -r --arg field "$field" '.[$field] // ""' "$AUTH_STATE_FILE")
200
+ if [ -n "$current" ] && [ -n "$saved" ] && [ "$current" != "$saved" ]; then
201
+ emit_control "AUTH_FLOW:CONTEXT_MISMATCH"
202
+ return 1
203
+ fi
204
+ done
205
+ }
206
+
78
207
  parse_auth_context_args() {
79
208
  while [[ $# -gt 0 ]]; do
80
209
  case $1 in
@@ -109,13 +238,17 @@ restore_auth_context_or_state() {
109
238
  parse_auth_context_args "$@" || return 1
110
239
  fi
111
240
 
112
- if [ -z "$SALES_CODE" ] || [ -z "$MCC_CODE" ]; then
241
+ verify_context_against_auth_state || return 1
242
+ if [ -e "$AUTH_STATE_FILE" ] || [ -L "$AUTH_STATE_FILE" ]; then
243
+ load_auth_state || return 1
244
+ elif [ -z "$SALES_CODE" ] || [ -z "$MCC_CODE" ] || [ -z "$PRODUCT_NAME" ] || [ -z "$MCC_NAME" ]; then
113
245
  load_auth_state || return 1
114
246
  fi
115
247
 
116
248
  if [ -z "$SCOPE" ]; then
117
249
  SCOPE=$(get_scope "$SALES_CODE")
118
250
  fi
251
+ validate_auth_context || return 1
119
252
  }
120
253
 
121
254
  # ─── 共用:根据 salesCode 获取 scope ─────────────────────────────────────────
@@ -128,27 +261,97 @@ get_scope() {
128
261
  esac
129
262
  }
130
263
 
264
+ validate_auth_context() {
265
+ local context_json
266
+ context_json=$(jq -n \
267
+ --arg productName "$PRODUCT_NAME" \
268
+ --arg salesCode "$SALES_CODE" \
269
+ --arg scope "$SCOPE" \
270
+ --arg mccCode "$MCC_CODE" \
271
+ --arg mccName "$MCC_NAME" \
272
+ '{productName:$productName,salesCode:$salesCode,scope:$scope,mccCode:$mccCode,mccName:$mccName}') || return 1
273
+ if ! printf '%s' "$context_json" | node "$RENDERER" --validate-authorization-context >/dev/null; then
274
+ echo "❌ 授权上下文校验失败:产品、产品码、scope 或 MCC 不匹配当前固定规则"
275
+ emit_control "AUTH_FLOW:FAILED"
276
+ return 1
277
+ fi
278
+ init_alipay_cli_product_from_sales_code "$SALES_CODE" || {
279
+ emit_control "AUTH_FLOW:FAILED"
280
+ return 1
281
+ }
282
+ }
283
+
284
+ build_authorization_url() {
285
+ local input_json platform_value
286
+ platform_value="${DEV_TOOL_NAME:-}"
287
+ [ "$platform_value" = "unknown" ] && platform_value=""
288
+ input_json=$(jq -n \
289
+ --arg productName "$PRODUCT_NAME" \
290
+ --arg salesCode "$SALES_CODE" \
291
+ --arg scope "$SCOPE" \
292
+ --arg mccCode "$MCC_CODE" \
293
+ --arg mccName "$MCC_NAME" \
294
+ --arg deviceCode "$DEVICE_CODE" \
295
+ --arg platform "$platform_value" \
296
+ '{productName:$productName,salesCode:$salesCode,scope:$scope,mccCode:$mccCode,mccName:$mccName,deviceCode:$deviceCode,platform:$platform}') || return 1
297
+ printf '%s' "$input_json" | node "$RENDERER" --build-authorization-url
298
+ }
299
+
131
300
  extract_json_payload() {
132
301
  local raw="$1"
133
- local line_count start candidate
302
+ local analysis status
303
+ analysis=$(extract_unique_json "$raw") || return 1
304
+ status=$(printf '%s' "$analysis" | jq -r '.status // "none"' 2>/dev/null)
305
+ [ "$status" = "ok" ] || return 1
306
+ printf '%s' "$analysis" | jq -ce '.value | select(type == "object")' 2>/dev/null
307
+ }
134
308
 
135
- if printf '%s' "$raw" | jq -e . >/dev/null 2>&1; then
136
- printf '%s' "$raw"
137
- return 0
309
+ auth_cli_environment_failure() {
310
+ local action="$1"
311
+ echo "❌ ${action} 未能在当前 Agent 执行环境取得可确认结果,已停止。"
312
+ echo "📋 这通常表示当前执行环境缺少联网权限、CLI wrapper 混入输出,或 stdout/stderr 无法唯一解析。"
313
+ echo "📋 这不代表支付宝业务失败,也不能据此判断用户本机 logout/login 失败。"
314
+ echo "📋 Agent 应申请可联网权限后重试同一条 auth.sh 命令;不要要求用户手动 logout/login 代替本轮事实。"
315
+ emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
316
+ }
317
+
318
+ wait_for_effective_whoami() {
319
+ local action="$1"
320
+ local max_attempts=3
321
+ local interval_seconds=2
322
+ local attempt=1
323
+ local logged_in is_expired
324
+
325
+ AUTH_WHOAMI_RESULT=""
326
+ if [ "${ALIPAY_AIPAY_TEST_MODE:-0}" = "1" ] && [[ "${AUTH_WHOAMI_RECHECK_INTERVAL_SECONDS:-}" =~ ^[0-9]+$ ]]; then
327
+ interval_seconds="$AUTH_WHOAMI_RECHECK_INTERVAL_SECONDS"
138
328
  fi
139
329
 
140
- line_count=$(printf '%s\n' "$raw" | wc -l | tr -d ' ')
141
- start=1
142
- while [ "$start" -le "$line_count" ]; do
143
- candidate=$(printf '%s\n' "$raw" | sed -n "${start},\$p")
144
- if printf '%s' "$candidate" | jq -e . >/dev/null 2>&1; then
145
- printf '%s' "$candidate"
330
+ while [ "$attempt" -le "$max_attempts" ]; do
331
+ AUTH_WHOAMI_RESULT=$(run_json_command "alipay-cli whoami --json" alipay-cli whoami --json) || {
332
+ auth_cli_environment_failure "$action"
333
+ return 1
334
+ }
335
+ if ! handle_error "$AUTH_WHOAMI_RESULT"; then
336
+ emit_control "AUTH_FLOW:FAILED"
337
+ return 1
338
+ fi
339
+
340
+ logged_in=$(echo "$AUTH_WHOAMI_RESULT" | jq -r '.data.logged_in // false')
341
+ is_expired=$(echo "$AUTH_WHOAMI_RESULT" | jq -r '.data.is_expired // false')
342
+ if [ "$logged_in" = "true" ] && [ "$is_expired" != "true" ]; then
146
343
  return 0
147
344
  fi
148
- start=$((start + 1))
345
+
346
+ emit_control "AUTH_WHOAMI_RECHECK=${attempt}/${max_attempts}"
347
+ if [ "$attempt" -lt "$max_attempts" ]; then
348
+ sleep "$interval_seconds"
349
+ fi
350
+ attempt=$((attempt + 1))
149
351
  done
150
352
 
151
- return 1
353
+ emit_control "AUTH_WHOAMI_NOT_EFFECTIVE"
354
+ return 2
152
355
  }
153
356
 
154
357
  run_json_command() {
@@ -157,7 +360,7 @@ run_json_command() {
157
360
  local stderr_file stdout stderr_output combined_output exit_code json_payload
158
361
 
159
362
  stderr_file="$(mktemp "${TMPDIR:-/tmp}/alipay_auth_stderr.XXXXXX")" || return 1
160
- stdout=$(PLATFORM="${DEV_TOOL_NAME}" "$@" 2>"$stderr_file")
363
+ stdout=$(PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}" PRODUCT="${PRODUCT:-}" "$@" 2>"$stderr_file")
161
364
  exit_code=$?
162
365
  stderr_output=$(cat "$stderr_file")
163
366
 
@@ -180,16 +383,7 @@ run_json_command() {
180
383
  return 0
181
384
  fi
182
385
 
183
- echo "❌ ${label} 未返回合法 JSON" >&2
184
- echo "📋 退出码: ${exit_code}" >&2
185
- if [ -n "$stderr_output" ]; then
186
- echo "📋 错误输出:" >&2
187
- printf '%s\n' "$stderr_output" >&2
188
- fi
189
- if [ -n "$stdout" ]; then
190
- echo "📋 标准输出:" >&2
191
- printf '%s\n' "$stdout" >&2
192
- fi
386
+ echo "❌ ${label} 未返回可唯一解析的合法 JSON(退出码: ${exit_code});原始输出已隐藏,请申请联网权限后重试同一条 auth.sh 命令" >&2
193
387
  rm -f "$stderr_file"
194
388
  return 1
195
389
  }
@@ -199,26 +393,27 @@ run_json_command() {
199
393
  validate_current_authorization() {
200
394
  local whoami_result="${1:-}"
201
395
  local logged_in is_expired granted_scope required_scope
202
- local scope_match mcc_verify_result mcc_verify_unwrapped mcc_match
396
+ local scope_match mcc_verify_result mcc_verify_unwrapped mcc_match mcc_raw retry_rc
203
397
  local mcc_error_type mcc_error_code mcc_success
204
- local granted_scope_item
205
- local -a granted_scope_items
398
+ local mismatch_type sign_status_output
399
+ local granted_scope_item required_scope_item required_scope_found
400
+ local -a granted_scope_items required_scope_items
206
401
 
207
402
  if [ -z "$SALES_CODE" ] || [ -z "$MCC_CODE" ]; then
208
403
  echo "❌ 缺少授权校验参数"
209
- echo "AUTH_FLOW:FAILED"
404
+ emit_control "AUTH_FLOW:FAILED"
210
405
  return 1
211
406
  fi
212
407
 
213
408
  if [ -z "$whoami_result" ]; then
214
409
  whoami_result=$(run_json_command "alipay-cli whoami --json" alipay-cli whoami --json) || {
215
- echo "AUTH_FLOW:FAILED"
410
+ auth_cli_environment_failure "登录状态复核"
216
411
  return 1
217
412
  }
218
413
  fi
219
414
 
220
415
  if ! handle_error "$whoami_result"; then
221
- echo "AUTH_FLOW:FAILED"
416
+ emit_control "AUTH_FLOW:FAILED"
222
417
  return 1
223
418
  fi
224
419
 
@@ -226,40 +421,52 @@ validate_current_authorization() {
226
421
  is_expired=$(echo "$whoami_result" | jq -r '.data.is_expired // false')
227
422
  if [ "$logged_in" != "true" ] || [ "$is_expired" = "true" ]; then
228
423
  echo "❌ 当前登录状态无效或已过期"
229
- echo "AUTH_FLOW:FAILED"
424
+ emit_control "AUTH_FLOW:FAILED"
230
425
  return 1
231
426
  fi
232
427
 
233
428
  granted_scope=$(echo "$whoami_result" | jq -r '.data.scope // empty')
234
- case "$SALES_CODE" in
235
- "I1080300001000041203") required_scope="fast_instant_trade_pay:write" ;;
236
- "I1080300001000041313") required_scope="auth_alipay_apppay:write" ;;
237
- "I1080300001000160457") required_scope="machine_pay:write" ;;
238
- *)
239
- echo "❌ 未知产品码: $SALES_CODE"
240
- echo "AUTH_FLOW:FAILED"
241
- return 1
242
- ;;
243
- esac
429
+ required_scope=$(get_scope "$SALES_CODE")
430
+ if [ -z "$required_scope" ] || [ "$SCOPE" != "$required_scope" ]; then
431
+ echo "❌ 当前产品的固定 scope 上下文无效"
432
+ emit_control "AUTH_FLOW:FAILED"
433
+ return 1
434
+ fi
244
435
 
245
- scope_match="false"
436
+ scope_match="true"
246
437
  IFS=',' read -r -a granted_scope_items <<< "$granted_scope"
247
- for granted_scope_item in "${granted_scope_items[@]}"; do
248
- granted_scope_item="${granted_scope_item#"${granted_scope_item%%[![:space:]]*}"}"
249
- granted_scope_item="${granted_scope_item%"${granted_scope_item##*[![:space:]]}"}"
250
- if [ "$granted_scope_item" = "$required_scope" ]; then
251
- scope_match="true"
438
+ IFS=',' read -r -a required_scope_items <<< "$required_scope"
439
+ for required_scope_item in "${required_scope_items[@]}"; do
440
+ required_scope_found="false"
441
+ for granted_scope_item in "${granted_scope_items[@]}"; do
442
+ granted_scope_item="${granted_scope_item#"${granted_scope_item%%[![:space:]]*}"}"
443
+ granted_scope_item="${granted_scope_item%"${granted_scope_item##*[![:space:]]}"}"
444
+ if [ "$granted_scope_item" = "$required_scope_item" ]; then
445
+ required_scope_found="true"
446
+ break
447
+ fi
448
+ done
449
+ if [ "$required_scope_found" != "true" ]; then
450
+ scope_match="false"
252
451
  break
253
452
  fi
254
453
  done
255
454
 
256
455
  mcc_match="true"
257
456
  if [ "$scope_match" = "true" ]; then
258
- mcc_verify_result=$(run_json_command "alipay-cli mcp call ar-query.queryArInfosBySalesProd" \
259
- alipay-cli mcp call ar-query.queryArInfosBySalesProd \
457
+ export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
458
+ run_network_retry mcc_raw read authorization_mcc_check -- alipay-cli mcp call ar-query.queryArInfosBySalesProd \
260
459
  -d "{\"request\":{\"salesProductCodes\":[\"${SALES_CODE}\"]},\"ctx\":{}}" \
261
- --json) || {
262
- echo "AUTH_FLOW:FAILED"
460
+ --json
461
+ retry_rc=$?
462
+ if [ "$retry_rc" -ne 0 ]; then
463
+ echo "❌ 授权 MCC 校验因网络或服务异常在自动重试两次后仍未恢复"
464
+ emit_control "AUTH_FLOW:FAILED"
465
+ return 1
466
+ fi
467
+ mcc_verify_result=$(extract_json_payload "$mcc_raw") || {
468
+ echo "❌ alipay-cli mcp call ar-query.queryArInfosBySalesProd 未返回合法 JSON"
469
+ emit_control "AUTH_FLOW:FAILED"
263
470
  return 1
264
471
  }
265
472
 
@@ -269,7 +476,7 @@ validate_current_authorization() {
269
476
  mcc_match="false"
270
477
  elif [ "$mcc_error_type" != "SUCCESS" ]; then
271
478
  echo "❌ MCC 校验失败"
272
- echo "AUTH_FLOW:FAILED"
479
+ emit_control "AUTH_FLOW:FAILED"
273
480
  return 1
274
481
  fi
275
482
 
@@ -282,37 +489,32 @@ validate_current_authorization() {
282
489
  elif echo "$mcc_verify_unwrapped" | grep -qi "mccCode.*is not auth"; then
283
490
  mcc_match="false"
284
491
  fi
492
+
493
+ # 同一请求也是 Step 3.1 的签约查询;仅在完整结构可由共享解析器证明时输出可复用事实。
494
+ if [ "$mcc_match" = "true" ] && sign_status_output=$(emit_sign_status_from_business "$mcc_verify_unwrapped" "$SALES_CODE" 2>/dev/null); then
495
+ printf '%s\n' "$sign_status_output" \
496
+ | while IFS= read -r marker; do emit_control "$marker"; done
497
+ emit_control "AUTH_SIGN_QUERY_REUSABLE=true"
498
+ fi
285
499
  fi
286
500
 
287
501
  if [ "$scope_match" = "true" ] && [ "$mcc_match" = "true" ]; then
288
- echo "✅ 授权范围校验通过(scope + MCC)"
502
+ emit_control "✅ 授权范围校验通过(scope + MCC 授权有效性)"
289
503
  return 0
290
504
  fi
291
505
 
292
- echo ""
293
- echo "⚠️ 授权范围不满足"
294
506
  if [ "$scope_match" != "true" ]; then
295
- echo "📋 原因:scope 权限不满足"
296
- echo "🤖 需要权限: $required_scope"
297
- echo "🤖 已授权 scope: $granted_scope"
298
- fi
299
- if [ "$mcc_match" != "true" ]; then
300
- echo "📋 原因:经营类目未授权"
301
- echo "🤖 当前类目: $MCC_CODE"
302
- fi
303
-
304
- echo ""
305
- echo "📋 必须通过 auth.sh mismatch 退出当前登录并重新授权"
306
- if [ "$scope_match" != "true" ]; then
307
- echo "AUTH_FLOW:SCOPE_MISMATCH"
507
+ mismatch_type="SCOPE_MISMATCH"
308
508
  else
309
- echo "AUTH_FLOW:MCC_MISMATCH"
509
+ mismatch_type="MCC_MISMATCH"
310
510
  fi
511
+ emit_control "AUTH_FLOW:${mismatch_type}"
311
512
  return 1
312
513
  }
313
514
 
314
515
  # ─── auth init: 检查登录状态 + 执行登录 + 构建授权链接 + 输出授权信息 ────────
315
516
  auth_init() {
517
+ local DEFER_OPEN=false
316
518
  SCOPE=""
317
519
  SALES_CODE=""
318
520
  MCC_CODE=""
@@ -321,6 +523,8 @@ auth_init() {
321
523
 
322
524
  while [[ $# -gt 0 ]]; do
323
525
  case $1 in
526
+ --defer-open)
527
+ DEFER_OPEN=true; shift ;;
324
528
  --scope)
325
529
  [ $# -ge 2 ] || { echo "❌ 缺少参数值: $1"; exit 1; }
326
530
  SCOPE="$2"; shift 2 ;;
@@ -340,40 +544,57 @@ auth_init() {
340
544
  esac
341
545
  done
342
546
 
343
- if [ -z "$SCOPE" ] || [ -z "$SALES_CODE" ] || [ -z "$MCC_CODE" ]; then
547
+ if [ -z "$SCOPE" ] || [ -z "$SALES_CODE" ] || [ -z "$MCC_CODE" ] || [ -z "$PRODUCT_NAME" ] || [ -z "$MCC_NAME" ]; then
344
548
  echo "❌ 缺少必填参数"
345
- echo "用法: bash auth.sh init --scope <scope> --sales-code <code> --mcc-code <code> [--product-name <name> --mcc-name <name>]"
549
+ echo "用法: bash auth.sh init --defer-open --scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>"
346
550
  exit 1
347
551
  fi
552
+ if [ "$DEFER_OPEN" != true ]; then
553
+ echo "❌ 参数格式错误:auth init 必须使用 --defer-open,确保先展示授权说明和链接,再自动打开页面"
554
+ echo "用法: bash auth.sh init --defer-open --scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>"
555
+ exit 1
556
+ fi
557
+ validate_auth_context || return 1
558
+ emit_stage_metric_async "onboarding.auth.start" "$PRODUCT_NAME"
559
+ # 新 init 表示按已确认上下文重建授权链路;只在机器校验通过后替换旧短时状态。
560
+ reset_auth_state_for_init || return 1
348
561
 
349
562
  # Step 1: 检查登录状态
350
563
  CHECK_RESULT=$(run_json_command "alipay-cli whoami --json" alipay-cli whoami --json) || {
351
- echo "AUTH_FLOW:FAILED"
564
+ auth_cli_environment_failure "登录状态检查"
352
565
  return 1
353
566
  }
354
567
  if ! handle_error "$CHECK_RESULT"; then
355
- echo "AUTH_FLOW:FAILED"
568
+ emit_control "AUTH_FLOW:FAILED"
356
569
  return 1
357
570
  fi
358
571
  LOGGED_IN=$(echo "$CHECK_RESULT" | jq -r '.data.logged_in // false')
359
572
  IS_EXPIRED=$(echo "$CHECK_RESULT" | jq -r '.data.is_expired // false')
360
573
 
361
574
  if [ "$LOGGED_IN" = "true" ] && [ "$IS_EXPIRED" = "false" ]; then
575
+ if [ -n "$AUTH_INIT_CONTEXT_MISMATCH_TYPE" ]; then
576
+ # 后端查询没有权威 MCC 字段,旧状态变化时不能把现有登录误判为当前上下文已授权。
577
+ DEVICE_CODE=""
578
+ BROWSER_URL=""
579
+ save_auth_state || return 1
580
+ emit_control "AUTH_FLOW:${AUTH_INIT_CONTEXT_MISMATCH_TYPE}"
581
+ return 1
582
+ fi
362
583
  validate_current_authorization "$CHECK_RESULT" || return 1
363
584
  DEVICE_CODE=""
364
- save_auth_state
365
- echo "✅ 当前登录及授权范围有效,可继续后续流程"
366
- echo "AUTH_FLOW:SKIP"
585
+ save_auth_state || return 1
586
+ emit_control "✅ 当前登录及授权范围有效,可继续后续流程"
587
+ emit_control "AUTH_FLOW:SKIP"
367
588
  return 0
368
589
  fi
369
590
 
370
591
  # Step 2: 执行登录
371
592
  LOGIN_RESULT=$(run_json_command "alipay-cli login --non-interactive --scope" alipay-cli login --non-interactive --scope "$SCOPE" --json) || {
372
- echo "AUTH_FLOW:FAILED"
593
+ auth_cli_environment_failure "授权登录发起"
373
594
  return 1
374
595
  }
375
596
  if ! handle_error "$LOGIN_RESULT"; then
376
- echo "AUTH_FLOW:FAILED"
597
+ emit_control "AUTH_FLOW:FAILED"
377
598
  return 1
378
599
  fi
379
600
  LOGIN_STATUS=$(echo "$LOGIN_RESULT" | jq -r '.data.status // ""')
@@ -381,9 +602,9 @@ auth_init() {
381
602
  if [ "$LOGIN_STATUS" = "already_logged_in" ]; then
382
603
  validate_current_authorization || return 1
383
604
  DEVICE_CODE=""
384
- save_auth_state
385
- echo "✅ 当前登录及授权范围有效,可继续后续流程"
386
- echo "AUTH_FLOW:SKIP"
605
+ save_auth_state || return 1
606
+ emit_control "✅ 当前登录及授权范围有效,可继续后续流程"
607
+ emit_control "AUTH_FLOW:SKIP"
387
608
  return 0
388
609
  fi
389
610
 
@@ -396,30 +617,21 @@ auth_init() {
396
617
  # Step 4: 参数校验
397
618
  if [ -z "$DEVICE_CODE" ] || [ -z "$SALES_CODE" ] || [ -z "$MCC_CODE" ]; then
398
619
  echo "❌ 授权链接参数不完整,无法生成授权链接"
399
- echo "📋 deviceCode: ${DEVICE_CODE:-空}"
400
- echo "📋 productCode: ${SALES_CODE:-空}"
401
- echo "📋 mccCode: ${MCC_CODE:-空}"
620
+ [ -n "$DEVICE_CODE" ] || echo "📋 缺少字段: deviceCode"
621
+ [ -n "$SALES_CODE" ] || echo "📋 缺少字段: productCode"
622
+ [ -n "$MCC_CODE" ] || echo "📋 缺少字段: mccCode"
402
623
  exit 1
403
624
  fi
404
625
 
405
- # Step 5: 构建并校验授权链接(禁止使用 CLI 返回的 verification_url)
406
- BROWSER_URL="https://aipay.alipay.com/cli-auth?deviceCode=${DEVICE_CODE}&productCode=${SALES_CODE}&mccCode=${MCC_CODE}"
407
- if [ -n "${DEV_TOOL_NAME:-}" ] && [ "$DEV_TOOL_NAME" != "unknown" ]; then
408
- AUTH_PLATFORM=$(jq -nr --arg value "$DEV_TOOL_NAME" '$value|@uri')
409
- BROWSER_URL="${BROWSER_URL}&platform=${AUTH_PLATFORM}"
410
- fi
411
- if [[ "$BROWSER_URL" != https://aipay.alipay.com/cli-auth\?* ]] ||
412
- [[ "$BROWSER_URL" != *"deviceCode=${DEVICE_CODE}"* ]] ||
413
- [[ "$BROWSER_URL" != *"productCode=${SALES_CODE}"* ]] ||
414
- [[ "$BROWSER_URL" != *"mccCode=${MCC_CODE}"* ]] ||
415
- [[ "$BROWSER_URL" == *"opengw.alipay.com/oauth/device"* ]]; then
626
+ # Step 5: 由统一校验器编码、构建并逐字段校验授权链接。
627
+ BROWSER_URL=$(build_authorization_url) || {
416
628
  echo "❌ 授权链接构建失败,缺少必要参数或误用了 CLI verification_url,已停止。"
417
629
  echo "📋 正确格式: https://aipay.alipay.com/cli-auth?deviceCode=...&productCode=...&mccCode=..."
418
- exit 1
419
- fi
630
+ return 1
631
+ }
420
632
 
421
633
  # Step 6: 保存参数到状态文件(供 confirm/mismatch 使用)
422
- save_auth_state
634
+ save_auth_state || return 1
423
635
 
424
636
  # Step 7: 有效期转换(对非数字做防护)
425
637
  if [[ "$EXPIRES_IN" =~ ^[0-9]+$ ]] && [ "$EXPIRES_IN" -ge 60 ]; then
@@ -431,35 +643,56 @@ auth_init() {
431
643
  EXPIRES_DISPLAY="10 分钟"
432
644
  fi
433
645
 
434
- # Step 8: 输出授权信息
435
- echo ""
436
- echo "🔐 支付宝授权登录"
437
- echo ""
438
- echo "📋 授权信息"
439
- echo ""
440
- echo "| 项目 | 信息 |"
441
- echo "|------|------|"
442
- echo "| 产品类型 | ${PRODUCT_NAME:-按量付费} |"
443
- echo "| 经营类目 | ${MCC_NAME:-${MCC_CODE}} (${MCC_CODE}) |"
444
- if [ -n "$VERIFICATION_CODE" ]; then
445
- echo "| 确认码 | ${VERIFICATION_CODE} |"
446
- else
447
- echo "| 确认码 | (请查看支付宝授权页面) |"
646
+ # Step 8: 由统一目录渲染授权信息;独立 open 动作在消息发出后打开。
647
+ DISPLAY_CODE="${VERIFICATION_CODE:-(请查看支付宝授权页面)}"
648
+ MESSAGE_INPUT=$(printf '%s\n' "$PRODUCT_NAME" "$DEVICE_CODE" "$MCC_CODE" "$MCC_NAME" "$DISPLAY_CODE" "$EXPIRES_DISPLAY" "$BROWSER_URL" | jq -Rs '
649
+ 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]}
650
+ ')
651
+ if ! printf '%s' "$MESSAGE_INPUT" | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.page --variant READY; then
652
+ cleanup_auth_state
653
+ emit_control "AUTH_FLOW:FAILED"
654
+ exit 1
448
655
  fi
449
- echo "| 授权链接有效期 | ${EXPIRES_DISPLAY} |"
450
- echo ""
451
- echo "⚠️ 安全提示:请核对授权页面显示的确认码是否与上方一致,如不一致,请勿授权,立即停止操作!"
452
656
  echo ""
453
- echo "提示:无法跳链时,请复制下方链接到网页浏览器打开。"
454
- echo "🌐 授权链接:[点击跳转进行授权](${BROWSER_URL})"
455
- echo ""
456
- echo "请在完成授权后告诉我\"好了\"继续后续流程。"
457
- echo ""
458
- echo "AUTH_FLOW:READY"
657
+ emit_control "AUTH_FLOW:READY_TO_OPEN"
658
+ }
659
+
660
+ auth_open() {
661
+ local saved_device saved_url validate_input open_status open_helper
662
+
663
+ 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")
666
+ if [ -z "$saved_device" ] || [ -z "$saved_url" ]; then
667
+ echo "❌ 缺少待打开的授权页面上下文,请重新执行 auth.sh init --defer-open"
668
+ emit_control "AUTH_FLOW:FAILED"
669
+ return 1
670
+ fi
671
+
672
+ validate_input=$(jq -n \
673
+ --arg productName "$PRODUCT_NAME" \
674
+ --arg salesCode "$SALES_CODE" \
675
+ --arg scope "$SCOPE" \
676
+ --arg mccCode "$MCC_CODE" \
677
+ --arg mccName "$MCC_NAME" \
678
+ --arg deviceCode "$saved_device" \
679
+ --arg officialUrl "$saved_url" \
680
+ '{productName:$productName,salesCode:$salesCode,scope:$scope,mccCode:$mccCode,mccName:$mccName,deviceCode:$deviceCode,officialUrl:$officialUrl}') || return 1
681
+ if ! printf '%s' "$validate_input" | node "$RENDERER" --validate-authorization-context >/dev/null; then
682
+ echo "❌ 授权页面上下文校验失败,已停止打开"
683
+ emit_control "AUTH_FLOW:FAILED"
684
+ return 1
685
+ fi
686
+
687
+ open_helper="${SCRIPT_DIR}/../../../normal/scripts/open_official_url.sh"
688
+ open_status=$(printf '%s\n' "$saved_url" | bash "$open_helper" authorization)
689
+ case "$open_status" in OPENED|OPEN_FAILED|GUI_UNAVAILABLE|LINK_ONLY) ;; *) open_status="OPEN_FAILED" ;; esac
690
+ emit_control "AUTH_OPEN:${open_status}"
459
691
  }
460
692
 
461
- # ─── auth confirm: 授权确认 + scope 校验 + MCC 校验 ──────────────────────────
693
+ # ─── auth confirm: 授权确认 + scope 校验 + MCC 授权有效性校验 ────────────────
462
694
  auth_confirm() {
695
+ AUTH_CONFIRM_RESULT=""
463
696
  # 优先使用显式传入的非敏感上下文;未传时再从状态文件兜底恢复。
464
697
  restore_auth_context_or_state "$@" || exit 1
465
698
 
@@ -474,51 +707,126 @@ auth_confirm() {
474
707
 
475
708
  # Step 1: 执行授权确认
476
709
  LOGIN_COMPLETE_RESULT=$(run_json_command "alipay-cli login --complete --json" alipay-cli login --complete --json) || {
477
- echo "AUTH_FLOW:FAILED"
710
+ auth_cli_environment_failure "授权完成确认"
478
711
  return 1
479
712
  }
480
713
 
481
714
  # Step 2: 判断授权结果
482
- # alipay-cli login --complete 返回:外层 .success + .data.status
715
+ # alipay-cli login --complete: success=true/logged_in 表示可进入 whoami 门禁;
716
+ # authorization_pending/auth_expired 可能出现在 .error.code、.data.error.code 或 status 中。
483
717
  OUTER_SUCCESS=$(echo "$LOGIN_COMPLETE_RESULT" | jq -r '.success // false')
484
718
  DATA_STATUS=$(echo "$LOGIN_COMPLETE_RESULT" | jq -r '.data.status // ""')
485
- ERROR_CODE=$(echo "$LOGIN_COMPLETE_RESULT" | jq -r '.data.error.code // ""')
486
-
487
- if [ "$OUTER_SUCCESS" = "true" ] || [ "$DATA_STATUS" = "already_logged_in" ] || [ "$DATA_STATUS" = "completed" ]; then
488
- echo "✅ 授权确认成功"
489
- elif [ "$ERROR_CODE" = "authorization_pending" ]; then
490
- echo "⏳ 尚未完成授权,请在手机上确认授权后再次告诉我"
491
- echo "AUTH_FLOW:PENDING"
719
+ ERROR_CODE=$(echo "$LOGIN_COMPLETE_RESULT" | jq -r '.error.code // .data.error.code // ""')
720
+
721
+ if [ "$ERROR_CODE" = "authorization_pending" ] || [ "$DATA_STATUS" = "authorization_pending" ] || [ "$DATA_STATUS" = "pending" ]; then
722
+ AUTH_CONFIRM_RESULT="PENDING"
723
+ if [ "${AUTH_POLLING:-false}" != "true" ]; then
724
+ if ! printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.pending --variant DEFAULT; then
725
+ emit_control "AUTH_FLOW:FAILED"
726
+ return 1
727
+ fi
728
+ emit_control "AUTH_FLOW:PENDING"
729
+ fi
492
730
  return 0
493
- elif [ "$ERROR_CODE" = "auth_expired" ]; then
494
- echo "⏰ 授权链接已过期(有效期为10分钟),需要重新生成"
731
+ elif [ "$ERROR_CODE" = "auth_expired" ] || [ "$DATA_STATUS" = "auth_expired" ] || [ "$DATA_STATUS" = "expired" ]; then
732
+ AUTH_CONFIRM_RESULT="EXPIRED"
495
733
  cleanup_auth_state
496
- echo "AUTH_FLOW:EXPIRED"
734
+ if ! printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.expired --variant DEFAULT; then
735
+ emit_control "AUTH_FLOW:FAILED"
736
+ return 1
737
+ fi
738
+ emit_control "AUTH_FLOW:EXPIRED"
497
739
  return 0
740
+ elif [ "$OUTER_SUCCESS" = "true" ] || [ "$DATA_STATUS" = "logged_in" ] || [ "$DATA_STATUS" = "already_logged_in" ] || [ "$DATA_STATUS" = "completed" ]; then
741
+ :
498
742
  else
499
743
  if handle_error "$LOGIN_COMPLETE_RESULT"; then
500
744
  echo "❌ 授权确认失败:返回结果未包含可识别的完成状态"
501
745
  fi
502
746
  cleanup_auth_state
503
- echo "AUTH_FLOW:FAILED"
747
+ emit_control "AUTH_FLOW:FAILED"
504
748
  return 1
505
749
  fi
506
750
 
507
- # Step 3: 复用 init 的 scope + MCC 校验
508
- WHOAMI_RESULT=$(run_json_command "alipay-cli whoami --json" alipay-cli whoami --json) || {
509
- echo "AUTH_FLOW:FAILED"
751
+ # Step 3: 复用 init 的 scope + MCC 授权有效性校验
752
+ wait_for_effective_whoami "授权后登录状态复核"
753
+ whoami_rc=$?
754
+ if [ "$whoami_rc" -eq 2 ]; then
755
+ AUTH_CONFIRM_RESULT="PENDING"
756
+ if [ "${AUTH_POLLING:-false}" != "true" ]; then
757
+ if ! printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.pending --variant DEFAULT; then
758
+ emit_control "AUTH_FLOW:FAILED"
759
+ return 1
760
+ fi
761
+ emit_control "AUTH_FLOW:PENDING"
762
+ fi
763
+ return 0
764
+ elif [ "$whoami_rc" -ne 0 ]; then
510
765
  return 1
511
- }
512
- validate_current_authorization "$WHOAMI_RESULT" || return 1
766
+ fi
767
+ validate_current_authorization "$AUTH_WHOAMI_RESULT" || return 1
513
768
  cleanup_auth_state
514
- echo "AUTH_FLOW:AUTH_SUCCESS"
769
+ AUTH_CONFIRM_RESULT="AUTH_SUCCESS"
770
+ emit_control "✅ 授权确认成功(本机 CLI 登录态、scope 与 MCC 授权有效性均已通过)"
771
+ emit_control "AUTH_FLOW:AUTH_SUCCESS"
515
772
  return 0
516
773
  }
517
774
 
775
+ # ─── auth wait: 授权页打开后有限轮询确认状态 ────────────────────────────────
776
+ auth_wait() {
777
+ local attempt=1
778
+ local max_attempts=12
779
+ local interval_seconds=5
780
+
781
+ if [ "${ALIPAY_AIPAY_TEST_MODE:-0}" = "1" ] && [[ "${AUTH_CONFIRM_POLL_INTERVAL_SECONDS:-}" =~ ^[0-9]+$ ]]; then
782
+ interval_seconds="$AUTH_CONFIRM_POLL_INTERVAL_SECONDS"
783
+ fi
784
+
785
+ while [ "$attempt" -le "$max_attempts" ]; do
786
+ AUTH_POLLING=true
787
+ if ! auth_confirm "$@"; then
788
+ AUTH_POLLING=false
789
+ return 1
790
+ fi
791
+ AUTH_POLLING=false
792
+
793
+ case "$AUTH_CONFIRM_RESULT" in
794
+ AUTH_SUCCESS|EXPIRED)
795
+ return 0
796
+ ;;
797
+ PENDING)
798
+ emit_control "AUTH_POLL_PENDING=${attempt}/${max_attempts}"
799
+ if [ "$attempt" -eq "$max_attempts" ]; then
800
+ if ! printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.pending --variant DEFAULT; then
801
+ emit_control "AUTH_FLOW:FAILED"
802
+ return 1
803
+ fi
804
+ emit_control "AUTH_FLOW:PENDING"
805
+ return 0
806
+ fi
807
+ sleep "$interval_seconds"
808
+ ;;
809
+ *)
810
+ emit_control "AUTH_FLOW:FAILED"
811
+ return 1
812
+ ;;
813
+ esac
814
+ attempt=$((attempt + 1))
815
+ done
816
+ }
817
+
518
818
  # ─── auth mismatch: 授权范围不满足 → logout → 重新授权 ──────────────────────
519
819
  auth_mismatch() {
520
820
  local logout_result logout_error_type
521
821
 
822
+ if [ "${1:-}" = "--defer-open" ]; then
823
+ shift
824
+ else
825
+ echo "❌ 参数格式错误:auth mismatch 必须使用 --defer-open,确保重新授权时先展示说明和链接,再自动打开页面"
826
+ echo "用法: bash auth.sh mismatch --defer-open [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]"
827
+ return 1
828
+ fi
829
+
522
830
  # 优先使用显式传入的非敏感上下文;未传时再从状态文件兜底恢复。
523
831
  restore_auth_context_or_state "$@" || exit 1
524
832
 
@@ -535,26 +843,25 @@ auth_mismatch() {
535
843
  fi
536
844
 
537
845
  # Step 2: mismatch 是授权不匹配恢复路径中唯一执行 logout 的入口。
538
- echo "⚠️ 授权范围不满足,正在退出当前登录并准备重新授权..."
846
+ if ! printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.mismatch --variant DEFAULT; then
847
+ emit_control "AUTH_FLOW:FAILED"
848
+ return 1
849
+ fi
539
850
  logout_result=$(run_json_command "alipay-cli logout --json" alipay-cli logout --json) || {
540
- echo "❌ 退出当前登录失败,已停止重新授权"
541
- echo "AUTH_FLOW:FAILED"
851
+ auth_cli_environment_failure "退出当前登录"
542
852
  return 1
543
853
  }
544
854
  logout_error_type=$(detect_error "$logout_result")
545
855
  if [ "$logout_error_type" != "SUCCESS" ]; then
546
856
  echo "❌ 退出当前登录失败,已停止重新授权"
547
- echo "AUTH_FLOW:FAILED"
857
+ emit_control "AUTH_FLOW:FAILED"
548
858
  return 1
549
859
  fi
550
- echo "✅ 已退出当前登录"
551
-
552
- # Step 3: 调用 auth init 执行登录 + 构建授权链接 + 输出授权信息
553
- # DEV_TOOL_NAME 已 export,子进程可继承
554
- echo "📋 请重新扫码授权,新的授权将包含正确的权限范围"
555
- echo ""
556
860
 
861
+ # Step 3: 调用 auth init 执行登录 + 构建授权链接 + 输出唯一授权消息。
862
+ # DEV_TOOL_NAME 已 export,子进程可继承。
557
863
  auth_init \
864
+ --defer-open \
558
865
  --scope "$SCOPE" \
559
866
  --sales-code "$SALES_CODE" \
560
867
  --mcc-code "$MCC_CODE" \
@@ -572,16 +879,26 @@ case "${1:-}" in
572
879
  shift
573
880
  auth_confirm "$@"
574
881
  ;;
882
+ open)
883
+ shift
884
+ auth_open "$@"
885
+ ;;
886
+ wait)
887
+ shift
888
+ auth_wait "$@"
889
+ ;;
575
890
  mismatch)
576
891
  shift
577
892
  auth_mismatch "$@"
578
893
  ;;
579
894
  *)
580
- echo "用法: bash auth.sh <init|confirm|mismatch> [参数...]"
895
+ echo "用法: bash auth.sh <init|open|wait|confirm|mismatch> [参数...]"
581
896
  echo ""
582
- echo " auth init --scope <scope> --sales-code <code> --mcc-code <code> [--product-name <name> --mcc-name <name>]"
897
+ echo " auth init --defer-open --scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>"
898
+ echo " auth open [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]"
899
+ echo " auth wait [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]"
583
900
  echo " auth confirm [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]"
584
- echo " auth mismatch [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]"
901
+ echo " auth mismatch --defer-open [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]"
585
902
  exit 1
586
903
  ;;
587
904
  esac