@alipay/alipay-aipay 1.7.2 → 1.7.4

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 (55) hide show
  1. package/README.md +0 -18
  2. package/dist/commands/install.js +24 -17
  3. package/package.json +3 -3
  4. package/skill/SKILL.md +24 -2
  5. package/skill/VERSION +1 -1
  6. package/skill/references/normal/json/customer-messages.json +1 -1
  7. package/skill/references/normal/scripts/channel_offer.mjs +83 -0
  8. package/skill/references/normal/scripts/runtime.mjs +3 -1
  9. package/skill-pre/SKILL.md +27 -5
  10. package/skill-pre/VERSION +1 -1
  11. package/skill-pre/references/integration/flow.md +43 -25
  12. package/skill-pre/references/integration/modules/checklist.md +4 -4
  13. package/skill-pre/references/integration/modules/sandbox/sandbox-setup-guide.md +33 -33
  14. package/skill-pre/references/integration/modules/scripts/local_402_sandbox_pay.py +3 -3
  15. package/skill-pre/references/integration/modules/scripts/sandbox_config.sh +12 -2
  16. package/skill-pre/references/normal/alipay-cli-env.md +3 -1
  17. package/skill-pre/references/normal/conversation-telemetry.md +4 -2
  18. package/skill-pre/references/normal/full-process-routing.md +2 -2
  19. package/skill-pre/references/normal/json/customer-messages.json +1 -1
  20. package/skill-pre/references/normal/json/policy-rules.json +1 -1
  21. package/skill-pre/references/normal/scripts/alipay_cli_env.mjs +9 -0
  22. package/skill-pre/references/normal/scripts/alipay_cli_refresh.mjs +10 -1
  23. package/skill-pre/references/normal/scripts/auth_runtime_runner.mjs +4 -1
  24. package/skill-pre/references/normal/scripts/common.sh +11 -1
  25. package/skill-pre/references/normal/scripts/onboarding_discovery_runner.mjs +4 -1
  26. package/skill-pre/references/normal/scripts/onboarding_message_runner.mjs +10 -1
  27. package/skill-pre/references/normal/scripts/onboarding_recovery_runner.mjs +4 -1
  28. package/skill-pre/references/normal/scripts/open_official_url.sh +10 -2
  29. package/skill-pre/references/normal/scripts/preview_runtime.mjs +32 -2
  30. package/skill-pre/references/normal/scripts/python_script_runner.mjs +9 -4
  31. package/skill-pre/references/normal/scripts/render_customer_message.mjs +4 -2
  32. package/skill-pre/references/normal/scripts/runtime.mjs +50 -6
  33. package/skill-pre/references/normal/scripts/telemetry.mjs +9 -0
  34. package/skill-pre/references/normal/scripts/windows/acl-native.mjs +3 -0
  35. package/skill-pre/references/normal/scripts/windows/acl.mjs +41 -0
  36. package/skill-pre/references/normal/scripts/windows/bin/alipay-cli +4 -0
  37. package/skill-pre/references/normal/scripts/windows/bootstrap-node.ps1 +140 -0
  38. package/skill-pre/references/normal/scripts/windows/cli-install.mjs +51 -0
  39. package/skill-pre/references/normal/scripts/windows/cli-process.mjs +11 -0
  40. package/skill-pre/references/normal/scripts/windows/execution.mjs +51 -0
  41. package/skill-pre/references/normal/scripts/windows/git-discovery.mjs +42 -0
  42. package/skill-pre/references/normal/scripts/windows/policy.json +13 -0
  43. package/skill-pre/references/normal/scripts/windows/preflight.mjs +182 -0
  44. package/skill-pre/references/normal/scripts/windows/prepare.mjs +83 -0
  45. package/skill-pre/references/normal/scripts/windows/probe.sh +14 -0
  46. package/skill-pre/references/normal/scripts/windows/python-execution.mjs +38 -0
  47. package/skill-pre/references/normal/scripts/windows/python-runtime.mjs +85 -0
  48. package/skill-pre/references/normal/scripts/windows/routing.mjs +83 -0
  49. package/skill-pre/references/normal/scripts/windows/shell_helpers.sh +83 -0
  50. package/skill-pre/references/normal/windows-environment.md +82 -0
  51. package/skill-pre/references/onboarding/flow.md +4 -1
  52. package/skill-pre/references/onboarding/modules/scripts/app.sh +1 -0
  53. package/skill-pre/references/onboarding/modules/scripts/ar_sign_apply.sh +10 -6
  54. package/skill-pre/references/onboarding/modules/scripts/auth.sh +1 -0
  55. package/skill-pre/references/runtime-entry.md +13 -4
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawnSync } from 'node:child_process';
3
+ import { spawnSync as nativeSpawn } from 'node:child_process';
4
+ const { spawnSync } = process.platform === 'win32'
5
+ ? await import('./windows/execution.mjs')
6
+ : { spawnSync: nativeSpawn };
4
7
  import process from 'node:process';
5
8
 
6
9
  const internalPrefix = 'ALIPAY_AIPAY_INTERNAL:';
@@ -5,6 +5,10 @@
5
5
  # 调用前置: 无
6
6
  #=============================================================================
7
7
 
8
+ case "$(uname -s)" in
9
+ MINGW*|MSYS*) source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/windows/shell_helpers.sh" || exit 78 ;;
10
+ esac
11
+
8
12
  case "${HOME:-}" in
9
13
  /*)
10
14
  user_bin="$HOME/.local/bin"
@@ -39,6 +43,9 @@ ALIPAY_AIPAY_LOCAL_CONFIG_PERMISSION_EXIT_CODE=77
39
43
  ALIPAY_AIPAY_LOCAL_CONFIG_ACCESS_CONFIRMED=false
40
44
 
41
45
  alipay_cli_credentials_dir() {
46
+ case "$(uname -s)" in
47
+ MINGW*|MSYS*) cygpath -u "$ALIPAY_WINDOWS_CREDENTIALS_DIR"; return $? ;;
48
+ esac
42
49
  case "${HOME:-}" in
43
50
  /*) [ "$HOME" != "/" ] || return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE" ;;
44
51
  *) return "$ALIPAY_CLI_LOCAL_STATE_PERMISSION_EXIT_CODE" ;;
@@ -47,6 +54,9 @@ alipay_cli_credentials_dir() {
47
54
  }
48
55
 
49
56
  alipay_cli_logs_dir() {
57
+ case "$(uname -s)" in
58
+ MINGW*|MSYS*) cygpath -u "$ALIPAY_WINDOWS_LOGS_DIR"; return $? ;;
59
+ esac
50
60
  local data_home="${XDG_DATA_HOME:-}"
51
61
  if [ -z "$data_home" ]; then
52
62
  case "${HOME:-}" in
@@ -67,7 +77,7 @@ alipay_cli_has_local_state_permission_error() {
67
77
  [ -n "$raw" ] || return 1
68
78
 
69
79
  permission_pattern='permission denied|operation not permitted|(^|[^[:alnum:]_])(EACCES|EPERM)([^[:alnum:]_]|$)'
70
- private_state_pattern='poll_state\.enc\.tmp|session\.enc|\.alipay-cli[/\\]credentials|alipay-cli[/\\]logs'
80
+ private_state_pattern='poll_state\.enc\.tmp|session\.enc|\.alipay-cli[/\\]credentials|alipay-cli[/\\](data[/\\])?logs'
71
81
  local_state_failure_pattern='写入临时文件失败|删除凭据文件失败|can.?t rename log file'
72
82
 
73
83
  printf '%s' "$raw" | grep -qiE "$local_state_failure_pattern" && return 0
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn } from 'node:child_process';
3
+ import { spawn as nativeSpawn } from 'node:child_process';
4
+ const { spawn } = process.platform === 'win32'
5
+ ? await import('./windows/execution.mjs')
6
+ : { spawn: nativeSpawn };
4
7
  import path from 'node:path';
5
8
  import process from 'node:process';
6
9
  import { fileURLToPath } from 'node:url';
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import fs from 'node:fs';
4
+ const windowsRouting = process.platform === 'win32' ? await import('./windows/routing.mjs') : null;
5
+ let windowsMccCatalog;
6
+
3
7
  import { createHash } from 'node:crypto';
4
8
  import path from 'node:path';
5
9
  import process from 'node:process';
@@ -156,6 +160,7 @@ async function readStdin() {
156
160
  }
157
161
 
158
162
  function runPlatformEntry(args = {}) {
163
+ if (windowsRouting && windowsRouting.onboardingRoute() === 'READY') return '';
159
164
  if (!isWindowsPlatform()) return '';
160
165
  const runContext = currentRunContext();
161
166
  if (runContext?.terminal) {
@@ -305,6 +310,10 @@ function mccCandidateFromRecord(record, productType = '', mccRunContext = captur
305
310
  }
306
311
 
307
312
  function dynamicMccRecordsFromCatalog(rawCatalog) {
313
+ if (windowsRouting && rawCatalog === '@windows-stdin') {
314
+ windowsMccCatalog ??= fs.readFileSync(0, 'utf8');
315
+ rawCatalog = windowsMccCatalog;
316
+ }
308
317
  let catalog;
309
318
  try {
310
319
  catalog = typeof rawCatalog === 'string' ? JSON.parse(rawCatalog) : rawCatalog;
@@ -1332,7 +1341,7 @@ export async function runOnboardingMessageCli(argv = process.argv) {
1332
1341
  if (command === 'platform-entry') {
1333
1342
  const message = runPlatformEntry(args);
1334
1343
  if (message) console.log(message);
1335
- return;
1344
+ return windowsRouting ? (message ? 'LEGACY' : 'READY') : undefined;
1336
1345
  }
1337
1346
  if (command === 'product-clarify') {
1338
1347
  console.log(runProductClarify());
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawnSync } from 'node:child_process';
3
+ import { spawnSync as nativeSpawn } from 'node:child_process';
4
+ const { spawnSync } = process.platform === 'win32'
5
+ ? await import('./windows/execution.mjs')
6
+ : { spawnSync: nativeSpawn };
4
7
  import path from 'node:path';
5
8
  import process from 'node:process';
6
9
  import { fileURLToPath } from 'node:url';
@@ -6,6 +6,12 @@ KIND="${1:-}"
6
6
  IFS= read -r URL || URL=""
7
7
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
8
  POLICY_FILE="${SCRIPT_DIR}/../json/policy-rules.json"
9
+ case "$(uname -s)" in
10
+ MINGW*|MSYS*)
11
+ source "$SCRIPT_DIR/windows/shell_helpers.sh" || exit 78
12
+ POLICY_FILE=$(cygpath -m "$POLICY_FILE") || exit 78
13
+ ;;
14
+ esac
9
15
 
10
16
  case "$KIND" in authorization|public-key-confirmation) ;; *) echo "OPEN_FAILED"; exit 1 ;; esac
11
17
  command -v node >/dev/null 2>&1 || { echo "OPEN_FAILED"; exit 1; }
@@ -18,8 +24,10 @@ const policy = JSON.parse(fs.readFileSync(policyFile, "utf8"));
18
24
  if (/[\u0000-\u0020\u007f`()\[\]<>|]/.test(raw)) process.exit(1);
19
25
  let url;
20
26
  try { url = new URL(raw); } catch { process.exit(1); }
21
- const allowed = policy.temporaryUrlAllowlist[kind === "authorization" ? "authorization" : "publicKeyConfirmation"];
22
- if (!allowed || url.protocol !== "https:" || url.hostname !== allowed.host || url.pathname !== allowed.path || url.username || url.password || url.port || url.hash) process.exit(1);
27
+ const entries = policy.temporaryUrlAllowlist[kind === "authorization" ? "authorization" : "publicKeyConfirmation"];
28
+ const allowed = (Array.isArray(entries) ? entries : [entries]).find((entry) =>
29
+ entry && url.hostname === entry.host && url.pathname === entry.path);
30
+ if (!allowed || url.protocol !== "https:" || url.username || url.password || url.port || url.hash) process.exit(1);
23
31
  if (kind === "authorization") {
24
32
  const required = allowed.requiredQueryParams;
25
33
  const optional = allowed.optionalQueryParams;
@@ -108,6 +108,36 @@ export function openLocalKeygen({ platform = runtimePlatform(), env = process.en
108
108
  }
109
109
  }
110
110
 
111
+ // Match complete declarations, never an affirmative substring inside a denial,
112
+ // question, condition, quotation, or a description of somebody else's choice.
113
+ export function parsePreviewConsent(value) {
114
+ if (typeof value !== 'string') return null;
115
+ let text = value.normalize('NFKC').trim();
116
+ text = text.replace(/^【([^【】]+)】([。.!!;;]*)$/u, '$1$2');
117
+ if (!text || /[??]|吗|么|嘛|是否|能否|可否|如果|假如|除非|前提|只要|也许|可能|不确定|考虑|再说|行吧|随便|或者|还是|先.*再/u.test(text)) return null;
118
+ const clauses = text.split(/[,,。.!!;;\n]|但是|不过|但/u).map(part => part.replace(/\s+/gu, '')).filter(Boolean);
119
+ const subject = '(?:我(?:们)?(?:明确)?|本人)?';
120
+ const actor = '(?:你们|你|支付宝|本Skill|Skill)?';
121
+ const action = '(?:采集|收集|记录)(?:(?:并|和|及)(?:上报|上传|使用))?';
122
+ const object = '(?:(?:我的|我们的|本次|当前)?(?:会话|对话|聊天)(?:信息|内容|记录|数据)?)?';
123
+ const allow = new RegExp(`^${subject}(?:同意|允许|授权|接受|可以)${actor}${action}${object}$`, 'u');
124
+ const deny = new RegExp(`^${subject}(?:不同意|不允许|不授权|不接受|不希望|不想|不愿意|不需要|不要|不用|无需|拒绝|禁止|停止|别|不)${actor}${action}${object}$`, 'u');
125
+ let grant = false;
126
+ let refusal = false;
127
+ let confirm = false;
128
+ for (const clause of clauses) {
129
+ const part = clause.replace(/^请/u, '');
130
+ if (deny.test(part) || /^(?:我)?(?:仅|只)(?:使用|用)(?:内测版Skill|内测版|服务)$/u.test(part)) refusal = true;
131
+ else if (allow.test(part)) grant = true;
132
+ else if (/^(?:我)?确认$/u.test(part)) confirm = true;
133
+ else if (/^(?:我)?(?:希望)?继续使用(?:内测版Skill|内测版|服务)$/u.test(part)) continue;
134
+ else return null;
135
+ }
136
+ if (grant && refusal) return null;
137
+ if (refusal) return 'DENIED';
138
+ return grant || confirm ? 'GRANTED' : null;
139
+ }
140
+
111
141
  export function runPreviewCli(argv = process.argv) {
112
142
  const action = argv[2];
113
143
  if (action === 'consent') {
@@ -120,10 +150,10 @@ export function runPreviewCli(argv = process.argv) {
120
150
  }
121
151
  if (!args.get('--input-id')) throw new Error('preview consent 缺少 --input-id');
122
152
  // A generic business confirmation (1/OK/继续) is not consent to collection.
123
- const reply = (args.get('--reply') || '').trim().replace(/[。!!]+$/u, '');
124
- const decision = reply === '确认' ? 'GRANTED' : reply === '仅使用服务' ? 'DENIED' : null;
153
+ const decision = parsePreviewConsent(args.get('--reply'));
125
154
  if (!decision) {
126
155
  process.stderr.write(`${internalPrefix}PREVIEW_DECISION=UNCONFIRMED\n`);
156
+ deliver('preview.consent.clarify');
127
157
  return;
128
158
  }
129
159
  const saved = setConversationConsent({ decision, inputId: args.get('--input-id') });
@@ -7,6 +7,8 @@ import process from 'node:process';
7
7
  import { fileURLToPath } from 'node:url';
8
8
 
9
9
  import { isWindowsPlatform } from './platform_compat.mjs';
10
+ import { runWindowsPython } from './windows/python-execution.mjs';
11
+ import { findWindowsPython } from './windows/python-runtime.mjs';
10
12
 
11
13
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
12
14
  const allowedScript = path.resolve(scriptDir, '../../integration/modules/scripts/local_402_sandbox_pay.py');
@@ -17,6 +19,7 @@ function commandRuns(command, args) {
17
19
  }
18
20
 
19
21
  function resolvePython() {
22
+ if (isWindowsPlatform()) return findWindowsPython();
20
23
  const candidates = isWindowsPlatform()
21
24
  ? [
22
25
  { command: 'py', prefix: ['-3'] },
@@ -40,10 +43,12 @@ export function runPythonScriptCli(argv = process.argv) {
40
43
 
41
44
  const python = resolvePython();
42
45
  if (!python) throw new Error('未找到可用的 Python 3 运行时');
43
- const result = spawnSync(python.command, [...python.prefix, resolvedScript, ...argv.slice(3)], {
44
- stdio: 'inherit',
45
- shell: false
46
- });
46
+ const result = isWindowsPlatform()
47
+ ? runWindowsPython(python, resolvedScript, argv.slice(3))
48
+ : spawnSync(python.command, [...python.prefix, resolvedScript, ...argv.slice(3)], {
49
+ stdio: 'inherit',
50
+ shell: false
51
+ });
47
52
  if (result.error) throw result.error;
48
53
  process.exitCode = Number.isInteger(result.status) ? result.status : 1;
49
54
  }
@@ -343,8 +343,10 @@ function validateUrl(value, policy, urlKind, expected = {}) {
343
343
  } catch {
344
344
  throw new Error('officialTemporaryUrl 不是合法 URL');
345
345
  }
346
- const allowed = policy.temporaryUrlAllowlist[urlKind];
347
- if (!allowed || parsed.protocol !== 'https:' || parsed.hostname !== allowed.host || parsed.pathname !== allowed.path || parsed.username || parsed.password || parsed.port || parsed.hash) {
346
+ const entries = policy.temporaryUrlAllowlist[urlKind];
347
+ const allowed = (Array.isArray(entries) ? entries : [entries]).find((entry) =>
348
+ entry && parsed.hostname === entry.host && parsed.pathname === entry.path);
349
+ if (!allowed || parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.port || parsed.hash) {
348
350
  throw new Error(`officialTemporaryUrl 不符合 ${urlKind} allowlist`);
349
351
  }
350
352
  if (urlKind === 'authorization') {
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawnSync } from 'node:child_process';
3
+ import { spawnSync as nativeSpawn } from 'node:child_process';
4
+ const { spawnSync } = process.platform === 'win32'
5
+ ? await import('./windows/execution.mjs')
6
+ : { spawnSync: nativeSpawn };
4
7
  import path from 'node:path';
5
8
  import process from 'node:process';
6
9
  import { fileURLToPath } from 'node:url';
@@ -8,6 +11,16 @@ import { fileURLToPath } from 'node:url';
8
11
  import { applyTelemetrySession, currentRunContext, startTelemetryRun } from './telemetry.mjs';
9
12
  import { applyAgentPlatform } from './agent_platform.mjs';
10
13
 
14
+ const windowsRouting = process.platform === 'win32' ? await import('./windows/routing.mjs') : null;
15
+
16
+ // A user-local Node bootstrap may not be on the host's PATH. Its bundled npm/npx
17
+ // and child Node commands must use the same verified runtime for this process.
18
+ if (process.platform === 'win32') {
19
+ const entry = Object.entries(process.env).find(([key]) => key.toLowerCase() === 'path');
20
+ for (const key of Object.keys(process.env)) if (key.toLowerCase() === 'path') delete process.env[key];
21
+ process.env.PATH = [path.dirname(process.execPath), entry?.[1] || ''].join(path.delimiter);
22
+ }
23
+
11
24
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
12
25
  const referencesDir = path.resolve(scriptDir, '../..');
13
26
  const flowDirs = Object.freeze({
@@ -41,6 +54,7 @@ const nodeEntrypoints = Object.freeze({
41
54
  });
42
55
 
43
56
  export const runtimeDomains = Object.freeze([
57
+ ...(process.platform === 'win32' ? ['windows-entry'] : []),
44
58
  'self-update',
45
59
  'integration-bootstrap',
46
60
  'onboarding-bootstrap',
@@ -56,12 +70,18 @@ function cliArgv(args) {
56
70
  }
57
71
 
58
72
  function runShell(script, cwd, args) {
73
+ const captureWindowsSandbox = process.platform === 'win32' && path.basename(script) === 'sandbox_config.sh';
59
74
  const result = spawnSync('bash', [script, ...args], {
60
75
  cwd,
61
76
  env: process.env,
62
- stdio: 'inherit',
77
+ stdio: captureWindowsSandbox ? 'pipe' : 'inherit',
78
+ ...(captureWindowsSandbox ? {encoding: 'utf8'} : {}),
63
79
  shell: false
64
80
  });
81
+ if (captureWindowsSandbox) {
82
+ if (result.stdout) process.stdout.write(result.stdout);
83
+ if (result.stderr) process.stderr.write(result.stderr);
84
+ }
65
85
  if (result.error) throw result.error;
66
86
  process.exitCode = Number.isInteger(result.status) ? result.status : 1;
67
87
  }
@@ -186,9 +206,9 @@ async function runOnboardingBootstrap(args) {
186
206
  await startTelemetryRun(rootIntent, productType);
187
207
  const { runOnboardingMessageCli } = await import('./onboarding_message_runner.mjs');
188
208
  const platformEntryArgs = ['platform-entry'];
189
- await runOnboardingMessageCli(cliArgv(platformEntryArgs));
209
+ const route = await runOnboardingMessageCli(cliArgv(platformEntryArgs));
190
210
  const { isWindowsPlatform } = await import('./platform_compat.mjs');
191
- if (isWindowsPlatform()) return;
211
+ if (isWindowsPlatform() && route !== 'READY') return;
192
212
  const { runFlowCli } = await import('./flow_loader.mjs');
193
213
  runFlowCli(cliArgv(['onboarding', 'entry']));
194
214
  }
@@ -203,6 +223,18 @@ async function runOnboardingExecuteReady(args) {
203
223
  }
204
224
 
205
225
  async function dispatch(domain, args) {
226
+ if (windowsRouting) {
227
+ if (domain === 'windows-entry') return windowsRouting.runWindowsEntry(args);
228
+ if (['sandbox', 'mcc', 'auth', 'screenshots', 'special-license', 'sign', 'service', 'app',
229
+ 'onboarding-discovery', 'onboarding-recovery', 'onboarding-execute-ready'].includes(domain)) {
230
+ windowsRouting.requireBusinessEnvironment();
231
+ }
232
+ if (domain === 'env' && ['prepare', 'prepare-jq', 'retry'].includes(args[0])) {
233
+ const { runEnvironment } = await import('./windows/prepare.mjs');
234
+ process.exitCode = runEnvironment(args);
235
+ return;
236
+ }
237
+ }
206
238
  if (domain === 'integration-bootstrap') return runIntegrationBootstrap(args);
207
239
  if (domain === 'onboarding-bootstrap') return runOnboardingBootstrap(args);
208
240
  if (domain === 'onboarding-execute-ready') return runOnboardingExecuteReady(args);
@@ -223,6 +255,11 @@ async function dispatch(domain, args) {
223
255
  return;
224
256
  }
225
257
  if (domain === 'env') {
258
+ if (process.platform === 'win32' && ['check-python', 'prepare-python'].includes(args[0])) {
259
+ const { runPythonEnvironment } = await import('./windows/python-runtime.mjs');
260
+ process.exitCode = await runPythonEnvironment(args);
261
+ return;
262
+ }
226
263
  if (args[0] === 'refresh-alipay-cli') {
227
264
  if (args.length !== 1) throw new Error('用法: runtime.mjs env refresh-alipay-cli');
228
265
  const { runAlipayCliRefreshCli } = await import('./alipay_cli_refresh.mjs');
@@ -259,9 +296,16 @@ async function dispatch(domain, args) {
259
296
  export async function runRuntimeCli(argv = process.argv) {
260
297
  const domain = argv[2] || '';
261
298
  try {
262
- await dispatch(domain, applyTelemetryContext(domain, applyTelemetrySession(applyAgentPlatform(argv.slice(3)))));
299
+ const received = applyTelemetrySession(applyAgentPlatform(argv.slice(3)));
300
+ const routed = windowsRouting ? windowsRouting.consumeRoute(received) : {args: received};
301
+ if (routed.route) process.env.ALIPAY_AIPAY_WINDOWS_ROUTE = routed.route;
302
+ await dispatch(domain, applyTelemetryContext(domain, routed.args));
263
303
  } catch (error) {
264
- runtimeError(domain, error);
304
+ if (windowsRouting && error.reason) {
305
+ const { report } = await import('./windows/preflight.mjs');
306
+ report(error);
307
+ process.stderr.write('ALIPAY_AIPAY_INTERNAL:WINDOWS_BUSINESS_BLOCKED=ENVIRONMENT_UNAVAILABLE\n');
308
+ } else runtimeError(domain, error);
265
309
  process.exitCode = domain === 'self-update' ? 0 : 1;
266
310
  }
267
311
  }
@@ -1,3 +1,7 @@
1
+
2
+ const windowsProtect = process.platform === 'win32'
3
+ ? (await import('./windows/acl.mjs')).protect
4
+ : null;
1
5
  import crypto from 'node:crypto';
2
6
  import fs from 'node:fs';
3
7
  import https from 'node:https';
@@ -364,6 +368,11 @@ export function telemetryStoragePaths(env = process.env) {
364
368
  }
365
369
 
366
370
  function ensurePrivateDir(dir) {
371
+ if (windowsProtect) {
372
+ fs.mkdirSync(dir, { recursive: true });
373
+ windowsProtect(dir);
374
+ return;
375
+ }
367
376
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
368
377
  try {
369
378
  fs.chmodSync(dir, 0o700);
@@ -0,0 +1,3 @@
1
+ import {protect} from './acl.mjs';
2
+ try { if(process.argv.length!==3)throw new Error('ACL target required');protect(process.argv[2]); }
3
+ catch(e){process.stderr.write('ALIPAY_AIPAY_INTERNAL:WINDOWS_ACL_ERROR='+(e.reason||'ACL_FAILED')+'\n');process.exitCode=78;}
@@ -0,0 +1,41 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {spawnSync} from 'node:child_process';
4
+
5
+ export function fail(reason, dependency = '') { throw Object.assign(new Error(reason), {reason, dependency}); }
6
+ export function run(command,args,options={}) {
7
+ return spawnSync(command,args,{encoding:'utf8',windowsHide:true,shell:false,timeout:15000,maxBuffer:4*1024*1024,...options});
8
+ }
9
+ export function powershell() {
10
+ const root=process.env.SystemRoot || 'C:\\Windows';
11
+ const command=path.join(root,'System32/WindowsPowerShell/v1.0/powershell.exe');
12
+ if(!fs.existsSync(command)) fail('DEPENDENCY_MISSING','PowerShell');
13
+ return command;
14
+ }
15
+ export function psEnv(env=process.env) {
16
+ return Object.fromEntries(Object.entries(env).filter(([k])=>k.toLowerCase()!=='psmodulepath'));
17
+ }
18
+ export function protect(target) {
19
+ if(fs.lstatSync(target).isSymbolicLink()||path.resolve(target)===path.parse(target).root)fail('ACL_FAILED','unsafe target');
20
+ const system=path.join(process.env.SystemRoot||'C:/Windows','System32');
21
+ const who=run(path.join(system,'whoami.exe'),['/user','/fo','csv','/nh']);
22
+ const sid=who.stdout?.match(/S-1-5-[0-9-]+/)?.[0];if(who.status!==0||!sid)fail('ACL_FAILED');
23
+ const env={...psEnv(),ALIPAY_WINDOWS_ACL_TARGET:path.resolve(target)};
24
+ // Native icacls changes permissions; PowerShell only reads ACL metadata.
25
+ // No .ps1 execution, ExecutionPolicy override, elevation, or policy mutation.
26
+ const inspect=()=>{
27
+ const r=run(powershell(),['-NoProfile','-NonInteractive','-Command',
28
+ '$ErrorActionPreference="Stop"; $p=$env:ALIPAY_WINDOWS_ACL_TARGET; $d=New-Object IO.DriveInfo([IO.Path]::GetPathRoot($p)); if($d.DriveFormat -ne "NTFS"){exit 2}; $a=Get-Acl -LiteralPath $p; @{protected=$a.AreAccessRulesProtected;rules=@($a.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier]) | ForEach-Object { @{sid=$_.IdentityReference.Value;type=[string]$_.AccessControlType;rights=[int]$_.FileSystemRights} })} | ConvertTo-Json -Depth 4 -Compress'],{env});
29
+ if(r.status!==0)fail('ACL_FAILED','NTFS');try{return JSON.parse(r.stdout);}catch{fail('ACL_FAILED');}
30
+ };
31
+ const allowed=[sid,'S-1-5-18','S-1-5-32-544'];inspect();
32
+ const inherited=fs.statSync(target).isDirectory()?'(OI)(CI)F':'F';
33
+ const args=[target,'/inheritance:r','/grant:r',...allowed.map(s=>'*'+s+':'+inherited)];
34
+ if(run(path.join(system,'icacls.exe'),args).status!==0)fail('ACL_FAILED','grant');
35
+ const remove=[...new Set(inspect().rules.filter(r=>!allowed.includes(r.sid)||r.type!=='Allow').map(r=>r.sid))];
36
+ for(const s of remove)if(run(path.join(system,'icacls.exe'),[target,'/remove','*'+s]).status!==0)fail('ACL_FAILED','remove');
37
+ // Reapply after removing any explicit deny ACE for an allowed identity.
38
+ if(remove.length&&run(path.join(system,'icacls.exe'),args).status!==0)fail('ACL_FAILED','regrant');
39
+ const after=inspect();
40
+ if(!after.protected||after.rules.some(r=>!allowed.includes(r.sid)||r.type!=='Allow')||!after.rules.some(r=>r.sid===sid&&(r.rights&2032127)===2032127))fail('ACL_FAILED','readback');
41
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # Real CLI forwarding entry for original `env ... alipay-cli` call sites.
3
+ set -eu
4
+ exec "$ALIPAY_WINDOWS_NODE" "$ALIPAY_WINDOWS_CLI_RUNNER" "$@"
@@ -0,0 +1,140 @@
1
+ param([ValidateSet('check', 'prepare', 'repair')][string]$Mode = 'check')
2
+
3
+ # The only pre-Node entry. No runtime/business dispatch, telemetry, elevation,
4
+ # ExecutionPolicy override, or persistent PATH change belongs in this script.
5
+ function Invoke-NodeProbe([string]$Executable) {
6
+ if (-not (Test-Path -LiteralPath $Executable -PathType Leaf)) { return $null }
7
+ $process = New-Object System.Diagnostics.Process
8
+ try {
9
+ $process.StartInfo.FileName = $Executable
10
+ $process.StartInfo.Arguments = '-p "JSON.stringify({version:process.versions.node,platform:process.platform,arch:process.arch,executable:process.execPath})"'
11
+ $process.StartInfo.UseShellExecute = $false
12
+ $process.StartInfo.CreateNoWindow = $true
13
+ $process.StartInfo.RedirectStandardOutput = $true
14
+ $process.StartInfo.RedirectStandardError = $true
15
+ if (-not $process.Start()) { return $null }
16
+ $stdout = $process.StandardOutput.ReadToEndAsync()
17
+ $stderr = $process.StandardError.ReadToEndAsync()
18
+ if (-not $process.WaitForExit(5000)) { $process.Kill(); return $null }
19
+ if ($process.ExitCode -ne 0) { return $null }
20
+ $value = $stdout.Result | ConvertFrom-Json -ErrorAction Stop
21
+ if ($value.platform -ne 'win32' -or $value.arch -ne 'x64' -or
22
+ $value.version -notmatch '^([0-9]+)\.[0-9]+\.[0-9]+$' -or [int]$Matches[1] -lt 18 -or
23
+ -not [IO.Path]::IsPathRooted($value.executable)) { return $null }
24
+ return $value
25
+ } catch { return $null } finally { $process.Dispose() }
26
+ }
27
+
28
+ function Find-LocalNode([string]$Root) {
29
+ $candidates = @()
30
+ $candidates += @(Get-Command node.exe -CommandType Application -All -ErrorAction SilentlyContinue | ForEach-Object { $_.Source })
31
+ foreach ($directory in @($env:ProgramFiles, $env:LOCALAPPDATA)) {
32
+ if ($directory) {
33
+ $candidates += (Join-Path $directory 'nodejs/node.exe')
34
+ $candidates += (Join-Path $directory 'Programs/nodejs/node.exe')
35
+ }
36
+ }
37
+ if ($env:NVM_SYMLINK) { $candidates += (Join-Path $env:NVM_SYMLINK 'node.exe') }
38
+ if (Test-Path -LiteralPath $Root -PathType Container) {
39
+ $candidates += @(Get-ChildItem -LiteralPath $Root -Directory -Filter 'node-*' -ErrorAction SilentlyContinue |
40
+ ForEach-Object { Get-ChildItem -LiteralPath $_.FullName -Directory -Filter 'node-v*-win-x64' -ErrorAction SilentlyContinue } |
41
+ ForEach-Object { Join-Path $_.FullName 'node.exe' })
42
+ }
43
+ foreach ($candidate in @($candidates | Select-Object -Unique)) {
44
+ $found = Invoke-NodeProbe $candidate
45
+ if ($null -ne $found) { return $found }
46
+ }
47
+ return $null
48
+ }
49
+
50
+ function Receive-NodeFile([string]$Uri, [string]$Destination) {
51
+ # URLs are constructed only from the official index and a validated version.
52
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
53
+ $ProgressPreference = 'SilentlyContinue'
54
+ try { Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $Destination -TimeoutSec 60 -ErrorAction Stop | Out-Null }
55
+ catch { throw 'DOWNLOAD_FAILED' }
56
+ }
57
+
58
+ function Install-UserNode([string]$Root) {
59
+ $staging = Join-Path $Root ('node-' + [Guid]::NewGuid().ToString('N'))
60
+ New-Item -ItemType Directory -Path $staging -ErrorAction Stop | Out-Null
61
+ $complete = $false
62
+ try {
63
+ $indexFile = Join-Path $staging 'index.json'
64
+ Receive-NodeFile 'https://nodejs.org/dist/index.json' $indexFile
65
+ $releases = @(Get-Content -LiteralPath $indexFile -Raw | ConvertFrom-Json)
66
+ $release = $releases | Where-Object {
67
+ $_.lts -is [string] -and $_.lts.Length -gt 0 -and $_.version -match '^v[0-9]+\.[0-9]+\.[0-9]+$' -and
68
+ [version]$_.version.Substring(1) -ge [version]'18.0.0' -and $_.files -contains 'win-x64-zip'
69
+ } | Sort-Object { [version]$_.version.Substring(1) } -Descending | Select-Object -First 1
70
+ if ($null -eq $release) { throw 'NO_LTS_ASSET' }
71
+ $name = 'node-' + $release.version + '-win-x64'
72
+ $zipName = $name + '.zip'
73
+ $base = 'https://nodejs.org/dist/' + $release.version + '/'
74
+ $checksums = Join-Path $staging 'SHASUMS256.txt'
75
+ Receive-NodeFile ($base + 'SHASUMS256.txt') $checksums
76
+ $pattern = '^([0-9a-fA-F]{64})\s+' + [regex]::Escape($zipName) + '$'
77
+ $lines = @(Get-Content -LiteralPath $checksums | Where-Object { $_ -match $pattern })
78
+ if ($lines.Count -ne 1 -or $lines[0] -notmatch $pattern) { throw 'INTEGRITY_FAILED' }
79
+ $expected = $Matches[1]
80
+ $archive = Join-Path $staging $zipName
81
+ Receive-NodeFile ($base + $zipName) $archive
82
+ if ((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash -ine $expected) { throw 'INTEGRITY_FAILED' }
83
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
84
+ [IO.Compression.ZipFile]::ExtractToDirectory($archive, $staging)
85
+ $node = Invoke-NodeProbe (Join-Path $staging ($name + '/node.exe'))
86
+ if ($null -eq $node -or $node.version -ne $release.version.Substring(1)) { throw 'VERIFY_FAILED' }
87
+ Remove-Item -LiteralPath $indexFile, $checksums, $archive -Force
88
+ $complete = $true
89
+ return $node
90
+ } finally {
91
+ if (-not $complete) { Remove-Item -LiteralPath $staging -Recurse -Force -ErrorAction SilentlyContinue }
92
+ }
93
+ }
94
+
95
+ function Invoke-NodeBootstrap([string]$Action, [string]$Root) {
96
+ $found = Find-LocalNode $Root
97
+ if ($null -ne $found) { return @{status='READY'; nodePath=$found.executable; version=$found.version} }
98
+ if ($Action -eq 'check') { return @{status='NEEDS_SETUP'; reason='NODE_MISSING_OR_UNSUPPORTED'} }
99
+ New-Item -ItemType Directory -Path $Root -Force -ErrorAction Stop | Out-Null
100
+ if ((Get-Item -LiteralPath $Root).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'UNSAFE_DIRECTORY' }
101
+ $lock = $null
102
+ try {
103
+ try { $lock = [IO.File]::Open((Join-Path $Root 'node.lock'), 'OpenOrCreate', 'ReadWrite', 'None') }
104
+ catch { throw 'ENVIRONMENT_BUSY' }
105
+ $stateFile = Join-Path $Root 'node-environment.json'
106
+ $state = @{prepareAttempts=0; repairAttempts=0}
107
+ if (Test-Path -LiteralPath $stateFile) {
108
+ try {
109
+ $loaded = Get-Content -LiteralPath $stateFile -Raw | ConvertFrom-Json
110
+ foreach ($key in @('prepareAttempts', 'repairAttempts')) {
111
+ if ($loaded.$key -isnot [int] -and $loaded.$key -isnot [long]) { throw 'invalid' }
112
+ if ($loaded.$key -lt 0) { throw 'invalid' }
113
+ $state[$key] = $loaded.$key
114
+ }
115
+ } catch { throw 'ENVIRONMENT_STATE_INVALID' }
116
+ }
117
+ if (($Action -eq 'repair' -and $state.repairAttempts -ge 1) -or
118
+ ($Action -eq 'prepare' -and ($state.prepareAttempts -ge 1 -or $state.repairAttempts -ge 1))) { throw 'RETRY_BUDGET_EXHAUSTED' }
119
+ $state.prepareAttempts++
120
+ if ($Action -eq 'repair') { $state.repairAttempts++ }
121
+ [IO.File]::WriteAllText($stateFile, ($state | ConvertTo-Json -Compress))
122
+ $found = Install-UserNode $Root
123
+ return @{status='READY'; nodePath=$found.executable; version=$found.version}
124
+ } finally { if ($null -ne $lock) { $lock.Dispose() } }
125
+ }
126
+
127
+ if ($MyInvocation.InvocationName -ne '.') {
128
+ $ErrorActionPreference = 'Stop'
129
+ try {
130
+ $architecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
131
+ if ($env:OS -ne 'Windows_NT' -or $architecture -ne 'AMD64' -or -not $env:LOCALAPPDATA) { throw 'UNSUPPORTED_PLATFORM' }
132
+ $result = Invoke-NodeBootstrap $Mode (Join-Path $env:LOCALAPPDATA 'alipay-aipay-pre/bootstrap')
133
+ } catch {
134
+ $reason = [string]$_.Exception.Message
135
+ if ($reason -notmatch '^(DOWNLOAD_FAILED|NO_LTS_ASSET|INTEGRITY_FAILED|VERIFY_FAILED|UNSAFE_DIRECTORY|ENVIRONMENT_BUSY|ENVIRONMENT_STATE_INVALID|RETRY_BUDGET_EXHAUSTED|UNSUPPORTED_PLATFORM)$') { $reason = 'SETUP_FAILED' }
136
+ $result = @{status='BLOCKED'; reason=$reason}
137
+ }
138
+ [Console]::Error.WriteLine('ALIPAY_AIPAY_INTERNAL:NODE_BOOTSTRAP=' + ($result | ConvertTo-Json -Compress))
139
+ if ($result.status -ne 'READY') { exit 78 }
140
+ }
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import {spawnSync} from 'node:child_process';
4
+
5
+ export const installCommand = String.raw`
6
+ $ErrorActionPreference = 'Stop'
7
+ $env:ALIPAY_CLI_BIN = "$HOME\.local\bin"
8
+ $env:ALIPAY_CLI_SKIP_VERIFY = 'true'
9
+ try {
10
+ Invoke-RestMethod -UseBasicParsing -Uri 'https://opengw.alipay.com/alipaycli/install_ps1' -TimeoutSec 20 -ErrorAction Stop | Invoke-Expression
11
+ if ($global:LASTEXITCODE -ne 0) { exit 1 }
12
+ } catch {
13
+ $code = $_.Exception.Data['InstallerExitCode']
14
+ if ($code -in @(3, 4, 5)) { exit ([int]$code) }
15
+ if ($_.Exception -is [System.Net.WebException]) { exit 3 }
16
+ if ($_.Exception -is [System.UnauthorizedAccessException]) { exit 4 }
17
+ exit 1
18
+ }
19
+ `;
20
+
21
+ export function refreshWindowsCli({env = process.env, home = os.homedir(), run = spawnSync} = {}) {
22
+ const childEnv = {...env};
23
+ // Keep business context and installer overrides out of the official install.
24
+ for (const key of Object.keys(childEnv)) {
25
+ if (['PLATFORM', 'PLATFORM_ID', 'PRODUCT', 'ALIPAYCLI_BIZ_INFO', 'PSMODULEPATH',
26
+ 'ALIPAY_CLI_BIN', 'ALIPAY_CLI_SKIP_VERIFY', 'ALIPAY_CLI_VERSION',
27
+ 'ALIPAY_CLI_CHANNEL', 'ALIPAY_CLI_MANIFEST_URL',
28
+ 'ALIPAY_CLI_AFTS_MANIFEST_BASE'].includes(key.toUpperCase())) delete childEnv[key];
29
+ }
30
+ const command = path.win32.join(env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
31
+ const options = {env: childEnv, encoding: 'utf8', shell: false, windowsHide: true,
32
+ maxBuffer: 30 * 1024 * 1024, timeout: 60000};
33
+ try {
34
+ const result = run(command, ['-NoProfile', '-NonInteractive', '-Command', installCommand], options);
35
+ const output = `${result.stdout || ''}\n${result.stderr || ''}\n${result.error?.message || ''}`;
36
+ if (result.error || result.status !== 0) {
37
+ if (result.status === 3) return 'RETRY_WITH_NETWORK';
38
+ if (result.status === 4) return 'RETRY_WITH_LOCAL_FS_PERMISSION';
39
+ if (result.status === 5) return 'VERIFY_FAILED';
40
+ if (result.error?.code === 'ETIMEDOUT' ||
41
+ /could not resolve|failed to connect|connection (?:refused|reset)|timed? out|timeout|network|socket|tls|ssl|ENOTFOUND|ECONNREFUSED|EAI_AGAIN/iu.test(output)) return 'RETRY_WITH_NETWORK';
42
+ if (/permission denied|operation not permitted|EACCES|EPERM|access denied/iu.test(output)) return 'RETRY_WITH_LOCAL_FS_PERMISSION';
43
+ return 'FAILED';
44
+ }
45
+ const verify = run(path.join(home, '.local', 'bin', 'alipay-cli.exe'), ['version'],
46
+ {...options, stdio: 'ignore', timeout: 5000});
47
+ return !verify.error && verify.status === 0 ? 'READY' : 'VERIFY_FAILED';
48
+ } catch {
49
+ return 'FAILED';
50
+ }
51
+ }
@@ -0,0 +1,11 @@
1
+ import {spawnSync} from 'node:child_process';
2
+ import {ensureReady} from './preflight.mjs';
3
+ try {
4
+ const {cli,env}=ensureReady();
5
+ const args=process.argv.slice(2);
6
+ if(args[0]==='file'&&args[1]==='upload'&&/^\/[a-zA-Z]\//.test(args[2]||''))args[2]=args[2][1]+':'+args[2].slice(2);
7
+ if(args.reduce((n,a)=>n+a.length+3,0)>28000)throw new Error('COMMAND_LINE_TOO_LONG');
8
+ const r=spawnSync(cli,args,{env,stdio:'inherit',windowsHide:true,shell:false,timeout:60000});
9
+ if(r.error)process.stderr.write('ALIPAY_AIPAY_INTERNAL:WINDOWS_CLI_ERROR='+r.error.code+'; result may be unknown; do not replay writes\n');
10
+ process.exitCode=r.status??124;
11
+ }catch(e){process.stderr.write('ALIPAY_AIPAY_INTERNAL:WINDOWS_CLI_ERROR='+e.message+'\n');process.exitCode=78;}