@alipay/alipay-aipay 1.7.4 → 1.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alipay/alipay-aipay",
3
- "version": "1.7.4",
3
+ "version": "1.7.5",
4
4
  "description": "支付宝 AI 付接入 Skill 安装包 - 优先安装 Agent Skill,并在 macOS/Linux 后台准备用户级 CLI 工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -55,7 +55,7 @@
55
55
  "typescript": "^5.3.3"
56
56
  },
57
57
  "alipayAipayChannels": {
58
- "stable": "1.7.1",
59
- "pre": "1.7.3-pre"
58
+ "stable": "1.7.2",
59
+ "pre": "1.7.4-pre"
60
60
  }
61
61
  }
package/skill/SKILL.md CHANGED
@@ -50,7 +50,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" channel-offer decide --
50
50
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" channel-offer available
51
51
  ```
52
52
 
53
- 仅唯一 `ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER=AVAILABLE` 才在 Windows PowerShell 或 CMD 执行 `npx.cmd -y @alipay/alipay-aipay@latest install --channel pre`。`UNAVAILABLE|CHECK_FAILED` 时说明线上内测版尚不可核实或尚未包含 Windows 能力,保留正式版并停在当前选择,不安装旧内测版;用户可明确要求继续正式版。安装退出码为 0 后,必须从安装器报告的当前 Agent 目标目录核验 `alipay-aipay-pre/SKILL.md`、`VERSION` 和 `references/runtime-entry.md` 均为普通文件,渠道为 `pre`、版本不低于 `1.7.3-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>"`,传入此前被判为 `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。
54
54
 
55
55
  仅确定继续正式版后,若当前上下文没有与规范化 `SKILL_DIR`、合法 `VERSION` 绑定的 `ALIPAY_AIPAY_CLI_REFRESHED=true`,必须刷新一次 `alipay-cli`;不得因自更新已检查而跳过。仅唯一终态 `READY` 可在当前对话上下文记录该成功事实、路径和版本,不写缓存或业务状态;三者仍可用且一致时,后续根触发、确认、补材料、候选选择、恢复、中断继续和 Full Process 衔接均不重复刷新。
56
56
 
package/skill/VERSION CHANGED
@@ -1 +1 @@
1
- 1.7.1
1
+ 1.7.2
@@ -1,29 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import fs from 'node:fs';
3
4
  import path from 'node:path';
5
+ import https from 'node:https';
4
6
  import process from 'node:process';
5
- import { spawnSync } from 'node:child_process';
7
+ import { spawn, spawnSync } from 'node:child_process';
6
8
  import { fileURLToPath } from 'node:url';
7
9
  import { isWindowsPlatform } from './platform_compat.mjs';
8
10
  import { renderMessage } from './render_customer_message.mjs';
9
11
 
10
12
  const marker = 'ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER=';
11
13
  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;
12
- const negativePre = /(?:不想|不|别|不要|无需|暂不|先不|取消|拒绝)\s*(?:给我|帮我|让我)?\s*(?:安装|切换|使用|选择|试用|用|装)?\s*(?:内测版|预发版)|(?:内测版|预发版)\s*(?:不用|不要|算了)|\b(?:no|not|don't|cancel)\b/iu;
14
+ const negativePre = /(?:不想|不|别|不要|无需|暂不|先不|取消|拒绝)\s*(?:给我|帮我|让我)?\s*(?:安装|切换到?|切到|使用|选择|试用|用|装)?\s*(?:内测版|预发版)|(?:内测版|预发版)\s*(?:不用|不要|算了)|\b(?:no|not|don't|cancel)\b/iu;
13
15
  const stableChoice = /(?:^|[,,。;;\s])(?:继续|使用|保持|选择|留在)(?:当前)?(?:的)?(?:正式版|稳定版)|(?:正式版|稳定版).{0,8}(?:继续|就好|即可)|\bstable\b/iu;
14
16
  const uncertain = /[??]|(?:吗|么|呢)$|是否|能否|可否|能不能|可不可以|要不要|是不是|多久|多少钱|如何|有没有|如果|假如|考虑|可能|也许|了解|介绍|区别|什么|怎么|为什么|之后再|以后再|稍后再|\b(?:if|maybe|perhaps|what|why|how|whether)\b/iu;
15
17
  const exactConfirm = /^(?:确认|确定|同意|是的|yes|confirm)$/iu;
16
- const contextualConfirm = /^(?:(?:好|好的|可以|没问题)[,,\s]*)?(?:确认|确定|同意)(?:[,,\s]*(?:请|现在)?(?:帮我|给我)?(?:安装|装|装上|切换|切过去|用上)(?:一下|吧)?(?:内测版|预发版)?)?$|^(?:(?:好|好的|可以|没问题)[,,\s]*(?:就)?)?按你说的(?:安装|装|切换|用)(?:内测版|预发版)?(?:吧)?$/iu;
18
+ const contextualConfirm = /^(?:(?:好|好的|可以|没问题)[,,\s]*)?(?:我)?(?:确认|确定|同意)(?:[,,\s]*(?:请|现在)?(?:帮我|给我)?(?:安装|装|装上|使用|切换|切过去|用上)(?:一下|吧)?(?:内测版|预发版)?)?$|^(?:(?:好|好的|可以|没问题)[,,\s]*(?:就)?)?按你说的(?:安装|装|切换|用)(?:内测版|预发版)?(?:吧)?$/iu;
17
19
  const genericStable = /^(?:继续|OK|1|照原任务继续)$/iu;
18
- const reportedChoice = /(?:朋友|同事|别人|有人|文档|示例|命令|文案).{0,8}(?:说|写|提到|包含|是)/u;
20
+ const reportedChoice = /(?:朋友|同事|别人|有人|文档|示例|命令|文案).{0,8}(?:说|写|提到|包含|是)|(?:他|她|他们|用户|客户)(?:回复|说的|说过|写的|提到|问).{0,20}(?:确认|内测版|预发版)|我(?:说的是|问的是|写的是).{0,20}(?:确认|内测版|预发版)/u;
21
+ const deniedConfirmation = /(?:不|没|未)\s*(?:想|要|曾|有|说|表示)?\s*(?:确认|确定|决定|同意|说过).{0,12}(?:内测版|预发版)|(?:没|未)\s*说.{0,8}(?:使用|安装|切换).{0,4}(?:内测版|预发版)|(?:内测版|预发版).{0,8}(?:还没|尚未|未|没)(?:确认|决定)/u;
22
+ const unclearIntent = /(?:你|您)(?:刚才|之前)?(?:说|建议|推荐).{0,15}(?:内测版|预发版)|(?:内测版|预发版).{0,10}(?:先不|暂不|以后|之后|稍后|再说)|(?:不要|不想|别|拒绝)[^,,。;;\n]{0,12}(?:安装|使用|切换)[^,,。;;\n]{0,8}(?:内测版|预发版)/u;
19
23
 
20
24
  export function decideChannelOffer(reply) {
21
25
  if (typeof reply !== 'string' || !reply.trim()) return 'UNDECIDED';
22
26
  const normalized = reply.normalize('NFKC').trim().replace(/[。!!]+$/gu, '').trim();
23
27
  if (uncertain.test(normalized)) return 'UNDECIDED';
24
28
  if (reportedChoice.test(normalized)) return 'UNDECIDED';
29
+ if (deniedConfirmation.test(normalized)) return 'UNDECIDED';
25
30
  const choice = normalized.replace(/[“”"'`]/gu, '');
26
31
  const rejectsPre = negativePre.test(choice);
32
+ if (!rejectsPre && unclearIntent.test(choice)) return 'UNDECIDED';
27
33
  const wantsPre = explicitPre.test(rejectsPre ? choice.replace(negativePre, '') : choice)
28
34
  || exactConfirm.test(choice) || contextualConfirm.test(choice);
29
35
  const wantsStable = rejectsPre || stableChoice.test(choice);
@@ -33,25 +39,157 @@ export function decideChannelOffer(reply) {
33
39
  return 'UNDECIDED';
34
40
  }
35
41
 
36
- function publishedWindowsPreAvailable() {
37
- const windows = process.platform === 'win32';
38
- const result = spawnSync(windows ? (process.env.ComSpec || 'cmd.exe') : 'npm',
39
- windows
40
- ? ['/d', '/s', '/c', 'npm.cmd view @alipay/alipay-aipay@latest alipayAipayChannels.pre --json']
41
- : ['view', '@alipay/alipay-aipay@latest', 'alipayAipayChannels.pre', '--json'],
42
- { encoding: 'utf8', timeout: 8000, maxBuffer: 64 * 1024, windowsHide: true });
43
- if (result.error || result.status !== 0) return 'CHECK_FAILED';
44
- let value;
45
- try { value = JSON.parse(result.stdout); } catch { return 'CHECK_FAILED'; }
46
- const match = typeof value === 'string' ? /^(\d+)\.(\d+)\.(\d+)-pre$/u.exec(value) : null;
47
- if (!match) return 'UNAVAILABLE';
42
+ function regularFile(file) {
43
+ try { return fs.statSync(file).isFile(); } catch { return false; }
44
+ }
45
+
46
+ // Discovery never creates shims, downloads Node, or edits the parent PATH.
47
+ export function packageManagerInvocation(tool, args, {
48
+ node = process.execPath, env = process.env, windows = process.platform === 'win32',
49
+ isFile = regularFile, readdir = dir => fs.readdirSync(dir, {withFileTypes: true}),
50
+ } = {}) {
51
+ if (!['npm', 'npx'].includes(tool) || args.some(arg => !/^[A-Za-z0-9@./=+_-]+$/u.test(arg))) throw new Error('invalid package-manager invocation');
52
+ if (!windows) return {command: tool, args, env};
53
+ const moduleName = tool === 'npm' ? 'npm-cli.js' : 'npx-cli.js';
54
+ const pathKey = Object.keys(env).find(key => key.toUpperCase() === 'PATH') || 'PATH';
55
+ const dirs = [...new Set([path.dirname(node), env.APPDATA && path.join(env.APPDATA, 'npm'),
56
+ ...(env[pathKey] || '').split(';')].filter(dir => dir && path.isAbsolute(dir)))];
57
+ const candidates = [
58
+ env.npm_execpath && path.join(path.dirname(env.npm_execpath), moduleName),
59
+ ...dirs.map(dir => path.join(dir, 'node_modules', 'npm', 'bin', moduleName)),
60
+ path.join(path.dirname(node), '..', 'node_modules', 'npm', 'bin', moduleName),
61
+ ];
62
+ const childEnv = {...env, [pathKey]: [path.dirname(node), ...dirs].join(';')};
63
+ const findModule = () => {
64
+ for (const candidate of candidates) {
65
+ if (candidate && path.isAbsolute(candidate) && isFile(candidate)) return {command: node, args: [candidate, ...args], env: childEnv};
66
+ }
67
+ return null;
68
+ };
69
+ const found = findModule();
70
+ if (found) return found;
71
+ for (const dir of dirs) {
72
+ if (isFile(path.join(dir, `${tool}.cmd`))) return {
73
+ command: env.ComSpec || 'cmd.exe',
74
+ args: ['/d', '/s', '/c', `${tool}.cmd ${args.join(' ')}`], env: childEnv,
75
+ };
76
+ }
77
+ // Reuse npm obtained by an earlier npx invocation, limited to known cache roots.
78
+ const cacheRoots = [env.npm_config_cache,
79
+ env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, 'npm-cache'),
80
+ env.APPDATA && path.join(env.APPDATA, 'npm-cache')];
81
+ candidates.length = 0;
82
+ for (const root of new Set(cacheRoots.filter(root => root && path.isAbsolute(root)))) {
83
+ try {
84
+ for (const entry of readdir(path.join(root, '_npx')).filter(e => e.isDirectory() && /^[a-f0-9]+$/u.test(e.name)).slice(0, 32)) {
85
+ candidates.push(path.join(root, '_npx', entry.name, 'node_modules', 'npm', 'bin', moduleName));
86
+ }
87
+ } catch { /* Cache is optional. */ }
88
+ }
89
+ return findModule();
90
+ }
91
+
92
+ const single = value => Array.isArray(value) && value.length === 1 ? value[0] : value;
93
+ const versionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
94
+ export function parsePublishedPre(stdout) {
95
+ let metadata;
96
+ try { metadata = single(JSON.parse(stdout)); } catch { return {status: 'CHECK_FAILED', reason: 'INVALID_METADATA'}; }
97
+ 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'};
100
+ const value = single(single(metadata?.alipayAipayChannels)?.pre);
101
+ 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;
103
+ if (!match) return {status: 'CHECK_FAILED', reason: 'INVALID_METADATA'};
48
104
  const [major, minor, patch] = match.slice(1).map(Number);
49
- if (![major, minor, patch].every(Number.isSafeInteger)) return 'UNAVAILABLE';
50
- return major > 1 || (major === 1 && (minor > 7 || (minor === 7 && patch >= 3)))
51
- ? 'AVAILABLE' : 'UNAVAILABLE';
105
+ if (![major, minor, patch].every(Number.isSafeInteger)) return {status: 'CHECK_FAILED', reason: 'INVALID_METADATA'};
106
+ return major > 1 || (major === 1 && (minor > 7 || (minor === 7 && patch >= 4)))
107
+ ? {status: 'AVAILABLE', packageVersion} : {status: 'UNAVAILABLE', reason: 'PRE_VERSION_TOO_OLD'};
108
+ }
109
+ export const publishedPreFromMetadata = stdout => parsePublishedPre(stdout).status;
110
+
111
+ export function registryMetadata() {
112
+ return new Promise((resolve, reject) => {
113
+ const request = https.get('https://registry.npmjs.org/@alipay%2falipay-aipay/latest',
114
+ {headers: {accept: 'application/json'}}, response => {
115
+ if (response.statusCode !== 200) { response.resume(); reject(new Error('REGISTRY_HTTP_ERROR')); return; }
116
+ let data = ''; let bytes = 0;
117
+ response.setEncoding('utf8');
118
+ response.on('data', chunk => {
119
+ bytes += Buffer.byteLength(chunk);
120
+ if (bytes > 256 * 1024) request.destroy(new Error('METADATA_TOO_LARGE'));
121
+ else data += chunk;
122
+ });
123
+ response.on('error', reject);
124
+ response.on('end', () => resolve(data));
125
+ });
126
+ const timer = setTimeout(() => request.destroy(new Error('REGISTRY_TIMEOUT')), 10000);
127
+ request.on('error', reject);
128
+ request.on('close', () => clearTimeout(timer));
129
+ });
130
+ }
131
+
132
+ export async function publishedWindowsPreAvailable({
133
+ run = spawnSync, readRegistry = registryMetadata,
134
+ invocationOptions = {windows: process.platform === 'win32'},
135
+ } = {}) {
136
+ const invocation = packageManagerInvocation('npm', ['view', '@alipay/alipay-aipay@latest', 'version', 'alipayAipayChannels', '--json'], invocationOptions);
137
+ if (!invocation) {
138
+ try { return parsePublishedPre(await readRegistry()); }
139
+ catch { return {status: 'CHECK_FAILED', reason: 'REGISTRY_UNREACHABLE'}; }
140
+ }
141
+ const result = run(invocation.command, invocation.args,
142
+ {env: invocation.env, encoding: 'utf8', timeout: 10000, maxBuffer: 64 * 1024, windowsHide: true, shell: false});
143
+ if (result.error || result.status !== 0) return {status: 'CHECK_FAILED', reason: 'NPM_QUERY_FAILED'};
144
+ return parsePublishedPre(result.stdout);
145
+ }
146
+
147
+ // Bound the whole installation tree so a timeout cannot leave a background install.
148
+ export function runPreInstaller(invocation) {
149
+ return new Promise(resolve => {
150
+ let child;
151
+ try {
152
+ child = spawn(invocation.command, invocation.args, {env: invocation.env,
153
+ windowsHide: true, shell: false, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe']});
154
+ } catch { resolve({status:'INSTALL_FAILED', reason:'INSTALL_PROCESS_FAILED'}); return; }
155
+ let stdout = ''; let timedOut = false;
156
+ const timer = setTimeout(() => {
157
+ timedOut = true;
158
+ if (process.platform === 'win32' && child.pid) spawnSync('taskkill.exe', ['/pid', String(child.pid), '/t', '/f'], {stdio:'ignore', timeout:2000, windowsHide:true});
159
+ else if (child.pid) { try { process.kill(-child.pid, 'SIGKILL'); } catch {} }
160
+ child.kill('SIGKILL');
161
+ }, 120000);
162
+ child.stdout?.on('data', chunk => { stdout = (stdout + chunk).slice(-1024 * 1024); });
163
+ child.stderr?.resume();
164
+ child.on('error', () => { clearTimeout(timer); resolve({status: 'INSTALL_FAILED', reason:'INSTALL_PROCESS_FAILED'}); });
165
+ child.on('close', code => {
166
+ clearTimeout(timer);
167
+ resolve(!timedOut && code === 0 ? {status:'INSTALLED', stdout} : {status:'INSTALL_FAILED', reason:timedOut ? 'INSTALL_TIMEOUT' : 'INSTALL_PROCESS_FAILED'});
168
+ });
169
+ });
170
+ }
171
+
172
+ export async function installWindowsPre(reply, {
173
+ query = publishedWindowsPreAvailable, install = runPreInstaller,
174
+ invocationOptions = {windows: process.platform === 'win32'},
175
+ } = {}) {
176
+ if (decideChannelOffer(reply) !== 'PRE') return {status: 'UNDECIDED', reason:'EXPLICIT_PRE_REQUIRED'};
177
+ const published = await query();
178
+ if (published.status !== 'AVAILABLE') return published;
179
+ const spec = `@alipay/alipay-aipay@${published.packageVersion}`;
180
+ const invocation = packageManagerInvocation('npx', ['-y', spec, 'install', '--channel', 'pre'], invocationOptions)
181
+ || packageManagerInvocation('npm', ['exec', '--yes', '--package', spec, '--', 'alipay-aipay', 'install', '--channel', 'pre'], invocationOptions);
182
+ if (!invocation) return {status: 'INSTALL_FAILED', reason:'NPM_TOOLCHAIN_MISSING'};
183
+ return install(invocation);
184
+ }
185
+
186
+ function report(outcome) {
187
+ if (outcome.reason) process.stderr.write(`ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER_REASON=${outcome.reason}\n`);
188
+ if (outcome.stdout) process.stdout.write(outcome.stdout);
189
+ process.stderr.write(`${marker}${outcome.status}\n`);
52
190
  }
53
191
 
54
- export function runChannelOfferCli(argv = process.argv) {
192
+ export async function runChannelOfferCli(argv = process.argv) {
55
193
  const action = argv[2];
56
194
  if (action === 'show' && argv.length === 3) {
57
195
  if (!isWindowsPlatform()) {
@@ -67,15 +205,21 @@ export function runChannelOfferCli(argv = process.argv) {
67
205
  return;
68
206
  }
69
207
  if (action === 'available' && argv.length === 3 && isWindowsPlatform()) {
70
- process.stderr.write(`${marker}${publishedWindowsPreAvailable()}\n`);
208
+ report(await publishedWindowsPreAvailable());
209
+ return;
210
+ }
211
+ if (action === 'install' && argv.length === 5 && argv[3] === '--reply' && isWindowsPlatform()) {
212
+ const outcome = await installWindowsPre(argv[4]);
213
+ report(outcome);
214
+ if (outcome.status !== 'INSTALLED') process.exitCode = 1;
71
215
  return;
72
216
  }
73
- throw new Error('用法: runtime.mjs channel-offer show | decide --reply <用户原始回复> | available');
217
+ throw new Error('用法: runtime.mjs channel-offer show | decide --reply <用户原始回复> | available | install --reply <用户原始回复>');
74
218
  }
75
219
 
76
220
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
77
221
  try {
78
- runChannelOfferCli();
222
+ await runChannelOfferCli();
79
223
  } catch (error) {
80
224
  process.stderr.write(`ALIPAY_AIPAY_INTERNAL:CHANNEL_OFFER_ERROR=${String(error?.message || error).replace(/[\r\n]/gu, ' ')}\n`);
81
225
  process.exitCode = 1;
package/skill-pre/VERSION CHANGED
@@ -1 +1 @@
1
- 1.7.3-pre
1
+ 1.7.4-pre
@@ -172,7 +172,7 @@ function runPlatformEntry(args = {}) {
172
172
  }
173
173
  const platformId = platformIdFromEnv(process.env)
174
174
  || runContext?.run_id
175
- || '';
175
+ || sessionPlatformId(process.env);
176
176
  if (!platformId) {
177
177
  throw new Error('Windows 产品开通引导缺少有效的当前 Onboarding run context');
178
178
  }
@@ -181,6 +181,12 @@ function runPlatformEntry(args = {}) {
181
181
  });
182
182
  }
183
183
 
184
+ function sessionPlatformId(env = process.env) {
185
+ const session = env.ALIPAY_AIPAY_SESSION_ID;
186
+ return typeof session === 'string' && session && !/[\u0000-\u001f\u007f,^=]/u.test(session)
187
+ ? `session_${createHash('sha256').update(session).digest('hex').slice(0, 32)}` : '';
188
+ }
189
+
184
190
  function platformIdFromEnv(env = process.env) {
185
191
  const candidate = env.COZE_PROJECT_ID
186
192
  || env.MEOO_PROJECT_ID
@@ -4,6 +4,7 @@ import os from 'node:os';
4
4
  import crypto from 'node:crypto';
5
5
  import {refreshWindowsCli} from './cli-install.mjs';
6
6
  import {findGitBash} from './git-discovery.mjs';
7
+ import {findWinget} from './winget.mjs';
7
8
  import {policy,run,stateRoot,readState,saveState,withLock,ensureReady,locateJq,fail,report} from './preflight.mjs';
8
9
 
9
10
  export function claimPreparation(state,{repair=false,authorized=false}={}) {
@@ -15,6 +16,14 @@ export function claimPreparation(state,{repair=false,authorized=false}={}) {
15
16
  state.prepareAttempts++;state.blocked=true;state.reason='PREPARATION_INCOMPLETE';
16
17
  return state;
17
18
  }
19
+ export function installGitWithWinget({env=process.env,execute=run,discover=findGitBash}={}) {
20
+ const command=findWinget({env,execute});
21
+ if(!command)fail('MANUAL_GIT_INSTALL_REQUIRED','Git Bash');
22
+ const result=execute(command,['install','--id','Git.Git','--exact','--source','winget','--scope','user',
23
+ '--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
+ if(result.error||result.status!==0||!discover({env}))fail('GIT_INSTALL_FAILED','Git Bash');
26
+ }
18
27
  function installAsset(name,asset) {
19
28
  const bin=path.join(os.homedir(),'.local/bin');fs.mkdirSync(bin,{recursive:true});
20
29
  const target=path.join(bin,name+'.exe');
@@ -36,10 +45,7 @@ export function prepare({authorized=false,repair=false}={}) {
36
45
  const state=claimPreparation(readState(root),{authorized,repair});saveState(root,state);
37
46
  try {
38
47
  if(!findGitBash()) {
39
- const winget=path.join(process.env.LOCALAPPDATA||'','Microsoft/WindowsApps/winget.exe');
40
- if(!fs.existsSync(winget))fail('MANUAL_GIT_INSTALL_REQUIRED','Git Bash');
41
- const r=run(winget,['install','--id','Git.Git','--exact','--source','winget','--scope','user','--silent','--disable-interactivity','--accept-package-agreements','--accept-source-agreements'],{timeout:120000});
42
- if(r.status!==0||!findGitBash())fail('GIT_INSTALL_FAILED','Git Bash');
48
+ installGitWithWinget();
43
49
  }
44
50
  if(!locateJq()) installAsset('jq',policy.jq);
45
51
  const cli=path.join(os.homedir(),'.local/bin/alipay-cli.exe');
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import {fileURLToPath} from 'node:url';
4
4
  import {run, fail, protect} from './acl.mjs';
5
+ import {findWinget} from './winget.mjs';
5
6
 
6
7
  const script = fileURLToPath(new URL('../../../integration/modules/scripts/local_402_sandbox_pay.py', import.meta.url));
7
8
  const probe = 'import json, os, runpy, sys; assert sys.version_info.major == 3 and sys.platform == "win32"; sys.path.insert(0, os.path.dirname(sys.argv[1])); runpy.run_path(sys.argv[1], run_name="__alipay_probe__"); print(json.dumps({"executable":sys.executable,"version":list(sys.version_info[:3])}))';
@@ -49,8 +50,8 @@ export function prepareWindowsPython({authorized = false, repair = false, find =
49
50
  if (repair ? state.repairAttempts >= 1 : state.prepareAttempts >= 1 || state.repairAttempts >= 1) fail('RETRY_BUDGET_EXHAUSTED');
50
51
  state.prepareAttempts++; if (repair) state.repairAttempts++;
51
52
  fs.writeFileSync(file, JSON.stringify(state));
52
- const winget = env.LOCALAPPDATA && path.win32.join(env.LOCALAPPDATA, 'Microsoft/WindowsApps/winget.exe');
53
- if (!winget || !exists(winget)) fail('MANUAL_PYTHON_INSTALL_REQUIRED', 'Python 3');
53
+ const winget = findWinget({env,execute});
54
+ if (!winget) fail('MANUAL_PYTHON_INSTALL_REQUIRED', 'Python 3');
54
55
  const result = execute(winget, ['install', '--id', 'Python.Python.3.14', '--exact', '--source', 'winget',
55
56
  '--scope', 'user', '--architecture', 'x64', '--silent', '--disable-interactivity',
56
57
  '--accept-package-agreements', '--accept-source-agreements'], {env, timeout: 180000});
@@ -0,0 +1,13 @@
1
+ import path from 'node:path';
2
+ import {run} from './acl.mjs';
3
+
4
+ // App execution aliases can be runnable even when fs.existsSync returns false.
5
+ // Probe only; installing remains the caller's authorized, bounded operation.
6
+ export function findWinget({env=process.env,execute=run}={}) {
7
+ const alias=env.LOCALAPPDATA && path.win32.join(env.LOCALAPPDATA,'Microsoft/WindowsApps/winget.exe');
8
+ for(const command of [...new Set([alias,'winget.exe'].filter(Boolean))]) {
9
+ const result=execute(command,['--version'],{env,timeout:5000});
10
+ if(!result.error && result.status===0) return command;
11
+ }
12
+ return null;
13
+ }
@@ -33,7 +33,7 @@ stdout 为空、原始安装器输出不对客,只映射根协议的唯一刷
33
33
 
34
34
  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
35
 
36
- 初次仅缺依赖或本机 `alipay-cli.exe` 无法启动且已有授权时,按 Git Bash → jq → 必要的 CLI 修复 → 完整复检准备一次。CLI 已可用时不重复安装;仅缺失/不可用时由同一正式 PowerShell 安装器修复。Git 仅在本机有 winget 时使用登记的用户级安装,最多 120 秒;winget 不是 Windows 必备组件,缺失时转下述 Git 官方手动安装指引。jq 单次资产下载最多 60 秒,验证 SHA-256 后落盘并检查实际启动;CLI 整次刷新最多 60 秒,完成后由 Node 单独验证目标 `.exe version`,限时 5 秒。jq 托管安装只替换用户目录中缺失或不可用的 jq,不覆盖健康的其他版本;网络、架构、权限或完整性失败不靠重装解决。失败即 LEGACY,不轮换镜像、包管理器、WSL 或 shell,不提权、不修改执行策略、系统级 PATH 或 HOME。Windows CLI 安装器可能持久更新用户 PATH。
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。
37
37
 
38
38
  环境诊断及次数保存在 `%LOCALAPPDATA%/alipay-aipay-pre/windows/environment.json`,不含业务材料。重载和普通回复不清零;短期锁最多等待 2 秒。锁持续占用或诊断状态损坏时,已通过完整工具检查的只读 READY 仍可继续;自动安装保持阻断,不删除锁或状态绕过预算。
39
39
 
@@ -64,7 +64,7 @@ Step 1: 方案规划 → Step 2: 环境检查 → Step 3: 登录授权
64
64
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry --agent-platform "<AGENT_PLATFORM>" --session-id "<SESSION_ID>"
65
65
  ```
66
66
 
67
- - Windows READY:平台入口复检通过且 stdout 为空时继续本地 Onboarding。Windows LEGACY:runner 优先从正式 run 取 `run_id` 生成 `platform_id`,没有正式 run 时退回当前会话可用的平台标识;禁止 Agent 传入或拼接;沿用原消息遥测映射,不额外生成 summary 或关闭 run;送达唯一 stdout 后停止 Onboarding;`full_process` 保留 Integration 结果。
67
+ - Windows READY:平台入口复检通过且 stdout 为空时继续本地 Onboarding。Windows LEGACY:runner 依次使用现有平台项目 ID、正式 run_id;产品未定而尚无 run 时,使用已绑定 `SESSION_ID` 的稳定匿名哈希生成 `platform_id`,不提前创建 run。禁止 Agent 传入或拼接;沿用原消息遥测映射,不额外生成 summary 或关闭 run;送达唯一 stdout 后停止 Onboarding;`full_process` 保留 Integration 结果。
68
68
  - 非 Windows:成功且 stdout 为空即继续;失败或输出不唯一时停止核对,禁止手写引导。
69
69
 
70
70
  用户明确要用企业账号通过 Skill 开通时执行下列消息并停止 Onboarding;不得主动询问账号类型,也不得从 MCC、营业执照或“公司”等描述推断。
@@ -73,7 +73,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" windows-entry --root-in
73
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、用户输入计数或阶段责任方。
74
74
 
75
75
 
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 引导渲染优先取现有正式 run_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`。
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`。
77
77
 
78
78
  若 bootstrap 阶段产品尚未唯一确定,则不会补开正式 run;首个产品确认或方案确认消息在产品已唯一确定后首次触发 `start-run` 并上报 `clarify_intent`,后续只按当前 active run 计数。
79
79