@alipay/alipay-aipay 1.5.6 → 1.5.7

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.
@@ -43,7 +43,11 @@ fi
43
43
 
44
44
  MISSING=()
45
45
  command -v node >/dev/null 2>&1 || MISSING+=("node")
46
- command -v jq >/dev/null 2>&1 || MISSING+=("jq")
46
+ if ! command -v jq >/dev/null 2>&1 \
47
+ || ! jq --version >/dev/null 2>&1 \
48
+ || ! jq -n -e --arg value ok '$value == "ok"' >/dev/null 2>&1; then
49
+ MISSING+=("jq")
50
+ fi
47
51
  if ! command -v alipay-cli >/dev/null 2>&1 || ! alipay-cli version >/dev/null 2>&1; then
48
52
  MISSING+=("alipay-cli")
49
53
  fi
@@ -5,6 +5,27 @@
5
5
  # 调用前置: 无
6
6
  #=============================================================================
7
7
 
8
+ case "${HOME:-}" in
9
+ /*)
10
+ user_bin="$HOME/.local/bin"
11
+ if [ "$HOME" != "/" ] && [ -x "$user_bin/jq" ]; then
12
+ case ":${PATH:-}:" in
13
+ *":$user_bin:"*) ;;
14
+ *) PATH="${PATH:+$PATH:}$user_bin"; export PATH ;;
15
+ esac
16
+ if ! command -v jq >/dev/null 2>&1 \
17
+ || ! jq --version >/dev/null 2>&1 \
18
+ || ! jq -n -e --arg value ok '$value == "ok"' >/dev/null 2>&1; then
19
+ if "$user_bin/jq" --version >/dev/null 2>&1 \
20
+ && "$user_bin/jq" -n -e --arg value ok '$value == "ok"' >/dev/null 2>&1; then
21
+ jq() { "$HOME/.local/bin/jq" "$@"; }
22
+ fi
23
+ fi
24
+ fi
25
+ unset user_bin
26
+ ;;
27
+ esac
28
+
8
29
  require_command() {
9
30
  if ! command -v "$1" >/dev/null 2>&1; then
10
31
  echo "❌ 缺少依赖命令: $1"
@@ -475,6 +475,10 @@ function projectSnapshotFingerprint(projectPath) {
475
475
  // Missing optional project facts do not enter the snapshot.
476
476
  }
477
477
  }
478
+ for (const entry of safeReaddir(canonicalPath)) {
479
+ if (!entry.isFile() || entry.isSymbolicLink() || !/\.(?:csproj|sln)$/iu.test(entry.name)) continue;
480
+ tracked.add(path.join(canonicalPath, entry.name));
481
+ }
478
482
  const hash = crypto.createHash('sha256');
479
483
  hash.update(`project=${canonicalPath}\n`);
480
484
  for (const file of [...tracked].sort((left, right) => left.localeCompare(right))) {
@@ -657,6 +661,170 @@ function normalizeLanguage(input) {
657
661
  throw new Error('服务端语言非法,仅支持 Java、Python、Node.js、C#、PHP');
658
662
  }
659
663
 
664
+ const existingProjectFrameworkFallback = '沿用现有项目结构';
665
+ const defaultFrameworks = Object.freeze({
666
+ Java: 'Spring Boot',
667
+ PHP: 'Laravel',
668
+ Python: 'FastAPI',
669
+ 'C#': 'ASP.NET Core',
670
+ 'Node.js': 'Express'
671
+ });
672
+ const frameworkSources = new Set([
673
+ 'EXPLICIT',
674
+ 'DEFAULT_NEW_PROJECT',
675
+ 'PROJECT_INFERRED',
676
+ 'PROJECT_STRUCTURE_FALLBACK'
677
+ ]);
678
+
679
+ function readProjectMarker(projectPath, relativePath) {
680
+ const file = path.join(projectPath, relativePath);
681
+ try {
682
+ const stat = fs.lstatSync(file);
683
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2 * 1024 * 1024) return '';
684
+ return fs.readFileSync(file, 'utf8');
685
+ } catch {
686
+ return '';
687
+ }
688
+ }
689
+
690
+ function readProjectJson(projectPath, relativePath) {
691
+ const content = readProjectMarker(projectPath, relativePath);
692
+ if (!content) return null;
693
+ try {
694
+ const parsed = JSON.parse(content);
695
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
696
+ } catch {
697
+ return null;
698
+ }
699
+ }
700
+
701
+ function dependencyNames(manifest) {
702
+ const names = new Set();
703
+ for (const key of ['dependencies', 'devDependencies', 'peerDependencies', 'require', 'require-dev']) {
704
+ const dependencies = manifest?.[key];
705
+ if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) continue;
706
+ for (const name of Object.keys(dependencies)) names.add(name.toLowerCase());
707
+ }
708
+ return names;
709
+ }
710
+
711
+ function firstMatchingFramework(names, rules) {
712
+ for (const [framework, matches] of rules) {
713
+ if (matches.some((name) => names.has(name) || [...names].some((item) => item.startsWith(`${name}/`)))) {
714
+ return framework;
715
+ }
716
+ }
717
+ return '';
718
+ }
719
+
720
+ function inferFramework(projectPath, language) {
721
+ if (language === 'Node.js') {
722
+ return firstMatchingFramework(dependencyNames(readProjectJson(projectPath, 'package.json')), [
723
+ ['NestJS', ['@nestjs/core']],
724
+ ['Next.js', ['next']],
725
+ ['Express', ['express']],
726
+ ['Fastify', ['fastify']],
727
+ ['Koa', ['koa']],
728
+ ['Hono', ['hono']]
729
+ ]);
730
+ }
731
+ if (language === 'PHP') {
732
+ return firstMatchingFramework(dependencyNames(readProjectJson(projectPath, 'composer.json')), [
733
+ ['Laravel', ['laravel/framework']],
734
+ ['Symfony', ['symfony/framework-bundle', 'symfony/http-kernel']]
735
+ ]);
736
+ }
737
+ if (language === 'Java') {
738
+ const content = [
739
+ readProjectMarker(projectPath, 'pom.xml'),
740
+ readProjectMarker(projectPath, 'build.gradle'),
741
+ readProjectMarker(projectPath, 'build.gradle.kts')
742
+ ].join('\n').toLowerCase();
743
+ if (/spring-boot|org\.springframework\.boot/u.test(content)) return 'Spring Boot';
744
+ if (/io\.quarkus|quarkus-/u.test(content)) return 'Quarkus';
745
+ if (/io\.micronaut|micronaut-/u.test(content)) return 'Micronaut';
746
+ return '';
747
+ }
748
+ if (language === 'Python') {
749
+ const content = [
750
+ readProjectMarker(projectPath, 'pyproject.toml'),
751
+ readProjectMarker(projectPath, 'requirements.txt'),
752
+ readProjectMarker(projectPath, 'Pipfile')
753
+ ].join('\n').toLowerCase();
754
+ if (/(^|[^a-z])django([^a-z]|$)/u.test(content)) return 'Django';
755
+ if (/(^|[^a-z])fastapi([^a-z]|$)/u.test(content)) return 'FastAPI';
756
+ if (/(^|[^a-z])flask([^a-z]|$)/u.test(content)) return 'Flask';
757
+ return '';
758
+ }
759
+ if (language === 'C#') {
760
+ const content = safeReaddir(projectPath)
761
+ .filter((entry) => entry.isFile() && !entry.isSymbolicLink() && entry.name.toLowerCase().endsWith('.csproj'))
762
+ .map((entry) => readProjectMarker(projectPath, entry.name))
763
+ .join('\n').toLowerCase();
764
+ if (/microsoft\.net\.sdk\.web|microsoft\.aspnetcore/u.test(content)) return 'ASP.NET Core';
765
+ return '';
766
+ }
767
+ return '';
768
+ }
769
+
770
+ function sanitizeFramework(value) {
771
+ const framework = value.trim();
772
+ if (!framework || framework.length > 120 || /[\u0000-\u001f\u007f|`]/u.test(framework) || /https?:\/\//iu.test(framework)) {
773
+ throw new Error('framework 必须是安全的单行框架名称');
774
+ }
775
+ return framework;
776
+ }
777
+
778
+ function deriveFramework(projectPath, projectSelection, language) {
779
+ if (projectSelection === 'NEW_PROJECT' || projectSelection === 'PREPARED_NEW_PROJECT') {
780
+ return { framework: defaultFrameworks[language], frameworkSource: 'DEFAULT_NEW_PROJECT' };
781
+ }
782
+ const inferred = inferFramework(projectPath, language);
783
+ return {
784
+ framework: inferred || existingProjectFrameworkFallback,
785
+ frameworkSource: inferred ? 'PROJECT_INFERRED' : 'PROJECT_STRUCTURE_FALLBACK'
786
+ };
787
+ }
788
+
789
+ function resolveFramework(args, projectPath, projectSelection, language) {
790
+ const suppliedFramework = typeof args.framework === 'string' && args.framework.length > 0
791
+ ? sanitizeFramework(args.framework)
792
+ : '';
793
+ const suppliedSource = typeof args['framework-source'] === 'string' ? args['framework-source'] : '';
794
+ if (suppliedSource && !frameworkSources.has(suppliedSource)) throw new Error('framework-source 非法');
795
+
796
+ if (!suppliedSource) {
797
+ return suppliedFramework
798
+ ? { framework: suppliedFramework, frameworkSource: 'EXPLICIT' }
799
+ : deriveFramework(projectPath, projectSelection, language);
800
+ }
801
+ if (!suppliedFramework) throw new Error('framework-source 必须与 framework 同时提供');
802
+ if (suppliedSource === 'EXPLICIT') {
803
+ return { framework: suppliedFramework, frameworkSource: suppliedSource };
804
+ }
805
+
806
+ const derived = deriveFramework(projectPath, projectSelection, language);
807
+ if (derived.framework !== suppliedFramework || derived.frameworkSource !== suppliedSource) {
808
+ throw new Error('framework 与当前项目推导事实不一致');
809
+ }
810
+ return derived;
811
+ }
812
+
813
+ function runFrameworkResolve(args) {
814
+ const language = normalizeLanguage(required(args, 'language'));
815
+ const projectSelection = required(args, 'project-selection');
816
+ if (!['NEW_PROJECT', 'PREPARED_NEW_PROJECT', 'CURRENT_PROJECT', 'OTHER_PROJECT'].includes(projectSelection)) {
817
+ throw new Error('project-selection 非法');
818
+ }
819
+ let projectPath = '';
820
+ if (projectSelection === 'CURRENT_PROJECT' || projectSelection === 'OTHER_PROJECT') {
821
+ projectPath = fs.realpathSync(required(args, 'project-path'));
822
+ if (!isReadableDirectory(projectPath)) throw new Error('现有项目路径不可访问');
823
+ }
824
+ const resolved = resolveFramework(args, projectPath, projectSelection, language);
825
+ return JSON.stringify({ language, ...resolved });
826
+ }
827
+
660
828
  function startConfirmationFacts(args) {
661
829
  const productType = required(args, 'product-type');
662
830
  const mapping = productVariants[productType];
@@ -665,7 +833,7 @@ function startConfirmationFacts(args) {
665
833
  const projectPath = required(args, 'project-path');
666
834
  const projectSelection = required(args, 'project-selection');
667
835
  const language = normalizeLanguage(required(args, 'language'));
668
- const framework = required(args, 'framework');
836
+ const { framework, frameworkSource } = resolveFramework(args, projectPath, projectSelection, language);
669
837
  const productName = args['product-name'] || mapping.productName;
670
838
  if (productName !== mapping.productName) throw new Error('product-name 与 product-type 不匹配');
671
839
  if (!path.isAbsolute(projectPath)) throw new Error('project-path 必须是绝对路径');
@@ -697,6 +865,7 @@ function startConfirmationFacts(args) {
697
865
  projectSelection,
698
866
  language,
699
867
  framework,
868
+ frameworkSource,
700
869
  route,
701
870
  projectFingerprint: projectSnapshotFingerprint(projectPath)
702
871
  };
@@ -712,6 +881,7 @@ function confirmationReceipt(facts) {
712
881
  project_selection: facts.projectSelection,
713
882
  language: facts.language,
714
883
  framework: facts.framework,
884
+ framework_source: facts.frameworkSource,
715
885
  project_fingerprint: facts.projectFingerprint,
716
886
  route: {
717
887
  project_origin: facts.route.projectOrigin,
@@ -743,14 +913,15 @@ export function createStartConfirmation(args) {
743
913
 
744
914
  export function validateStartConfirmationReceipt(receipt) {
745
915
  const saved = decodeReceipt(receipt, receiptKinds.startConfirmation);
746
- const facts = startConfirmationFacts({
916
+ const args = {
747
917
  'product-type': saved.product_type,
748
918
  'product-name': saved.product_name,
749
919
  'project-path': saved.project_path,
750
920
  'project-selection': saved.project_selection,
751
- language: saved.language,
752
- framework: saved.framework
753
- });
921
+ language: saved.language
922
+ };
923
+ if (saved.framework_source === 'EXPLICIT') args.framework = saved.framework;
924
+ const facts = startConfirmationFacts(args);
754
925
  const currentReceipt = confirmationReceipt(facts);
755
926
  if (currentReceipt === receipt) return { valid: true, receipt, productType: saved.product_type };
756
927
  return {
@@ -973,6 +1144,10 @@ export async function runIntegrationMessageCli(argv = process.argv) {
973
1144
  process.stderr.write(`${internalPrefix}PROJECT_CONFIRM_RECEIPT=${result.receipt}\n`);
974
1145
  return;
975
1146
  }
1147
+ if (command === 'framework-resolve') {
1148
+ console.log(runFrameworkResolve(args));
1149
+ return;
1150
+ }
976
1151
  if (command === 'checklist-scope-receipt') {
977
1152
  process.stderr.write(`${internalPrefix}CHECKLIST_SCOPE_RECEIPT=${createChecklistScopeReceipt(required(args, 'project-path'))}\n`);
978
1153
  return;
@@ -1030,7 +1205,7 @@ export async function runIntegrationMessageCli(argv = process.argv) {
1030
1205
  console.log(await runChecklistResult(args));
1031
1206
  return;
1032
1207
  }
1033
- throw new Error('用法: integration_message_runner.mjs <resolve-project|locate-projects|start-confirm|checklist-scope-receipt|a2m-execution-guard|a2m-execution-result|context-required|product-clarify|product-unsupported|windows-sandbox-setup|local-acceptance-notice|a2m-test-start|sandbox-android-client|service-not-ready|safety-closeout|windows-sandbox-summary|checklist-result> ...');
1208
+ throw new Error('用法: integration_message_runner.mjs <resolve-project|locate-projects|framework-resolve|start-confirm|checklist-scope-receipt|a2m-execution-guard|a2m-execution-result|context-required|product-clarify|product-unsupported|windows-sandbox-setup|local-acceptance-notice|a2m-test-start|sandbox-android-client|service-not-ready|safety-closeout|windows-sandbox-summary|checklist-result> ...');
1034
1209
  }
1035
1210
 
1036
1211
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from 'node:child_process';
4
+ import { createHash, randomBytes } from 'node:crypto';
5
+ import fs from 'node:fs';
6
+ import fsp from 'node:fs/promises';
7
+ import https from 'node:https';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import process from 'node:process';
11
+ import { promisify } from 'node:util';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const execFileAsync = promisify(execFile);
15
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
16
+ const manifestPath = path.resolve(scriptDir, '../json/managed-jq-assets.json');
17
+ const internalPrefix = 'ALIPAY_AIPAY_INTERNAL:JQ_PREPARE=';
18
+
19
+ function readManifest() {
20
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
21
+ if (
22
+ parsed?.schemaVersion !== 1 ||
23
+ !/^\d+\.\d+\.\d+$/u.test(parsed.version || '') ||
24
+ !Number.isInteger(parsed.downloadTimeoutMs) || parsed.downloadTimeoutMs <= 0 ||
25
+ !Number.isInteger(parsed.idleTimeoutMs) || parsed.idleTimeoutMs <= 0 ||
26
+ !Number.isInteger(parsed.maxBytes) || parsed.maxBytes <= 0 ||
27
+ !Number.isInteger(parsed.maxRedirects) || parsed.maxRedirects < 0 ||
28
+ !parsed.assets || typeof parsed.assets !== 'object' || Array.isArray(parsed.assets)
29
+ ) {
30
+ throw new Error('managed jq 资产清单非法');
31
+ }
32
+ return Object.freeze(parsed);
33
+ }
34
+
35
+ export const managedJqManifest = readManifest();
36
+
37
+ export function managedJqPath(homeDir = process.env.HOME || os.homedir()) {
38
+ if (!path.isAbsolute(homeDir) || path.resolve(homeDir) === path.parse(homeDir).root) {
39
+ throw new Error('jq 用户安装目录必须位于绝对 Home 路径下');
40
+ }
41
+ return path.join(path.resolve(homeDir), '.local', 'bin', 'jq');
42
+ }
43
+
44
+ export function resolveManagedJqAsset(platform = process.platform, arch = process.arch) {
45
+ const asset = managedJqManifest.assets[`${platform}-${arch}`];
46
+ return asset ? Object.freeze({ ...asset, version: managedJqManifest.version }) : null;
47
+ }
48
+
49
+ export async function checkManagedJqAtPath(jqPath, expectedVersion = managedJqManifest.version) {
50
+ try {
51
+ const { stdout } = await execFileAsync(jqPath, ['--version'], {
52
+ encoding: 'utf8',
53
+ timeout: 5000,
54
+ windowsHide: true
55
+ });
56
+ const version = String(stdout).trim().replace(/^jq-/u, '');
57
+ if (version !== expectedVersion) return { installed: false };
58
+ const { stdout: smokeOutput } = await execFileAsync(
59
+ jqPath,
60
+ ['-n', '-c', '--arg', 'value', 'ok', '{value:$value}'],
61
+ { encoding: 'utf8', timeout: 5000, windowsHide: true }
62
+ );
63
+ if (String(smokeOutput).trim() !== '{"value":"ok"}') return { installed: false };
64
+ return { installed: true, version, path: jqPath };
65
+ } catch {
66
+ return { installed: false };
67
+ }
68
+ }
69
+
70
+ function assertAllowedDownloadUrl(value) {
71
+ const url = new URL(value);
72
+ const githubHost = url.hostname === 'github.com' || url.hostname.endsWith('.githubusercontent.com');
73
+ if (url.protocol !== 'https:' || !githubHost || url.username || url.password) {
74
+ throw new Error('jq 下载地址未通过 GitHub HTTPS 白名单校验');
75
+ }
76
+ return url;
77
+ }
78
+
79
+ function downloadBuffer(
80
+ urlValue,
81
+ deadline = Date.now() + managedJqManifest.downloadTimeoutMs,
82
+ redirectCount = 0
83
+ ) {
84
+ return new Promise((resolve, reject) => {
85
+ let url;
86
+ try {
87
+ url = assertAllowedDownloadUrl(urlValue);
88
+ } catch (error) {
89
+ reject(error);
90
+ return;
91
+ }
92
+
93
+ const remaining = deadline - Date.now();
94
+ if (remaining <= 0) {
95
+ reject(new Error(`jq 下载超过 ${managedJqManifest.downloadTimeoutMs / 1000} 秒`));
96
+ return;
97
+ }
98
+
99
+ let settled = false;
100
+ const request = https.get(url, { headers: { 'User-Agent': 'alipay-aipay-jq-installer' } });
101
+ const totalTimer = setTimeout(() => {
102
+ request.destroy(new Error(`jq 下载超过 ${managedJqManifest.downloadTimeoutMs / 1000} 秒`));
103
+ }, remaining);
104
+
105
+ function fail(error) {
106
+ if (settled) return;
107
+ settled = true;
108
+ clearTimeout(totalTimer);
109
+ reject(error);
110
+ }
111
+
112
+ request.setTimeout(Math.min(managedJqManifest.idleTimeoutMs, remaining), () => {
113
+ request.destroy(new Error(`jq 下载连续 ${managedJqManifest.idleTimeoutMs / 1000} 秒无响应`));
114
+ });
115
+
116
+ request.on('response', (response) => {
117
+ const status = response.statusCode || 0;
118
+ const location = response.headers.location;
119
+ if (status >= 300 && status < 400 && location) {
120
+ response.resume();
121
+ clearTimeout(totalTimer);
122
+ settled = true;
123
+ if (redirectCount >= managedJqManifest.maxRedirects) {
124
+ reject(new Error('jq 下载重定向次数过多'));
125
+ return;
126
+ }
127
+ downloadBuffer(new URL(location, url).toString(), deadline, redirectCount + 1).then(resolve, reject);
128
+ return;
129
+ }
130
+ if (status !== 200) {
131
+ response.resume();
132
+ fail(new Error(`jq 下载返回 HTTP ${status || 'UNKNOWN'}`));
133
+ return;
134
+ }
135
+
136
+ const declaredLength = Number(response.headers['content-length'] || 0);
137
+ if (declaredLength > managedJqManifest.maxBytes) {
138
+ response.resume();
139
+ fail(new Error('jq 下载文件超过大小上限'));
140
+ return;
141
+ }
142
+
143
+ const chunks = [];
144
+ let size = 0;
145
+ response.on('data', (chunk) => {
146
+ size += chunk.length;
147
+ if (size > managedJqManifest.maxBytes) {
148
+ response.destroy(new Error('jq 下载文件超过大小上限'));
149
+ return;
150
+ }
151
+ chunks.push(chunk);
152
+ });
153
+ response.on('end', () => {
154
+ if (settled) return;
155
+ settled = true;
156
+ clearTimeout(totalTimer);
157
+ resolve(Buffer.concat(chunks));
158
+ });
159
+ response.on('error', fail);
160
+ });
161
+ request.on('error', fail);
162
+ });
163
+ }
164
+
165
+ export async function installManagedJqAsset(asset, targetPath, downloader = downloadBuffer) {
166
+ const targetDir = path.dirname(targetPath);
167
+ const tempPath = path.join(targetDir, `.jq-${process.pid}-${randomBytes(6).toString('hex')}.tmp`);
168
+ await fsp.mkdir(targetDir, { recursive: true, mode: 0o700 });
169
+ try {
170
+ const body = await downloader(asset.url);
171
+ const digest = createHash('sha256').update(body).digest('hex');
172
+ if (digest !== asset.sha256) throw new Error('jq 下载文件 SHA-256 校验失败');
173
+ await fsp.writeFile(tempPath, body, { flag: 'wx', mode: 0o700 });
174
+ await fsp.chmod(tempPath, 0o755);
175
+ const check = await checkManagedJqAtPath(tempPath, asset.version);
176
+ if (!check.installed) throw new Error('jq 用户级文件版本或 JSON 功能验证失败');
177
+ await fsp.rename(tempPath, targetPath);
178
+ return { ...check, path: targetPath };
179
+ } finally {
180
+ await fsp.rm(tempPath, { force: true }).catch(() => undefined);
181
+ }
182
+ }
183
+
184
+ export async function prepareManagedJq(options = {}) {
185
+ const platform = options.platform || process.platform;
186
+ const arch = options.arch || process.arch;
187
+ const asset = resolveManagedJqAsset(platform, arch);
188
+ if (!asset) return { status: 'UNSUPPORTED', platform, arch };
189
+ const targetPath = managedJqPath(options.homeDir);
190
+ const existing = await checkManagedJqAtPath(targetPath, asset.version);
191
+ if (existing.installed) return { status: 'READY', source: 'EXISTING', ...existing };
192
+ const installed = await installManagedJqAsset(asset, targetPath, options.downloader);
193
+ return { status: 'READY', source: 'INSTALLED', ...installed };
194
+ }
195
+
196
+ export async function runManagedJqCli(argv = process.argv) {
197
+ if (argv.length !== 3 || argv[2] !== 'prepare') throw new Error('用法: managed_jq.mjs prepare');
198
+ try {
199
+ const result = await prepareManagedJq();
200
+ process.stderr.write(`${internalPrefix}${result.status}\n`);
201
+ return result.status === 'READY' ? 0 : 2;
202
+ } catch {
203
+ process.stderr.write(`${internalPrefix}FAILED\n`);
204
+ return 1;
205
+ }
206
+ }
207
+
208
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
209
+ runManagedJqCli().then((code) => {
210
+ process.exitCode = code;
211
+ }).catch((error) => {
212
+ process.stderr.write(`${internalPrefix}FAILED\n`);
213
+ process.exitCode = 1;
214
+ });
215
+ }
@@ -32,7 +32,10 @@ export function checkCliEnvironment(options = {}) {
32
32
  const missing = [];
33
33
 
34
34
  if (!currentNodeAvailable(env)) missing.push('node');
35
- if (requireJq && !commandRuns('jq', ['--version'], { env })) missing.push('jq');
35
+ if (requireJq && (
36
+ !commandRuns('jq', ['--version'], { env }) ||
37
+ !commandRuns('jq', ['-n', '-e', '--arg', 'value', 'ok', '$value == "ok"'], { env })
38
+ )) missing.push('jq');
36
39
  if (requireAlipayCli && !commandRuns('alipay-cli', ['version'], { env })) missing.push('alipay-cli');
37
40
 
38
41
  return missing;
@@ -165,6 +165,12 @@ async function dispatch(domain, args) {
165
165
  return;
166
166
  }
167
167
  if (domain === 'env') {
168
+ if (args[0] === 'prepare-jq') {
169
+ if (args.length !== 1) throw new Error('用法: runtime.mjs env prepare-jq');
170
+ const { runManagedJqCli } = await import('./managed_jq.mjs');
171
+ process.exitCode = await runManagedJqCli(cliArgv(['prepare']));
172
+ return;
173
+ }
168
174
  const { isWindowsPlatform } = await import('./platform_compat.mjs');
169
175
  if (!isWindowsPlatform()) {
170
176
  return runShell(path.join(scriptDir, 'alipay_cli_env.sh'), path.dirname(scriptDir), args);
@@ -175,7 +175,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-execute-read
175
175
  ```
176
176
 
177
177
  - 唯一 `CLI_ENV:READY`:直接进入 Step 3,不读取完整安装文档。
178
- - `CLI_ENV:NEEDS_SETUP`:此时才读取 `../normal/alipay-cli-env.md`,按其现有安装、PATH、联网权限和验证规则补齐缺失项,再重跑同一检查;未取得 `READY` 不得进入 Step 3。
178
+ - `CLI_ENV:NEEDS_SETUP`:此时才读取 `../normal/alipay-cli-env.md`。若唯一 `CLI_ENV_MISSING` 包含 `jq`,先以所需网络和 `~/.local/bin` 写权限执行一次 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" env prepare-jq`;唯一 `ALIPAY_AIPAY_INTERNAL:JQ_PREPARE=READY` 后原样重跑 `env check`。`FAILED|UNSUPPORTED`,或 `READY` 后复检仍缺 jq 时,由 Agent 按环境文档执行当前系统首选包管理器兜底并再次复检;macOS 必须从清华镜像开始,明确失败后才依次选择阿里云、中科大和直连,不并发、不回到 `prepare-jq` 循环。其他缺失项按现有安装、PATH、联网权限和验证规则补齐后重跑同一检查;不得重复执行 `prepare-jq` 或新增业务确认点,未取得 `CLI_ENV:READY` 不得进入 Step 3。
179
179
  - 其他输出、非 0 或 marker 冲突:停在本步骤核对脚本,不得猜测环境已就绪。
180
180
 
181
181
  若组合动作本身不可用,原子 fallback 固定为先执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" flow onboarding execute --product-type "$PRODUCT_TYPE"`,再执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" env check --product-type "$PRODUCT_TYPE"`;fallback 不得重复 stage 或重复环境检查。
@@ -52,7 +52,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" render <messageId> --va
52
52
 
53
53
  三种入口均只在无法从已有产品名称或业务描述可靠确定唯一产品时执行 flow 登记的 `product-clarify` runner;不得推定或手写澄清。产品明确但 MCC 尚无检索输入时使用 `mcc-clarify`;已有经营描述后先按 Onboarding flow 读取一次当前 `modules/mcc-reference.md` 做 Agent 语义候选,再统一调用 `mcc-resolve-plan` 由 runner 复核、合并和渲染;用户直接提供当前表内完整 MCC 编码时可跳过 Agent 泛化并由 runner 精确校验。不得拆回 `mcc-search + mcc-candidates`、先旧检索失败后临时猜测,或为方案展示再拼 renderer 输入。
54
54
 
55
- 代码开发或完整接入缺项目位置/技术栈时,执行 `integration.context.required` 或 `integration.project_path.required` 标准消息。先区分新建与复用;`PROJECT_INPUT` 保留原话,明确新建项目/网站/商城/应用等产物时必须 `resolve-project --intent new`,不得因当前目录有项目改为复用。未知现有项目先轻量定位,所有路径再由 `resolve-project` 解析;补齐前不初始化、装依赖或改文件。
55
+ 代码开发或完整接入缺项目位置/服务端语言时,执行 `integration.context.required` 或 `integration.project_path.required` 标准消息;框架缺失不触发补问。先区分新建与复用;`PROJECT_INPUT` 保留原话,明确新建项目/网站/商城/应用等产物时必须 `resolve-project --intent new`,不得因当前目录有项目改为复用。未知现有项目先轻量定位,所有路径再由 `resolve-project` 解析。路径和服务端语言明确后使用 `integration-message framework-resolve`:新项目按语言采用固定默认框架,现有项目优先从依赖推导,无法推导时沿用现有结构,用户明确框架始终优先;补齐前不初始化、装依赖或改文件。
56
56
 
57
57
  每轮代码开发只渲染一次非阻塞 `integration.start.confirm`,两种意图复用同一次检查器事实并覆盖 Integration 步骤 1 至步骤8:代码开发校验;`full_process` 在方案展示前已完成步骤 1,方案送达且 receipt 有效后进入步骤 3。Onboarding 只保留一次非阻塞方案展示;方案送达且 MCC 唯一事实有效后,正常路径执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-execute-ready --product-type "$PRODUCT_TYPE"`,该组合动作只串行加载 `flow onboarding execute` 并执行既有 `env check`,不跨用户输入、授权页或任何写操作;执行后不得再重复单独加载 execute phase 或重复环境检查。服务修改仍保留绑定当前摘要的阻塞确认。签约材料完整后直接提交,服务创建摘要后直接创建,新建应用公钥校验成功后直接提审。
58
58