@alipay/alipay-aipay 1.2.2 → 1.4.9

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 (98) 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 +6 -5
  25. package/skill/SKILL.md +15 -169
  26. package/skill/VERSION +1 -1
  27. package/skill/references/integration/flow.md +321 -292
  28. package/skill/references/integration/modules/alipay-sdk-reminder.md +5 -3
  29. package/skill/references/integration/modules/checklist.md +41 -27
  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 +53 -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 +37 -72
  56. package/skill/references/integration/modules/product-decision.md +10 -27
  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 +357 -147
  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 +80 -0
  65. package/skill/references/normal/json/customer-messages.json +937 -0
  66. package/skill/references/normal/json/policy-rules.json +179 -0
  67. package/skill/references/normal/rejection-guide.md +11 -6
  68. package/skill/references/normal/scripts/alipay_cli_env.mjs +29 -0
  69. package/skill/references/normal/scripts/alipay_cli_env.sh +21 -0
  70. package/skill/references/normal/scripts/common.sh +87 -0
  71. package/skill/references/normal/scripts/detect_dev_tool.sh +5 -1
  72. package/skill/references/normal/scripts/integration_message_runner.mjs +801 -0
  73. package/skill/references/normal/scripts/onboarding_message_runner.mjs +436 -0
  74. package/skill/references/normal/scripts/open_official_url.sh +82 -0
  75. package/skill/references/normal/scripts/platform_compat.mjs +39 -0
  76. package/skill/references/normal/scripts/project_route_inspector.sh +132 -0
  77. package/skill/references/normal/scripts/python_script_runner.mjs +56 -0
  78. package/skill/references/normal/scripts/render_customer_message.mjs +1263 -0
  79. package/skill/references/normal/scripts/self_update.mjs +160 -0
  80. package/skill/references/normal/scripts/telemetry.mjs +1278 -0
  81. package/skill/references/normal/self-update.md +18 -51
  82. package/skill/references/onboarding/flow.md +381 -283
  83. package/skill/references/onboarding/modules/app-release.md +132 -179
  84. package/skill/references/onboarding/modules/authorization.md +53 -64
  85. package/skill/references/onboarding/modules/error-handling.md +31 -34
  86. package/skill/references/onboarding/modules/product-sign.md +62 -66
  87. package/skill/references/onboarding/modules/scripts/app.sh +397 -158
  88. package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +38 -22
  89. package/skill/references/onboarding/modules/scripts/auth.sh +479 -167
  90. package/skill/references/onboarding/modules/scripts/download_key_tool.sh +157 -0
  91. package/skill/references/onboarding/modules/scripts/error_handler.sh +100 -130
  92. package/skill/references/onboarding/modules/scripts/network_retry.sh +96 -0
  93. package/skill/references/onboarding/modules/scripts/query_sign_status.sh +17 -70
  94. package/skill/references/onboarding/modules/scripts/service.sh +153 -89
  95. package/skill/references/onboarding/modules/scripts/sign_status_parser.sh +58 -0
  96. package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +16 -14
  97. package/skill/references/onboarding/modules/service-registration.md +67 -72
  98. package/skill/references/runtime-entry.md +62 -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,93 @@ 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
+ }
279
+
280
+ build_authorization_url() {
281
+ local input_json platform_value
282
+ platform_value="${DEV_TOOL_NAME:-}"
283
+ [ "$platform_value" = "unknown" ] && platform_value=""
284
+ input_json=$(jq -n \
285
+ --arg productName "$PRODUCT_NAME" \
286
+ --arg salesCode "$SALES_CODE" \
287
+ --arg scope "$SCOPE" \
288
+ --arg mccCode "$MCC_CODE" \
289
+ --arg mccName "$MCC_NAME" \
290
+ --arg deviceCode "$DEVICE_CODE" \
291
+ --arg platform "$platform_value" \
292
+ '{productName:$productName,salesCode:$salesCode,scope:$scope,mccCode:$mccCode,mccName:$mccName,deviceCode:$deviceCode,platform:$platform}') || return 1
293
+ printf '%s' "$input_json" | node "$RENDERER" --build-authorization-url
294
+ }
295
+
131
296
  extract_json_payload() {
132
297
  local raw="$1"
133
- local line_count start candidate
298
+ local analysis status
299
+ analysis=$(extract_unique_json "$raw") || return 1
300
+ status=$(printf '%s' "$analysis" | jq -r '.status // "none"' 2>/dev/null)
301
+ [ "$status" = "ok" ] || return 1
302
+ printf '%s' "$analysis" | jq -ce '.value | select(type == "object")' 2>/dev/null
303
+ }
134
304
 
135
- if printf '%s' "$raw" | jq -e . >/dev/null 2>&1; then
136
- printf '%s' "$raw"
137
- return 0
305
+ auth_cli_environment_failure() {
306
+ local action="$1"
307
+ echo "❌ ${action} 未能在当前 Agent 执行环境取得可确认结果,已停止。"
308
+ echo "📋 这通常表示当前执行环境缺少联网权限、CLI wrapper 混入输出,或 stdout/stderr 无法唯一解析。"
309
+ echo "📋 这不代表支付宝业务失败,也不能据此判断用户本机 logout/login 失败。"
310
+ echo "📋 Agent 应申请可联网权限后重试同一条 auth.sh 命令;不要要求用户手动 logout/login 代替本轮事实。"
311
+ emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
312
+ }
313
+
314
+ wait_for_effective_whoami() {
315
+ local action="$1"
316
+ local max_attempts=3
317
+ local interval_seconds=2
318
+ local attempt=1
319
+ local logged_in is_expired
320
+
321
+ AUTH_WHOAMI_RESULT=""
322
+ if [ "${ALIPAY_AIPAY_TEST_MODE:-0}" = "1" ] && [[ "${AUTH_WHOAMI_RECHECK_INTERVAL_SECONDS:-}" =~ ^[0-9]+$ ]]; then
323
+ interval_seconds="$AUTH_WHOAMI_RECHECK_INTERVAL_SECONDS"
138
324
  fi
139
325
 
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"
326
+ while [ "$attempt" -le "$max_attempts" ]; do
327
+ AUTH_WHOAMI_RESULT=$(run_json_command "alipay-cli whoami --json" alipay-cli whoami --json) || {
328
+ auth_cli_environment_failure "$action"
329
+ return 1
330
+ }
331
+ if ! handle_error "$AUTH_WHOAMI_RESULT"; then
332
+ emit_control "AUTH_FLOW:FAILED"
333
+ return 1
334
+ fi
335
+
336
+ logged_in=$(echo "$AUTH_WHOAMI_RESULT" | jq -r '.data.logged_in // false')
337
+ is_expired=$(echo "$AUTH_WHOAMI_RESULT" | jq -r '.data.is_expired // false')
338
+ if [ "$logged_in" = "true" ] && [ "$is_expired" != "true" ]; then
146
339
  return 0
147
340
  fi
148
- start=$((start + 1))
341
+
342
+ emit_control "AUTH_WHOAMI_RECHECK=${attempt}/${max_attempts}"
343
+ if [ "$attempt" -lt "$max_attempts" ]; then
344
+ sleep "$interval_seconds"
345
+ fi
346
+ attempt=$((attempt + 1))
149
347
  done
150
348
 
151
- return 1
349
+ emit_control "AUTH_WHOAMI_NOT_EFFECTIVE"
350
+ return 2
152
351
  }
153
352
 
154
353
  run_json_command() {
@@ -157,7 +356,7 @@ run_json_command() {
157
356
  local stderr_file stdout stderr_output combined_output exit_code json_payload
158
357
 
159
358
  stderr_file="$(mktemp "${TMPDIR:-/tmp}/alipay_auth_stderr.XXXXXX")" || return 1
160
- stdout=$(PLATFORM="${DEV_TOOL_NAME}" "$@" 2>"$stderr_file")
359
+ stdout=$(PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}" "$@" 2>"$stderr_file")
161
360
  exit_code=$?
162
361
  stderr_output=$(cat "$stderr_file")
163
362
 
@@ -180,16 +379,7 @@ run_json_command() {
180
379
  return 0
181
380
  fi
182
381
 
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
382
+ echo "❌ ${label} 未返回可唯一解析的合法 JSON(退出码: ${exit_code});原始输出已隐藏,请申请联网权限后重试同一条 auth.sh 命令" >&2
193
383
  rm -f "$stderr_file"
194
384
  return 1
195
385
  }
@@ -199,26 +389,27 @@ run_json_command() {
199
389
  validate_current_authorization() {
200
390
  local whoami_result="${1:-}"
201
391
  local logged_in is_expired granted_scope required_scope
202
- local scope_match mcc_verify_result mcc_verify_unwrapped mcc_match
392
+ local scope_match mcc_verify_result mcc_verify_unwrapped mcc_match mcc_raw retry_rc
203
393
  local mcc_error_type mcc_error_code mcc_success
204
- local granted_scope_item
205
- local -a granted_scope_items
394
+ local mismatch_type sign_status_output
395
+ local granted_scope_item required_scope_item required_scope_found
396
+ local -a granted_scope_items required_scope_items
206
397
 
207
398
  if [ -z "$SALES_CODE" ] || [ -z "$MCC_CODE" ]; then
208
399
  echo "❌ 缺少授权校验参数"
209
- echo "AUTH_FLOW:FAILED"
400
+ emit_control "AUTH_FLOW:FAILED"
210
401
  return 1
211
402
  fi
212
403
 
213
404
  if [ -z "$whoami_result" ]; then
214
405
  whoami_result=$(run_json_command "alipay-cli whoami --json" alipay-cli whoami --json) || {
215
- echo "AUTH_FLOW:FAILED"
406
+ auth_cli_environment_failure "登录状态复核"
216
407
  return 1
217
408
  }
218
409
  fi
219
410
 
220
411
  if ! handle_error "$whoami_result"; then
221
- echo "AUTH_FLOW:FAILED"
412
+ emit_control "AUTH_FLOW:FAILED"
222
413
  return 1
223
414
  fi
224
415
 
@@ -226,40 +417,52 @@ validate_current_authorization() {
226
417
  is_expired=$(echo "$whoami_result" | jq -r '.data.is_expired // false')
227
418
  if [ "$logged_in" != "true" ] || [ "$is_expired" = "true" ]; then
228
419
  echo "❌ 当前登录状态无效或已过期"
229
- echo "AUTH_FLOW:FAILED"
420
+ emit_control "AUTH_FLOW:FAILED"
230
421
  return 1
231
422
  fi
232
423
 
233
424
  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
425
+ required_scope=$(get_scope "$SALES_CODE")
426
+ if [ -z "$required_scope" ] || [ "$SCOPE" != "$required_scope" ]; then
427
+ echo "❌ 当前产品的固定 scope 上下文无效"
428
+ emit_control "AUTH_FLOW:FAILED"
429
+ return 1
430
+ fi
244
431
 
245
- scope_match="false"
432
+ scope_match="true"
246
433
  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"
434
+ IFS=',' read -r -a required_scope_items <<< "$required_scope"
435
+ for required_scope_item in "${required_scope_items[@]}"; do
436
+ required_scope_found="false"
437
+ for granted_scope_item in "${granted_scope_items[@]}"; do
438
+ granted_scope_item="${granted_scope_item#"${granted_scope_item%%[![:space:]]*}"}"
439
+ granted_scope_item="${granted_scope_item%"${granted_scope_item##*[![:space:]]}"}"
440
+ if [ "$granted_scope_item" = "$required_scope_item" ]; then
441
+ required_scope_found="true"
442
+ break
443
+ fi
444
+ done
445
+ if [ "$required_scope_found" != "true" ]; then
446
+ scope_match="false"
252
447
  break
253
448
  fi
254
449
  done
255
450
 
256
451
  mcc_match="true"
257
452
  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 \
453
+ export PLATFORM="${DEV_TOOL_NAME}" PLATFORM_ID="${PLATFORM_ID:-}"
454
+ run_network_retry mcc_raw read authorization_mcc_check -- alipay-cli mcp call ar-query.queryArInfosBySalesProd \
260
455
  -d "{\"request\":{\"salesProductCodes\":[\"${SALES_CODE}\"]},\"ctx\":{}}" \
261
- --json) || {
262
- echo "AUTH_FLOW:FAILED"
456
+ --json
457
+ retry_rc=$?
458
+ if [ "$retry_rc" -ne 0 ]; then
459
+ echo "❌ 授权 MCC 校验因网络或服务异常在自动重试两次后仍未恢复"
460
+ emit_control "AUTH_FLOW:FAILED"
461
+ return 1
462
+ fi
463
+ mcc_verify_result=$(extract_json_payload "$mcc_raw") || {
464
+ echo "❌ alipay-cli mcp call ar-query.queryArInfosBySalesProd 未返回合法 JSON"
465
+ emit_control "AUTH_FLOW:FAILED"
263
466
  return 1
264
467
  }
265
468
 
@@ -269,7 +472,7 @@ validate_current_authorization() {
269
472
  mcc_match="false"
270
473
  elif [ "$mcc_error_type" != "SUCCESS" ]; then
271
474
  echo "❌ MCC 校验失败"
272
- echo "AUTH_FLOW:FAILED"
475
+ emit_control "AUTH_FLOW:FAILED"
273
476
  return 1
274
477
  fi
275
478
 
@@ -282,37 +485,32 @@ validate_current_authorization() {
282
485
  elif echo "$mcc_verify_unwrapped" | grep -qi "mccCode.*is not auth"; then
283
486
  mcc_match="false"
284
487
  fi
488
+
489
+ # 同一请求也是 Step 3.1 的签约查询;仅在完整结构可由共享解析器证明时输出可复用事实。
490
+ if [ "$mcc_match" = "true" ] && sign_status_output=$(emit_sign_status_from_business "$mcc_verify_unwrapped" "$SALES_CODE" 2>/dev/null); then
491
+ printf '%s\n' "$sign_status_output" \
492
+ | while IFS= read -r marker; do emit_control "$marker"; done
493
+ emit_control "AUTH_SIGN_QUERY_REUSABLE=true"
494
+ fi
285
495
  fi
286
496
 
287
497
  if [ "$scope_match" = "true" ] && [ "$mcc_match" = "true" ]; then
288
- echo "✅ 授权范围校验通过(scope + MCC)"
498
+ emit_control "✅ 授权范围校验通过(scope + MCC 授权有效性)"
289
499
  return 0
290
500
  fi
291
501
 
292
- echo ""
293
- echo "⚠️ 授权范围不满足"
294
502
  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"
503
+ mismatch_type="SCOPE_MISMATCH"
308
504
  else
309
- echo "AUTH_FLOW:MCC_MISMATCH"
505
+ mismatch_type="MCC_MISMATCH"
310
506
  fi
507
+ emit_control "AUTH_FLOW:${mismatch_type}"
311
508
  return 1
312
509
  }
313
510
 
314
511
  # ─── auth init: 检查登录状态 + 执行登录 + 构建授权链接 + 输出授权信息 ────────
315
512
  auth_init() {
513
+ local DEFER_OPEN=false
316
514
  SCOPE=""
317
515
  SALES_CODE=""
318
516
  MCC_CODE=""
@@ -321,6 +519,8 @@ auth_init() {
321
519
 
322
520
  while [[ $# -gt 0 ]]; do
323
521
  case $1 in
522
+ --defer-open)
523
+ DEFER_OPEN=true; shift ;;
324
524
  --scope)
325
525
  [ $# -ge 2 ] || { echo "❌ 缺少参数值: $1"; exit 1; }
326
526
  SCOPE="$2"; shift 2 ;;
@@ -340,40 +540,56 @@ auth_init() {
340
540
  esac
341
541
  done
342
542
 
343
- if [ -z "$SCOPE" ] || [ -z "$SALES_CODE" ] || [ -z "$MCC_CODE" ]; then
543
+ if [ -z "$SCOPE" ] || [ -z "$SALES_CODE" ] || [ -z "$MCC_CODE" ] || [ -z "$PRODUCT_NAME" ] || [ -z "$MCC_NAME" ]; then
344
544
  echo "❌ 缺少必填参数"
345
- echo "用法: bash auth.sh init --scope <scope> --sales-code <code> --mcc-code <code> [--product-name <name> --mcc-name <name>]"
545
+ echo "用法: bash auth.sh init --defer-open --scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>"
546
+ exit 1
547
+ fi
548
+ if [ "$DEFER_OPEN" != true ]; then
549
+ echo "❌ 参数格式错误:auth init 必须使用 --defer-open,确保先展示授权说明和链接,再自动打开页面"
550
+ echo "用法: bash auth.sh init --defer-open --scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>"
346
551
  exit 1
347
552
  fi
553
+ validate_auth_context || return 1
554
+ # 新 init 表示按已确认上下文重建授权链路;只在机器校验通过后替换旧短时状态。
555
+ reset_auth_state_for_init || return 1
348
556
 
349
557
  # Step 1: 检查登录状态
350
558
  CHECK_RESULT=$(run_json_command "alipay-cli whoami --json" alipay-cli whoami --json) || {
351
- echo "AUTH_FLOW:FAILED"
559
+ auth_cli_environment_failure "登录状态检查"
352
560
  return 1
353
561
  }
354
562
  if ! handle_error "$CHECK_RESULT"; then
355
- echo "AUTH_FLOW:FAILED"
563
+ emit_control "AUTH_FLOW:FAILED"
356
564
  return 1
357
565
  fi
358
566
  LOGGED_IN=$(echo "$CHECK_RESULT" | jq -r '.data.logged_in // false')
359
567
  IS_EXPIRED=$(echo "$CHECK_RESULT" | jq -r '.data.is_expired // false')
360
568
 
361
569
  if [ "$LOGGED_IN" = "true" ] && [ "$IS_EXPIRED" = "false" ]; then
570
+ if [ -n "$AUTH_INIT_CONTEXT_MISMATCH_TYPE" ]; then
571
+ # 后端查询没有权威 MCC 字段,旧状态变化时不能把现有登录误判为当前上下文已授权。
572
+ DEVICE_CODE=""
573
+ BROWSER_URL=""
574
+ save_auth_state || return 1
575
+ emit_control "AUTH_FLOW:${AUTH_INIT_CONTEXT_MISMATCH_TYPE}"
576
+ return 1
577
+ fi
362
578
  validate_current_authorization "$CHECK_RESULT" || return 1
363
579
  DEVICE_CODE=""
364
- save_auth_state
365
- echo "✅ 当前登录及授权范围有效,可继续后续流程"
366
- echo "AUTH_FLOW:SKIP"
580
+ save_auth_state || return 1
581
+ emit_control "✅ 当前登录及授权范围有效,可继续后续流程"
582
+ emit_control "AUTH_FLOW:SKIP"
367
583
  return 0
368
584
  fi
369
585
 
370
586
  # Step 2: 执行登录
371
587
  LOGIN_RESULT=$(run_json_command "alipay-cli login --non-interactive --scope" alipay-cli login --non-interactive --scope "$SCOPE" --json) || {
372
- echo "AUTH_FLOW:FAILED"
588
+ auth_cli_environment_failure "授权登录发起"
373
589
  return 1
374
590
  }
375
591
  if ! handle_error "$LOGIN_RESULT"; then
376
- echo "AUTH_FLOW:FAILED"
592
+ emit_control "AUTH_FLOW:FAILED"
377
593
  return 1
378
594
  fi
379
595
  LOGIN_STATUS=$(echo "$LOGIN_RESULT" | jq -r '.data.status // ""')
@@ -381,9 +597,9 @@ auth_init() {
381
597
  if [ "$LOGIN_STATUS" = "already_logged_in" ]; then
382
598
  validate_current_authorization || return 1
383
599
  DEVICE_CODE=""
384
- save_auth_state
385
- echo "✅ 当前登录及授权范围有效,可继续后续流程"
386
- echo "AUTH_FLOW:SKIP"
600
+ save_auth_state || return 1
601
+ emit_control "✅ 当前登录及授权范围有效,可继续后续流程"
602
+ emit_control "AUTH_FLOW:SKIP"
387
603
  return 0
388
604
  fi
389
605
 
@@ -396,30 +612,21 @@ auth_init() {
396
612
  # Step 4: 参数校验
397
613
  if [ -z "$DEVICE_CODE" ] || [ -z "$SALES_CODE" ] || [ -z "$MCC_CODE" ]; then
398
614
  echo "❌ 授权链接参数不完整,无法生成授权链接"
399
- echo "📋 deviceCode: ${DEVICE_CODE:-空}"
400
- echo "📋 productCode: ${SALES_CODE:-空}"
401
- echo "📋 mccCode: ${MCC_CODE:-空}"
615
+ [ -n "$DEVICE_CODE" ] || echo "📋 缺少字段: deviceCode"
616
+ [ -n "$SALES_CODE" ] || echo "📋 缺少字段: productCode"
617
+ [ -n "$MCC_CODE" ] || echo "📋 缺少字段: mccCode"
402
618
  exit 1
403
619
  fi
404
620
 
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
621
+ # Step 5: 由统一校验器编码、构建并逐字段校验授权链接。
622
+ BROWSER_URL=$(build_authorization_url) || {
416
623
  echo "❌ 授权链接构建失败,缺少必要参数或误用了 CLI verification_url,已停止。"
417
624
  echo "📋 正确格式: https://aipay.alipay.com/cli-auth?deviceCode=...&productCode=...&mccCode=..."
418
- exit 1
419
- fi
625
+ return 1
626
+ }
420
627
 
421
628
  # Step 6: 保存参数到状态文件(供 confirm/mismatch 使用)
422
- save_auth_state
629
+ save_auth_state || return 1
423
630
 
424
631
  # Step 7: 有效期转换(对非数字做防护)
425
632
  if [[ "$EXPIRES_IN" =~ ^[0-9]+$ ]] && [ "$EXPIRES_IN" -ge 60 ]; then
@@ -431,35 +638,56 @@ auth_init() {
431
638
  EXPIRES_DISPLAY="10 分钟"
432
639
  fi
433
640
 
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 "| 确认码 | (请查看支付宝授权页面) |"
641
+ # Step 8: 由统一目录渲染授权信息;独立 open 动作在消息发出后打开。
642
+ DISPLAY_CODE="${VERIFICATION_CODE:-(请查看支付宝授权页面)}"
643
+ MESSAGE_INPUT=$(printf '%s\n' "$PRODUCT_NAME" "$DEVICE_CODE" "$MCC_CODE" "$MCC_NAME" "$DISPLAY_CODE" "$EXPIRES_DISPLAY" "$BROWSER_URL" | jq -Rs '
644
+ 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]}
645
+ ')
646
+ if ! printf '%s' "$MESSAGE_INPUT" | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.page --variant READY; then
647
+ cleanup_auth_state
648
+ emit_control "AUTH_FLOW:FAILED"
649
+ exit 1
448
650
  fi
449
- echo "| 授权链接有效期 | ${EXPIRES_DISPLAY} |"
450
- echo ""
451
- echo "⚠️ 安全提示:请核对授权页面显示的确认码是否与上方一致,如不一致,请勿授权,立即停止操作!"
452
- echo ""
453
- echo "提示:无法跳链时,请复制下方链接到网页浏览器打开。"
454
- echo "🌐 授权链接:[点击跳转进行授权](${BROWSER_URL})"
455
651
  echo ""
456
- echo "请在完成授权后告诉我\"好了\"继续后续流程。"
457
- echo ""
458
- echo "AUTH_FLOW:READY"
652
+ emit_control "AUTH_FLOW:READY_TO_OPEN"
459
653
  }
460
654
 
461
- # ─── auth confirm: 授权确认 + scope 校验 + MCC 校验 ──────────────────────────
655
+ auth_open() {
656
+ local saved_device saved_url validate_input open_status open_helper
657
+
658
+ restore_auth_context_or_state "$@" || exit 1
659
+ saved_device=$(jq -r '.deviceCode // ""' "$AUTH_STATE_FILE")
660
+ saved_url=$(jq -r '.authorizationUrl // ""' "$AUTH_STATE_FILE")
661
+ if [ -z "$saved_device" ] || [ -z "$saved_url" ]; then
662
+ echo "❌ 缺少待打开的授权页面上下文,请重新执行 auth.sh init --defer-open"
663
+ emit_control "AUTH_FLOW:FAILED"
664
+ return 1
665
+ fi
666
+
667
+ validate_input=$(jq -n \
668
+ --arg productName "$PRODUCT_NAME" \
669
+ --arg salesCode "$SALES_CODE" \
670
+ --arg scope "$SCOPE" \
671
+ --arg mccCode "$MCC_CODE" \
672
+ --arg mccName "$MCC_NAME" \
673
+ --arg deviceCode "$saved_device" \
674
+ --arg officialUrl "$saved_url" \
675
+ '{productName:$productName,salesCode:$salesCode,scope:$scope,mccCode:$mccCode,mccName:$mccName,deviceCode:$deviceCode,officialUrl:$officialUrl}') || return 1
676
+ if ! printf '%s' "$validate_input" | node "$RENDERER" --validate-authorization-context >/dev/null; then
677
+ echo "❌ 授权页面上下文校验失败,已停止打开"
678
+ emit_control "AUTH_FLOW:FAILED"
679
+ return 1
680
+ fi
681
+
682
+ open_helper="${SCRIPT_DIR}/../../../normal/scripts/open_official_url.sh"
683
+ open_status=$(printf '%s\n' "$saved_url" | bash "$open_helper" authorization)
684
+ case "$open_status" in OPENED|OPEN_FAILED|GUI_UNAVAILABLE|LINK_ONLY) ;; *) open_status="OPEN_FAILED" ;; esac
685
+ emit_control "AUTH_OPEN:${open_status}"
686
+ }
687
+
688
+ # ─── auth confirm: 授权确认 + scope 校验 + MCC 授权有效性校验 ────────────────
462
689
  auth_confirm() {
690
+ AUTH_CONFIRM_RESULT=""
463
691
  # 优先使用显式传入的非敏感上下文;未传时再从状态文件兜底恢复。
464
692
  restore_auth_context_or_state "$@" || exit 1
465
693
 
@@ -474,51 +702,126 @@ auth_confirm() {
474
702
 
475
703
  # Step 1: 执行授权确认
476
704
  LOGIN_COMPLETE_RESULT=$(run_json_command "alipay-cli login --complete --json" alipay-cli login --complete --json) || {
477
- echo "AUTH_FLOW:FAILED"
705
+ auth_cli_environment_failure "授权完成确认"
478
706
  return 1
479
707
  }
480
708
 
481
709
  # Step 2: 判断授权结果
482
- # alipay-cli login --complete 返回:外层 .success + .data.status
710
+ # alipay-cli login --complete: success=true/logged_in 表示可进入 whoami 门禁;
711
+ # authorization_pending/auth_expired 可能出现在 .error.code、.data.error.code 或 status 中。
483
712
  OUTER_SUCCESS=$(echo "$LOGIN_COMPLETE_RESULT" | jq -r '.success // false')
484
713
  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"
714
+ ERROR_CODE=$(echo "$LOGIN_COMPLETE_RESULT" | jq -r '.error.code // .data.error.code // ""')
715
+
716
+ if [ "$ERROR_CODE" = "authorization_pending" ] || [ "$DATA_STATUS" = "authorization_pending" ] || [ "$DATA_STATUS" = "pending" ]; then
717
+ AUTH_CONFIRM_RESULT="PENDING"
718
+ if [ "${AUTH_POLLING:-false}" != "true" ]; then
719
+ if ! printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.pending --variant DEFAULT; then
720
+ emit_control "AUTH_FLOW:FAILED"
721
+ return 1
722
+ fi
723
+ emit_control "AUTH_FLOW:PENDING"
724
+ fi
492
725
  return 0
493
- elif [ "$ERROR_CODE" = "auth_expired" ]; then
494
- echo "⏰ 授权链接已过期(有效期为10分钟),需要重新生成"
726
+ elif [ "$ERROR_CODE" = "auth_expired" ] || [ "$DATA_STATUS" = "auth_expired" ] || [ "$DATA_STATUS" = "expired" ]; then
727
+ AUTH_CONFIRM_RESULT="EXPIRED"
495
728
  cleanup_auth_state
496
- echo "AUTH_FLOW:EXPIRED"
729
+ if ! printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.expired --variant DEFAULT; then
730
+ emit_control "AUTH_FLOW:FAILED"
731
+ return 1
732
+ fi
733
+ emit_control "AUTH_FLOW:EXPIRED"
497
734
  return 0
735
+ elif [ "$OUTER_SUCCESS" = "true" ] || [ "$DATA_STATUS" = "logged_in" ] || [ "$DATA_STATUS" = "already_logged_in" ] || [ "$DATA_STATUS" = "completed" ]; then
736
+ :
498
737
  else
499
738
  if handle_error "$LOGIN_COMPLETE_RESULT"; then
500
739
  echo "❌ 授权确认失败:返回结果未包含可识别的完成状态"
501
740
  fi
502
741
  cleanup_auth_state
503
- echo "AUTH_FLOW:FAILED"
742
+ emit_control "AUTH_FLOW:FAILED"
504
743
  return 1
505
744
  fi
506
745
 
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"
746
+ # Step 3: 复用 init 的 scope + MCC 授权有效性校验
747
+ wait_for_effective_whoami "授权后登录状态复核"
748
+ whoami_rc=$?
749
+ if [ "$whoami_rc" -eq 2 ]; then
750
+ AUTH_CONFIRM_RESULT="PENDING"
751
+ if [ "${AUTH_POLLING:-false}" != "true" ]; then
752
+ if ! printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.pending --variant DEFAULT; then
753
+ emit_control "AUTH_FLOW:FAILED"
754
+ return 1
755
+ fi
756
+ emit_control "AUTH_FLOW:PENDING"
757
+ fi
758
+ return 0
759
+ elif [ "$whoami_rc" -ne 0 ]; then
510
760
  return 1
511
- }
512
- validate_current_authorization "$WHOAMI_RESULT" || return 1
761
+ fi
762
+ validate_current_authorization "$AUTH_WHOAMI_RESULT" || return 1
513
763
  cleanup_auth_state
514
- echo "AUTH_FLOW:AUTH_SUCCESS"
764
+ AUTH_CONFIRM_RESULT="AUTH_SUCCESS"
765
+ emit_control "✅ 授权确认成功(本机 CLI 登录态、scope 与 MCC 授权有效性均已通过)"
766
+ emit_control "AUTH_FLOW:AUTH_SUCCESS"
515
767
  return 0
516
768
  }
517
769
 
770
+ # ─── auth wait: 授权页打开后有限轮询确认状态 ────────────────────────────────
771
+ auth_wait() {
772
+ local attempt=1
773
+ local max_attempts=12
774
+ local interval_seconds=5
775
+
776
+ if [ "${ALIPAY_AIPAY_TEST_MODE:-0}" = "1" ] && [[ "${AUTH_CONFIRM_POLL_INTERVAL_SECONDS:-}" =~ ^[0-9]+$ ]]; then
777
+ interval_seconds="$AUTH_CONFIRM_POLL_INTERVAL_SECONDS"
778
+ fi
779
+
780
+ while [ "$attempt" -le "$max_attempts" ]; do
781
+ AUTH_POLLING=true
782
+ if ! auth_confirm "$@"; then
783
+ AUTH_POLLING=false
784
+ return 1
785
+ fi
786
+ AUTH_POLLING=false
787
+
788
+ case "$AUTH_CONFIRM_RESULT" in
789
+ AUTH_SUCCESS|EXPIRED)
790
+ return 0
791
+ ;;
792
+ PENDING)
793
+ emit_control "AUTH_POLL_PENDING=${attempt}/${max_attempts}"
794
+ if [ "$attempt" -eq "$max_attempts" ]; then
795
+ if ! printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.pending --variant DEFAULT; then
796
+ emit_control "AUTH_FLOW:FAILED"
797
+ return 1
798
+ fi
799
+ emit_control "AUTH_FLOW:PENDING"
800
+ return 0
801
+ fi
802
+ sleep "$interval_seconds"
803
+ ;;
804
+ *)
805
+ emit_control "AUTH_FLOW:FAILED"
806
+ return 1
807
+ ;;
808
+ esac
809
+ attempt=$((attempt + 1))
810
+ done
811
+ }
812
+
518
813
  # ─── auth mismatch: 授权范围不满足 → logout → 重新授权 ──────────────────────
519
814
  auth_mismatch() {
520
815
  local logout_result logout_error_type
521
816
 
817
+ if [ "${1:-}" = "--defer-open" ]; then
818
+ shift
819
+ else
820
+ echo "❌ 参数格式错误:auth mismatch 必须使用 --defer-open,确保重新授权时先展示说明和链接,再自动打开页面"
821
+ echo "用法: bash auth.sh mismatch --defer-open [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]"
822
+ return 1
823
+ fi
824
+
522
825
  # 优先使用显式传入的非敏感上下文;未传时再从状态文件兜底恢复。
523
826
  restore_auth_context_or_state "$@" || exit 1
524
827
 
@@ -535,26 +838,25 @@ auth_mismatch() {
535
838
  fi
536
839
 
537
840
  # Step 2: mismatch 是授权不匹配恢复路径中唯一执行 logout 的入口。
538
- echo "⚠️ 授权范围不满足,正在退出当前登录并准备重新授权..."
841
+ if ! printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.mismatch --variant DEFAULT; then
842
+ emit_control "AUTH_FLOW:FAILED"
843
+ return 1
844
+ fi
539
845
  logout_result=$(run_json_command "alipay-cli logout --json" alipay-cli logout --json) || {
540
- echo "❌ 退出当前登录失败,已停止重新授权"
541
- echo "AUTH_FLOW:FAILED"
846
+ auth_cli_environment_failure "退出当前登录"
542
847
  return 1
543
848
  }
544
849
  logout_error_type=$(detect_error "$logout_result")
545
850
  if [ "$logout_error_type" != "SUCCESS" ]; then
546
851
  echo "❌ 退出当前登录失败,已停止重新授权"
547
- echo "AUTH_FLOW:FAILED"
852
+ emit_control "AUTH_FLOW:FAILED"
548
853
  return 1
549
854
  fi
550
- echo "✅ 已退出当前登录"
551
-
552
- # Step 3: 调用 auth init 执行登录 + 构建授权链接 + 输出授权信息
553
- # DEV_TOOL_NAME 已 export,子进程可继承
554
- echo "📋 请重新扫码授权,新的授权将包含正确的权限范围"
555
- echo ""
556
855
 
856
+ # Step 3: 调用 auth init 执行登录 + 构建授权链接 + 输出唯一授权消息。
857
+ # DEV_TOOL_NAME 已 export,子进程可继承。
557
858
  auth_init \
859
+ --defer-open \
558
860
  --scope "$SCOPE" \
559
861
  --sales-code "$SALES_CODE" \
560
862
  --mcc-code "$MCC_CODE" \
@@ -572,16 +874,26 @@ case "${1:-}" in
572
874
  shift
573
875
  auth_confirm "$@"
574
876
  ;;
877
+ open)
878
+ shift
879
+ auth_open "$@"
880
+ ;;
881
+ wait)
882
+ shift
883
+ auth_wait "$@"
884
+ ;;
575
885
  mismatch)
576
886
  shift
577
887
  auth_mismatch "$@"
578
888
  ;;
579
889
  *)
580
- echo "用法: bash auth.sh <init|confirm|mismatch> [参数...]"
890
+ echo "用法: bash auth.sh <init|open|wait|confirm|mismatch> [参数...]"
581
891
  echo ""
582
- echo " auth init --scope <scope> --sales-code <code> --mcc-code <code> [--product-name <name> --mcc-name <name>]"
892
+ echo " auth init --defer-open --scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>"
893
+ echo " auth open [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]"
894
+ echo " auth wait [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]"
583
895
  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>]"
896
+ echo " auth mismatch --defer-open [--scope <scope> --sales-code <code> --mcc-code <code> --product-name <name> --mcc-name <name>]"
585
897
  exit 1
586
898
  ;;
587
899
  esac