@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.
- package/dist/commands/install.d.ts +0 -1
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +32 -19
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/prepare-tools.d.ts.map +1 -1
- package/dist/commands/prepare-tools.js +33 -27
- package/dist/commands/prepare-tools.js.map +1 -1
- package/dist/commands/update.js +10 -6
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +5 -6
- package/dist/index.js.map +1 -1
- package/dist/utils/jq.d.ts +12 -9
- package/dist/utils/jq.d.ts.map +1 -1
- package/dist/utils/jq.js +76 -185
- package/dist/utils/jq.js.map +1 -1
- package/package.json +2 -2
- package/skill/VERSION +1 -1
- package/skill/references/integration/flow.md +18 -6
- package/skill/references/integration/modules/scripts/sandbox_config.sh +13 -11
- package/skill/references/normal/alipay-cli-env.md +22 -20
- package/skill/references/normal/full-process-routing.md +5 -5
- package/skill/references/normal/json/managed-jq-assets.json +1 -0
- package/skill/references/normal/scripts/alipay_cli_env.sh +5 -1
- package/skill/references/normal/scripts/common.sh +21 -0
- package/skill/references/normal/scripts/integration_message_runner.mjs +181 -6
- package/skill/references/normal/scripts/managed_jq.mjs +215 -0
- package/skill/references/normal/scripts/platform_compat.mjs +4 -1
- package/skill/references/normal/scripts/runtime.mjs +6 -0
- package/skill/references/onboarding/flow.md +1 -1
- package/skill/references/runtime-entry.md +1 -1
|
@@ -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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
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
|
|
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
|
-
|
|
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
|
|