@alipay/alipay-aipay 1.7.5 → 1.7.6-beta.1

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 (27) hide show
  1. package/bin/alipay-aipay.js +9 -4
  2. package/dist/utils/file.js +27 -4
  3. package/package.json +2 -2
  4. package/skill/SKILL.md +11 -3
  5. package/skill/references/normal/json/npm-origin.json +1 -0
  6. package/skill/references/normal/scripts/channel_offer.mjs +58 -16
  7. package/skill/references/normal/scripts/self_update.mjs +3 -1
  8. package/skill/references/normal/self-update.md +1 -1
  9. package/skill-pre/SKILL.md +3 -1
  10. package/skill-pre/VERSION +1 -1
  11. package/skill-pre/references/integration/flow.md +2 -2
  12. package/skill-pre/references/normal/full-process-routing.md +1 -1
  13. package/skill-pre/references/normal/json/npm-origin.json +1 -0
  14. package/skill-pre/references/normal/scripts/onboarding_message_runner.mjs +5 -1
  15. package/skill-pre/references/normal/scripts/runtime.mjs +2 -0
  16. package/skill-pre/references/normal/scripts/self_update.mjs +3 -1
  17. package/skill-pre/references/normal/scripts/windows/git-discovery.mjs +5 -2
  18. package/skill-pre/references/normal/scripts/windows/git-install.mjs +42 -0
  19. package/skill-pre/references/normal/scripts/windows/policy.json +5 -0
  20. package/skill-pre/references/normal/scripts/windows/preflight.mjs +23 -3
  21. package/skill-pre/references/normal/scripts/windows/prepare.mjs +96 -32
  22. package/skill-pre/references/normal/scripts/windows/python-runtime.mjs +2 -2
  23. package/skill-pre/references/normal/scripts/windows/routing.mjs +15 -17
  24. package/skill-pre/references/normal/self-update.md +1 -1
  25. package/skill-pre/references/normal/windows-environment.md +12 -15
  26. package/skill-pre/references/onboarding/flow.md +1 -1
  27. package/skill-pre/references/runtime-entry.md +1 -1
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import('../dist/index.js').catch((err) => {
4
- console.error('Failed to start CLI:', err);
5
- process.exit(1);
6
- });
3
+ if (process.platform === 'win32' && Number(process.versions.node.split('.')[0]) < 18) {
4
+ console.error('Windows 安装需要 Node.js >=18 及其配套 npm/npx。请安装受支持的 Node.js LTS,重新打开终端后再执行安装命令:https://nodejs.org/en/download');
5
+ process.exitCode = 1;
6
+ } else {
7
+ import('../dist/index.js').catch((err) => {
8
+ console.error('Failed to start CLI:', err);
9
+ process.exit(1);
10
+ });
11
+ }
@@ -239,6 +239,24 @@ export async function writeVersionRecordWithFallback(ctx, record) {
239
239
  export async function installSkillFiles(packageRoot, targetDir, channel = 'stable') {
240
240
  return installSkillFilesAtPath(packageRoot, path.join(targetDir, getChannelConfig(channel).skillName), { channel });
241
241
  }
242
+ // Directory replacement can be briefly refused by Windows. Keep the original
243
+ // transaction/rollback semantics; never fall back to mixing files in place.
244
+ export async function renameInstallDirectory(source, target, options = {}) {
245
+ const waits = (options.platform ?? process.platform) === 'win32' ? [100, 200, 400, 800, 1000] : [];
246
+ const rename = options.rename ?? fs.rename;
247
+ const pause = options.pause ?? delay;
248
+ for (let attempt = 0;; attempt += 1) {
249
+ try {
250
+ await rename(source, target);
251
+ return;
252
+ }
253
+ catch (error) {
254
+ if (!['EPERM', 'EACCES', 'EBUSY'].includes(error.code ?? '') || attempt >= waits.length)
255
+ throw error;
256
+ await pause(waits[attempt]);
257
+ }
258
+ }
259
+ }
242
260
  /**
243
261
  * 原子安装到已经确定的单个 Skill 目录。自更新使用事务 ID,便于超时恢复。
244
262
  */
@@ -284,16 +302,21 @@ export async function installSkillFilesAtPath(packageRoot, skillTargetDir, optio
284
302
  await fs.copyFile(path.join(skillSourceDir, 'VERSION'), path.join(stagingDir, 'VERSION'));
285
303
  if (await pathExists(resolvedSkillTargetDir)) {
286
304
  try {
287
- await fs.rename(resolvedSkillTargetDir, backupDir);
305
+ await renameInstallDirectory(resolvedSkillTargetDir, backupDir);
288
306
  previousInstallMoved = true;
289
307
  }
290
308
  catch (error) {
291
- if (error.code !== 'ENOENT')
309
+ if (error.code !== 'ENOENT') {
310
+ if (process.platform === 'win32') {
311
+ const code = error.code || 'UNKNOWN';
312
+ throw Object.assign(new Error(`Windows Skill 目录备份失败(${code}),未切换新版,旧目录保持原位:${resolvedSkillTargetDir}`), { code, phase: 'BACKUP', cause: error });
313
+ }
292
314
  throw error;
315
+ }
293
316
  }
294
317
  }
295
318
  try {
296
- await fs.rename(stagingDir, resolvedSkillTargetDir);
319
+ await renameInstallDirectory(stagingDir, resolvedSkillTargetDir);
297
320
  stagingCommitted = true;
298
321
  }
299
322
  catch (installError) {
@@ -310,7 +333,7 @@ export async function installSkillFilesAtPath(packageRoot, skillTargetDir, optio
310
333
  }
311
334
  if (previousInstallMoved) {
312
335
  try {
313
- await fs.rename(backupDir, resolvedSkillTargetDir);
336
+ await renameInstallDirectory(backupDir, resolvedSkillTargetDir);
314
337
  previousInstallMoved = false;
315
338
  }
316
339
  catch (restoreError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alipay/alipay-aipay",
3
- "version": "1.7.5",
3
+ "version": "1.7.6-beta.1",
4
4
  "description": "支付宝 AI 付接入 Skill 安装包 - 优先安装 Agent Skill,并在 macOS/Linux 后台准备用户级 CLI 工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -56,6 +56,6 @@
56
56
  },
57
57
  "alipayAipayChannels": {
58
58
  "stable": "1.7.2",
59
- "pre": "1.7.4-pre"
59
+ "pre": "1.7.5-pre"
60
60
  }
61
61
  }
package/skill/SKILL.md CHANGED
@@ -11,7 +11,7 @@ description: >-
11
11
 
12
12
  ## 前置启动协议
13
13
 
14
- 本目录的 `references/normal/json/release-channel.json` 固定当前渠道;正式版为 `stable`,预发版为 `pre`。自更新只比较 npm latest 元数据中的本渠道 Skill 版本与本地 `VERSION`,不以 npm 包版本判断是否需要更新,不切换或扫描另一渠道。缺少合法渠道元数据时保留当前版本。 本地 beta 测试版本固定使用当前版本,不查询 npm、不自动升级或转正;常规版本只接收本渠道非 beta 版本。此判断由同一自更新命令完成,Agent 不自行判断或修改 VERSION。
14
+ 本目录的 `references/normal/json/release-channel.json` 固定当前渠道;正式版为 `stable`,预发版为 `pre`。自更新只比较 npm latest 元数据中的本渠道 Skill 版本与本地 `VERSION`,不以 npm 包版本判断是否需要更新,不切换或扫描另一渠道。缺少合法渠道元数据时保留当前版本。来自 npm beta 包的安装目录带有内部来源文件 `references/normal/json/npm-origin.json`,测试期间固定当前目录,不查询 latest、不自动升级或转正;常规版本仍只接收本渠道正式版本。此判断由同一自更新命令完成,Agent 不自行判断或修改 VERSION。
15
15
 
16
16
  本次 Agent 对话首次使用 `alipay-aipay` 时,先把本文件所在目录解析为规范化绝对路径 `<SKILL_DIR>`,记录一次性事实 `ALIPAY_AIPAY_SELF_UPDATE_CHECKED=true`,再执行以下单条命令。`<SKILL_DIR>` 是执行时替换的路径占位符;禁止用 shell 变量赋值、命令替换或依赖特定 shell:
17
17
 
@@ -44,13 +44,21 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" channel-offer decide --
44
44
 
45
45
  仅唯一 `ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER=PRE` 可进入内测安装;`STABLE` 表示继续本正式版,记录已处理并按下文刷新 CLI。`UNDECIDED` 表示用户在提问或表达不确定,先回答或澄清,仍等待明确选择;不能把业务催促、泛泛的“继续/OK/1”或通用 `reply confirm` 当作内测授权。任一错误或 marker 不唯一时停止渠道切换,禁止安装。用户以后明确提出切换时,可重新进行这一选择,不受已处理事实限制。
46
46
 
47
- `PRE` 后先执行固定只读预检,确认 npm latest 的预发渠道已包含 Windows 改造版;本地当前工作区或尚未发布的版本不能代替线上事实:
47
+ `PRE` 后先执行固定只读预检。常规正式版核对 npm latest 的预发渠道是否包含 Windows 改造版;来自 npm beta 包的正式版只核对 npm `beta` 标签,要求包版本与当前安装来源文件一致、其中 stable 版本与当前 `VERSION` 一致,并包含支持 Windows 的内测版。两者均以线上事实为准,本地工作区或尚未发布的版本不能代替:
48
48
 
49
49
  ```text
50
50
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" channel-offer available
51
51
  ```
52
52
 
53
- 仅唯一 `ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER=AVAILABLE` 才执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" channel-offer install --reply "<USER_REPLY>"`,传入此前被判为 `PRE` 的同一条用户原始回复;安装器会在执行前再次确认明确选择和线上版本。Windows 优先复用当前 Node 附带或已有 npm 缓存中的 npm/npx JS 入口,其次查找现成 `.cmd` shim;缺少 npm 命令时版本核验可只读查询 npm 官方 registry。此步骤不下载 Node、不创建临时 shim 或修改 PATH。`UNAVAILABLE|CHECK_FAILED` 时说明线上内测版尚不可核实或尚未包含 Windows 能力,保留正式版并停在当前选择,不安装旧内测版;用户可明确要求继续正式版。核验器输出的内部 `CHANNEL_OFFER_REASON` 只用于区分工具、网络或元数据失败,不对客。仅安装退出码为 0 且唯一 `CHANNEL_OFFER=INSTALLED` 时,才从安装器报告的当前 Agent 目标目录核验 `alipay-aipay-pre/SKILL.md`、`VERSION` 和 `references/runtime-entry.md` 均为普通文件,渠道为 `pre`、版本不低于 `1.7.4-pre`,且该目录确实可由当前 Agent 加载;不能从旧预发版路径或当前正式版目录推定安装成功。核验成功后废弃正式版入口及刷新事实,使用实际预发版规范化绝对路径重新读取其 `SKILL.md` 并按预发版启动协议继续原任务。安装失败或核验失败时保持正式版,不宣称切换成功,也不进入预发业务流程。此选择发生在正式版 telemetry 启动前,不为渠道提示或预发安装创建正式版 run。
53
+ 仅唯一 `ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER=AVAILABLE` 才安装。常规正式版仍执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" channel-offer install --reply "<USER_REPLY>"`。来自 npm beta 包的正式版必须先取得唯一合法的 `ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER_PACKAGE_VERSION=<npm beta SemVer>` 和 `ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER_PRE_VERSION=<内测版 Skill 版本>`,再执行:
54
+
55
+ ```text
56
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" channel-offer install --reply "<USER_REPLY>" --expected-package-version "<核验得到的 npm beta 版本>" --expected-pre-version "<核验得到的内测版 Skill 版本>"
57
+ ```
58
+
59
+ `USER_REPLY` 必须是此前判为 `PRE` 的同一条用户原始回复。安装器重新查询线上标签;包版本、正式版版本或内测版版本变化时在写入前停止,成功时使用核验所得精确 npm 包版本。Windows 优先复用当前 Node 附带或已有 npm 缓存中的 npm/npx JS 入口,其次查找现成 `.cmd` shim;缺少 npm 命令时版本核验可只读查询 npm 官方 registry。此步骤不下载 Node、不创建临时 shim 或修改 PATH。`UNAVAILABLE|CHECK_FAILED` 时说明线上内测版尚不可核实或尚未包含 Windows 能力,保留正式版并停在当前选择,不安装旧内测版;用户可明确要求继续正式版。核验器输出的内部 `CHANNEL_OFFER_REASON` 只用于区分工具、网络或元数据失败,不对客。仅安装退出码为 0 且唯一 `CHANNEL_OFFER=INSTALLED` 时,才从安装器报告的当前 Agent 目标目录核验 `alipay-aipay-pre/SKILL.md`、`VERSION` 和 `references/runtime-entry.md` 均为普通文件,渠道为 `pre`、版本不低于 `1.7.4-pre`;来自 npm beta 包时还须核对内测版 `VERSION` 等于刚才线上核验的版本,且其 `npm-origin.json` 与正式版安装来源的包版本和标签一致。该目录还须确实可由当前 Agent 加载;不能从旧预发版路径或当前正式版目录推定安装成功。核验成功后废弃正式版入口及刷新事实,使用实际预发版规范化绝对路径重新读取其 `SKILL.md` 并按预发版启动协议继续原任务。安装失败或核验失败时保持正式版,不宣称切换成功,也不进入预发业务流程。此选择发生在正式版 telemetry 启动前,不为渠道提示或预发安装创建正式版 run。
60
+
61
+ 来自 npm beta 包的 `available` 和 `install` 还须各取得唯一合法的 `ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER_PRE_VERSION=<内测版 Skill 版本>`,两次值完全一致,且安装后目录中的 `VERSION` 等于该值;任一缺失或冲突都不视为切换成功。现有对客提示保持原文,其中手动 `@latest` 安装命令属于常规发布路径;本测试包收到明确内测意图后必须走上述受控切换,不能执行该手动命令代替。
54
62
 
55
63
  仅确定继续正式版后,若当前上下文没有与规范化 `SKILL_DIR`、合法 `VERSION` 绑定的 `ALIPAY_AIPAY_CLI_REFRESHED=true`,必须刷新一次 `alipay-cli`;不得因自更新已检查而跳过。仅唯一终态 `READY` 可在当前对话上下文记录该成功事实、路径和版本,不写缓存或业务状态;三者仍可用且一致时,后续根触发、确认、补材料、候选选择、恢复、中断继续和 Full Process 衔接均不重复刷新。
56
64
 
@@ -0,0 +1 @@
1
+ {"packageVersion":"1.7.6-beta.1","distTag":"beta"}
@@ -10,6 +10,20 @@ import { isWindowsPlatform } from './platform_compat.mjs';
10
10
  import { renderMessage } from './render_customer_message.mjs';
11
11
 
12
12
  const marker = 'ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER=';
13
+ const betaPackagePattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-beta\.\d+$/u;
14
+ const localBetaOrigin = () => {
15
+ const skillDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
16
+ const originFile = path.join(skillDir, 'references/normal/json/npm-origin.json');
17
+ if (!fs.existsSync(originFile)) return null;
18
+ try {
19
+ const origin = JSON.parse(fs.readFileSync(originFile, 'utf8'));
20
+ const stableVersion = fs.readFileSync(path.join(skillDir, 'VERSION'), 'utf8').trim();
21
+ if (origin.distTag === 'beta' && betaPackagePattern.test(origin.packageVersion) && versionPattern.test(stableVersion)) {
22
+ return {packageVersion: origin.packageVersion, stableVersion};
23
+ }
24
+ } catch {}
25
+ throw new Error('测试包来源元数据无效');
26
+ };
13
27
  const explicitPre = /(?:安装|使用|切换到?|切到|转到|换到|换成|改用|改成|选择|试用|试试|体验|启用|用|装|我要|我选|给我)(?:这个|该|一下|下)?(?:内测版|预发版|alipay-aipay-pre)|(?:内测版|预发版|alipay-aipay-pre).{0,6}(?:我想|我要|请)(?:安装|使用|切换|试用|体验)|\b(?:use|install|switch\s+to|try)\s+(?:the\s+)?(?:pre(?:view)?\s+version|alipay-aipay-pre)\b/iu;
14
28
  const negativePre = /(?:不想|不|别|不要|无需|暂不|先不|取消|拒绝)\s*(?:给我|帮我|让我)?\s*(?:安装|切换到?|切到|使用|选择|试用|用|装)?\s*(?:内测版|预发版)|(?:内测版|预发版)\s*(?:不用|不要|算了)|\b(?:no|not|don't|cancel)\b/iu;
15
29
  const stableChoice = /(?:^|[,,。;;\s])(?:继续|使用|保持|选择|留在)(?:当前)?(?:的)?(?:正式版|稳定版)|(?:正式版|稳定版).{0,8}(?:继续|就好|即可)|\bstable\b/iu;
@@ -91,26 +105,35 @@ export function packageManagerInvocation(tool, args, {
91
105
 
92
106
  const single = value => Array.isArray(value) && value.length === 1 ? value[0] : value;
93
107
  const versionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
94
- export function parsePublishedPre(stdout) {
108
+ export function parsePublishedPre(stdout, {betaOrigin = null} = {}) {
95
109
  let metadata;
96
110
  try { metadata = single(JSON.parse(stdout)); } catch { return {status: 'CHECK_FAILED', reason: 'INVALID_METADATA'}; }
97
111
  const packageVersion = single(metadata?.version);
98
- if (typeof packageVersion !== 'string' || !versionPattern.test(packageVersion) ||
99
- !packageVersion.split('.').map(Number).every(Number.isSafeInteger)) return {status: 'CHECK_FAILED', reason: 'INVALID_METADATA'};
112
+ const packagePattern = betaOrigin ? betaPackagePattern : versionPattern;
113
+ const packageMatch = typeof packageVersion === 'string' ? packagePattern.exec(packageVersion) : null;
114
+ if (!packageMatch || !packageMatch.slice(1,4).map(Number).every(Number.isSafeInteger)) {
115
+ return {status: 'CHECK_FAILED', reason: 'INVALID_METADATA'};
116
+ }
117
+ if (betaOrigin && (packageVersion !== betaOrigin.packageVersion ||
118
+ single(single(metadata?.alipayAipayChannels)?.stable) !== betaOrigin.stableVersion)) {
119
+ return {status: 'UNAVAILABLE', reason: 'BETA_STABLE_MISMATCH'};
120
+ }
100
121
  const value = single(single(metadata?.alipayAipayChannels)?.pre);
101
122
  if (value === undefined) return {status: 'UNAVAILABLE', reason: 'PRE_CHANNEL_MISSING'};
102
- const match = typeof value === 'string' ? /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-pre$/u.exec(value) : null;
123
+ const prePattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-pre$/u;
124
+ const match = typeof value === 'string' ? prePattern.exec(value) : null;
103
125
  if (!match) return {status: 'CHECK_FAILED', reason: 'INVALID_METADATA'};
104
126
  const [major, minor, patch] = match.slice(1).map(Number);
105
127
  if (![major, minor, patch].every(Number.isSafeInteger)) return {status: 'CHECK_FAILED', reason: 'INVALID_METADATA'};
106
128
  return major > 1 || (major === 1 && (minor > 7 || (minor === 7 && patch >= 4)))
107
- ? {status: 'AVAILABLE', packageVersion} : {status: 'UNAVAILABLE', reason: 'PRE_VERSION_TOO_OLD'};
129
+ ? {status: 'AVAILABLE', packageVersion, ...(betaOrigin ? {preVersion: value} : {})}
130
+ : {status: 'UNAVAILABLE', reason: 'PRE_VERSION_TOO_OLD'};
108
131
  }
109
132
  export const publishedPreFromMetadata = stdout => parsePublishedPre(stdout).status;
110
133
 
111
- export function registryMetadata() {
134
+ export function registryMetadata(tag = 'latest') {
112
135
  return new Promise((resolve, reject) => {
113
- const request = https.get('https://registry.npmjs.org/@alipay%2falipay-aipay/latest',
136
+ const request = https.get(`https://registry.npmjs.org/@alipay%2falipay-aipay/${tag}`,
114
137
  {headers: {accept: 'application/json'}}, response => {
115
138
  if (response.statusCode !== 200) { response.resume(); reject(new Error('REGISTRY_HTTP_ERROR')); return; }
116
139
  let data = ''; let bytes = 0;
@@ -130,18 +153,19 @@ export function registryMetadata() {
130
153
  }
131
154
 
132
155
  export async function publishedWindowsPreAvailable({
133
- run = spawnSync, readRegistry = registryMetadata,
156
+ run = spawnSync, readRegistry = registryMetadata, betaOrigin = null,
134
157
  invocationOptions = {windows: process.platform === 'win32'},
135
158
  } = {}) {
136
- const invocation = packageManagerInvocation('npm', ['view', '@alipay/alipay-aipay@latest', 'version', 'alipayAipayChannels', '--json'], invocationOptions);
159
+ const tag = betaOrigin ? 'beta' : 'latest';
160
+ const invocation = packageManagerInvocation('npm', ['view', `@alipay/alipay-aipay@${tag}`, 'version', 'alipayAipayChannels', '--json'], invocationOptions);
137
161
  if (!invocation) {
138
- try { return parsePublishedPre(await readRegistry()); }
162
+ try { return parsePublishedPre(await readRegistry(tag), {betaOrigin}); }
139
163
  catch { return {status: 'CHECK_FAILED', reason: 'REGISTRY_UNREACHABLE'}; }
140
164
  }
141
165
  const result = run(invocation.command, invocation.args,
142
166
  {env: invocation.env, encoding: 'utf8', timeout: 10000, maxBuffer: 64 * 1024, windowsHide: true, shell: false});
143
167
  if (result.error || result.status !== 0) return {status: 'CHECK_FAILED', reason: 'NPM_QUERY_FAILED'};
144
- return parsePublishedPre(result.stdout);
168
+ return parsePublishedPre(result.stdout, {betaOrigin});
145
169
  }
146
170
 
147
171
  // Bound the whole installation tree so a timeout cannot leave a background install.
@@ -170,21 +194,34 @@ export function runPreInstaller(invocation) {
170
194
  }
171
195
 
172
196
  export async function installWindowsPre(reply, {
173
- query = publishedWindowsPreAvailable, install = runPreInstaller,
197
+ query = () => publishedWindowsPreAvailable({betaOrigin: localBetaOrigin()}), install = runPreInstaller,
174
198
  invocationOptions = {windows: process.platform === 'win32'},
199
+ expectedPackageVersion = null, expectedPreVersion = null,
175
200
  } = {}) {
176
201
  if (decideChannelOffer(reply) !== 'PRE') return {status: 'UNDECIDED', reason:'EXPLICIT_PRE_REQUIRED'};
177
202
  const published = await query();
178
203
  if (published.status !== 'AVAILABLE') return published;
204
+ if (expectedPackageVersion && (published.packageVersion !== expectedPackageVersion || published.preVersion !== expectedPreVersion)) {
205
+ return {status:'CHECK_FAILED',reason:'PUBLISHED_PACKAGE_CHANGED'};
206
+ }
179
207
  const spec = `@alipay/alipay-aipay@${published.packageVersion}`;
180
208
  const invocation = packageManagerInvocation('npx', ['-y', spec, 'install', '--channel', 'pre'], invocationOptions)
181
209
  || packageManagerInvocation('npm', ['exec', '--yes', '--package', spec, '--', 'alipay-aipay', 'install', '--channel', 'pre'], invocationOptions);
182
210
  if (!invocation) return {status: 'INSTALL_FAILED', reason:'NPM_TOOLCHAIN_MISSING'};
183
- return install(invocation);
211
+ return {...await install(invocation), preVersion: published.preVersion};
184
212
  }
185
213
 
186
214
  function report(outcome) {
187
215
  if (outcome.reason) process.stderr.write(`ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER_REASON=${outcome.reason}\n`);
216
+ if (typeof outcome.preVersion === 'string' &&
217
+ /^\d+\.\d+\.\d+-pre$/u.test(outcome.preVersion)) {
218
+ process.stderr.write(`ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER_PRE_VERSION=${outcome.preVersion}\n`);
219
+ }
220
+ if (outcome.status === 'AVAILABLE' &&
221
+ typeof outcome.packageVersion === 'string' &&
222
+ (versionPattern.test(outcome.packageVersion) || betaPackagePattern.test(outcome.packageVersion))) {
223
+ process.stderr.write(`ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER_PACKAGE_VERSION=${outcome.packageVersion}\n`);
224
+ }
188
225
  if (outcome.stdout) process.stdout.write(outcome.stdout);
189
226
  process.stderr.write(`${marker}${outcome.status}\n`);
190
227
  }
@@ -205,11 +242,16 @@ export async function runChannelOfferCli(argv = process.argv) {
205
242
  return;
206
243
  }
207
244
  if (action === 'available' && argv.length === 3 && isWindowsPlatform()) {
208
- report(await publishedWindowsPreAvailable());
245
+ report(await publishedWindowsPreAvailable({betaOrigin: localBetaOrigin()}));
209
246
  return;
210
247
  }
211
- if (action === 'install' && argv.length === 5 && argv[3] === '--reply' && isWindowsPlatform()) {
212
- const outcome = await installWindowsPre(argv[4]);
248
+ if (action === 'install' && isWindowsPlatform()) {
249
+ const betaOrigin=localBetaOrigin();
250
+ const regularArgs=argv.length===5 && argv[3]==='--reply';
251
+ const betaArgs=argv.length===9 && argv[3]==='--reply' && argv[5]==='--expected-package-version' && argv[7]==='--expected-pre-version' &&
252
+ betaPackagePattern.test(argv[6]) && /^\d+\.\d+\.\d+-pre$/u.test(argv[8]);
253
+ if (betaOrigin ? !betaArgs : !regularArgs) throw new Error('内测安装参数不完整');
254
+ const outcome = await installWindowsPre(argv[4],betaOrigin ? {expectedPackageVersion:argv[6],expectedPreVersion:argv[8]} : {});
213
255
  report(outcome);
214
256
  if (outcome.status !== 'INSTALLED') process.exitCode = 1;
215
257
  return;
@@ -345,7 +345,9 @@ export async function runSelfUpdateCli(argv = process.argv) {
345
345
  const installedVersion = readVersionFile(skillDir);
346
346
  const kind = skillVersionKind(installedVersion?.raw, channel);
347
347
  // A developer-installed beta is pinned, including when its entry is damaged.
348
- if (kind === 'beta') return local ? { kind: 'CURRENT', version: local } : { kind: 'VERIFY_FAILED' };
348
+ if (kind === 'beta' || fs.existsSync(path.join(skillDir, 'references', 'normal', 'json', 'npm-origin.json'))) {
349
+ return local ? { kind: 'CURRENT', version: local } : { kind: 'VERIFY_FAILED' };
350
+ }
349
351
  if (installedVersion && !kind) return { kind: 'CHECK_FAILED' };
350
352
  const checked = await npmLatest(channel);
351
353
  if (!checked.ok) {
@@ -4,4 +4,4 @@
4
4
 
5
5
  唯一 stderr 终态:`SELF_UPDATE:CURRENT=<合法 SemVer>`、`SELF_UPDATE:UPDATED=<合法 SemVer>`、`SELF_UPDATE:RETRY_WITH_NETWORK`、`SELF_UPDATE:SKIPPED_NETWORK_RESTRICTED`、`SELF_UPDATE:CHECK_FAILED`、`SELF_UPDATE:INSTALL_FAILED`、`SELF_UPDATE:VERIFY_FAILED`。`UPDATED` 用原子替换后的新版,其余用当前可用版;marker 异常读当前入口,不得称最新或更新成功;停止读旧版,读取同一根目录的新版入口。仅 `RETRY_WITH_NETWORK` 可取权后原样重试一次,拒绝、不支持或再次返回时读当前入口;一次网络恢复外,不重查、取权、用 `sudo`、要求用户操作或写更新状态。每次只查一次 npm latest 的 `version` 和 `alipayAipayChannels`;只比较本渠道 Skill 版本,npm 包版本仅用于精确下载;`VERSION` 缺失、非法、落后或业务入口缺失时只从查询所得精确 npm 包版本安装当前渠道一次。渠道由当前目录的 `references/normal/json/release-channel.json` 与 Skill 名称共同校验;旧正式版缺少渠道文件时只兼容 `stable`,预发版禁止回退正式版。缺少或非法的远端渠道元数据返回 `CHECK_FAILED`,不得按 npm 包版本兜底。不调用公开 `install --target all`;超时终止同一次调用的进程树并清理。
6
6
 
7
- 本地正式渠道的 `X.Y.Z-beta`、`X.Y.Z-betaN`(兼容 `-beta.N`)和预发渠道的 `X.Y.Z-pre-beta`、`X.Y.Z-pre-betaN` 均为固定测试版本。完整安装直接返回 `CURRENT` 且不查询 npm;业务入口损坏时返回 `VERIFY_FAILED`,仍不下载修复或转正。更换版本必须由测试人员显式执行 npm 安装器的 `install`,普通 `update` 同样不推进 beta。正常版本分别为 `X.Y.Z` 与 `X.Y.Z-pre`,远端对应渠道若为 beta 或名称不匹配,返回 `CHECK_FAILED`,不自动安装。beta 不参与版本排序,无自动追踪和自动转正。
7
+ 本地正式渠道的 `X.Y.Z-beta`、`X.Y.Z-betaN`(兼容 `-beta.N`)和预发渠道的 `X.Y.Z-pre-beta`、`X.Y.Z-pre-betaN`,以及带内部 `references/normal/json/npm-origin.json` 的 npm beta 测试包,均固定当前安装目录。完整安装直接返回 `CURRENT` 且不查询 npm;业务入口损坏时返回 `VERIFY_FAILED`,仍不下载修复或转正。更换测试包必须由测试人员显式执行 npm 安装器的 `install`。正常版本分别为 `X.Y.Z` 与 `X.Y.Z-pre`,远端对应渠道若为 beta 或名称不匹配,返回 `CHECK_FAILED`,不自动安装。beta 不参与版本排序,无自动追踪和自动转正。
@@ -10,6 +10,8 @@ description: >-
10
10
 
11
11
  ## Windows 的 Node 准备例外
12
12
 
13
+ 首次使用 `npx.cmd -y @alipay/alipay-aipay@latest install --channel pre` 前,需要本机有可用 Node.js >=18 及配套 npm/npx;Node 执行 JavaScript,npm/npx 获取并启动安装包,不能互相替代。若命令无法启动,先在已授权范围内准备官方 Node.js LTS 并重新打开终端,或按组织策略让宿主发现已有工具;此时 Skill 尚未下载,不能声称包内引导已执行。不要为运行安装命令修改 PowerShell 执行策略;有配套工具时使用 `npx.cmd`。下列引导只用于已经落盘的 Skill。
14
+
13
15
  仅 Windows 首次使用当前 Skill 且尚未绑定本轮 Node 时,在下面的自更新前先尝试直接执行 PATH 中的 `node.exe -p "JSON.stringify({version:process.versions.node,platform:process.platform,arch:process.arch,executable:process.execPath})"`。只有退出 0、输出为唯一 JSON 对象、`platform=win32`、`arch=x64`、版本为合法且 >=18 的 SemVer、`executable` 是本机普通文件的规范化绝对路径时,才绑定该 Node 并跳过 PowerShell 文件引导。其他结果不作为可用 Node。此只读探测不执行业务、不访问会话正文、不改变采集选择。先确定当前 Skill 的规范化绝对路径 `<SKILL_DIR>`;PATH 探测未找到合格 Node 时,再用下列固定 PowerShell 引导检查常见安装目录及此前准备的用户级 Node:
14
16
 
15
17
  ```powershell
@@ -28,7 +30,7 @@ powershell.exe -NoProfile -NonInteractive -File "<SKILL_DIR>/references/normal/s
28
30
 
29
31
  ## 前置启动协议
30
32
 
31
- 本目录的 `references/normal/json/release-channel.json` 固定当前渠道;正式版为 `stable`,预发版为 `pre`。自更新只比较 npm latest 元数据中的本渠道 Skill 版本与本地 `VERSION`,不以 npm 包版本判断是否需要更新,不切换或扫描另一渠道。缺少合法渠道元数据时保留当前版本。 本地 beta 测试版本固定使用当前版本,不查询 npm、不自动升级或转正;常规版本只接收本渠道非 beta 版本。此判断由同一自更新命令完成,Agent 不自行判断或修改 VERSION。
33
+ 本目录的 `references/normal/json/release-channel.json` 固定当前渠道;正式版为 `stable`,预发版为 `pre`。自更新只比较 npm latest 元数据中的本渠道 Skill 版本与本地 `VERSION`,不以 npm 包版本判断是否需要更新,不切换或扫描另一渠道。缺少合法渠道元数据时保留当前版本。来自 npm beta 包的安装目录带有内部来源文件 `references/normal/json/npm-origin.json`,测试期间固定当前目录,不查询 latest、不自动升级或转正;常规版本仍只接收本渠道正式版本。此判断由同一自更新命令完成,Agent 不自行判断或修改 VERSION。
32
34
 
33
35
  本次 Agent 对话首次使用 `alipay-aipay-pre` 时,先把本文件所在目录解析为规范化绝对路径 `<SKILL_DIR>`,记录一次性事实 `ALIPAY_AIPAY_PRE_SELF_UPDATE_CHECKED=true`,再执行以下单条命令。`<SKILL_DIR>` 是执行时替换的路径占位符;禁止用 shell 变量赋值、命令替换或依赖特定 shell:
34
36
 
package/skill-pre/VERSION CHANGED
@@ -1 +1 @@
1
- 1.7.4-pre
1
+ 1.7.5-pre
@@ -1,7 +1,7 @@
1
1
  <!-- ALIPAY_AIPAY_FLOW:INTEGRATION_CORE:START -->
2
2
  # 支付产品-代码开发流程
3
3
 
4
- 本 flow 的 Windows READY/LEGACY 指根入口的唯一兼容环境分流事实,或 Windows 环境文档明确修复后唯一、根意图匹配的新 READY 事实;后续命令附加当前有效的 `--windows-route`。Windows READY 通过 Git Bash 执行登记的快速沙箱脚本;Windows LEGACY 保留原 Node 独立开发和手工沙箱分支。两者都由当前 Agent 串行执行,不启用 Integration Worker。Windows 敏感文件以 NTFS DACL 设置并读回验证代替 POSIX `0600/0700`:只允许当前用户、SYSTEM 和 Administrators;不能证明保护有效时不得使用配置。CLI 业务命令、产品参数、沙箱终态和完成条件不变。
4
+ 本 flow 的 Windows READY/LEGACY 指根入口的唯一兼容环境分流事实,或 Windows 环境文档明确修复后唯一、根意图匹配的新 READY 事实;BLOCKED 停在环境恢复点,不进入本 flow。后续命令附加当前有效的 `--windows-route`。Windows READY 通过 Git Bash 执行登记的快速沙箱脚本;Windows LEGACY 保留原 Node 独立开发和手工沙箱分支。两者都由当前 Agent 串行执行,不启用 Integration Worker。Windows 敏感文件以 NTFS DACL 设置并读回验证代替 POSIX `0600/0700`:只允许当前用户、SYSTEM 和 Administrators;不能证明保护有效时不得使用配置。CLI 业务命令、产品参数、沙箱终态和完成条件不变。
5
5
 
6
6
  > ⚠️ **前置声明**:本 flow 仅支持 **AI 网页应用收款、AI 移动应用收款、AI 按量付费**三种产品的集成。原有“网站支付”“APP 支付”“按量付费”仍作为输入别名和资源目录名。**其他产品(当面付、订单码支付、JSAPI支付、预授权支付、商家扣款等)暂不支持**,如需集成请前往[支付宝开放平台](https://open.alipay.com/)查阅相关文档。
7
7
 
@@ -172,7 +172,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message win
172
172
  - `VERIFY_PENDING`:退出码为 0;脚本 stdout 已渲染唯一 `sandbox.configuration.pending/VERIFY` 正文,并产生唯一 `ALIPAY_AIPAY_INTERNAL:CUSTOMER_MESSAGE_DELIVERY_REQUIRED=sandbox.configuration.pending`;另有唯一带 `ALIPAY_AIPAY_INTERNAL:` 前缀的 `SANDBOX_PENDING_PATH=<同一目标项目绝对路径>/.alipay-sandbox.json` 和 `FLOW:SANDBOX_CONFIG_PENDING_VERIFY` 内部事实;不得出现 READY 标记。
173
173
  - `LOCAL_FS_RECOVERY`:仅创建分支、退出码 `77`,唯一包含上述 CREATE pending 两个事实及 `SANDBOX_LOCAL_FS_PERMISSION_MODE=CREATE`、`CLI_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION`;stdout 是现有 pending 正文,先暂存且不得发送,CLI 调用次数为 0。允许权限升级后原样重试一次:`READY` 时丢弃暂存正文;普通 `CREATE_PENDING` 或再次命中本结果时只发送重试 stdout 并接受其 pending 事实;拒绝或宿主不支持时发送首次 stdout 并接受首次 pending 事实。不得再次申请、新增消息或确认。
174
174
  8. 所有 `ALIPAY_AIPAY_INTERNAL:` 行只用于转移;宿主合并输出时也先整行剔除。`READY` 的 stdout 为空,静默记录 `READY` 并直接进入步骤 2;普通 pending 或按上项降级时,必须校验唯一 `CUSTOMER_MESSAGE_DELIVERY_REQUIRED=sandbox.configuration.pending`,下一动作将剔除内部行后的 stdout 原文独立送达,再记录 `CREATE_PENDING|VERIFY_PENDING` 并直接进入步骤 2。禁止追加 `jq|cat|grep|ls|stat` 或临时脚本;待配置时禁止读取、引用或复制无效/不存在配置的字段。
175
- Windows READY 若在调用沙箱脚本前收到非零及 `WINDOWS_BUSINESS_BLOCKED=ENVIRONMENT_UNAVAILABLE`,本次 CLI 调用为零,转 LEGACY,按第 4 项记录 `VERIFY_PENDING` 并继续独立开发。脚本已启动后的失败仍按第 9 项处理,不把超时或未知结果伪装为待配置。
175
+ Windows READY 若在调用沙箱脚本前收到非零及 `WINDOWS_BUSINESS_BLOCKED=ENVIRONMENT_UNAVAILABLE`,本次 CLI 调用为零,停在环境阻断点;按 Windows 环境文档只读复检或有限准备,取得 READY 后再继续。不得转 LEGACY 或重放已发出的业务请求。脚本已启动后的失败仍按第 9 项处理,不把超时或未知结果伪装为待配置。
176
176
 
177
177
  9. 除上项精确的 `LOCAL_FS_RECOVERY` 外,命令非 0、终态标记缺失/重复/冲突、路径不一致或输出含 `SANDBOX_ERROR` 时属于脚本执行协议失败,仍停留在步骤 1;不得把这种执行异常降级为待配置,也不得临场改写 jq、重放原始 MCP 命令或自行检查敏感配置。CLI 已执行后才出现本地状态权限证据时,创建请求属于 `MAYBE_SENT`,脚本不得返回恢复 marker 或重放,`ensure` 直接使用既有 `CREATE_PENDING` 正文和终态继续代码开发。Windows LEGACY 配置检查未通过仍按 Windows LEGACY 登记分支处理。项目内自建 JSON、Agent 自报或补充诊断结果都不能替代上述终态。
178
178
  10. 准备阶段不得渲染 `sandbox.environment.summary` 或 `sandbox.environment.reminder`,不得展示沙箱买家密码,也不得输出“沙箱测试通过”。只有 `sandboxConfigState=READY` 且代码开发完成并通过配置后置校验后,才按步骤 5 的摘要展示规则对客展示沙箱环境配置。
@@ -1,6 +1,6 @@
1
1
  # 完整接入编排
2
2
 
3
- 本文件是 `full_process` 唯一入口。Windows 复用根入口的 READY/LEGACY 分流,或明确修复后唯一、根意图匹配的新 READY 事实;命令携带当前有效的 `--windows-route`。READY 执行 Integration 快速沙箱及 Onboarding 本地流程,LEGACY 先完成可独立开发内容,再在原衔接点输出站点引导。两者都不重新安装、不创建 Integration Worker。启动方案送达前只复用 Integration 步骤 1 的项目、语言、当前系统分支环境和沙箱准备;不得开发代码、进入 Onboarding 或读取开通信息。
3
+ 本文件是 `full_process` 唯一入口。Windows 复用根入口的 READY/LEGACY 分流,或明确修复后唯一、根意图匹配的新 READY 事实;BLOCKED 先恢复环境,不进入本流程。命令携带当前有效的 `--windows-route`。READY 执行 Integration 快速沙箱及 Onboarding 本地流程,LEGACY 仅在不支持的平台映射或用户明确选站点时先完成可独立开发内容,再在原衔接点输出站点引导。两者都不重新安装、不创建 Integration Worker。启动方案送达前只复用 Integration 步骤 1 的项目、语言、当前系统分支环境和沙箱准备;不得开发代码、进入 Onboarding 或读取开通信息。
4
4
 
5
5
  目标项目和技术栈确定后执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" flow integration entry --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"`,只加载当前入口阶段并按步骤 1/2 准备及展示方案;代码开发满足衔接条件后执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" flow onboarding entry --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"`。禁止整读两个 flow。所有 Skill 自有运行时动作只用当前 Skill 绝对路径下的统一入口单命令执行,不前置 `cd`、不拼接管道;用户项目命令仍以已确认项目根为 workdir。
6
6
 
@@ -0,0 +1 @@
1
+ {"packageVersion":"1.7.6-beta.1","distTag":"beta"}
@@ -160,7 +160,11 @@ async function readStdin() {
160
160
  }
161
161
 
162
162
  function runPlatformEntry(args = {}) {
163
- if (windowsRouting && windowsRouting.onboardingRoute() === 'READY') return '';
163
+ if (windowsRouting) {
164
+ const route=windowsRouting.onboardingRoute();
165
+ if (route==='READY') return '';
166
+ if (route==='BLOCKED') throw Object.assign(new Error('Windows 环境准备未完成'),{reason:'WINDOWS_ENVIRONMENT_UNAVAILABLE'});
167
+ }
164
168
  if (!isWindowsPlatform()) return '';
165
169
  const runContext = currentRunContext();
166
170
  if (runContext?.terminal) {
@@ -225,6 +225,8 @@ async function runOnboardingExecuteReady(args) {
225
225
  async function dispatch(domain, args) {
226
226
  if (windowsRouting) {
227
227
  if (domain === 'windows-entry') return windowsRouting.runWindowsEntry(args);
228
+ if (process.env.ALIPAY_AIPAY_WINDOWS_ROUTE === 'BLOCKED' &&
229
+ ['integration-bootstrap','onboarding-bootstrap','flow'].includes(domain)) windowsRouting.requireBusinessEnvironment();
228
230
  if (['sandbox', 'mcc', 'auth', 'screenshots', 'special-license', 'sign', 'service', 'app',
229
231
  'onboarding-discovery', 'onboarding-recovery', 'onboarding-execute-ready'].includes(domain)) {
230
232
  windowsRouting.requireBusinessEnvironment();
@@ -345,7 +345,9 @@ export async function runSelfUpdateCli(argv = process.argv) {
345
345
  const installedVersion = readVersionFile(skillDir);
346
346
  const kind = skillVersionKind(installedVersion?.raw, channel);
347
347
  // A developer-installed beta is pinned, including when its entry is damaged.
348
- if (kind === 'beta') return local ? { kind: 'CURRENT', version: local } : { kind: 'VERIFY_FAILED' };
348
+ if (kind === 'beta' || fs.existsSync(path.join(skillDir, 'references', 'normal', 'json', 'npm-origin.json'))) {
349
+ return local ? { kind: 'CURRENT', version: local } : { kind: 'VERIFY_FAILED' };
350
+ }
349
351
  if (installedVersion && !kind) return { kind: 'CHECK_FAILED' };
350
352
  const checked = await npmLatest(channel);
351
353
  if (!checked.ok) {
@@ -1,12 +1,14 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import {run, fail} from './acl.mjs';
4
+ const managedGitVersion=JSON.parse(fs.readFileSync(new URL('./policy.json',import.meta.url),'utf8')).git.version;
4
5
 
5
6
  export function gitCandidates(env = process.env) {
6
7
  if (env.ALIPAY_GIT_BASH) return [env.ALIPAY_GIT_BASH];
7
8
  const candidates = [
8
9
  path.win32.join(env.ProgramFiles || 'C:/Program Files', 'Git/bin/bash.exe'),
9
- ...(env.LOCALAPPDATA ? [path.win32.join(env.LOCALAPPDATA, 'Programs/Git/bin/bash.exe')] : [])
10
+ ...(env.LOCALAPPDATA ? [path.win32.join(env.LOCALAPPDATA, 'Programs/Git/bin/bash.exe'),
11
+ path.win32.join(env.LOCALAPPDATA, `Programs/Git-Aipay-${managedGitVersion}/bin/bash.exe`)] : [])
10
12
  ];
11
13
  const search = Object.entries(env).find(([key]) => key.toLowerCase() === 'path')?.[1] || '';
12
14
  for (const directory of search.split(';').filter(p => path.win32.isAbsolute(p))) {
@@ -35,7 +37,8 @@ export function findGitBash({env = process.env, exists = fs.existsSync, execute
35
37
  if (version.status !== 0 || !/^git version \d[^\r\n]*\.windows\.\d+/m.test(version.stdout || '')) continue;
36
38
  const probe = execute(bash, ['--noprofile', '--norc', '-c',
37
39
  'case "$(uname -s)" in MINGW*|MSYS*) exit 0;; *) exit 1;; esac'], {env: cleanEnv, timeout: 5000});
38
- if (probe.status === 0) return {bash, git};
40
+ const converted=probe.status===0 ? execute(path.win32.join(git,'usr/bin/cygpath.exe'),['-w','/'],{env:cleanEnv,timeout:5000}) : null;
41
+ if (converted?.status === 0 && path.win32.isAbsolute(converted.stdout?.trim() || '')) return {bash, git};
39
42
  }
40
43
  if (env.ALIPAY_GIT_BASH) fail('NOT_GIT_BASH', 'ALIPAY_GIT_BASH');
41
44
  return null;
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import {policy,run,fail} from './preflight.mjs';
5
+ import {findGitBash} from './git-discovery.mjs';
6
+ import {powershell,psEnv} from './acl.mjs';
7
+
8
+ export function installGitOfficial({env=process.env,execute=run,exists=fs.existsSync,
9
+ read=fs.readFileSync,remove=fs.rmSync,discover=findGitBash,root,
10
+ signatureCommand=powershell,beforeAttempt=()=>{}}={}) {
11
+ const asset=policy.git;
12
+ if(!root || !env.LOCALAPPDATA || !path.win32.isAbsolute(env.LOCALAPPDATA) ||
13
+ !/^https:\/\/github\.com\/git-for-windows\/git\/releases\/download\/v[0-9.]+\.windows\.[0-9]+\/Git-[0-9.]+-64-bit\.exe$/u.test(asset.url) ||
14
+ !/^[a-f0-9]{64}$/u.test(asset.sha256)) fail('GIT_ASSET_INVALID','Git Bash');
15
+ const temporary=path.join(root,'git-'+crypto.randomUUID()+'.exe');
16
+ const curl=path.join(env.SystemRoot||'C:/Windows','System32/curl.exe');
17
+ const destination=path.win32.join(env.LOCALAPPDATA,'Programs','Git-Aipay-'+asset.version);
18
+ if(!exists(curl)) fail('DEPENDENCY_MISSING','curl');
19
+ const ps=signatureCommand();
20
+ const scope=execute(ps,['-NoProfile','-NonInteractive','-Command',
21
+ '$ErrorActionPreference="Stop"; try { $p=[Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()); if($p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)){exit 2}; exit 0 } catch { exit 3 }'],
22
+ {env:psEnv(env),timeout:15000});
23
+ if(scope.status!==0) fail('USER_SCOPE_REQUIRED','Git Bash');
24
+ beforeAttempt();
25
+ try {
26
+ const downloaded=execute(curl,['--compressed','--fail','--silent','--show-error','--location','--proto','=https',
27
+ '--proto-redir','=https','--connect-timeout','10','--max-time','90','--output',temporary,asset.url],{env,timeout:95000});
28
+ if(downloaded.status!==0) fail('DOWNLOAD_FAILED','Git Bash');
29
+ if(crypto.createHash('sha256').update(read(temporary)).digest('hex')!==asset.sha256) fail('INTEGRITY_FAILED','Git Bash');
30
+ const checked=execute(ps,['-NoProfile','-NonInteractive','-Command',
31
+ '$ErrorActionPreference="Stop"; try { $s=Get-AuthenticodeSignature -LiteralPath $env:ALIPAY_WINDOWS_GIT_INSTALLER; if($s.Status -ne "Valid"){exit 2}; exit 0 } catch { exit 3 }'],
32
+ {env:{...psEnv(env),ALIPAY_WINDOWS_GIT_INSTALLER:temporary},timeout:15000});
33
+ if(checked.status!==0) fail('SIGNATURE_FAILED','Git Bash');
34
+ const installed=execute(temporary,['/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART','/SP-',
35
+ '/o:PathOption=BashOnly',`/DIR=${destination}`],{env,timeout:120000});
36
+ if(installed.status!==0) fail('GIT_INSTALL_FAILED','Git Bash');
37
+ const preparedEnv={...env,ALIPAY_GIT_BASH:path.win32.join(destination,'bin','bash.exe')};
38
+ if(!discover({env:preparedEnv})) fail('GIT_VERIFY_FAILED','Git Bash');
39
+ } finally {
40
+ if(exists(temporary)) remove(temporary,{force:true});
41
+ }
42
+ }
@@ -9,5 +9,10 @@
9
9
  "version": "1.7.1",
10
10
  "sha256": "7451fbbf37feffb9bf262bd97c54f0da558c63f0748e64152dd87b0a07b6d6ab",
11
11
  "url": "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-windows-amd64.exe"
12
+ },
13
+ "git": {
14
+ "version": "2.56.0.2",
15
+ "sha256": "52188f917b378f00c70ec136bcf090005f30d44fbc4eba0bce759cc6592d60f6",
16
+ "url": "https://github.com/git-for-windows/git/releases/download/v2.56.0.windows.2/Git-2.56.0.2-64-bit.exe"
12
17
  }
13
18
  }
@@ -20,10 +20,20 @@ export function stateRoot() {
20
20
  }
21
21
  export function readState(root) {
22
22
  const file=path.join(root,'environment.json');
23
- if(!fs.existsSync(file)) return {schemaVersion:1,repairAttempts:0,prepareAttempts:0};
23
+ if(!fs.existsSync(file)) return {schemaVersion:2,attempts:{git:0,jq:0,cli:0},repairs:{git:0,jq:0,cli:0},sources:{}};
24
24
  try {
25
25
  const s=JSON.parse(fs.readFileSync(file,'utf8'));
26
- if(s.schemaVersion!==1||!Number.isInteger(s.repairAttempts)||!Number.isInteger(s.prepareAttempts)) throw new Error();
26
+ if(s.schemaVersion===1 && Number.isInteger(s.prepareAttempts) && s.prepareAttempts>=0 && Number.isInteger(s.repairAttempts) && s.repairAttempts>=0) {
27
+ // Old global counts could be spent before finding any installer and
28
+ // cannot be attributed to a dependency. New per-stage budgets stay bounded.
29
+ return {schemaVersion:2,attempts:{git:0,jq:0,cli:0},repairs:{git:0,jq:0,cli:0},
30
+ sources:{},blocked:s.blocked,reason:s.reason};
31
+ }
32
+ if(s.schemaVersion!==2 || !s.sources || typeof s.sources!=='object' || !['attempts','repairs'].every(key =>
33
+ ['git','jq','cli'].every(name => Number.isInteger(s[key]?.[name]) && s[key][name]>=0)) ||
34
+ Object.entries(s.sources).some(([stage,sources])=> !['git','jq','cli'].includes(stage) ||
35
+ !sources || typeof sources!=='object' || Object.entries(sources).some(([source,count])=>
36
+ !['official',...(stage==='git'?['winget']:[])].includes(source) || !Number.isInteger(count) || count<0 || count>2))) throw new Error();
27
37
  return s;
28
38
  } catch { fail('ENVIRONMENT_STATE_INVALID'); }
29
39
  }
@@ -160,7 +170,17 @@ export function applyReady(options) {
160
170
  export function resultError(error) {
161
171
  const needsSetup = error.reason === 'DEPENDENCY_MISSING' ||
162
172
  (error.reason === 'EXECUTION_FAILED' && error.dependency === 'alipay-cli');
163
- return {schemaVersion:1,status:needsSetup?'NEEDS_SETUP':'BLOCKED',reason:error.reason||'CHECK_FAILED',dependency:error.dependency||'',validationCoverage:'NOT_VERIFIED'};
173
+ const reason=error.reason || error.code || 'CHECK_FAILED';
174
+ const recovery=reason==='NO_COMPATIBLE_EXECUTION_MAPPING'?'USE_SUPPORTED_PLATFORM_OR_SITE':
175
+ (reason==='DEPENDENCY_MISSING' && error.dependency==='curl')?'RESTORE_WINDOWS_CURL_THEN_RECHECK':
176
+ needsSetup?'PREPARE_MISSING_DEPENDENCIES':
177
+ reason==='DOWNLOAD_FAILED'?'RESTORE_NETWORK_THEN_REPAIR':
178
+ ['INTEGRITY_FAILED','SIGNATURE_FAILED'].includes(reason)?'VERIFY_OFFICIAL_ASSET_BEFORE_REPAIR':
179
+ reason==='RETRY_BUDGET_EXHAUSTED'?'MANUAL_REPAIR_THEN_RECHECK':
180
+ reason==='USER_SCOPE_REQUIRED'?'USE_NON_ELEVATED_SESSION':
181
+ ['ACL_FAILED','STORAGE_FAILED','ENVIRONMENT_BUSY','ENVIRONMENT_STATE_INVALID'].includes(reason)?'REPAIR_LOCAL_ENVIRONMENT_THEN_RECHECK':
182
+ 'INSPECT_FAILURE_THEN_RECHECK';
183
+ return {schemaVersion:1,status:needsSetup?'NEEDS_SETUP':'BLOCKED',reason,dependency:error.dependency||'',recovery,validationCoverage:'NOT_VERIFIED'};
164
184
  }
165
185
  export function report(error) {
166
186
  const r=resultError(error);
@@ -5,56 +5,120 @@ import crypto from 'node:crypto';
5
5
  import {refreshWindowsCli} from './cli-install.mjs';
6
6
  import {findGitBash} from './git-discovery.mjs';
7
7
  import {findWinget} from './winget.mjs';
8
+ import {installGitOfficial} from './git-install.mjs';
9
+ import {powershell} from './acl.mjs';
8
10
  import {policy,run,stateRoot,readState,saveState,withLock,ensureReady,locateJq,fail,report} from './preflight.mjs';
9
11
 
10
- export function claimPreparation(state,{repair=false,authorized=false}={}) {
12
+ export function claimPreparation(state,stage,{source='official',repair=false,authorized=false}={}) {
11
13
  if(!authorized) fail('PREPARATION_AUTHORIZATION_REQUIRED');
12
- if(repair) {
13
- if(state.repairAttempts>=policy.explicitRepairRetries) fail('RETRY_BUDGET_EXHAUSTED');
14
- state.repairAttempts++;
15
- } else if(state.prepareAttempts>=1 || state.repairAttempts>=1) fail('RETRY_BUDGET_EXHAUSTED');
16
- state.prepareAttempts++;state.blocked=true;state.reason='PREPARATION_INCOMPLETE';
14
+ if(!['git','jq','cli'].includes(stage) || !['official','winget'].includes(source) ||
15
+ (source==='winget' && stage!=='git')) fail('ENVIRONMENT_ARGUMENT_INVALID');
16
+ state.attempts ||= {git:0,jq:0,cli:0};
17
+ state.repairs ||= {git:0,jq:0,cli:0};
18
+ state.sources ||= {};
19
+ state.sources[stage] ||= {};
20
+ const used=Number(state.sources[stage][source] || 0);
21
+ if(used>=2 || (used>=1 && !repair)) fail('RETRY_BUDGET_EXHAUSTED',stage);
22
+ state.sources[stage][source]=used+1;
23
+ state.attempts[stage]=(state.attempts[stage]||0)+1;
24
+ if(used>=1) state.repairs[stage]++;
25
+ state.blocked=true;state.reason='PREPARATION_INCOMPLETE';
17
26
  return state;
18
27
  }
19
- export function installGitWithWinget({env=process.env,execute=run,discover=findGitBash}={}) {
28
+ export function installGitWithWinget({env=process.env,execute=run,discover=findGitBash,beforeAttempt=()=>{}}={}) {
20
29
  const command=findWinget({env,execute});
21
- if(!command)fail('MANUAL_GIT_INSTALL_REQUIRED','Git Bash');
30
+ if(!command)fail('WINGET_UNAVAILABLE','Git Bash');
31
+ beforeAttempt();
22
32
  const result=execute(command,['install','--id','Git.Git','--exact','--source','winget','--scope','user',
23
33
  '--silent','--disable-interactivity','--accept-package-agreements','--accept-source-agreements'],{env,timeout:120000});
24
- if(result.error?.code==='ENOENT')fail('MANUAL_GIT_INSTALL_REQUIRED','Git Bash');
25
34
  if(result.error||result.status!==0||!discover({env}))fail('GIT_INSTALL_FAILED','Git Bash');
26
35
  }
27
- function installAsset(name,asset) {
28
- const bin=path.join(os.homedir(),'.local/bin');fs.mkdirSync(bin,{recursive:true});
36
+ export function installAsset(name,asset,{bin=path.join(os.homedir(),'.local/bin'),
37
+ curl=path.join(process.env.SystemRoot||'C:/Windows','System32/curl.exe'),execute=run}={}) {
38
+ fs.mkdirSync(bin,{recursive:true});
29
39
  const target=path.join(bin,name+'.exe');
30
40
  const hash=b=>crypto.createHash('sha256').update(b).digest('hex');
31
- if(fs.existsSync(target)&&hash(fs.readFileSync(target))===asset.sha256)return;
32
41
  const temporary=path.join(bin,name+'-'+crypto.randomUUID()+'.download');
33
- const curl=path.join(process.env.SystemRoot||'C:/Windows','System32/curl.exe');
42
+ const backup=path.join(bin,name+'-'+crypto.randomUUID()+'.backup');
43
+ let moved=false,committed=false;
34
44
  try {
35
- const r=run(curl,['--compressed','--fail','--silent','--show-error','--location','--proto','=https','--proto-redir','=https','--connect-timeout','10','--max-time','60','--output',temporary,asset.url],{timeout:65000});
45
+ const r=execute(curl,['--compressed','--fail','--silent','--show-error','--location','--proto','=https','--proto-redir','=https','--connect-timeout','10','--max-time','60','--output',temporary,asset.url],{timeout:65000});
36
46
  if(r.status!==0)fail('DOWNLOAD_FAILED',name);
37
47
  if(hash(fs.readFileSync(temporary))!==asset.sha256)fail('INTEGRITY_FAILED',name);
48
+ if(fs.existsSync(target)){fs.renameSync(target,backup);moved=true;}
38
49
  fs.renameSync(temporary,target);
39
- if(run(target,name==='jq'?['--version']:['version']).status!==0)fail('EXECUTION_FAILED',name);
40
- }finally{if(fs.existsSync(temporary))fs.unlinkSync(temporary);}
50
+ if(execute(target,name==='jq'?['--version']:['version']).status!==0)fail('EXECUTION_FAILED',name);
51
+ committed=true;
52
+ }catch(error){
53
+ if(moved&&!committed){
54
+ try {if(fs.existsSync(target))fs.unlinkSync(target);fs.renameSync(backup,target);moved=false;}
55
+ catch {fail('INSTALL_ROLLBACK_FAILED',name);}
56
+ }
57
+ throw error;
58
+ }finally{
59
+ if(fs.existsSync(temporary))fs.unlinkSync(temporary);
60
+ if(committed&&moved&&fs.existsSync(backup))fs.unlinkSync(backup);
61
+ }
41
62
  }
42
- export function prepare({authorized=false,repair=false}={}) {
43
- const root=stateRoot();
44
- withLock(root,()=>{
45
- const state=claimPreparation(readState(root),{authorized,repair});saveState(root,state);
46
- try {
47
- if(!findGitBash()) {
48
- installGitWithWinget();
63
+ // Keep installation budgets independent: a failed Git stage cannot consume jq/CLI.
64
+ export function prepareDependencies(state,{authorized=false,repair=false,save=()=>{},root,
65
+ gitReady=()=>findGitBash(),jqReady=()=>locateJq(),
66
+ cliReady=()=>{const cli=path.join(os.homedir(),'.local/bin/alipay-cli.exe');return fs.existsSync(cli)&&run(cli,['version']).status===0;},
67
+ installWinget=installGitWithWinget,installOfficial=installGitOfficial,
68
+ prepareJq=(beforeAttempt)=>{requireAssetDownloader();beforeAttempt();installAsset('jq',policy.jq);},
69
+ prepareCli=(beforeAttempt)=>{powershell();beforeAttempt();if(refreshWindowsCli()!=='READY')fail('CLI_INSTALL_FAILED','alipay-cli');}
70
+ }={}) {
71
+ if(!authorized) fail('PREPARATION_AUTHORIZATION_REQUIRED');
72
+ state.results ||= {};
73
+ const attempt=(stage,source,action)=>{
74
+ let started=false;
75
+ const beforeAttempt=()=>{
76
+ claimPreparation(state,stage,{source,authorized,repair});
77
+ state.results[stage]={source,status:'RUNNING'};save(state);started=true;
78
+ };
79
+ try {action(beforeAttempt);state.results[stage]={source,status:'READY'};save(state);}
80
+ catch(error){
81
+ if(started){state.results[stage]={source,status:'FAILED',reason:error.reason||'INSTALL_FAILED'};save(state);}
82
+ throw error;
83
+ }
84
+ };
85
+ try {
86
+ const missing=[];
87
+ if(!gitReady())missing.push('Git Bash');
88
+ if(!jqReady())missing.push('jq');
89
+ if(!cliReady())missing.push('alipay-cli');
90
+ process.stderr.write('ALIPAY_AIPAY_INTERNAL:WINDOWS_ENV_MISSING='+JSON.stringify(missing)+'\n');
91
+ if(missing.includes('Git Bash')) {
92
+ try {
93
+ if(state.sources?.git?.winget && !repair) fail('WINGET_ALREADY_ATTEMPTED','Git Bash');
94
+ attempt('git','winget',beforeAttempt=>installWinget({beforeAttempt}));
95
+ } catch(error) {
96
+ if(!['WINGET_UNAVAILABLE','WINGET_ALREADY_ATTEMPTED','GIT_INSTALL_FAILED','RETRY_BUDGET_EXHAUSTED'].includes(error.reason)) throw error;
97
+ if(!gitReady()) attempt('git','official',beforeAttempt=>installOfficial({root,beforeAttempt}));
49
98
  }
50
- if(!locateJq()) installAsset('jq',policy.jq);
51
- const cli=path.join(os.homedir(),'.local/bin/alipay-cli.exe');
52
- if(!fs.existsSync(cli)||run(cli,['version']).status!==0) {
53
- if(refreshWindowsCli()!=='READY') fail('CLI_INSTALL_FAILED','alipay-cli');
99
+ if(!gitReady()){
100
+ state.results.git={...state.results.git,status:'FAILED',reason:'GIT_VERIFY_FAILED'};
101
+ fail('GIT_VERIFY_FAILED','Git Bash');
54
102
  }
55
- state.blocked=false;state.reason='';saveState(root,state);
56
- }catch(e){state.blocked=true;state.reason=e.reason||'INSTALL_FAILED';saveState(root,state);throw e;}
57
- });
103
+ }
104
+ if(missing.includes('jq')) {
105
+ attempt('jq','official',beforeAttempt=>{prepareJq(beforeAttempt);if(!jqReady())fail('EXECUTION_FAILED','jq');});
106
+ }
107
+ if(missing.includes('alipay-cli')) {
108
+ attempt('cli','official',beforeAttempt=>{prepareCli(beforeAttempt);if(!cliReady())fail('CLI_INSTALL_FAILED','alipay-cli');});
109
+ }
110
+ for(const stage of ['git','jq','cli'])state.results[stage]={source:state.results[stage]?.source||'existing',status:'READY'};
111
+ state.blocked=false;state.reason='';save(state);
112
+ }catch(e){state.blocked=true;state.reason=e.reason||'INSTALL_FAILED';save(state);throw e;}
113
+ }
114
+ function requireAssetDownloader() {
115
+ const curl=path.join(process.env.SystemRoot||'C:/Windows','System32/curl.exe');
116
+ if(!fs.existsSync(curl))fail('DEPENDENCY_MISSING','curl');
117
+ }
118
+ export function prepare({authorized=false,repair=false}={}) {
119
+ if(!authorized) fail('PREPARATION_AUTHORIZATION_REQUIRED');
120
+ const root=stateRoot();
121
+ withLock(root,()=>prepareDependencies(readState(root),{authorized,repair,root,save:state=>saveState(root,state)}));
58
122
  return ensureReady();
59
123
  }
60
124
  export function parseEnvironmentRequest(args, route=process.env.ALIPAY_AIPAY_WINDOWS_ROUTE) {
@@ -69,9 +133,9 @@ export function parseEnvironmentRequest(args, route=process.env.ALIPAY_AIPAY_WIN
69
133
  if (options[key] !== undefined && !['true','false'].includes(options[key])) fail('ENVIRONMENT_ARGUMENT_INVALID');
70
134
  }
71
135
  if (mode === 'retry' && options['--repair-confirmed'] !== 'true') fail('PREPARATION_AUTHORIZATION_REQUIRED');
72
- const recovering = mode === 'retry' || (mode === 'prepare' && options['--repair-confirmed'] === 'true');
136
+ const recovering = mode === 'retry' || (mode === 'prepare' && (route==='BLOCKED' || options['--repair-confirmed'] === 'true'));
73
137
  if (recovering && (!['integration_only','onboarding_only','full_process'].includes(options['--root-intent']) ||
74
- route !== 'LEGACY')) fail('ENVIRONMENT_ARGUMENT_INVALID');
138
+ route !== 'BLOCKED')) fail('ENVIRONMENT_ARGUMENT_INVALID');
75
139
  if (!recovering && options['--root-intent'] !== undefined) fail('ENVIRONMENT_ARGUMENT_INVALID');
76
140
  return {mode,options,recovering};
77
141
  }
@@ -48,10 +48,10 @@ export function prepareWindowsPython({authorized = false, repair = false, find =
48
48
  if (!Number.isInteger(state.prepareAttempts) || state.prepareAttempts < 0 ||
49
49
  !Number.isInteger(state.repairAttempts) || state.repairAttempts < 0) fail('ENVIRONMENT_STATE_INVALID');
50
50
  if (repair ? state.repairAttempts >= 1 : state.prepareAttempts >= 1 || state.repairAttempts >= 1) fail('RETRY_BUDGET_EXHAUSTED');
51
- state.prepareAttempts++; if (repair) state.repairAttempts++;
52
- fs.writeFileSync(file, JSON.stringify(state));
53
51
  const winget = findWinget({env,execute});
54
52
  if (!winget) fail('MANUAL_PYTHON_INSTALL_REQUIRED', 'Python 3');
53
+ state.prepareAttempts++; if (repair) state.repairAttempts++;
54
+ fs.writeFileSync(file, JSON.stringify(state));
55
55
  const result = execute(winget, ['install', '--id', 'Python.Python.3.14', '--exact', '--source', 'winget',
56
56
  '--scope', 'user', '--architecture', 'x64', '--silent', '--disable-interactivity',
57
57
  '--accept-package-agreements', '--accept-source-agreements'], {env, timeout: 180000});
@@ -1,33 +1,30 @@
1
- import {applyReady, report, resultError, readState, stateRoot} from './preflight.mjs';
1
+ import {applyReady, report, resultError} from './preflight.mjs';
2
2
  import {prepare} from './prepare.mjs';
3
3
 
4
4
  const intents = ['integration_only', 'onboarding_only', 'full_process'];
5
5
  const prefix = 'ALIPAY_AIPAY_INTERNAL:';
6
6
 
7
7
  // This is a routing result, never proof of a successful payment or onboarding.
8
- export function selectRoute({rootIntent = '', authorized = false, repair = false} = {}) {
8
+ export function selectRoute({rootIntent = '', authorized = false, repair = false, siteSelected = false} = {}) {
9
9
  if (rootIntent && !intents.includes(rootIntent)) throw new Error('非法 Windows 根意图');
10
+ if (siteSelected) return emit({rootIntent,route:'LEGACY',reason:'USER_SELECTED_SITE'});
10
11
  let error;
11
12
  try {
12
13
  applyReady({retry: repair});
13
14
  return emit({rootIntent, route: 'READY'});
14
15
  } catch (e) { error = e; }
15
16
  if (authorized) {
16
- // A missing dependency or an unlaunchable local CLI permits preparation;
17
- // network, permissions, architecture, integrity and business errors do not.
17
+ // Preparation itself enforces per-dependency attempts and reports the actual failure.
18
18
  try {
19
- const state = readState(stateRoot());
20
19
  const cliUnusable = error.reason === 'EXECUTION_FAILED' && error.dependency === 'alipay-cli';
21
- if ((error.reason === 'DEPENDENCY_MISSING' || cliUnusable ||
22
- (error.reason === 'REPAIR_REQUIRED' && state.reason === 'DEPENDENCY_MISSING')) &&
23
- state.prepareAttempts === 0 && state.repairAttempts === 0) {
24
- prepare({authorized: true});
20
+ if (error.reason === 'DEPENDENCY_MISSING' || cliUnusable) {
21
+ prepare({authorized: true,repair});
25
22
  return emit({rootIntent, route: 'READY'});
26
23
  }
27
24
  } catch (e) { error = e; }
28
25
  }
29
26
  report(error);
30
- return emit({rootIntent, route: 'LEGACY', ...resultError(error)});
27
+ return emit({rootIntent, route: error.reason === 'NO_COMPATIBLE_EXECUTION_MAPPING' ? 'LEGACY' : 'BLOCKED', ...resultError(error)});
31
28
  }
32
29
 
33
30
  function emit(result) {
@@ -40,16 +37,17 @@ export function runWindowsEntry(args) {
40
37
  const options = {};
41
38
  for (let i = 0; i < args.length; i += 2) {
42
39
  const key = args[i], value = args[i + 1];
43
- if (!['--root-intent', '--prepare-authorized', '--repair-confirmed'].includes(key) ||
40
+ if (!['--root-intent', '--prepare-authorized', '--repair-confirmed','--site-selected'].includes(key) ||
44
41
  value === undefined || options[key] !== undefined) throw new Error('Windows 入口参数非法');
45
42
  options[key] = value;
46
43
  }
47
44
  if (!intents.includes(options['--root-intent'])) throw new Error('必须先识别三种根意图之一');
48
- for (const key of ['--prepare-authorized', '--repair-confirmed']) {
45
+ for (const key of ['--prepare-authorized', '--repair-confirmed','--site-selected']) {
49
46
  if (options[key] !== undefined && !['true', 'false'].includes(options[key])) throw new Error('Windows 入口布尔参数非法');
50
47
  }
51
48
  return selectRoute({rootIntent: options['--root-intent'],
52
- authorized: options['--prepare-authorized'] === 'true', repair: options['--repair-confirmed'] === 'true'});
49
+ authorized: options['--prepare-authorized'] === 'true', repair: options['--repair-confirmed'] === 'true',
50
+ siteSelected:options['--site-selected']==='true'});
53
51
  }
54
52
 
55
53
  // A caller's LEGACY fact can only disable operations. READY is never a capability token.
@@ -59,7 +57,7 @@ export function consumeRoute(args) {
59
57
  let seen = false;
60
58
  for (let i = 0; i < args.length; i++) {
61
59
  if (args[i] !== '--windows-route') { remaining.push(args[i]); continue; }
62
- if (seen || !['READY','LEGACY'].includes(args[i + 1])) throw new Error('Windows 分流参数非法');
60
+ if (seen || !['READY','LEGACY','BLOCKED'].includes(args[i + 1])) throw new Error('Windows 分流参数非法');
63
61
  seen = true;
64
62
  route = args[++i];
65
63
  }
@@ -69,7 +67,7 @@ export function consumeRoute(args) {
69
67
  // Native Node messages, telemetry and project inspection do not depend on Bash.
70
68
  // READY is never trusted as proof; the actual business executor checks again.
71
69
  export function requireBusinessEnvironment() {
72
- if (process.env.ALIPAY_AIPAY_WINDOWS_ROUTE === 'LEGACY') {
70
+ if (['LEGACY','BLOCKED'].includes(process.env.ALIPAY_AIPAY_WINDOWS_ROUTE)) {
73
71
  const error = Object.assign(new Error('WINDOWS_ENVIRONMENT_UNAVAILABLE'), {reason: 'WINDOWS_ENVIRONMENT_UNAVAILABLE'});
74
72
  throw error;
75
73
  }
@@ -77,7 +75,7 @@ export function requireBusinessEnvironment() {
77
75
  }
78
76
 
79
77
  export function onboardingRoute() {
80
- if (process.env.ALIPAY_AIPAY_WINDOWS_ROUTE === 'LEGACY') return 'LEGACY';
78
+ if (['LEGACY','BLOCKED'].includes(process.env.ALIPAY_AIPAY_WINDOWS_ROUTE)) return process.env.ALIPAY_AIPAY_WINDOWS_ROUTE;
81
79
  try { requireBusinessEnvironment(); return 'READY'; }
82
- catch (error) { report(error); return 'LEGACY'; }
80
+ catch (error) { report(error); return 'BLOCKED'; }
83
81
  }
@@ -4,4 +4,4 @@
4
4
 
5
5
  唯一 stderr 终态:`SELF_UPDATE:CURRENT=<合法 SemVer>`、`SELF_UPDATE:UPDATED=<合法 SemVer>`、`SELF_UPDATE:RETRY_WITH_NETWORK`、`SELF_UPDATE:SKIPPED_NETWORK_RESTRICTED`、`SELF_UPDATE:CHECK_FAILED`、`SELF_UPDATE:INSTALL_FAILED`、`SELF_UPDATE:VERIFY_FAILED`。`UPDATED` 用原子替换后的新版,其余用当前可用版;marker 异常读当前入口,不得称最新或更新成功;停止读旧版,读取同一根目录的新版入口。仅 `RETRY_WITH_NETWORK` 可取权后原样重试一次,拒绝、不支持或再次返回时读当前入口;一次网络恢复外,不重查、取权、用 `sudo`、要求用户操作或写更新状态。每次只查一次 npm latest 的 `version` 和 `alipayAipayChannels`;只比较本渠道 Skill 版本,npm 包版本仅用于精确下载;`VERSION` 缺失、非法、落后或业务入口缺失时只从查询所得精确 npm 包版本安装当前渠道一次。渠道由当前目录的 `references/normal/json/release-channel.json` 与 Skill 名称共同校验;旧正式版缺少渠道文件时只兼容 `stable`,预发版禁止回退正式版。缺少或非法的远端渠道元数据返回 `CHECK_FAILED`,不得按 npm 包版本兜底。不调用公开 `install --target all`;超时终止同一次调用的进程树并清理。
6
6
 
7
- 本地正式渠道的 `X.Y.Z-beta`、`X.Y.Z-betaN`(兼容 `-beta.N`)和预发渠道的 `X.Y.Z-pre-beta`、`X.Y.Z-pre-betaN` 均为固定测试版本。完整安装直接返回 `CURRENT` 且不查询 npm;业务入口损坏时返回 `VERIFY_FAILED`,仍不下载修复或转正。更换版本必须由测试人员显式执行 npm 安装器的 `install`,普通 `update` 同样不推进 beta。正常版本分别为 `X.Y.Z` 与 `X.Y.Z-pre`,远端对应渠道若为 beta 或名称不匹配,返回 `CHECK_FAILED`,不自动安装。beta 不参与版本排序,无自动追踪和自动转正。
7
+ 本地正式渠道的 `X.Y.Z-beta`、`X.Y.Z-betaN`(兼容 `-beta.N`)和预发渠道的 `X.Y.Z-pre-beta`、`X.Y.Z-pre-betaN`,以及带内部 `references/normal/json/npm-origin.json` 的 npm beta 测试包,均固定当前安装目录。完整安装直接返回 `CURRENT` 且不查询 npm;业务入口损坏时返回 `VERIFY_FAILED`,仍不下载修复或转正。更换测试包必须由测试人员显式执行 npm 安装器的 `install`。正常版本分别为 `X.Y.Z` 与 `X.Y.Z-pre`,远端对应渠道若为 beta 或名称不匹配,返回 `CHECK_FAILED`,不自动安装。beta 不参与版本排序,无自动追踪和自动转正。
@@ -12,14 +12,9 @@ Windows 首次使用时先按根 `SKILL.md` 直接探测 PATH 中的原生 Node
12
12
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" windows-entry --root-intent "<ROOT_INTENT>" --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"
13
13
  ```
14
14
 
15
- 当前任务已有准备依赖授权时追加 `--prepare-authorized true`,无授权则只检查;不新增必答安装确认,也不把依赖准备当作登录、签约或应用写入授权。宿主权限边界保持不变。
15
+ 用户明确要求安装并通过 Skill 完成签约/开通或完整接入时附加 `--prepare-authorized true`;仅代码开发且没有依赖准备授权时先只读检查。用户明确选择站点时附加 `--site-selected true`。准备授权只覆盖用户级依赖,不包含登录、签约或应用写入。
16
16
 
17
- 退出 0 只表示分流已完成。必须取得唯一 `ALIPAY_AIPAY_INTERNAL:WINDOWS_ROUTE=<JSON>`,校验 schemaVersion=1、rootIntent 等于当前根意图、route 为 READY 或 LEGACY;非零、超时、缺失或冲突按 LEGACY,不猜测就绪。所有后续 Windows runtime 命令附加 `--windows-route "<READY|LEGACY>"`。普通回复、切换业务阶段和 Full Process 衔接不重新安装。只有下文明确修复后的复检成功可更新该事实;READY 只用于选择原 flow 分支,实际 Shell/CLI 业务前仍复检能力。
18
-
19
- - READY:Integration 使用 flow 登记的快速沙箱 ensure/reverify/summary;Onboarding 平台入口复检通过、stdout 为空后执行原本地流程。
20
- - LEGACY:Integration 的 `env integration-check` 仅检查原生 Node,保留原手工沙箱/VERIFY_PENDING 和独立代码开发;Onboarding 仍由 `onboarding-message platform-entry` 渲染原登记站点消息并按原规则收口。Full Process 必须先完成 Integration 及原衔接门禁,保留其实际结果。
21
-
22
- 环境失败不关闭遥测,不改 SESSION_ID、run_id、计数、stage 或采集授权。Native Node 的声明、消息、项目检查和遥测不要求 Bash/jq/CLI。站点引导仍使用原 runner 的平台标识与 summary 规则;缺少必要上下文时按原错误分支停止,不编造标识或另写消息。
17
+ 退出 0 只表示分流完成。必须取得唯一 `ALIPAY_AIPAY_INTERNAL:WINDOWS_ROUTE=<JSON>`,校验 schemaVersion=1、rootIntent 等于当前根意图、route 为 READY、BLOCKED 或 LEGACY;非零、超时、缺失或冲突均按 BLOCKED 停止。READY 才执行本地快速沙箱和签约;BLOCKED 说明内部 `WINDOWS_ENV_RESULT` 的实际 reason/dependency 及恢复动作,等待环境修复,不能输出站点引导;LEGACY 仅用于不支持的执行映射或用户明确选择站点。后续 Windows runtime 命令附加当前 `--windows-route`;BLOCKED 不进入 bootstrap、业务 flow 或项目写入。修复后必须重新只读复检,取得唯一 READY 才继续原任务。环境故障不关闭遥测,也不改变 SESSION_ID、run_id、计数或会话采集选择。
23
18
 
24
19
  ## Windows CLI 安装契约
25
20
 
@@ -33,23 +28,25 @@ stdout 为空、原始安装器输出不对客,只映射根协议的唯一刷
33
28
 
34
29
  CLI 刷新策略与非 Windows 保持同一会话预算,不锁定 0.1.7 或其哈希;Windows 交由正式 PowerShell 安装器选择版本。jq 优先复用能通过版本和实际 filter 检查的既有原生工具;缺失或不可用时安装固定 1.7.1,官方 HTTPS 资产和 SHA-256 位于 `scripts/windows/policy.json`。Node 优先复用当前原生 x64 Node >=18;缺失时由根启动器的 PowerShell 引导准备官方 LTS 用户级 ZIP,SHA-256 验证后返回绝对可执行路径,首次一次、明确修复最多一次。Git Bash 从常见目录和本机 PATH 的 Git 安装布局定位,也可由 `ALIPAY_GIT_BASH` 指定绝对路径;必须验证 Git for Windows 版本、cygpath 和 Bash 的 MINGW/MSYS 平台,不使用 System32/WSL Bash,显式指定错误时停止而不偷偷换工具。
35
30
 
36
- 初次仅缺依赖或本机 `alipay-cli.exe` 无法启动且已有授权时,按 Git Bash → jq → 必要的 CLI 修复 → 完整复检准备一次。CLI 已可用时不重复安装;仅缺失/不可用时由同一正式 PowerShell 安装器修复。Git 优先使用可定位的 winget 路径;文件检查未命中时仍尝试可执行的 `winget.exe` 别名。仅命令确实不存在时转下述 Git 官方手动安装指引;登记的用户级安装最多 120 秒,之后须重新验证 Git Bash。jq 单次资产下载最多 60 秒,验证 SHA-256 后落盘并检查实际启动;CLI 整次刷新最多 60 秒,完成后由 Node 单独验证目标 `.exe version`,限时 5 秒。jq 托管安装只替换用户目录中缺失或不可用的 jq,不覆盖健康的其他版本;网络、架构、权限或完整性失败不靠重装解决。失败即 LEGACY,不轮换镜像、包管理器、WSL 或 shell,不提权、不修改执行策略、系统级 PATH 或 HOME。Windows CLI 安装器可能持久更新用户 PATH。
31
+ 初次缺 Git Bash、jq 或本机 CLI 无法启动且当前任务授权准备时,按 Git Bash → jq → 必要的 CLI 修复 → 完整复检执行。CLI 已可用时不重复安装。Git 优先复用已有可运行的 Git for Windows;缺失时优先调用用户级 winget,无法运行或未通过复检时使用 `scripts/windows/policy.json` 固定的 Git for Windows 官方 x64 安装包(当前 2.56.0.2)。官方包下载后先校验固定 SHA-256,再由系统 PowerShell 验证 Authenticode 有效,之后安装到 `%LOCALAPPDATA%/Programs/Git-Aipay-<version>`,并实测 git、bash、cygpath。下载或校验失败不运行安装包。两种来源均不可用时保持 BLOCKED,展示原因及官方手工安装路径,不静默跳站点。
37
32
 
38
- 环境诊断及次数保存在 `%LOCALAPPDATA%/alipay-aipay-pre/windows/environment.json`,不含业务材料。重载和普通回复不清零;短期锁最多等待 2 秒。锁持续占用或诊断状态损坏时,已通过完整工具检查的只读 READY 仍可继续;自动安装保持阻断,不删除锁或状态绕过预算。
33
+ jq 使用固定 1.7.1 官方资产和 SHA-256,已有健康版本直接复用;CLI 仍使用正式 PowerShell 安装器选择当前版本。每个依赖和 Git 的两种安装来源分别记录实际开始的安装尝试,确认来源前的只读探测不消耗预算;Git 的 winget 失败可在同次准备中尝试官方固定包。初次每个来源最多一次,用户明确修复后每个来源最多再一次;失败阶段可续跑,已健康的阶段不重装。状态记录不含业务材料;复检始终只读且不消耗安装预算。网络、架构、权限、签名或完整性失败保持 BLOCKED,不轮换未经验证的镜像、使用 WSL、提权或修改执行策略。
39
34
 
40
- 用户明确修复并要求重试后,可再次只读复检当前环境;每次复检仍完整检查工具执行、路径和 ACL,成功才解除已有阻断。复检不执行安装、不清零安装次数,后续再次发生故障时也可在用户修好后复检。自动安装仍仅首次一次、明确修复后最多额外一次。手工修复后的复检:
35
+ 环境诊断和次数保存在 `%LOCALAPPDATA%/alipay-aipay-pre/windows/environment.json`;旧版全局次数无法归属具体安装阶段,迁移时保留阻断原因并从新版本的有限分阶段预算重新计数。锁短时占用或状态损坏时,完整工具检查通过仍可 READY;自动安装保持阻断,不删除锁或状态绕过预算。用户修好依赖后,只读复检可再次执行,无论此前安装预算是否用尽:
41
36
 
42
37
  ```text
43
- node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" env retry --repair-confirmed true --root-intent "<ROOT_INTENT>" --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>" --windows-route LEGACY
38
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" env retry --repair-confirmed true --root-intent "<ROOT_INTENT>" --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>" --windows-route BLOCKED
44
39
  ```
45
40
 
46
- 需要再次安装且已有依赖准备授权时:
41
+ 需要再次自动安装且当前任务授权时:
47
42
 
48
43
  ```text
49
- node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" env prepare --authorized true --repair-confirmed true --root-intent "<ROOT_INTENT>" --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>" --windows-route LEGACY
44
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" env prepare --authorized true --repair-confirmed true --root-intent "<ROOT_INTENT>" --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>" --windows-route BLOCKED
50
45
  ```
51
46
 
52
- 必须退出 0,同时取得唯一 `CLI_ENV:READY` 和唯一 `ALIPAY_AIPAY_INTERNAL:WINDOWS_ROUTE=<JSON>`,且 JSON 的 schemaVersion=1、rootIntent 等于本轮根意图、route=READY,才将本轮后续 runtime 命令的 `--windows-route` 更新为 READY。缺失、冲突、非零或仅有 `CLI_ENV:READY` 时保持 LEGACY 并停止依赖动作;不重建 SESSION_ID/run_id,不重计用户输入或重跑 `windows-entry`。旧阻断只记录最近失败原因,不代替当前完整本地检查;当前复检失败仍阻断,成功才允许更新为 READY,准备/安装次数不重置。winget 不可用、用户级 Git 安装不适用或版本冲突时,由用户按 [Git for Windows 官方入口](https://gitforwindows.org/)及实际失败项手工修复,不自动改为系统级安装,也不因缺少 winget 循环尝试。
47
+ 如果之前只是缺少授权或没有找到安装来源,尚未实际尝试该阶段,则使用同一命令但省略 `--repair-confirmed true`,继续首次准备;不能把新授权当成已消耗预算后的额外重试许可。
48
+
49
+ 仅退出 0、唯一 `CLI_ENV:READY` 和唯一根意图匹配的 `WINDOWS_ROUTE=READY` 可更新分流。其他结果保持 BLOCKED,说明具体失败原因;不重建会话、重复计数或重放业务操作。用户明确选择站点或平台执行映射不支持时,可走原 LEGACY 分支;可恢复的依赖失败不能自动转站点。
53
50
 
54
51
  ## 路径、权限与进程
55
52
 
@@ -71,7 +68,7 @@ Windows 签约请求的 UUID 使用已有 Node 生成,仅此步骤不要求 py
71
68
 
72
69
  所有兼容层内部 marker 只写 stderr 且以 `ALIPAY_AIPAY_INTERNAL:` 开头,不对客。`WINDOWS_ENV_RESULT` 表示环境诊断;`WINDOWS_BUSINESS_BLOCKED=ENVIRONMENT_UNAVAILABLE` 表示 gateway 在启动本次业务脚本前阻断,退出非零,本次业务 CLI 调用为零。
73
70
 
74
- Integration 在上述明确的调用前阻断时按 flow 转 LEGACY,继续独立代码开发,沙箱及依赖动作保持未完成。沙箱脚本已经启动后,只接受原 flow 的 READY、CREATE_PENDING、VERIFY_PENDING 和精确 LOCAL_FS_RECOVERY;非零、超时、协议冲突仍停止核对,不将异常伪装成待配置、不重放原 MCP、不读取敏感配置补造成功证据。
71
+ Integration 在上述明确的调用前阻断时停在环境恢复点,不改判 LEGACY。用户修复后复检 READY 才继续;沙箱脚本已经启动后,只接受原 flow 的 READY、CREATE_PENDING、VERIFY_PENDING 和精确 LOCAL_FS_RECOVERY;非零、超时、协议冲突仍停止核对,不将异常伪装成待配置、不重放原 MCP、不读取敏感配置补造成功证据。
75
72
 
76
73
  Onboarding 的环境失败停止依赖动作;修复后先按原只读入口查询实际状态。授权、材料、资格和审核错误仍归原业务分支,不能统一解释为 Windows 失败。所有原有交付屏障、阻塞确认、未知写结果规则和最终收口仍有效。
77
74
 
@@ -1,7 +1,7 @@
1
1
  <!-- ALIPAY_AIPAY_FLOW:ONBOARDING_CORE:START -->
2
2
  # 支付产品-产品开通流程说明
3
3
 
4
- Windows 的 READY/LEGACY 取根入口唯一分流,或 Windows 环境文档明确修复后唯一、根意图匹配的新 READY 事实;后续 runtime 命令追加当前有效的 `--windows-route`。READY 通过兼容层执行本 flow 原授权、查询、材料、写操作和收口;LEGACY 只执行原站点引导。环境检查失败不修改业务状态、遥测开关或会话采集选择。业务调用前若返回非零及 `WINDOWS_BUSINESS_BLOCKED=ENVIRONMENT_UNAVAILABLE`,停止依赖该环境的动作,不重放写入;修复后按 Windows 环境文档有限复检,再按原只读查询核验实际状态。未知业务结果仍沿用原恢复规则。
4
+ Windows 的 READY/LEGACY 取根入口唯一分流,或 Windows 环境文档明确修复后唯一、根意图匹配的新 READY 事实;BLOCKED 停在环境恢复点,不进入本 flow。后续 runtime 命令追加当前有效的 `--windows-route`。READY 通过兼容层执行本 flow 原授权、查询、材料、写操作和收口;LEGACY 仅在不支持的平台映射或用户明确选站点时执行原站点引导。环境检查失败不修改业务状态、遥测开关或会话采集选择。业务调用前若返回非零及 `WINDOWS_BUSINESS_BLOCKED=ENVIRONMENT_UNAVAILABLE`,停止依赖该环境的动作,不重放写入;修复后按 Windows 环境文档有限复检,再按原只读查询核验实际状态。未知业务结果仍沿用原恢复规则。
5
5
 
6
6
  > ⚠️ **前置声明**:本 flow 仅支持 **AI 网页应用收款、AI 移动应用收款、AI 按量付费**三种产品的产品开通。原有“网站支付”“APP 支付”“按量付费”仍作为输入别名和资源目录名。**其他产品(当面付、订单码支付、JSAPI支付、预授权支付、商家扣款等)暂不支持**,如需开通请前往[支付宝商家平台](https://b.alipay.com/page/portal/home)完成。
7
7
 
@@ -70,7 +70,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry resolve-sessi
70
70
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" windows-entry --root-intent "<ROOT_INTENT>" --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"
71
71
  ```
72
72
 
73
- 任务已有依赖准备授权时附加 `--prepare-authorized true`,不新增业务确认;无安装授权仅检查。只接受退出 0、唯一 `ALIPAY_AIPAY_INTERNAL:WINDOWS_ROUTE=<JSON>` 且 rootIntent 匹配、route 为 `READY|LEGACY`。非零、缺失或冲突按 `LEGACY`,不等于 CLI 就绪。所有后续 Windows runtime 命令追加 `--windows-route "<READY|LEGACY>"`,替换为本轮事实;同一会话、材料回复和 Full Process 衔接不重新准备。仅用户明确修复后,`normal/windows-environment.md` 登记的 `env retry|prepare` 复检成功并返回唯一、根意图匹配的 `WINDOWS_ROUTE=READY` 时才更新后续命令为 READY;`CLI_ENV:READY` 单独不构成分流事实,不重跑 `windows-entry`。`READY` 的快速沙箱与本地开通使用原 flow;`LEGACY` 保留原 Windows 独立开发/手工沙箱及站点开通分支。实际业务执行前仍复检能力,READY 不是绕过门禁的令牌。环境、沙箱准备或业务失败不修改采集选择、SESSION_ID、run_id、用户输入计数或阶段责任方。
73
+ 用户明确要求安装并通过 Skill 完成签约/开通或完整接入时,当前任务已授权准备所需用户级依赖,附加 `--prepare-authorized true`;仅代码开发且未授权准备依赖时先只读检查。用户明确选择站点时才附加 `--site-selected true`。这不构成登录、签约或应用写入授权。只接受退出 0、唯一 `ALIPAY_AIPAY_INTERNAL:WINDOWS_ROUTE=<JSON>` 且 rootIntent 匹配、route 为 `READY|BLOCKED|LEGACY`。非零、缺失或冲突按 `BLOCKED` 停止,不推定站点分支。`BLOCKED` 须说明内部结果给出的实际缺项/失败原因和有限恢复动作;用户修好依赖后先只读复检,必要且获授权时才按环境文档继续准备。只有不支持的执行映射或用户明确选择站点才进入 `LEGACY`。所有后续 Windows runtime 命令追加 `--windows-route "<READY|BLOCKED|LEGACY>"`;`BLOCKED` 不进入业务 bootstrap/flow。仅修复后唯一、根意图匹配的 `WINDOWS_ROUTE=READY` 才更新分流事实;`READY` 的快速沙箱与本地开通使用原 flow,`LEGACY` 保留原独立开发/手工沙箱及站点开通分支。实际业务执行前仍复检能力。环境、沙箱准备或业务失败不修改采集选择、SESSION_ID、run_id、用户输入计数或阶段责任方。
74
74
 
75
75
 
76
76
  `integration_only` 的 `integration-bootstrap` 在产品已唯一确定时必须携带 `--product-type`,并串行执行 `telemetry start-run` 与 `flow integration entry`;产品尚未唯一确定时省略产品且不创建正式 run,后续由 `integration-message start-confirm` 首次开 run并生成 `clarify_intent`。`onboarding_only` 的 `onboarding-bootstrap` 同理:产品已唯一时携带产品并先启动 run,尚未唯一时由 `mcc resolve-plan` 或 `onboarding-message plan-confirm` 首次启动。两者均为既有无业务写入动作,执行后不得重复原子命令。Windows READY 返回本地 entry 契约;Windows LEGACY 引导渲染依次使用现有平台项目 ID、正式 run_id;两者均不存在时使用已绑定 `SESSION_ID` 的稳定匿名哈希,不提前创建 run。非 Windows 返回 entry 契约。`full_process` 不使用 `integration-bootstrap`;衔接 Onboarding 也不使用带 `start-run` 的 bootstrap,仍执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"` 后按需加载 entry,避免重复 `clarify_intent`。