@alipay/alipay-aipay 1.5.3 → 1.5.5-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alipay/alipay-aipay",
3
- "version": "1.5.3",
3
+ "version": "1.5.5-beta",
4
4
  "description": "支付宝 AI 付 Skill 安装包 - 安装 Agent Skill,并检测/后台准备 alipay-cli;jq 可通过 --with-jq 尝试准备",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/VERSION CHANGED
@@ -1 +1 @@
1
- 1.5.3
1
+ 1.5.5-beta
@@ -43,6 +43,7 @@ fi
43
43
  - 对已知需要联网的命令,如果当前 Agent 环境网络受限且支持显式授权,应首次执行即申请可联网权限,不要先在受限沙箱内试跑。
44
44
  - 若命令已经因网络失败,重试时必须复用同一条命令并申请可联网权限;不要把该失败解释为用户网络故障。
45
45
  - `auth.sh init|confirm|mismatch` 返回 `AUTH_FLOW:RETRY_WITH_NETWORK` 时,表示当前 Agent 执行环境没有取得可确认 CLI 结果;必须申请联网权限后重试同一条完整脚本命令,不得解释为支付宝业务失败、logout 失败或 login 失败,也不得要求用户手动 logout/login 代替本轮脚本事实。
46
+ - `auth.sh init|wait|confirm|mismatch` 返回 `AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION` 时,仅表示授权状态命令缺少 CLI 私有状态目录权限。宿主支持时,允许当前同一条受控 runtime 命令访问 `$HOME/.alipay-cli/credentials` 和 `${XDG_DATA_HOME:-$HOME/.local/share}/alipay-cli/logs` 后原样重试一次;权限只供 CLI 自身使用,Agent 禁止读取、列举、打印或复制已有凭据与日志。该规则不改变 MCP、上传或沙箱脚本边界。
46
47
  - 本地纯解析、文件检查、`bash -n`、`jq` 校验等不联网命令仍可在默认沙箱内执行。
47
48
 
48
49
  **减少重复授权:**
@@ -43,10 +43,10 @@
43
43
  "trigger": "onboarding.windows_platform",
44
44
  "blocking": false,
45
45
  "allowedReplies": [],
46
- "variables": [],
46
+ "variables": [{ "name": "officialUrl", "type": "officialTemporaryUrl", "required": true, "urlKind": "windowsOnboardingRedirect" }],
47
47
  "requiredPolicyIds": [],
48
48
  "variants": {
49
- "DEFAULT": { "templateLines": ["当前为 Windows 系统,请您暂时前往支付宝 AI 付站点完成产品开通:", "https://aipay.alipay.com/?from=alipay-aipay-skill"] }
49
+ "DEFAULT": { "templateLines": ["当前为 Windows 系统,请您暂时前往支付宝 AI 付站点完成产品开通:", "{{officialUrl}}"] }
50
50
  }
51
51
  },
52
52
  {
@@ -174,6 +174,12 @@
174
174
  "path": "/public-key-confirm",
175
175
  "requiredQueryParams": ["keyConfirmToken"],
176
176
  "optionalQueryParams": []
177
+ },
178
+ "windowsOnboardingRedirect": {
179
+ "host": "aipay.alipay.com",
180
+ "path": "/",
181
+ "requiredQueryParams": ["from", "reason", "platform_id"],
182
+ "optionalQueryParams": []
177
183
  }
178
184
  }
179
185
  }
@@ -12,6 +12,81 @@ require_command() {
12
12
  fi
13
13
  }
14
14
 
15
+ ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE=77
16
+ ALIPAY_CLI_LOCAL_STATE_ACCESS_CONFIRMED=false
17
+
18
+ alipay_cli_credentials_dir() {
19
+ case "${HOME:-}" in
20
+ /*) [ "$HOME" != "/" ] || return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE" ;;
21
+ *) return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE" ;;
22
+ esac
23
+ printf '%s/.alipay-cli/credentials' "$HOME"
24
+ }
25
+
26
+ alipay_cli_logs_dir() {
27
+ local data_home="${XDG_DATA_HOME:-}"
28
+ if [ -z "$data_home" ]; then
29
+ case "${HOME:-}" in
30
+ /*) [ "$HOME" != "/" ] || return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE" ;;
31
+ *) return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE" ;;
32
+ esac
33
+ data_home="$HOME/.local/share"
34
+ fi
35
+ case "$data_home" in
36
+ /*) ;;
37
+ *) return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE" ;;
38
+ esac
39
+ printf '%s/alipay-cli/logs' "$data_home"
40
+ }
41
+
42
+ alipay_cli_has_local_state_permission_error() {
43
+ local raw="${1:-}" permission_pattern private_state_pattern local_state_failure_pattern
44
+ [ -n "$raw" ] || return 1
45
+
46
+ permission_pattern='permission denied|operation not permitted|(^|[^[:alnum:]_])(EACCES|EPERM)([^[:alnum:]_]|$)'
47
+ private_state_pattern='poll_state\.enc\.tmp|session\.enc|\.alipay-cli[/\\]credentials|alipay-cli[/\\]logs'
48
+ local_state_failure_pattern='写入临时文件失败|删除凭据文件失败|can.?t rename log file'
49
+
50
+ printf '%s' "$raw" | grep -qiE "$local_state_failure_pattern" && return 0
51
+ printf '%s' "$raw" | grep -qiE "$permission_pattern" || return 1
52
+ printf '%s' "$raw" | grep -qiE "$private_state_pattern"
53
+ }
54
+
55
+ require_alipay_cli_local_state_access() {
56
+ local state_dir probe_file renamed_file
57
+ if [ "$ALIPAY_CLI_LOCAL_STATE_ACCESS_CONFIRMED" = true ]; then
58
+ return 0
59
+ fi
60
+
61
+ for state_dir in "$(alipay_cli_credentials_dir)" "$(alipay_cli_logs_dir)"; do
62
+ probe_file=""
63
+ renamed_file=""
64
+ [ -d "$state_dir" ] || mkdir -p "$state_dir" 2>/dev/null \
65
+ || return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE"
66
+ probe_file=$(mktemp "$state_dir/.alipay-aipay-access.XXXXXX" 2>/dev/null) \
67
+ || return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE"
68
+ chmod 600 "$probe_file" 2>/dev/null || {
69
+ rm -f "$probe_file" 2>/dev/null || true
70
+ return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE"
71
+ }
72
+ if ! printf 'alipay-cli-local-state-access\n' >"$probe_file" 2>/dev/null; then
73
+ rm -f "$probe_file" 2>/dev/null || true
74
+ return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE"
75
+ fi
76
+ renamed_file="${probe_file}.renamed"
77
+ if ! mv "$probe_file" "$renamed_file" 2>/dev/null; then
78
+ rm -f "$probe_file" "$renamed_file" 2>/dev/null || true
79
+ return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE"
80
+ fi
81
+ if ! rm -f "$renamed_file" 2>/dev/null; then
82
+ rm -f "$renamed_file" 2>/dev/null || true
83
+ return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE"
84
+ fi
85
+ done
86
+
87
+ ALIPAY_CLI_LOCAL_STATE_ACCESS_CONFIRMED=true
88
+ }
89
+
15
90
  emit_control() {
16
91
  printf 'ALIPAY_AIPAY_INTERNAL:%s\n' "$*" >&2
17
92
  }
@@ -306,20 +306,22 @@ async function runDiscovery(args) {
306
306
  if (globalBlocker) envelope.global_blocker = globalBlocker;
307
307
  emitEnvelope(envelope);
308
308
 
309
- let customerOutputs = executedResults
309
+ const customerOutputResults = globalBlocker
310
+ ? executedResults.filter(hasAuthBlocker)
311
+ : executedResults;
312
+ const customerOutputs = customerOutputResults
310
313
  .map((result) => result.stdout.trim())
311
314
  .filter(Boolean);
312
- customerOutputs = [...new Set(customerOutputs)];
313
- if (globalBlocker) {
314
- customerOutputs = customerOutputs.slice(0, 1);
315
- } else {
316
- customerOutputs.push(renderDiscoverySummary(productType, {
315
+ let uniqueCustomerOutputs = [...new Set(customerOutputs)];
316
+ if (globalBlocker) uniqueCustomerOutputs = uniqueCustomerOutputs.slice(0, 1);
317
+ if (!globalBlocker) {
318
+ uniqueCustomerOutputs.push(renderDiscoverySummary(productType, {
317
319
  signStatus: envelope.signing.query_status === 'SUCCESS' ? envelope.signing.sign_status : 'QUERY_FAILED',
318
320
  serviceFlow: envelope.service.query_status === 'SUCCESS' ? envelope.service.flow : 'QUERY_FAILED',
319
321
  appFlow: envelope.application.query_status === 'SUCCESS' ? envelope.application.flow : 'QUERY_FAILED'
320
322
  }));
321
323
  }
322
- if (customerOutputs.length > 0) process.stdout.write(`${customerOutputs.join('\n\n')}\n`);
324
+ if (uniqueCustomerOutputs.length > 0) process.stdout.write(`${uniqueCustomerOutputs.join('\n\n')}\n`);
323
325
  return globalBlocker ? 2 : 0;
324
326
  }
325
327
 
@@ -5,8 +5,8 @@ import path from 'node:path';
5
5
  import process from 'node:process';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { isWindowsPlatform } from './platform_compat.mjs';
8
- import { loadMccRecords, renderMessage } from './render_customer_message.mjs';
9
- import { emitStageMetricAsync, emitTerminalMetric } from './telemetry.mjs';
8
+ import { buildWindowsOnboardingRedirectUrl, loadMccRecords, renderMessage } from './render_customer_message.mjs';
9
+ import { currentRunContext, emitStageMetricAsync, emitTerminalMetric } from './telemetry.mjs';
10
10
 
11
11
  const productTypes = {
12
12
  aipay: { variant: 'AIPAY', productName: 'AI 按量付费' },
@@ -129,9 +129,14 @@ async function readStdin() {
129
129
  }
130
130
 
131
131
  function runPlatformEntry() {
132
- return isWindowsPlatform()
133
- ? renderMessage('onboarding.windows.redirect', 'DEFAULT', {})
134
- : '';
132
+ if (!isWindowsPlatform()) return '';
133
+ const runContext = currentRunContext();
134
+ if (!runContext || runContext.terminal || !['onboarding_only', 'full_process'].includes(runContext.root_intent)) {
135
+ throw new Error('Windows 产品开通引导缺少有效的当前 Onboarding run context');
136
+ }
137
+ return renderMessage('onboarding.windows.redirect', 'DEFAULT', {
138
+ officialUrl: buildWindowsOnboardingRedirectUrl(runContext.run_id)
139
+ });
135
140
  }
136
141
 
137
142
  function optional(args, name) {
@@ -297,16 +302,18 @@ function resolveMccResult(query, args) {
297
302
  const deterministicCodes = deterministicMatches.map(({ record }) => record.code);
298
303
  const agentCodes = agent.codes;
299
304
  if (agent.semanticStatus !== 'MATCHED') {
305
+ const finalCodes = deterministicStrongCodes.slice(0, 8);
306
+ const candidates = finalCodes.map((code) => mccCandidateFromRecord(records.get(code)));
300
307
  return {
301
- matchType: 'NONE',
308
+ matchType: candidates.length === 1 ? 'UNIQUE' : candidates.length === 0 ? 'NONE' : 'MULTIPLE',
302
309
  semanticStatus: agent.semanticStatus,
303
310
  deterministicCandidateCodes: deterministicCodes,
304
311
  deterministicStrongCandidateCodes: deterministicStrongCodes,
305
312
  deterministicWeakCandidateCodes: deterministicWeakCodes,
306
313
  agentCandidateCodes: agentCodes,
307
- mergedCandidateCodes: [],
314
+ mergedCandidateCodes: finalCodes,
308
315
  mccTableSummary: mccTableSummary(records),
309
- candidates: []
316
+ candidates
310
317
  };
311
318
  }
312
319
  const deterministicCodeSet = new Set(deterministicCodes);
@@ -140,6 +140,26 @@ function validateUrl(value, policy, urlKind, expected = {}) {
140
140
  }
141
141
  if (/[\r\n]/.test(parsed.searchParams.get('keyConfirmToken'))) throw new Error('publicKeyConfirmation URL keyConfirmToken 无效');
142
142
  }
143
+ if (urlKind === 'windowsOnboardingRedirect') {
144
+ const required = allowed.requiredQueryParams || ['from', 'reason', 'platform_id'];
145
+ const allowedKeys = new Set(required);
146
+ const actualKeys = [...parsed.searchParams.keys()];
147
+ if (actualKeys.some((key) => !allowedKeys.has(key))) throw new Error('windowsOnboardingRedirect URL 包含未允许的查询参数');
148
+ for (const key of required) {
149
+ if (parsed.searchParams.getAll(key).length !== 1 || !parsed.searchParams.get(key)) {
150
+ throw new Error('windowsOnboardingRedirect URL 缺少必要查询参数或参数重复');
151
+ }
152
+ }
153
+ if (parsed.searchParams.get('from') !== 'alipay-aipay-skill') {
154
+ throw new Error('windowsOnboardingRedirect URL from 无效');
155
+ }
156
+ if (parsed.searchParams.get('reason') !== 'alipay-aipay-skill-onboarding-not-support-windows') {
157
+ throw new Error('windowsOnboardingRedirect URL reason 无效');
158
+ }
159
+ if (/[\u0000-\u001f\u007f,^=]/u.test(parsed.searchParams.get('platform_id'))) {
160
+ throw new Error('windowsOnboardingRedirect URL platform_id 无效');
161
+ }
162
+ }
143
163
  return parsed;
144
164
  }
145
165
 
@@ -191,6 +211,19 @@ export function buildAuthorizationUrl(input) {
191
211
  return value;
192
212
  }
193
213
 
214
+ export function buildWindowsOnboardingRedirectUrl(platformId) {
215
+ if (typeof platformId !== 'string' || !platformId || /[\u0000-\u001f\u007f,^=]/u.test(platformId)) {
216
+ throw new Error('Windows 产品开通引导缺少合法的当前流程 run_id');
217
+ }
218
+ const url = new URL('https://aipay.alipay.com/');
219
+ url.searchParams.set('from', 'alipay-aipay-skill');
220
+ url.searchParams.set('reason', 'alipay-aipay-skill-onboarding-not-support-windows');
221
+ url.searchParams.set('platform_id', platformId);
222
+ const value = url.toString();
223
+ validateUrl(value, loadJson(policyPath), 'windowsOnboardingRedirect');
224
+ return value;
225
+ }
226
+
194
227
  function validateValue(definition, value, policy) {
195
228
  if (value === undefined || value === null || value === '') {
196
229
  if (definition.required) throw new Error(`缺少必填变量 ${definition.name}`);
@@ -240,6 +240,17 @@ function readRunState(env = process.env) {
240
240
  }
241
241
  }
242
242
 
243
+ export function currentRunContext(env = process.env) {
244
+ const state = readRunState(env);
245
+ if (!state) return null;
246
+ return Object.freeze({
247
+ run_id: state.run_id,
248
+ root_intent: state.root_intent,
249
+ product_type: state.product_type,
250
+ terminal: state.terminal === true
251
+ });
252
+ }
253
+
243
254
  function canReadImplicitRunState(env = process.env) {
244
255
  return !isMaintenanceProcess(env);
245
256
  }
@@ -42,7 +42,7 @@
42
42
  - `P-DETERMINISTIC-HANDOFF`:flow 已登记脚本或 renderer 时只执行动作发生位置的固定命令;退出码和登记终态标记满足后立即按 flow 转移,禁止追加 jq、cat、grep、curl 或临时脚本重复解析、复核或解释同一结果。失败只走登记恢复分支;项目代码实现和运行验证除外,但不得把沙箱配置或其他敏感文件打印到工具输出。
43
43
  <!-- GENERATED:ONBOARDING-GUARDS:END -->
44
44
 
45
- 生成区块与 `SKILL.md` 红线适用于全部步骤。Onboarding 禁止修改用户项目代码或支付配置文件,只输出需由用户自行应用的配置;CLI 调用均由脚本设置 `PLATFORM`、`PLATFORM_ID`、`PRODUCT`;`aipay|webpay|apppay` 严格映射为 `AIPAY|WEBPAY|APPPAY`,`PRODUCT` 不进入 MCP 请求 JSON;禁止硬编码 `PLATFORM` 或分号拼环境变量。scope/MCC 不匹配只走 `auth.sh mismatch`;logout 后置条件未成立不得重授权;`AUTH_FLOW:RETRY_WITH_NETWORK` 或输出不可唯一解析只表示当前 Agent 执行环境无法确认结果,必须申请联网权限后重试同一条脚本命令,不得解释为用户本机 logout/login 失败或支付宝业务失败。签约 `request+ctx` 与应用/服务 `request` 由脚本冻结,禁止绕开脚本混用或重组。
45
+ 生成区块与 `SKILL.md` 红线适用于全部步骤。Onboarding 禁止修改用户项目代码或支付配置文件,只输出需由用户自行应用的配置;CLI 调用均由脚本设置 `PLATFORM`、`PLATFORM_ID`、`PRODUCT`;`aipay|webpay|apppay` 严格映射为 `AIPAY|WEBPAY|APPPAY`,`PRODUCT` 不进入 MCP 请求 JSON;禁止硬编码 `PLATFORM` 或分号拼环境变量。scope/MCC 不匹配只走 `auth.sh mismatch`;logout 后置条件未成立不得重授权;`RETRY_WITH_NETWORK|RETRY_WITH_LOCAL_FS_PERMISSION` 分别申请联网或 CLI 私有状态目录权限后原样重试,均不得解释为用户本机 login/logout 失败或支付宝业务失败。签约 `request+ctx` 与应用/服务 `request` 由脚本冻结,禁止绕开脚本混用或重组。
46
46
 
47
47
  ### 标准消息执行规则
48
48
 
@@ -82,7 +82,7 @@ Step 1: 方案规划 → Step 2: 环境检查 → Step 3: 登录授权
82
82
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry
83
83
  ```
84
84
 
85
- - Windows:runner stdout 是唯一对客正文,必须原文发送;发送后停止本 flow 的产品开通动作,不再执行 Step 1、MCC 澄清、方案展示、登录或材料收集。`onboarding_only` 在根意图识别后进入本 flow 即执行;`full_process` 保留已完成的 Integration 实际结果,在衔接本 flow 时执行。
85
+ - Windows:runner 从当前私有 run context 读取同一个 `run_id`,编码为站点引导 URL 的 `platform_id`;禁止 Agent 读取 marker 后手写、传入或拼接该参数。runner stdout 是唯一对客正文,必须原文发送;发送后停止本 flow 的产品开通动作,不再执行 Step 1、MCC 澄清、方案展示、登录或材料收集。`onboarding_only` 在根意图识别后进入本 flow 即执行;`full_process` 保留已完成的 Integration 实际结果,在衔接本 flow 时执行。
86
86
  - 非 Windows:runner 成功且 stdout 为空,直接继续下方既有流程,不输出平台提示,不改变步骤、确认或完成条件。
87
87
  - 命令失败或输出无法唯一判断时停止并核对 runner,禁止手写、改写或补发近似引导。
88
88
 
@@ -134,7 +134,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-
134
134
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-resolve-plan --query "$MCC_SEARCH_QUERY" --product-type "$PRODUCT_TYPE" --agent-mcc-status "$AGENT_MCC_STATUS" [--agent-mcc-codes "$AGENT_MCC_CODES"]
135
135
  ```
136
136
 
137
- `AGENT_MCC_STATUS` 仅允许 `MATCHED|INSUFFICIENT|UNSUPPORTED`;`MATCHED` 必须提供 1 到 3 个用英文逗号分隔的完整编码,后两者不得提供编码。runner 会重新执行确定性召回并校验 Agent 编码确实存在于当前 MCC 表,名称、适用商家和特殊资质全部从同一表重建;非法、重复、超量、状态与候选数量矛盾或表外编码均为 Skill 契约错误,禁止降级为 Agent 自报结果。只有 `MATCHED` 才允许把确定性候选与 Agent 候选合并推进;`INSUFFICIENT|UNSUPPORTED` 时确定性候选只作为内部诊断记录,不得覆盖语义状态自动输出方案或进入授权。确定性候选最多 5 个,最终合并候选最多 8 个;交集候选优先,其次是确定性强命中、Agent 独立候选和确定性弱命中。按次收费、支付渠道和目标产品只用于产品决策,不能改变 MCC 排名。
137
+ `AGENT_MCC_STATUS` 仅允许 `MATCHED|INSUFFICIENT|UNSUPPORTED`;`MATCHED` 必须提供 1 到 3 个用英文逗号分隔的完整编码,后两者不得提供编码。runner 会重新执行确定性召回并校验 Agent 编码确实存在于当前 MCC 表,名称、适用商家和特殊资质全部从同一表重建;非法、重复、超量、状态与候选数量矛盾或表外编码均为 Skill 契约错误,禁止降级为 Agent 自报结果。`MATCHED` 时按交集候选、确定性强命中、Agent 独立候选和确定性弱命中的顺序合并;`INSUFFICIENT|UNSUPPORTED` 不得否决确定性强命中,强命中仍可形成唯一或多候选结果,但确定性弱命中不得单独推进。确定性候选最多 5 个,最终合并候选最多 8 个。按次收费、支付渠道和目标产品只用于产品决策,不能改变 MCC 排名。
138
138
 
139
139
  只接受唯一 `ALIPAY_AIPAY_INTERNAL:MCC_RESOLUTION=<json>`,其中必须包含 `semanticStatus`、`deterministicCandidateCodes`、`agentCandidateCodes`、`mergedCandidateCodes` 和 `mccTableSummary`:`matchType=UNIQUE` 时必须恰有一条候选,stdout 已直接渲染同一候选绑定的 `onboarding.plan.confirm`,必须原文发送但不停止、不等待回复,随后立即进入 `execute`;`MULTIPLE` 时 runner stdout 已直接渲染固定候选消息,必须原文发送并停止,等待用户回复序号、MCC 二级类目名称或编码;`NONE` 时 runner stdout 已直接渲染独立 `onboarding.mcc.no_match`,必须原文发送并停止。禁止再次调用旧的 `mcc-search`/`mcc-candidates` 组合、自由拼候选表、解释、示例或“是否继续”等近似问法。
140
140
 
@@ -190,7 +190,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-execute-read
190
190
 
191
191
  当前方案基于完整确定事实送达后,`execute` 阶段 loader 已在原位置记录兼容 stage `onboarding.user_confirmed`;`auth.sh init` 会在固定授权上下文机器校验通过、开始授权业务动作前记录 `onboarding.auth.start`。两者都不替代登录、scope 或 MCC 完成条件,也不表示收到额外用户回复;不得另行执行 telemetry 命令造成重复事件。
192
192
 
193
- 这些脚本是联网命令,首次执行即按 `../normal/alipay-cli-env.md` 申请权限。`AUTH_FLOW:RETRY_WITH_NETWORK` 只表示当前执行环境无法确认结果:取得权限后重试同一完整命令,不得解释为用户本机或支付宝业务失败,也不得改为手工 logout/login。
193
+ 这些脚本是联网命令,首次执行即按 `../normal/alipay-cli-env.md` 申请权限。`AUTH_FLOW:RETRY_WITH_NETWORK` 只申请联网后重试同一完整命令。`AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION` 只表示授权状态命令 preflight 失败或命中 `poll_state/session/log` 本地状态写失败证据;可恢复阶段 stdout 为空,取得 CLI 私有状态目录权限后原样重试当前 `auth.sh` 命令一次,`wait|confirm` 不重建授权页或轮询。拒绝、不支持或重试仍命中同一 marker 时才输出最小失败说明。两者都不得解释为本机 login/logout 失败、网络故障或支付宝业务失败。
194
194
 
195
195
  Step 1 的产品/MCC 唯一事实校验和方案送达、Step 2 的环境 READY 都是授权前置;产品或 MCC 仍缺失、冲突、非唯一或环境未就绪时禁止 login,`full_process` 也不得绕过。
196
196
 
@@ -204,6 +204,7 @@ Step 1 的产品/MCC 唯一事实校验和方案送达、Step 2 的环境 READY
204
204
  │ └─ 若同时存在 AUTH_STATE:SAVE_FAILED:授权页 stdout 已送达,后续 open 仅可降级为 AUTH_OPEN:LINK_ONLY;wait/confirm 仍使用本条命令显式上下文继续,禁止重新生成授权页
205
205
  ├─ AUTH_FLOW:SCOPE_MISMATCH / MCC_MISMATCH → 执行第 4 项
206
206
  ├─ AUTH_FLOW:RETRY_WITH_NETWORK → 取得联网权限后重试同一完整命令
207
+ ├─ AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION → 取得 CLI 私有状态目录权限后原样重试本命令一次
207
208
  └─ AUTH_FLOW:FAILED → 停止并按错误输出恢复,禁止进入 Step 3.1
208
209
  2. 仅 AUTH_FLOW:READY_TO_OPEN:把 init 的 stdout 原文作为独立对客消息送达后,立即执行 node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" auth open --scope "$SCOPE" --sales-code "$SALES_CODE" --mcc-code "$MCC_CODE" --product-name "$PRODUCT_NAME" --mcc-name "$MCC_NAME";open 无对客 stdout,打开失败不影响已交付 URL。随后执行 node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" auth wait --scope "$SCOPE" --sales-code "$SALES_CODE" --mcc-code "$MCC_CODE" --product-name "$PRODUCT_NAME" --mcc-name "$MCC_NAME"
209
210
  ├─ open 返回 AUTH_FLOW:CONTEXT_MISMATCH → 不执行 wait;使用当前已确认的完整参数重新执行第 1 项
@@ -215,6 +216,7 @@ Step 1 的产品/MCC 唯一事实校验和方案送达、Step 2 的环境 READY
215
216
  ├─ AUTH_FLOW:EXPIRED → 重新执行第 1 项生成授权链接
216
217
  ├─ AUTH_FLOW:CONTEXT_MISMATCH → 当前动作未执行 login --complete/logout;使用当前已确认的完整参数重新执行第 1 项
217
218
  ├─ AUTH_FLOW:RETRY_WITH_NETWORK → 取得联网权限后重试同一完整命令
219
+ ├─ AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION → 保留当前授权页短时状态,取得 CLI 私有状态目录权限后原样重试当前 wait 命令一次
218
220
  ├─ AUTH_FLOW:FAILED → 停止并按错误输出恢复,禁止进入 Step 3.1
219
221
  └─ AUTH_FLOW:SCOPE_MISMATCH / MCC_MISMATCH → 执行第 4 项
220
222
  3. 仅 PENDING 后把用户原始回复传给 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-recovery auth-confirm --reply "$USER_REPLY" --scope "$SCOPE" --sales-code "$SALES_CODE" --mcc-code "$MCC_CODE" --product-name "$PRODUCT_NAME" --mcc-name "$MCC_NAME"`;runner 复用统一肯定语义判定(包括 `1`、“继续”“好了”“已完成”“OK”等),接受后只执行一次原 `auth.sh confirm`。否定、疑问、取消、修改意见或含糊输入返回内部拒绝且不得查询;成功后的转移同第 2 项,PENDING 继续等待且不重启轮询
@@ -222,10 +224,13 @@ Step 1 的产品/MCC 唯一事实校验和方案送达、Step 2 的环境 READY
222
224
  ├─ AUTH_FLOW:READY_TO_OPEN → 输出非阻塞授权页并按第 2 项执行 open + wait
223
225
  ├─ AUTH_FLOW:CONTEXT_MISMATCH → 未执行 logout;使用当前已确认的完整参数重新执行第 1 项
224
226
  ├─ AUTH_FLOW:RETRY_WITH_NETWORK → 取得联网权限后重试同一完整命令
227
+ ├─ AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION → 未重启授权链路,取得 CLI 私有状态目录权限后原样重试本命令一次
225
228
  ├─ AUTH_FLOW:LOGOUT_STILL_LOGGED_IN → logout 后置条件未成立;禁止 login、签约查询和其他 onboarding 动作,排除本机登录态后重试第 4 项
226
229
  └─ mismatch 统一 logout;后置条件成立后用正确 scope 重建授权页
227
230
  ```
228
231
 
232
+ `mismatch` 中 logout 明确业务失败但 `whoami` 仍登录时,脚本内部以“初次 + 2 次”固定预算、间隔 3 秒重试完全相同的 logout。每次 logout 失败后只执行一次 `whoami` 后置核验;logout 明确成功时无需额外核验。任一次证明已退出即继续重新授权;预算耗尽且仍登录才输出 `AUTH_FLOW:LOGOUT_STILL_LOGGED_IN`。网络/服务异常、冲突输出或结果无法确认不消耗该业务重试预算,只转 `AUTH_FLOW:RETRY_WITH_NETWORK`。整个过程不增加用户确认或 Agent 命令,logout 未证明成功前禁止 login。
233
+
229
234
  `AUTH_FLOW:CONTEXT_MISMATCH` 适用于 `open|wait|confirm|mismatch` 的显式上下文与同一次 init 短时状态不一致;脚本必须在任何 `login --complete/logout` 前停止且不输出对客正文。Agent 不得忽略差异、改用旧状态继续或手工清理状态;必须立即用当前 Step 1 已确认的完整参数重新执行第 1 项。新的 init 先完成固定产品/MCC 机器校验,再安全替换旧短时状态;若 CLI 仍处于登录态,因为签约查询没有权威 MCC 字段,init 必须返回对应 `SCOPE_MISMATCH/MCC_MISMATCH` 并转第 4 项,不得返回 `SKIP`。CLI 未登录时直接生成当前上下文的新授权页。两种分支都不得复用旧 deviceCode 或新增用户确认。
230
235
 
231
236
  `auth.page` 非阻塞并追加“扫码完成后请回到 Agent 对话页面”;`auth.pending` 仅由 wait 耗尽或单次 confirm 的 PENDING 渲染,其他对客终态由脚本渲染对应消息。自动轮询期间即使 `login --complete` 已返回可复核状态,只要 `whoami` 尚未取得有效本机登录态,也继续按 PENDING 处理并消耗本次 wait 预算,不输出断头失败文案。剔除 `ALIPAY_AIPAY_INTERNAL:` 内部行后的 stdout 是当前动作唯一对客正文;禁止再次调用 renderer、另写近似话术、输出 stderr 或内部行、重复转述。授权脚本明确返回链接过期时必须重建,旧回复失效。
@@ -23,6 +23,7 @@
23
23
  - `AUTH_FLOW:EXPIRED`:当前页面实际返回过期,旧页面和旧回复失效,重新执行 flow 登记的 init。
24
24
  - `AUTH_FLOW:SCOPE_MISMATCH|MCC_MISMATCH`:只能走 `auth.sh mismatch`;logout 后置条件未成立前不得重授权;仍登录返回 `AUTH_FLOW:LOGOUT_STILL_LOGGED_IN`。
25
25
  - `AUTH_FLOW:RETRY_WITH_NETWORK`:当前 Agent 环境无法确认结果,申请联网权限后原样重试同一命令;不得解释为用户本机登录失败。
26
+ - `AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION`:授权状态命令 preflight 失败,或实际输出命中 CLI 私有状态写失败证据。可恢复阶段 stdout 为空;仅允许当前受控 runtime 命令访问 CLI 私有状态目录后原样重试一次;`wait|confirm` 保留短时授权状态,不重建授权页。Agent 禁止读取、列举、打印或复制已有凭据与日志;拒绝、不支持或重试仍命中同一 marker 时才输出最小失败说明。
26
27
 
27
28
  CLI `login --complete` 成功只表示页面侧确认完成,不代表本机已取得可用登录态。返回成功但状态字段不在白名单时,脚本先用 `whoami` 复核:有效登录继续 scope/MCC 校验,仍未形成登录态则保持 `PENDING`,不可确认则 `RETRY_WITH_NETWORK`。脚本仍需复核 `whoami`、scope,并以固定授权 URL 绑定 MCC;当前后端查询没有权威 MCC 返回字段,禁止声称做了响应 MCC 等值比较。
28
29
 
@@ -9,6 +9,7 @@
9
9
  | `MCP_AUTH_ERROR` | HTTP 401、`Authorization is empty`、非法认证信息 | 停止查询组;logout 后置条件成立后重授权。仍登录则阻断;不可确认或无网络时只重试同一动作,不解释为用户本机或支付宝业务失败 |
10
10
  | `AUTH_MISMATCH` | `mccCode is not auth`、`salesProductCodes is not auth`、`scope is not auth` | 停止当前动作,只进 `auth.sh mismatch`;logout 后置条件未成立前禁重授权 |
11
11
  | `MCP_SERVICE_ERROR` / `SERVICE_UNSTABLE` | 已识别连接、网络或服务异常 | 按下方有限重试规则处理;耗尽后记录受影响分支,依赖允许时继续其他独立分支,最终统一收口 |
12
+ | `CLI_LOCAL_FS_PERMISSION` | 授权状态命令 preflight 失败;或原始 stdout+stderr 命中私有状态写失败强证据,或私有状态路径与权限错误组合 | 仅用于 `auth.sh init|wait|confirm|mismatch` 的 `login/login --complete/logout` 链路;可恢复阶段 stdout 为空,只输出唯一 `AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION`,取得 CLI 私有状态目录权限后原样重试一次,不改产品、scope、MCC 或授权页 |
12
13
  | `ERROR:<code>` | 当前响应中存在可识别业务错误 | 展示登记且脱敏的业务错误,修正业务条件前不重试 |
13
14
  | `CLI_ERROR:<message>` | CLI 失败、异常结构或结果不可唯一解析 | 隐藏原始输出,停止并按登记 CLI 错误恢复,不猜成功或空结果 |
14
15
  | `SUCCESS` | 唯一合法成功结构 | 才允许进入当前脚本的业务字段解析 |
@@ -19,7 +20,9 @@ discovery runner 和截图上传这类并发/分组动作不得让每个子进
19
20
 
20
21
  ## 解包与业务错误
21
22
 
22
- `unwrap_mcp` 兼容纯业务 JSON、唯一 MCP `content[0].text` 信封和混合日志。混合输出只从完整闭合结构形成候选;多个信封或 JSON 候选冲突时不选择、不回显原始响应,并由错误检测阻断。每次 MCP 调用先把原 stdout 交给 `handle_error`,返回成功后才调用 `unwrap_mcp` 和当前脚本的业务解析;不得统一假定成功字段位于 `.success`。
23
+ `unwrap_mcp` 兼容纯业务 JSON、唯一 MCP `content[0].text` 信封和混合日志。授权状态命令捕获先合并 stdout/stderr 检查 `写入临时文件失败|删除凭据文件失败|can't rename log file` 强证据,或 `poll_state.enc.tmp|session.enc|.alipay-cli/credentials|alipay-cli/logs` 与 `permission denied|operation not permitted|EACCES|EPERM` 组合;命中时不得按 `network_error` 处理,裸后端 `permission denied` 不算本地权限。其他 CLI 捕获再执行唯一性解析:单个 JSON 加普通日志可接受,相同 JSON 重复可去重,多个不同 JSON 或成功/失败冲突不得选择任一路,也不得回显原始响应。每次 MCP 调用先把唯一原始结果交给 `handle_error`,返回成功后才调用 `unwrap_mcp` 和当前脚本的业务解析;不得统一假定成功字段位于 `.success`。
24
+
25
+ 每次 logout 失败后只执行一次 `whoami` 后置核验;logout 明确成功时无需额外核验,最多尝试 3 次 logout。`whoami` 后置核验必须先满足唯一 JSON、退出码为 0 且 `detect_error=SUCCESS`,再读取类型正确的 `logged_in/is_expired`;业务失败响应即使携带 `logged_in=false` 也不能证明已退出。logout 网络/服务异常、冲突输出或结果无法确认时,即使 `whoami` 仍显示已登录,也只能转 `AUTH_FLOW:RETRY_WITH_NETWORK` 并重试同一动作。logout 是可由 `whoami` 验证最终状态的幂等恢复动作:明确业务失败且仍登录时,脚本内部使用“初次 + 2 次”固定预算、间隔 3 秒重试完全相同的 logout;任一次确认已退出即继续,全部耗尽且仍登录才转 `AUTH_FLOW:LOGOUT_STILL_LOGGED_IN`。所有失败分支都不得执行 login。每个 auth 动作只允许产生一个最终 `AUTH_FLOW:*`,具体恢复 marker 已由错误处理器输出时,调用方不得追加 `AUTH_FLOW:FAILED`。
23
26
 
24
27
  业务错误对象可能位于顶层、`data.response` 或 `errorContext.errorStack[]`。保留脚本当前字段优先级:读取 `errorCode` 的字符串或对象形式、`errorMessage|errorMsg`、`bizTips`、`checkedError`、`errorScene` 和 `errorSpecific`;只有已验证的服务写入协议 `data.success=false` 才优先使用 `data.subMsg|data.msg`,并忽略只表示外层调用成功的顶层 `msg="Success"`。`APP_MAX_ERROR` 只说明应用数量达到上限,引导复用已有上线应用或前往支付宝开放平台处理配额,不推断其他审核规则。
25
28
 
@@ -28,7 +31,7 @@ discovery runner 和截图上传这类并发/分组动作不得让每个子进
28
31
  ## 网络重试与写后核验
29
32
 
30
33
  - `MCP_SERVICE_ERROR`、`SERVICE_UNSTABLE` 和已识别本地网络错误:初次调用后最多重试 2 次,每次间隔 3 秒,始终复用完全相同的 argv、请求 JSON、业务请求号、资源名和动态字段。
31
- - 认证、授权不匹配、参数、业务错误和结构歧义不进入网络重试。stderr 不拼入待解析 JSON。
34
+ - 认证、授权不匹配、参数、业务错误和结构歧义不进入通用网络重试。唯一例外是上述可验证最终状态的幂等 logout 恢复,它使用同一三次总预算且不经通用 `network_retry.sh`。stderr 不拼入待解析 JSON。
32
35
  - 只读调用可使用完整重试预算。非幂等写操作只有明确 `NOT_SENT` 才自动重试;DNS 失败、connection refused 或宿主在连接前明确拒绝网络访问可归为 `NOT_SENT`。
33
36
  - timeout、连接中断、发送阶段不明和 `SERVICE_UNSTABLE` 一律归为 `MAYBE_SENT`,停止写重试并使用现有只读能力核验;无法确认输出 `UNKNOWN`。
34
37
  - 签约提交用签约查询核验;服务修改只有列表按完整 `serviceId` 和五项资料精确匹配才算成功;服务新建和应用创建不得按同名候选猜测;应用提审只看现有应用信息状态。没有登记核验能力的公钥确认页不得凭页面或同名结果推断成功。
@@ -30,6 +30,7 @@ fi
30
30
  source "${SCRIPT_DIR}/network_retry.sh"
31
31
  source "${SCRIPT_DIR}/sign_status_parser.sh"
32
32
  RENDERER="${SCRIPT_DIR}/../../../normal/scripts/render_customer_message.mjs"
33
+ AUTH_LOCAL_STATE_PERMISSION_EXIT_CODE="${ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}"
33
34
 
34
35
  require_command jq || exit 1
35
36
  require_command alipay-cli || exit 1
@@ -336,8 +337,27 @@ extract_json_payload() {
336
337
  printf '%s' "$analysis" | jq -ce '.value | select(type == "object")' 2>/dev/null
337
338
  }
338
339
 
340
+ auth_has_local_state_permission_error() {
341
+ alipay_cli_has_local_state_permission_error "$1"
342
+ }
343
+
344
+ auth_local_state_permission_failure() {
345
+ emit_control "AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION"
346
+ }
347
+
348
+ auth_require_local_state_access() {
349
+ require_alipay_cli_local_state_access || {
350
+ auth_local_state_permission_failure
351
+ return 1
352
+ }
353
+ }
354
+
339
355
  auth_cli_environment_failure() {
340
- local action="$1"
356
+ local action="$1" failure_code="${2:-1}"
357
+ if [ "$failure_code" -eq "$AUTH_LOCAL_STATE_PERMISSION_EXIT_CODE" ]; then
358
+ auth_local_state_permission_failure
359
+ return
360
+ fi
341
361
  echo "❌ ${action} 未能在当前 Agent 执行环境取得可确认结果,已停止。"
342
362
  echo "📋 这通常表示当前执行环境缺少联网权限、CLI wrapper 混入输出,或 stdout/stderr 无法唯一解析。"
343
363
  echo "📋 这不代表支付宝业务失败,也不能据此判断用户本机 logout/login 失败。"
@@ -345,12 +365,32 @@ auth_cli_environment_failure() {
345
365
  emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
346
366
  }
347
367
 
368
+ handle_auth_cli_result() {
369
+ local cli_result="$1" error_type
370
+ error_type=$(detect_error "$cli_result")
371
+ if handle_error "$cli_result"; then
372
+ return 0
373
+ fi
374
+ case "$error_type" in
375
+ MCP_AUTH_ERROR|AUTH_MISMATCH|CLI_LOCAL_FS_PERMISSION)
376
+ # error_handler.sh already emitted the unique recovery marker.
377
+ ;;
378
+ MCP_SERVICE_ERROR|SERVICE_UNSTABLE)
379
+ emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
380
+ ;;
381
+ *)
382
+ emit_control "AUTH_FLOW:FAILED"
383
+ ;;
384
+ esac
385
+ return 1
386
+ }
387
+
348
388
  wait_for_effective_whoami() {
349
389
  local action="$1"
350
390
  local max_attempts=3
351
391
  local interval_seconds=2
352
392
  local attempt=1
353
- local logged_in is_expired whoami_error_type
393
+ local logged_in is_expired
354
394
 
355
395
  AUTH_WHOAMI_RESULT=""
356
396
  if [ "${ALIPAY_AIPAY_TEST_MODE:-0}" = "1" ] && [[ "${AUTH_WHOAMI_RECHECK_INTERVAL_SECONDS:-}" =~ ^[0-9]+$ ]]; then
@@ -359,22 +399,10 @@ wait_for_effective_whoami() {
359
399
 
360
400
  while [ "$attempt" -le "$max_attempts" ]; do
361
401
  AUTH_WHOAMI_RESULT=$(run_json_command "alipay-cli whoami --json" alipay-cli whoami --json) || {
362
- auth_cli_environment_failure "$action"
402
+ auth_cli_environment_failure "$action" "$?"
363
403
  return 1
364
404
  }
365
- whoami_error_type=$(detect_error "$AUTH_WHOAMI_RESULT")
366
- if ! handle_error "$AUTH_WHOAMI_RESULT"; then
367
- case "$whoami_error_type" in
368
- MCP_AUTH_ERROR|AUTH_MISMATCH) ;;
369
- MCP_SERVICE_ERROR|SERVICE_UNSTABLE)
370
- emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
371
- ;;
372
- *)
373
- emit_control "AUTH_FLOW:FAILED"
374
- ;;
375
- esac
376
- return 1
377
- fi
405
+ handle_auth_cli_result "$AUTH_WHOAMI_RESULT" || return 1
378
406
 
379
407
  logged_in=$(echo "$AUTH_WHOAMI_RESULT" | jq -r '.data.logged_in // false')
380
408
  is_expired=$(echo "$AUTH_WHOAMI_RESULT" | jq -r '.data.is_expired // false')
@@ -403,19 +431,11 @@ run_json_command() {
403
431
  exit_code=$?
404
432
  stderr_output=$(cat "$stderr_file")
405
433
 
406
- if json_payload=$(extract_json_payload "$stdout"); then
407
- rm -f "$stderr_file"
408
- printf '%s' "$json_payload"
409
- return 0
410
- fi
411
-
412
- if json_payload=$(extract_json_payload "$stderr_output"); then
434
+ combined_output=$(printf '%s\n%s' "$stdout" "$stderr_output")
435
+ if auth_has_local_state_permission_error "$combined_output"; then
413
436
  rm -f "$stderr_file"
414
- printf '%s' "$json_payload"
415
- return 0
437
+ return "$AUTH_LOCAL_STATE_PERMISSION_EXIT_CODE"
416
438
  fi
417
-
418
- combined_output=$(printf '%s\n%s' "$stdout" "$stderr_output")
419
439
  if json_payload=$(extract_json_payload "$combined_output"); then
420
440
  rm -f "$stderr_file"
421
441
  printf '%s' "$json_payload"
@@ -433,7 +453,7 @@ validate_current_authorization() {
433
453
  local whoami_result="${1:-}"
434
454
  local logged_in is_expired granted_scope required_scope
435
455
  local scope_match mcc_verify_result mcc_verify_unwrapped mcc_match mcc_raw retry_rc
436
- local mcc_error_type mcc_error_code mcc_success whoami_error_type
456
+ local mcc_error_type mcc_error_code mcc_success
437
457
  local mismatch_type sign_status_output sign_status_marker sign_flow_marker sign_reuse_receipt
438
458
  local granted_scope_item required_scope_item required_scope_found
439
459
  local -a granted_scope_items required_scope_items
@@ -446,24 +466,12 @@ validate_current_authorization() {
446
466
 
447
467
  if [ -z "$whoami_result" ]; then
448
468
  whoami_result=$(run_json_command "alipay-cli whoami --json" alipay-cli whoami --json) || {
449
- auth_cli_environment_failure "登录状态复核"
469
+ auth_cli_environment_failure "登录状态复核" "$?"
450
470
  return 1
451
471
  }
452
472
  fi
453
473
 
454
- whoami_error_type=$(detect_error "$whoami_result")
455
- if ! handle_error "$whoami_result"; then
456
- case "$whoami_error_type" in
457
- MCP_AUTH_ERROR|AUTH_MISMATCH) ;;
458
- MCP_SERVICE_ERROR|SERVICE_UNSTABLE)
459
- emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
460
- ;;
461
- *)
462
- emit_control "AUTH_FLOW:FAILED"
463
- ;;
464
- esac
465
- return 1
466
- fi
474
+ handle_auth_cli_result "$whoami_result" || return 1
467
475
 
468
476
  logged_in=$(echo "$whoami_result" | jq -r '.data.logged_in // false')
469
477
  is_expired=$(echo "$whoami_result" | jq -r '.data.is_expired // false')
@@ -614,13 +622,10 @@ auth_init() {
614
622
 
615
623
  # Step 1: 检查登录状态
616
624
  CHECK_RESULT=$(run_json_command "alipay-cli whoami --json" alipay-cli whoami --json) || {
617
- auth_cli_environment_failure "登录状态检查"
625
+ auth_cli_environment_failure "登录状态检查" "$?"
618
626
  return 1
619
627
  }
620
- if ! handle_error "$CHECK_RESULT"; then
621
- emit_control "AUTH_FLOW:FAILED"
622
- return 1
623
- fi
628
+ handle_auth_cli_result "$CHECK_RESULT" || return 1
624
629
  LOGGED_IN=$(echo "$CHECK_RESULT" | jq -r '.data.logged_in // false')
625
630
  IS_EXPIRED=$(echo "$CHECK_RESULT" | jq -r '.data.is_expired // false')
626
631
 
@@ -646,14 +651,12 @@ auth_init() {
646
651
  fi
647
652
 
648
653
  # Step 2: 执行登录
654
+ auth_require_local_state_access || return 1
649
655
  LOGIN_RESULT=$(run_json_command "alipay-cli login --non-interactive --scope" alipay-cli login --non-interactive --scope "$SCOPE" --json) || {
650
- auth_cli_environment_failure "授权登录发起"
656
+ auth_cli_environment_failure "授权登录发起" "$?"
651
657
  return 1
652
658
  }
653
- if ! handle_error "$LOGIN_RESULT"; then
654
- emit_control "AUTH_FLOW:FAILED"
655
- return 1
656
- fi
659
+ handle_auth_cli_result "$LOGIN_RESULT" || return 1
657
660
  LOGIN_STATUS=$(echo "$LOGIN_RESULT" | jq -r '.data.status // ""')
658
661
 
659
662
  if [ "$LOGIN_STATUS" = "already_logged_in" ]; then
@@ -750,7 +753,6 @@ auth_open() {
750
753
  # ─── auth confirm: 授权确认 + scope 校验 + MCC 授权有效性校验 ────────────────
751
754
  auth_confirm() {
752
755
  AUTH_CONFIRM_RESULT=""
753
- local complete_error_type
754
756
  # 优先使用显式传入的非敏感上下文;未传时再从状态文件兜底恢复。
755
757
  restore_auth_context_or_state "$@" || exit 1
756
758
 
@@ -764,8 +766,9 @@ auth_confirm() {
764
766
  fi
765
767
 
766
768
  # Step 1: 执行授权确认
769
+ auth_require_local_state_access || return 1
767
770
  LOGIN_COMPLETE_RESULT=$(run_json_command "alipay-cli login --complete --json" alipay-cli login --complete --json) || {
768
- auth_cli_environment_failure "授权完成确认"
771
+ auth_cli_environment_failure "授权完成确认" "$?"
769
772
  return 1
770
773
  }
771
774
 
@@ -798,18 +801,8 @@ auth_confirm() {
798
801
  elif [ "$OUTER_SUCCESS" = "true" ] || [ "$DATA_STATUS" = "logged_in" ] || [ "$DATA_STATUS" = "already_logged_in" ] || [ "$DATA_STATUS" = "completed" ]; then
799
802
  :
800
803
  else
801
- complete_error_type=$(detect_error "$LOGIN_COMPLETE_RESULT")
802
- if ! handle_error "$LOGIN_COMPLETE_RESULT"; then
804
+ if ! handle_auth_cli_result "$LOGIN_COMPLETE_RESULT"; then
803
805
  cleanup_auth_state
804
- case "$complete_error_type" in
805
- MCP_AUTH_ERROR|AUTH_MISMATCH) ;;
806
- MCP_SERVICE_ERROR|SERVICE_UNSTABLE)
807
- emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
808
- ;;
809
- *)
810
- emit_control "AUTH_FLOW:FAILED"
811
- ;;
812
- esac
813
806
  return 1
814
807
  fi
815
808
  wait_for_effective_whoami "授权完成确认状态复核"
@@ -904,6 +897,7 @@ auth_wait() {
904
897
 
905
898
  # ─── auth mismatch: 授权范围不满足 → logout → 重新授权 ──────────────────────
906
899
  auth_mismatch() {
900
+ local mismatch_message=""
907
901
  if [ "${1:-}" = "--defer-open" ]; then
908
902
  shift
909
903
  else
@@ -928,7 +922,8 @@ auth_mismatch() {
928
922
  fi
929
923
 
930
924
  # Step 2: mismatch 是授权不匹配恢复路径中唯一执行 logout 的入口。
931
- if ! printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.mismatch --variant DEFAULT; then
925
+ auth_require_local_state_access || return 1
926
+ if ! mismatch_message=$(printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=auth.sh node "$RENDERER" auth.mismatch --variant DEFAULT); then
932
927
  emit_control "AUTH_FLOW:FAILED"
933
928
  return 1
934
929
  fi
@@ -941,12 +936,17 @@ auth_mismatch() {
941
936
  emit_control "AUTH_FLOW:LOGOUT_STILL_LOGGED_IN"
942
937
  return 1
943
938
  ;;
939
+ LOCAL_FS_PERMISSION)
940
+ auth_local_state_permission_failure
941
+ return 1
942
+ ;;
944
943
  *)
945
944
  render_logout_unconfirmed || return 1
946
945
  emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
947
946
  return 1
948
947
  ;;
949
948
  esac
949
+ printf '%s\n' "$mismatch_message"
950
950
 
951
951
  # Step 3: 调用 auth init 执行登录 + 构建授权链接 + 输出唯一授权消息。
952
952
  # DEV_TOOL_NAME 已 export,子进程可继承。
@@ -322,7 +322,7 @@ extract_checked_errors() {
322
322
 
323
323
  # ─── 统一错误检测 ──────────────────────────────────────────────────────────
324
324
  # 参数: $1 - CLI 执行结果文本(原始输出,含信封或不含)
325
- # 返回: MCP_AUTH_ERROR | MCP_SERVICE_ERROR | AUTH_MISMATCH | SERVICE_UNSTABLE | ERROR:xxx | CLI_ERROR:xxx | SUCCESS
325
+ # 返回: MCP_AUTH_ERROR | MCP_SERVICE_ERROR | AUTH_MISMATCH | SERVICE_UNSTABLE | CLI_LOCAL_FS_PERMISSION | ERROR:xxx | CLI_ERROR:xxx | SUCCESS
326
326
  detect_error() {
327
327
  local CLI_RESULT="$1"
328
328
 
@@ -332,6 +332,11 @@ detect_error() {
332
332
  return
333
333
  fi
334
334
 
335
+ if alipay_cli_has_local_state_permission_error "$CLI_RESULT"; then
336
+ echo "CLI_LOCAL_FS_PERMISSION"
337
+ return
338
+ fi
339
+
335
340
  # 先解包 MCP 信封,再进行错误检测
336
341
  local UNWRAPPED=$(unwrap_mcp "$CLI_RESULT")
337
342
 
@@ -348,7 +353,7 @@ detect_error() {
348
353
  fi
349
354
 
350
355
  # 3. MCP 调用失败(网络/连接错误)
351
- if echo "$UNWRAPPED" | grep -qiE "MCP 调用失败|connection refused|timeout|network error"; then
356
+ if echo "$UNWRAPPED" | grep -qiE "MCP 调用失败|connection refused|timeout|network[ _]error|网络连接失败"; then
352
357
  echo "MCP_SERVICE_ERROR"
353
358
  return
354
359
  fi
@@ -403,13 +408,12 @@ run_alipay_cli_json_capture() {
403
408
  stderr_output=$(cat "$stderr_file")
404
409
  rm -f "$stderr_file"
405
410
 
406
- if ALIPAY_CLI_JSON_CAPTURE=$(extract_captured_json "$stdout"); then
407
- return 0
408
- fi
409
- if ALIPAY_CLI_JSON_CAPTURE=$(extract_captured_json "$stderr_output"); then
410
- return 0
411
- fi
412
411
  combined_output=$(printf '%s\n%s' "$stdout" "$stderr_output")
412
+ if type auth_has_local_state_permission_error >/dev/null 2>&1 &&
413
+ auth_has_local_state_permission_error "$combined_output"; then
414
+ ALIPAY_CLI_JSON_CAPTURE=""
415
+ return "${AUTH_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}"
416
+ fi
413
417
  if ALIPAY_CLI_JSON_CAPTURE=$(extract_captured_json "$combined_output"); then
414
418
  return 0
415
419
  fi
@@ -419,48 +423,90 @@ run_alipay_cli_json_capture() {
419
423
  }
420
424
 
421
425
  confirm_logout_postcondition() {
422
- local logout_error_type whoami_error_type whoami_has_state logged_in is_expired
426
+ local logout_error_type whoami_error_type whoami_state
427
+ local logout_result_state logout_capture_rc whoami_capture_rc attempt=1
428
+ local max_attempts=3
429
+ local interval_seconds=3
423
430
 
424
431
  ALIPAY_LOGOUT_POSTCONDITION=""
425
432
  ALIPAY_LOGOUT_ERROR_MESSAGE=""
426
433
 
427
- if run_alipay_cli_json_capture alipay-cli logout --json; then
428
- logout_error_type=$(detect_error "$ALIPAY_CLI_JSON_CAPTURE")
429
- if [ "$ALIPAY_CLI_JSON_EXIT_CODE" -eq 0 ] && [ "$logout_error_type" = "SUCCESS" ]; then
430
- ALIPAY_LOGOUT_POSTCONDITION="COMMAND_CONFIRMED_SUCCESS"
431
- return 0
432
- fi
433
- ALIPAY_LOGOUT_ERROR_MESSAGE=$(sanitize_customer_error_text "$(extract_error_message "$ALIPAY_CLI_JSON_CAPTURE")")
434
- else
435
- ALIPAY_LOGOUT_ERROR_MESSAGE="退出命令未返回可唯一解析结果"
434
+ if [ "${ALIPAY_AIPAY_TEST_MODE:-0}" = "1" ]; then
435
+ interval_seconds=0
436
436
  fi
437
437
 
438
- if ! run_alipay_cli_json_capture alipay-cli whoami --json; then
439
- ALIPAY_LOGOUT_POSTCONDITION="UNCONFIRMED"
440
- return 1
441
- fi
438
+ while [ "$attempt" -le "$max_attempts" ]; do
439
+ logout_result_state="UNCONFIRMED"
440
+ run_alipay_cli_json_capture alipay-cli logout --json
441
+ logout_capture_rc=$?
442
+ if [ "$logout_capture_rc" -eq 0 ]; then
443
+ logout_error_type=$(detect_error "$ALIPAY_CLI_JSON_CAPTURE")
444
+ if [ "$ALIPAY_CLI_JSON_EXIT_CODE" -eq 0 ] && [ "$logout_error_type" = "SUCCESS" ]; then
445
+ ALIPAY_LOGOUT_POSTCONDITION="COMMAND_CONFIRMED_SUCCESS"
446
+ return 0
447
+ fi
448
+ ALIPAY_LOGOUT_ERROR_MESSAGE=$(sanitize_customer_error_text "$(extract_error_message "$ALIPAY_CLI_JSON_CAPTURE")")
449
+ case "$logout_error_type" in
450
+ MCP_SERVICE_ERROR|SERVICE_UNSTABLE)
451
+ logout_result_state="RETRYABLE"
452
+ ;;
453
+ SUCCESS)
454
+ # A success payload paired with a non-zero exit code cannot prove the command result.
455
+ logout_result_state="UNCONFIRMED"
456
+ ;;
457
+ *)
458
+ logout_result_state="EXPLICIT_FAILURE"
459
+ ;;
460
+ esac
461
+ elif [ "$logout_capture_rc" -eq "${AUTH_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
462
+ ALIPAY_LOGOUT_POSTCONDITION="LOCAL_FS_PERMISSION"
463
+ return 1
464
+ else
465
+ ALIPAY_LOGOUT_ERROR_MESSAGE="退出命令未返回可唯一解析结果"
466
+ fi
442
467
 
443
- whoami_has_state=$(echo "$ALIPAY_CLI_JSON_CAPTURE" | jq -r '
444
- if (.data | type) == "object" and ((.data | has("logged_in")) or (.data | has("is_expired"))) then "true" else "false" end
445
- ' 2>/dev/null)
446
- if [ "$whoami_has_state" != "true" ]; then
447
- ALIPAY_LOGOUT_POSTCONDITION="UNCONFIRMED"
448
- return 1
449
- fi
450
- logged_in=$(echo "$ALIPAY_CLI_JSON_CAPTURE" | jq -r '.data.logged_in // false' 2>/dev/null)
451
- is_expired=$(echo "$ALIPAY_CLI_JSON_CAPTURE" | jq -r '.data.is_expired // false' 2>/dev/null)
452
- if [ "$logged_in" = "true" ] && [ "$is_expired" != "true" ]; then
453
- ALIPAY_LOGOUT_POSTCONDITION="STILL_LOGGED_IN"
454
- return 1
455
- fi
456
- whoami_error_type=$(detect_error "$ALIPAY_CLI_JSON_CAPTURE")
457
- if [ "$ALIPAY_CLI_JSON_EXIT_CODE" -ne 0 ] || { [ "$whoami_error_type" != "SUCCESS" ] && [ "$logged_in" = "true" ] && [ "$is_expired" != "true" ]; }; then
458
- ALIPAY_LOGOUT_POSTCONDITION="UNCONFIRMED"
459
- return 1
460
- fi
468
+ run_alipay_cli_json_capture alipay-cli whoami --json
469
+ whoami_capture_rc=$?
470
+ if [ "$whoami_capture_rc" -eq "${AUTH_LOCAL_STATE_PERMISSION_EXIT_CODE:-77}" ]; then
471
+ ALIPAY_LOGOUT_POSTCONDITION="LOCAL_FS_PERMISSION"
472
+ return 1
473
+ fi
474
+ if [ "$whoami_capture_rc" -ne 0 ]; then
475
+ ALIPAY_LOGOUT_POSTCONDITION="UNCONFIRMED"
476
+ return 1
477
+ fi
478
+
479
+ whoami_error_type=$(detect_error "$ALIPAY_CLI_JSON_CAPTURE")
480
+ if [ "$ALIPAY_CLI_JSON_EXIT_CODE" -ne 0 ] || [ "$whoami_error_type" != "SUCCESS" ]; then
481
+ ALIPAY_LOGOUT_POSTCONDITION="UNCONFIRMED"
482
+ return 1
483
+ fi
461
484
 
462
- ALIPAY_LOGOUT_POSTCONDITION="POSTCONDITION_LOGGED_OUT"
463
- return 0
485
+ whoami_state=$(echo "$ALIPAY_CLI_JSON_CAPTURE" | jq -r '
486
+ if (.data | type) != "object" then "UNCONFIRMED"
487
+ elif (.data.logged_in? | type) == "boolean" and .data.logged_in == false then "LOGGED_OUT"
488
+ elif (.data.is_expired? | type) == "boolean" and .data.is_expired == true then "LOGGED_OUT"
489
+ elif (.data.logged_in? | type) == "boolean" and .data.logged_in == true and
490
+ ((.data | has("is_expired") | not) or ((.data.is_expired? | type) == "boolean" and .data.is_expired == false)) then "STILL_LOGGED_IN"
491
+ else "UNCONFIRMED"
492
+ end
493
+ ' 2>/dev/null)
494
+ if [ "$whoami_state" = "LOGGED_OUT" ]; then
495
+ ALIPAY_LOGOUT_POSTCONDITION="POSTCONDITION_LOGGED_OUT"
496
+ return 0
497
+ fi
498
+ if [ "$whoami_state" != "STILL_LOGGED_IN" ] || [ "$logout_result_state" != "EXPLICIT_FAILURE" ]; then
499
+ ALIPAY_LOGOUT_POSTCONDITION="UNCONFIRMED"
500
+ return 1
501
+ fi
502
+
503
+ if [ "$attempt" -eq "$max_attempts" ]; then
504
+ ALIPAY_LOGOUT_POSTCONDITION="STILL_LOGGED_IN"
505
+ return 1
506
+ fi
507
+ sleep "$interval_seconds"
508
+ attempt=$((attempt + 1))
509
+ done
464
510
  }
465
511
 
466
512
  render_logout_unconfirmed() {
@@ -476,7 +522,10 @@ render_logout_still_logged_in() {
476
522
 
477
523
  # ─── MCP 认证错误处理 ──────────────────────────────────────────────────────
478
524
  handle_mcp_auth_error() {
479
- render_error_message error.mcp.auth START '{}' || return 1
525
+ render_error_message error.mcp.auth START '{}' || {
526
+ emit_control "AUTH_FLOW:FAILED"
527
+ return 1
528
+ }
480
529
  if [ "${ALIPAY_AIPAY_SUPPRESS_AUTH_LOGOUT:-}" = "1" ]; then
481
530
  emit_control "AUTH_FLOW:AUTH_REQUIRED"
482
531
  return 1
@@ -484,15 +533,31 @@ handle_mcp_auth_error() {
484
533
  confirm_logout_postcondition
485
534
  case "$ALIPAY_LOGOUT_POSTCONDITION" in
486
535
  COMMAND_CONFIRMED_SUCCESS|POSTCONDITION_LOGGED_OUT)
487
- render_error_message error.mcp.auth LOGOUT_SUCCESS '{}' || return 1
536
+ render_error_message error.mcp.auth LOGOUT_SUCCESS '{}' || {
537
+ emit_control "AUTH_FLOW:FAILED"
538
+ return 1
539
+ }
488
540
  emit_control "AUTH_FLOW:AUTH_REQUIRED"
489
541
  ;;
490
542
  STILL_LOGGED_IN)
491
- render_logout_still_logged_in || return 1
543
+ render_logout_still_logged_in || {
544
+ emit_control "AUTH_FLOW:FAILED"
545
+ return 1
546
+ }
492
547
  emit_control "AUTH_FLOW:LOGOUT_STILL_LOGGED_IN"
493
548
  ;;
549
+ LOCAL_FS_PERMISSION)
550
+ if type auth_local_state_permission_failure >/dev/null 2>&1; then
551
+ auth_local_state_permission_failure
552
+ else
553
+ emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
554
+ fi
555
+ ;;
494
556
  *)
495
- render_logout_unconfirmed || return 1
557
+ render_logout_unconfirmed || {
558
+ emit_control "AUTH_FLOW:FAILED"
559
+ return 1
560
+ }
496
561
  emit_control "AUTH_FLOW:RETRY_WITH_NETWORK"
497
562
  ;;
498
563
  esac
@@ -602,6 +667,16 @@ handle_error() {
602
667
  "SERVICE_UNSTABLE")
603
668
  handle_service_unstable
604
669
  ;;
670
+ "CLI_LOCAL_FS_PERMISSION")
671
+ if type auth_local_state_permission_failure >/dev/null 2>&1; then
672
+ auth_local_state_permission_failure
673
+ else
674
+ MESSAGE_INPUT=$(jq -cn \
675
+ --arg errorMessage "alipay-cli 本地状态目录权限不足" \
676
+ '{errorMessage:$errorMessage,bizTips:"",checkedErrors:[]}')
677
+ render_error_message error.cli DEFAULT "$MESSAGE_INPUT"
678
+ fi
679
+ ;;
605
680
  "ERROR:"*)
606
681
  local CODE="${ERROR_TYPE#ERROR:}"
607
682
  handle_backend_error "$CLI_RESULT" "$CODE"
@@ -44,7 +44,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" render <messageId> --va
44
44
  | `onboarding_only` | 只要求签约、入驻或产品开通,未同时要求代码开发 | 执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-bootstrap --root-intent onboarding_only` | 当前阶段的授权、查询或写分支模块 |
45
45
  | `full_process` | 明确要求完整接入、一站式,或同一请求同时要求代码开发和产品开通 | `normal/full-process-routing.md` | 先 Integration,满足衔接条件后 Onboarding |
46
46
 
47
- `onboarding_only` 正常入口使用 `onboarding-bootstrap --root-intent onboarding_only`,该组合动作只串行执行 `telemetry start-run`、`onboarding-message platform-entry` 和 `flow onboarding entry` 三个既有无业务写入动作;Windows 只发送登记引导并停止 Skill 内产品开通,非 Windows 继续返回 entry 执行契约。执行该组合动作后不得再单独重复执行这三条原子命令。`full_process` 衔接 Onboarding 时已存在同一根流程 run,不使用带 `start-run` 的 bootstrap;仍先执行 flow 登记的 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry`,再按需加载 Onboarding entry,避免重复 `run.intent_detected`。
47
+ `onboarding_only` 正常入口使用 `onboarding-bootstrap --root-intent onboarding_only`,该组合动作只串行执行 `telemetry start-run`、`onboarding-message platform-entry` 和 `flow onboarding entry` 三个既有无业务写入动作;Windows 只发送登记引导并停止 Skill 内产品开通,且引导 URL 的 `platform_id` 由 runner 读取本次已创建的同一 `run_id` 后编码生成,禁止 Agent 传入或拼接;非 Windows 继续返回 entry 执行契约。执行该组合动作后不得再单独重复执行这三条原子命令。`full_process` 衔接 Onboarding 时已存在同一根流程 run,不使用带 `start-run` 的 bootstrap;仍先执行 flow 登记的 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry`,再按需加载 Onboarding entry,避免重复 `run.intent_detected`。
48
48
 
49
49
  识别根意图后立即执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry start-run --root-intent <integration_only|onboarding_only|full_process>`;产品已唯一确定时追加 `--product-type <aipay|webpay|apppay>`。唯一例外是 `onboarding_only` 已按上表执行 `onboarding-bootstrap --root-intent onboarding_only` 时,bootstrap 已包含同一次 `start-run`,不得重复执行本条 telemetry。命令创建/复用当前流程 `run_id`,并在遥测开启时记录 `run.intent_detected`:同一意图和产品继续复用,`UNKNOWN` 补全产品仍复用同一 `run_id`;明确换产品、换根意图或既有 run 已终态才重建。第三方平台项目 ID 仍优先固定为 `run_id`,不得被显式 run ID 或本地生成值覆盖。业务 stage 只由登记 runner/script 异步记录。
50
50