huaweicloud-devkit 1.1.0-next.8 → 1.1.0
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/README.md +46 -2
- package/README.zh-CN.md +45 -2
- package/package.json +3 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +29 -12
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +136 -24
- package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +15 -14
- package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +5 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/hc-activity.md +27 -0
- package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +11 -11
- package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +6 -5
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +291 -53
- package/plugins/huaweicloud-core/skills/huawei-sandbox/references/nginx-templates.md +4 -0
- package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +19 -12
- package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +26 -26
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +12 -6
- package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +7 -4
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +7 -7
- package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +14 -0
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +30 -1
- package/plugins/huaweicloud-core/src/auth/service.mjs +33 -0
- package/plugins/huaweicloud-core/src/detect-framework.mjs +25 -3
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +67 -5
- package/plugins/huaweicloud-core/src/mcp-server.mjs +23 -0
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +11 -3
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +365 -11
- package/plugins/huaweicloud-core/src/search-market.mjs +1 -0
- package/plugins/huaweicloud-core/src/setup-cli.mjs +491 -64
- package/plugins/huaweicloud-core/src/tools.mjs +184 -14
|
@@ -299,21 +299,34 @@ async function createTarGz(localDir, exclude = []) {
|
|
|
299
299
|
mkdirSync(archiveDir, { recursive: true });
|
|
300
300
|
const archivePath = join(archiveDir, archiveName);
|
|
301
301
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
302
|
+
const hasGit = existsSync(join(localDir, '.git'));
|
|
303
|
+
if (hasGit) {
|
|
304
|
+
await execFileAsync('git', [
|
|
305
|
+
'-C',
|
|
306
|
+
localDir,
|
|
307
|
+
'archive',
|
|
308
|
+
'--format=tar.gz',
|
|
309
|
+
`--prefix=${basename(localDir)}/`,
|
|
310
|
+
`--output=${archivePath}`,
|
|
311
|
+
'HEAD',
|
|
312
|
+
]);
|
|
313
|
+
} else {
|
|
314
|
+
const args = [];
|
|
315
|
+
for (const pattern of exclude) {
|
|
316
|
+
if (pattern.startsWith('**/')) {
|
|
317
|
+
const base = pattern.slice(3);
|
|
318
|
+
for (let depth = 0; depth <= 4; depth++) {
|
|
319
|
+
const prefix = depth === 0 ? '' : '*/'.repeat(depth);
|
|
320
|
+
args.push('--exclude', `${prefix}${base}`);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
args.push('--exclude', pattern);
|
|
309
324
|
}
|
|
310
|
-
} else {
|
|
311
|
-
args.push('--exclude', pattern);
|
|
312
325
|
}
|
|
326
|
+
args.push('-czf', archivePath, '-C', dirname(localDir), basename(localDir));
|
|
327
|
+
await execFileAsync('tar', args);
|
|
313
328
|
}
|
|
314
|
-
args.push('-czf', archivePath, '-C', dirname(localDir), basename(localDir));
|
|
315
329
|
|
|
316
|
-
await execFileAsync('tar', args);
|
|
317
330
|
return archivePath;
|
|
318
331
|
}
|
|
319
332
|
|
|
@@ -643,6 +656,22 @@ export async function uploadProjectWithSession(
|
|
|
643
656
|
username,
|
|
644
657
|
timeoutMs,
|
|
645
658
|
);
|
|
659
|
+
try {
|
|
660
|
+
await execWithSession(
|
|
661
|
+
workspaceId,
|
|
662
|
+
[
|
|
663
|
+
`REAL_PATH=$(readlink -f "${targetParentDir}/${projectName}" 2>/dev/null || echo "${targetParentDir}/${projectName}")`,
|
|
664
|
+
`if [ ! -d "$REAL_PATH" ] && [ -d "${targetParentDir}" ]; then`,
|
|
665
|
+
` REAL_PATH="${targetParentDir}/${projectName}"`,
|
|
666
|
+
`fi`,
|
|
667
|
+
`chmod -R o+rX "$REAL_PATH" 2>/dev/null || true`,
|
|
668
|
+
`find "$REAL_PATH" -type d -exec chmod o+x {} \\; 2>/dev/null || true`,
|
|
669
|
+
`find "$REAL_PATH" -type f -path "*/node_modules/.bin/*" -exec chmod +x {} \\; 2>/dev/null || true`,
|
|
670
|
+
].join('\n'),
|
|
671
|
+
username,
|
|
672
|
+
15000,
|
|
673
|
+
);
|
|
674
|
+
} catch {}
|
|
646
675
|
}
|
|
647
676
|
|
|
648
677
|
try {
|
|
@@ -661,6 +690,331 @@ export async function uploadProjectWithSession(
|
|
|
661
690
|
};
|
|
662
691
|
}
|
|
663
692
|
|
|
693
|
+
export async function deployNginx(
|
|
694
|
+
workspaceId,
|
|
695
|
+
{ nginxType, port, project, outputDir, nodePort, publicPort, configName },
|
|
696
|
+
username = 'root',
|
|
697
|
+
timeoutMs = 60000,
|
|
698
|
+
) {
|
|
699
|
+
if (!workspaceId) {
|
|
700
|
+
throw new Error('sandbox deploy nginx: workspace_id is required.');
|
|
701
|
+
}
|
|
702
|
+
if (!nginxType || !port || !project || !outputDir) {
|
|
703
|
+
throw new Error('sandbox deploy nginx: nginxType, port, project, and outputDir are required.');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const nginxCheck = await execOneShot(
|
|
707
|
+
workspaceId,
|
|
708
|
+
'command -v nginx >/dev/null 2>&1 && echo "INSTALLED" || echo "MISSING"',
|
|
709
|
+
username,
|
|
710
|
+
10000,
|
|
711
|
+
);
|
|
712
|
+
if (!String(nginxCheck.stdout || '').includes('INSTALLED')) {
|
|
713
|
+
throw new Error(
|
|
714
|
+
'sandbox deploy nginx: nginx is not installed. Install it first:\n' +
|
|
715
|
+
' Detect OS: source /etc/os-release && echo $ID\n' +
|
|
716
|
+
' apt: sudo apt-get update -qq && sudo apt-get install -y -qq nginx\n' +
|
|
717
|
+
' yum: sudo yum install -y nginx\n' +
|
|
718
|
+
' dnf: sudo dnf install -y nginx\n' +
|
|
719
|
+
'Alternatively, skip nginx and use Python HTTP server (see nginx-templates.md).',
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const listenPort = publicPort || port;
|
|
724
|
+
const basePort = nginxType === 'proxy' ? listenPort : port;
|
|
725
|
+
|
|
726
|
+
let targetPort = basePort;
|
|
727
|
+
let portWarning;
|
|
728
|
+
const maxPortAttempts = 10;
|
|
729
|
+
for (let offset = 0; offset < maxPortAttempts; offset += 1) {
|
|
730
|
+
targetPort = basePort + offset;
|
|
731
|
+
try {
|
|
732
|
+
const portCheck = await execOneShot(
|
|
733
|
+
workspaceId,
|
|
734
|
+
`ss -tlnp 2>/dev/null | grep -q ":${targetPort} " && echo "IN_USE" || echo "FREE"`,
|
|
735
|
+
username,
|
|
736
|
+
10000,
|
|
737
|
+
);
|
|
738
|
+
if (!String(portCheck.stdout || '').includes('IN_USE')) break;
|
|
739
|
+
if (offset === 0) {
|
|
740
|
+
portWarning = `Port ${basePort} is in use — auto-assigned port ${targetPort}`;
|
|
741
|
+
}
|
|
742
|
+
} catch {}
|
|
743
|
+
if (offset === maxPortAttempts - 1) {
|
|
744
|
+
throw new Error(
|
|
745
|
+
`sandbox deploy nginx: all ports ${basePort}-${basePort + maxPortAttempts - 1} are in use. Free a port and try again.`,
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const effectiveNodePort =
|
|
751
|
+
nginxType === 'proxy' ? (nodePort && nodePort !== listenPort ? nodePort : listenPort + 1) : undefined;
|
|
752
|
+
|
|
753
|
+
const projectPath = `/workspace/${project}`;
|
|
754
|
+
const outputPath = outputDir.startsWith('/') ? outputDir : `${projectPath}/${outputDir}`;
|
|
755
|
+
|
|
756
|
+
const resolveScript = `REAL_PROJECT=$(readlink -f "${projectPath}" 2>/dev/null || echo "${projectPath}")
|
|
757
|
+
REAL_OUTPUT="${outputPath}"
|
|
758
|
+
if [ "\${REAL_PROJECT}" != "${projectPath}" ]; then
|
|
759
|
+
REL_OUTPUT=$(echo "${outputDir}" | sed "s|${projectPath}/||")
|
|
760
|
+
REAL_OUTPUT="\${REAL_PROJECT}/\${REL_OUTPUT}"
|
|
761
|
+
fi`;
|
|
762
|
+
|
|
763
|
+
const templates = {
|
|
764
|
+
spa: `server {
|
|
765
|
+
listen ${targetPort};
|
|
766
|
+
root ${outputPath};
|
|
767
|
+
index index.html;
|
|
768
|
+
|
|
769
|
+
location / {
|
|
770
|
+
try_files $uri /index.html;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
774
|
+
expires 1h;
|
|
775
|
+
add_header Cache-Control "public, immutable";
|
|
776
|
+
}
|
|
777
|
+
}`,
|
|
778
|
+
proxy: `server {
|
|
779
|
+
listen ${listenPort};
|
|
780
|
+
server_name _;
|
|
781
|
+
large_client_header_buffers 4 32k;
|
|
782
|
+
|
|
783
|
+
location / {
|
|
784
|
+
proxy_pass http://127.0.0.1:${effectiveNodePort};
|
|
785
|
+
proxy_http_version 1.1;
|
|
786
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
787
|
+
proxy_set_header Connection 'upgrade';
|
|
788
|
+
proxy_set_header Host $host;
|
|
789
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
790
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
791
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
792
|
+
proxy_cache_bypass $http_upgrade;
|
|
793
|
+
proxy_read_timeout 60s;
|
|
794
|
+
proxy_buffer_size 128k;
|
|
795
|
+
proxy_buffers 4 256k;
|
|
796
|
+
proxy_busy_buffers_size 256k;
|
|
797
|
+
}
|
|
798
|
+
}`,
|
|
799
|
+
static: `server {
|
|
800
|
+
listen ${targetPort};
|
|
801
|
+
root ${outputPath};
|
|
802
|
+
index index.html;
|
|
803
|
+
|
|
804
|
+
location / {
|
|
805
|
+
autoindex off;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
809
|
+
expires 1h;
|
|
810
|
+
add_header Cache-Control "public, immutable";
|
|
811
|
+
}
|
|
812
|
+
}`,
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const config = templates[nginxType];
|
|
816
|
+
if (!config) {
|
|
817
|
+
throw new Error(`sandbox deploy nginx: unknown nginxType "${nginxType}". Must be one of: spa, proxy, static`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const cmd = [
|
|
821
|
+
resolveScript,
|
|
822
|
+
`sudo mkdir -p /etc/nginx/conf.d`,
|
|
823
|
+
`sudo tee /etc/nginx/conf.d/${configName || project}.conf > /dev/null << 'NGINX_EOF'`,
|
|
824
|
+
config,
|
|
825
|
+
`NGINX_EOF`,
|
|
826
|
+
`# Resolve symlinks for chmod (chmod does not follow symlinks on Linux)`,
|
|
827
|
+
`chmod -R o+rX "$REAL_PROJECT" 2>/dev/null || true`,
|
|
828
|
+
`find "$REAL_PROJECT" -type d -exec chmod o+x {} \\; 2>/dev/null || true`,
|
|
829
|
+
`find "$REAL_PROJECT" -type f -path "*/node_modules/.bin/*" -exec chmod +x {} \\; 2>/dev/null || true`,
|
|
830
|
+
`if pgrep -x nginx > /dev/null 2>&1; then sudo nginx -s reload 2>/dev/null || { sudo killall -9 nginx 2>/dev/null; sleep 1; sudo nginx; }; else sudo nginx; fi`,
|
|
831
|
+
].join('\n');
|
|
832
|
+
|
|
833
|
+
const result = await execOneShot(workspaceId, cmd, username, timeoutMs);
|
|
834
|
+
|
|
835
|
+
let tunnelActive = false;
|
|
836
|
+
try {
|
|
837
|
+
const tunnelCheck = await execOneShot(
|
|
838
|
+
workspaceId,
|
|
839
|
+
'devbridge list -j 2>/dev/null | grep -q \'"tunnelId"\' && echo "ACTIVE" || echo "INACTIVE"',
|
|
840
|
+
username,
|
|
841
|
+
10000,
|
|
842
|
+
);
|
|
843
|
+
tunnelActive = String(tunnelCheck.stdout || '').includes('ACTIVE');
|
|
844
|
+
} catch {}
|
|
845
|
+
|
|
846
|
+
return {
|
|
847
|
+
ok: result.exitCode === 0,
|
|
848
|
+
nginxType,
|
|
849
|
+
port: targetPort,
|
|
850
|
+
nodePort: effectiveNodePort || undefined,
|
|
851
|
+
outputPath,
|
|
852
|
+
projectPath,
|
|
853
|
+
exitCode: result.exitCode,
|
|
854
|
+
stdout: result.stdout,
|
|
855
|
+
nextStep: 'expose_via_devbridge',
|
|
856
|
+
warning:
|
|
857
|
+
(!tunnelActive
|
|
858
|
+
? 'No active DevBridge tunnel — deployment is incomplete. Proceed to Step 7 to expose the app.'
|
|
859
|
+
: portWarning) || undefined,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export async function deployCheck(
|
|
864
|
+
workspaceId,
|
|
865
|
+
{ port, project, outputDir, frameworkType },
|
|
866
|
+
username = 'root',
|
|
867
|
+
timeoutMs = 30000,
|
|
868
|
+
) {
|
|
869
|
+
if (!workspaceId) {
|
|
870
|
+
throw new Error('sandbox deploy check: workspace_id is required.');
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const projectPath = `/workspace/${project}`;
|
|
874
|
+
const outputPath = outputDir.startsWith('/') ? outputDir : `${projectPath}/${outputDir}`;
|
|
875
|
+
const isCrossPlatform = frameworkType === 'cross-platform';
|
|
876
|
+
|
|
877
|
+
const checkScript = [
|
|
878
|
+
`echo "=== DEPLOY CHECK ==="`,
|
|
879
|
+
`PASS=0`,
|
|
880
|
+
`TOTAL=0`,
|
|
881
|
+
``,
|
|
882
|
+
`TOTAL=$((TOTAL+1))`,
|
|
883
|
+
`if curl -s -o /dev/null -w "%{http_code}" http://localhost:${port} 2>/dev/null | grep -qE "^(2|3)"; then`,
|
|
884
|
+
` echo "nginx_serving:PASS (port ${port})"`,
|
|
885
|
+
` PASS=$((PASS+1))`,
|
|
886
|
+
`else`,
|
|
887
|
+
` echo "nginx_serving:FAIL"`,
|
|
888
|
+
`fi`,
|
|
889
|
+
``,
|
|
890
|
+
`TOTAL=$((TOTAL+1))`,
|
|
891
|
+
`if [ -d "${outputPath}" ] && ls -A "${outputPath}" 2>/dev/null | grep -q .; then`,
|
|
892
|
+
` echo "output_dir:PASS (${outputPath})"`,
|
|
893
|
+
` PASS=$((PASS+1))`,
|
|
894
|
+
`else`,
|
|
895
|
+
` echo "output_dir:FAIL (${outputPath} empty or missing)"`,
|
|
896
|
+
`fi`,
|
|
897
|
+
``,
|
|
898
|
+
`TOTAL=$((TOTAL+1))`,
|
|
899
|
+
`FINGERPRINT_FILE="${outputPath}/.deploy_fingerprint"`,
|
|
900
|
+
`FINGERPRINT_EXPECTED=$(cat "$FINGERPRINT_FILE" 2>/dev/null)`,
|
|
901
|
+
`if [ -n "$FINGERPRINT_EXPECTED" ]; then`,
|
|
902
|
+
` FINGERPRINT_ACTUAL=$(curl -s http://localhost:${port}/.deploy_fingerprint 2>/dev/null)`,
|
|
903
|
+
` if [ "$FINGERPRINT_EXPECTED" = "$FINGERPRINT_ACTUAL" ]; then`,
|
|
904
|
+
` echo "content_verified:PASS"`,
|
|
905
|
+
` PASS=$((PASS+1))`,
|
|
906
|
+
` else`,
|
|
907
|
+
` echo "content_verified:FAIL (fingerprint mismatch — nginx may be serving stale content from a previous deployment)"`,
|
|
908
|
+
` fi`,
|
|
909
|
+
`else`,
|
|
910
|
+
` echo "content_verified:SKIP (no fingerprint file)"`,
|
|
911
|
+
` TOTAL=$((TOTAL-1))`,
|
|
912
|
+
`fi`,
|
|
913
|
+
``,
|
|
914
|
+
`TOTAL=$((TOTAL+1))`,
|
|
915
|
+
`if devbridge list -j 2>/dev/null | grep -q '"tunnelId"'; then`,
|
|
916
|
+
` echo "devbridge_tunnel:PASS"`,
|
|
917
|
+
` PASS=$((PASS+1))`,
|
|
918
|
+
`else`,
|
|
919
|
+
` echo "devbridge_tunnel:FAIL"`,
|
|
920
|
+
`fi`,
|
|
921
|
+
``,
|
|
922
|
+
`TOTAL=$((TOTAL+1))`,
|
|
923
|
+
`TUNNEL_ID=$(devbridge list -j 2>/dev/null | grep -oP '"tunnelId":\\s*"\\K[^"]+' | head -1)`,
|
|
924
|
+
`TUNNEL_URL="https://\${TUNNEL_ID}-${port}.cn-north-4-bridge.myhuaweicloud.com"`,
|
|
925
|
+
`if [ -n "$TUNNEL_ID" ] && [ -n "$TUNNEL_URL" ]; then`,
|
|
926
|
+
` HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$TUNNEL_URL" 2>/dev/null || echo "000")`,
|
|
927
|
+
` if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "304" ]; then`,
|
|
928
|
+
` echo "tunnel_url_accessible:PASS ($TUNNEL_URL -> $HTTP_CODE)"`,
|
|
929
|
+
` PASS=$((PASS+1))`,
|
|
930
|
+
` else`,
|
|
931
|
+
` echo "tunnel_url_accessible:FAIL ($TUNNEL_URL -> HTTP $HTTP_CODE)"`,
|
|
932
|
+
` fi`,
|
|
933
|
+
`else`,
|
|
934
|
+
` echo "tunnel_url_accessible:FAIL (no tunnel URL)"`,
|
|
935
|
+
`fi`,
|
|
936
|
+
``,
|
|
937
|
+
`${`
|
|
938
|
+
TOTAL=$((TOTAL+1))
|
|
939
|
+
if [ -f "${outputPath}/qr.png" ]; then
|
|
940
|
+
echo "qr_code:PASS"
|
|
941
|
+
PASS=$((PASS+1))
|
|
942
|
+
else
|
|
943
|
+
# Auto-detect cross-platform from project files (more reliable than frameworkType param)
|
|
944
|
+
PROJ_DIR="/workspace/${project}"
|
|
945
|
+
IS_CROSS=0
|
|
946
|
+
if [ -f "$PROJ_DIR/manifest.json" ] || grep -qE '"@tarojs/taro"|"@dcloudio/uni-app"' "$PROJ_DIR/package.json" 2>/dev/null; then
|
|
947
|
+
IS_CROSS=1
|
|
948
|
+
fi
|
|
949
|
+
if [ $IS_CROSS -eq 0 ] && ( [ -f "$PROJ_DIR/app.config.ts" ] || [ -f "$PROJ_DIR/app.config.js" ] ) && grep -qE "pages|tabBar" "$PROJ_DIR/app.config."* 2>/dev/null; then
|
|
950
|
+
IS_CROSS=1
|
|
951
|
+
fi
|
|
952
|
+
|
|
953
|
+
if [ $IS_CROSS -eq 1 ]; then
|
|
954
|
+
if [ -n "$TUNNEL_URL" ]; then
|
|
955
|
+
curl -s "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$TUNNEL_URL" 2>/dev/null)" -o "${outputPath}/qr.png" 2>/dev/null
|
|
956
|
+
chmod o+r "${outputPath}/qr.png" 2>/dev/null || true
|
|
957
|
+
if [ -f "${outputPath}/qr.png" ] && [ -s "${outputPath}/qr.png" ]; then
|
|
958
|
+
echo "qr_code:PASS (auto-generated)"
|
|
959
|
+
PASS=$((PASS+1))
|
|
960
|
+
else
|
|
961
|
+
echo "qr_code:FAIL (QR generation failed)"
|
|
962
|
+
fi
|
|
963
|
+
else
|
|
964
|
+
echo "qr_code:FAIL (no tunnel URL for QR generation)"
|
|
965
|
+
fi
|
|
966
|
+
else
|
|
967
|
+
echo "qr_code:SKIP (not a cross-platform project)"
|
|
968
|
+
TOTAL=$((TOTAL-1))
|
|
969
|
+
fi
|
|
970
|
+
fi
|
|
971
|
+
`}`,
|
|
972
|
+
`echo "SCORE:\${PASS}/\${TOTAL}"`,
|
|
973
|
+
`echo "TUNNEL_URL:\${TUNNEL_URL:-}"`,
|
|
974
|
+
`[ "\${PASS}" = "\${TOTAL}" ] && echo "VERDICT:COMPLETE" || echo "VERDICT:INCOMPLETE"`,
|
|
975
|
+
]
|
|
976
|
+
.filter(Boolean)
|
|
977
|
+
.join('\n');
|
|
978
|
+
|
|
979
|
+
const result = await execOneShot(workspaceId, checkScript, username, timeoutMs);
|
|
980
|
+
const stdout = String(result.stdout || '');
|
|
981
|
+
const checks = {};
|
|
982
|
+
const lines = stdout.split('\n');
|
|
983
|
+
for (const line of lines) {
|
|
984
|
+
const m = line.match(/^(\w+):(\w+)\b(.*)/);
|
|
985
|
+
if (m) checks[m[1]] = { status: m[2], detail: (m[3] || '').trim() };
|
|
986
|
+
}
|
|
987
|
+
const scoreMatch = stdout.match(/SCORE:(\d+)\/(\d+)/);
|
|
988
|
+
const tunnelMatch = stdout.match(/TUNNEL_URL:(https:\/\/[^\s]+)/);
|
|
989
|
+
const complete = /VERDICT:COMPLETE/.test(stdout);
|
|
990
|
+
|
|
991
|
+
const missing = [];
|
|
992
|
+
if (!complete) {
|
|
993
|
+
for (const [key, val] of Object.entries(checks)) {
|
|
994
|
+
if (val.status === 'FAIL') missing.push(key);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return {
|
|
999
|
+
ok: true,
|
|
1000
|
+
complete,
|
|
1001
|
+
checkType: isCrossPlatform ? 'cross-platform' : 'standard',
|
|
1002
|
+
checks,
|
|
1003
|
+
score: scoreMatch ? { pass: parseInt(scoreMatch[1], 10), total: parseInt(scoreMatch[2], 10) } : null,
|
|
1004
|
+
publicUrl: tunnelMatch ? tunnelMatch[1] : undefined,
|
|
1005
|
+
missingSteps: missing.length > 0 ? missing.join(', ') : undefined,
|
|
1006
|
+
nextStep: !complete
|
|
1007
|
+
? missing.includes('devbridge_tunnel') || missing.includes('tunnel_url_accessible')
|
|
1008
|
+
? 'expose_via_devbridge'
|
|
1009
|
+
: missing.includes('nginx_serving')
|
|
1010
|
+
? 'configure_nginx'
|
|
1011
|
+
: missing.includes('qr_code')
|
|
1012
|
+
? 'generate_qr_code'
|
|
1013
|
+
: 'review_checks'
|
|
1014
|
+
: 'complete',
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
664
1018
|
export async function closeSession(workspaceId, username) {
|
|
665
1019
|
const key = `${workspaceId}:${username}`;
|
|
666
1020
|
const session = sessions.get(key);
|
|
@@ -171,6 +171,7 @@ export async function searchMarketplace(query = '', category = '') {
|
|
|
171
171
|
description: truncate(skill.description),
|
|
172
172
|
triggers: (skill.triggers || []).slice(0, 5),
|
|
173
173
|
matched,
|
|
174
|
+
installCommand: `npx skills add huaweicloud/huaweicloud-skills --skill ${skill.name}`,
|
|
174
175
|
});
|
|
175
176
|
}
|
|
176
177
|
results.sort((a, b) => b.score - a.score);
|