huaweicloud-devkit 1.0.2-next.2 → 1.0.2-next.21
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/.agents/plugins/marketplace.json +18 -18
- package/README.md +81 -51
- package/README.zh-CN.md +81 -49
- package/bin/setup.cjs +5 -3
- package/integrations/opencode/opencode.json +1 -4
- package/package.json +61 -44
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +4 -17
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +4 -17
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +4 -17
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +30 -0
- package/plugins/huaweicloud-core/.mcp.json +2 -5
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +4 -18
- package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +32 -9
- package/plugins/huaweicloud-core/openclaw.plugin.json +20 -0
- package/plugins/huaweicloud-core/safety/policy.json +75 -99
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +41 -13
- package/plugins/huaweicloud-core/skills/huawei-apig/SKILL.md +34 -34
- package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +18 -18
- package/plugins/huaweicloud-core/skills/huawei-cbr/SKILL.md +19 -19
- package/plugins/huaweicloud-core/skills/huawei-cce/SKILL.md +34 -32
- package/plugins/huaweicloud-core/skills/huawei-cloud-eye/SKILL.md +30 -30
- package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/SKILL.md +13 -12
- package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/scripts/search-skills.mjs +64 -17
- package/plugins/huaweicloud-core/skills/huawei-cts/SKILL.md +20 -20
- package/plugins/huaweicloud-core/skills/huawei-dds-dcs/SKILL.md +31 -31
- package/plugins/huaweicloud-core/skills/huawei-deployment/SKILL.md +21 -20
- package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +39 -30
- package/plugins/huaweicloud-core/skills/huawei-dew/references/csms-usage.md +8 -1
- package/plugins/huaweicloud-core/skills/huawei-dew/references/kms-usage.md +6 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +39 -36
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +15 -4
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +11 -9
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +13 -13
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +40 -39
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/apig-event-format.md +12 -10
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/create-function.md +21 -20
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/triggers.md +19 -19
- package/plugins/huaweicloud-core/skills/huawei-gaussdb/SKILL.md +20 -20
- package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +27 -23
- package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +27 -27
- package/plugins/huaweicloud-core/skills/huawei-iam/references/policy-examples.md +65 -39
- package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +9 -9
- package/plugins/huaweicloud-core/skills/huawei-modelarts/SKILL.md +25 -25
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +45 -44
- package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-create.md +19 -19
- package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-lifecycle.md +12 -7
- package/plugins/huaweicloud-core/skills/huawei-obs/references/common-pitfalls.md +13 -12
- package/plugins/huaweicloud-core/skills/huawei-obs/references/single-file-share.md +4 -4
- package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +63 -58
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +127 -42
- package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +26 -26
- package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +39 -39
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +13 -12
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/security-group.md +20 -19
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/subnet.md +15 -15
- package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +18 -18
- package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/SKILL.md +13 -13
- package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/agents/openai.yaml +3 -3
- package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +17 -1
- package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/agents/openai.yaml +3 -3
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +27 -1
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/agents/openai.yaml +3 -3
- package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +56 -53
- package/plugins/huaweicloud-core/skills/huaweicloud-core/agents/openai.yaml +3 -3
- package/plugins/huaweicloud-core/skills/huaweicloud-core/references/report-issue.md +6 -6
- package/plugins/huaweicloud-core/skills/huaweicloud-core/references/select.md +15 -1
- package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +0 -1
- package/plugins/huaweicloud-core/skills/huaweicloud-safety/agents/openai.yaml +3 -3
- package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +33 -33
- package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/agents/openai.yaml +3 -3
- package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +108 -5
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +77 -4
- package/plugins/huaweicloud-core/src/auth/service.mjs +1 -1
- package/plugins/huaweicloud-core/src/data/icons-manifest.v1.json +3517 -0
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +25 -7
- package/plugins/huaweicloud-core/src/icon-library.mjs +171 -0
- package/plugins/huaweicloud-core/src/mcp-server.mjs +30 -3
- package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +20 -102
- package/plugins/huaweicloud-core/src/proxy/proxy-config.mjs +11 -4
- package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +4 -1
- package/plugins/huaweicloud-core/src/safety-policy.mjs +122 -68
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +9 -9
- package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +24 -26
- package/plugins/huaweicloud-core/src/sandbox/sandbox-file-server.py +45 -0
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +510 -10
- package/plugins/huaweicloud-core/src/search-market.mjs +46 -12
- package/plugins/huaweicloud-core/src/setup-cli.mjs +1251 -181
- package/plugins/huaweicloud-core/src/tools.mjs +600 -122
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +72 -38
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +10 -5
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +12 -15
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +12 -16
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +46 -31
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-tunnel-channel.mjs +267 -0
- package/plugins/huaweicloud-core/src/ws-exec/index.js +3 -0
- package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +9 -5
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { createConnection as netConnect } from 'node:net';
|
|
4
|
+
import { existsSync, readFileSync, statSync, mkdirSync, rmSync, createReadStream, appendFileSync } from 'node:fs';
|
|
5
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
6
|
+
import { join, dirname, basename } from 'node:path';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
3
8
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
9
|
import { createConnection, getCredentials } from './hwlink-api.mjs';
|
|
5
10
|
import { getWebSocketImpl } from '../proxy/proxy-agent.mjs';
|
|
6
11
|
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
|
|
7
14
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
15
|
export const WS_EXEC_INDEX_URL = pathToFileURL(join(__dirname, '..', 'ws-exec', 'index.js')).href;
|
|
9
16
|
|
|
10
|
-
|
|
17
|
+
let currentWorkspaceId = process.env.HW_WORKSPACE_ID || null;
|
|
18
|
+
|
|
19
|
+
function setWorkspaceId(id) {
|
|
20
|
+
currentWorkspaceId = id;
|
|
21
|
+
process.env.HW_WORKSPACE_ID = id;
|
|
22
|
+
}
|
|
11
23
|
|
|
12
24
|
function resolveEnv() {
|
|
13
25
|
const env = { ...process.env };
|
|
@@ -21,8 +33,12 @@ async function runNodeExec(args, timeoutMs = 30000) {
|
|
|
21
33
|
const proc = spawn('node', args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
22
34
|
let stdout = '';
|
|
23
35
|
let stderr = '';
|
|
24
|
-
proc.stdout.on('data', (d) => {
|
|
25
|
-
|
|
36
|
+
proc.stdout.on('data', (d) => {
|
|
37
|
+
stdout += d.toString();
|
|
38
|
+
});
|
|
39
|
+
proc.stderr.on('data', (d) => {
|
|
40
|
+
stderr += d.toString();
|
|
41
|
+
});
|
|
26
42
|
|
|
27
43
|
const timer = setTimeout(() => {
|
|
28
44
|
proc.kill();
|
|
@@ -56,7 +72,7 @@ async function getSession(workspaceId, username, timeoutMs) {
|
|
|
56
72
|
const { ak, sk, securitytoken } = getCredentials();
|
|
57
73
|
const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
|
|
58
74
|
|
|
59
|
-
const WebSocketImpl = getWebSocketImpl(wsUrl);
|
|
75
|
+
const WebSocketImpl = await getWebSocketImpl(wsUrl);
|
|
60
76
|
|
|
61
77
|
const { connectHwlinkTerminalSession } = await import(WS_EXEC_INDEX_URL);
|
|
62
78
|
const session = await connectHwlinkTerminalSession({
|
|
@@ -71,11 +87,50 @@ async function getSession(workspaceId, username, timeoutMs) {
|
|
|
71
87
|
return session;
|
|
72
88
|
}
|
|
73
89
|
|
|
90
|
+
async function createTunnelSession(workspaceId, username, timeoutMs = 30000) {
|
|
91
|
+
const { ak, sk, securitytoken } = getCredentials();
|
|
92
|
+
const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
|
|
93
|
+
const WebSocketImpl = await getWebSocketImpl(wsUrl);
|
|
94
|
+
|
|
95
|
+
const { HwlinkWebSocketMultiplexer } = await import(WS_EXEC_INDEX_URL);
|
|
96
|
+
const mux = new HwlinkWebSocketMultiplexer(wsUrl, source, { WebSocketImpl, protocol: 'devenv' });
|
|
97
|
+
|
|
98
|
+
await new Promise((resolve, reject) => {
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
clearInterval(interval);
|
|
101
|
+
reject(new Error('tunnel session WebSocket open timeout'));
|
|
102
|
+
}, timeoutMs);
|
|
103
|
+
const interval = setInterval(() => {
|
|
104
|
+
if (mux.readyState === 1) {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
clearInterval(interval);
|
|
107
|
+
resolve();
|
|
108
|
+
} else if (mux.readyState === 3) {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
clearInterval(interval);
|
|
111
|
+
reject(new Error('tunnel session WebSocket closed'));
|
|
112
|
+
}
|
|
113
|
+
}, 100);
|
|
114
|
+
mux.onClose = () => {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
clearInterval(interval);
|
|
117
|
+
reject(new Error('tunnel session WebSocket closed'));
|
|
118
|
+
};
|
|
119
|
+
mux.onError = (err) => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
clearInterval(interval);
|
|
122
|
+
reject(err);
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return { mux, close: () => mux.close() };
|
|
127
|
+
}
|
|
128
|
+
|
|
74
129
|
export async function execOneShot(workspaceId, command, username, timeoutMs) {
|
|
75
130
|
const { ak, sk, securitytoken } = getCredentials();
|
|
76
131
|
const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
|
|
77
132
|
|
|
78
|
-
const WebSocketImpl = getWebSocketImpl(wsUrl);
|
|
133
|
+
const WebSocketImpl = await getWebSocketImpl(wsUrl);
|
|
79
134
|
|
|
80
135
|
const { executeHwlinkCommand } = await import(WS_EXEC_INDEX_URL);
|
|
81
136
|
return await executeHwlinkCommand({
|
|
@@ -93,20 +148,465 @@ export async function execWithSession(workspaceId, command, username, timeoutMs)
|
|
|
93
148
|
return await session.exec(command, { timeoutMs });
|
|
94
149
|
}
|
|
95
150
|
|
|
151
|
+
export const UPLOAD_CHUNK_SIZE = 30000;
|
|
152
|
+
|
|
153
|
+
export const UPLOAD_BATCH_SIZE = 5;
|
|
154
|
+
|
|
155
|
+
export function splitBase64Chunks(base64, chunkSize = UPLOAD_CHUNK_SIZE) {
|
|
156
|
+
const chunks = [];
|
|
157
|
+
for (let offset = 0; offset < base64.length; offset += chunkSize) {
|
|
158
|
+
chunks.push(base64.slice(offset, offset + chunkSize));
|
|
159
|
+
}
|
|
160
|
+
return chunks;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function uploadFileWithSession(workspaceId, localPath, remotePath, username = 'root', timeoutMs = 30000) {
|
|
164
|
+
if (!existsSync(localPath)) {
|
|
165
|
+
throw new Error(`sandbox upload: local file not found: ${localPath}`);
|
|
166
|
+
}
|
|
167
|
+
if (!statSync(localPath).isFile()) {
|
|
168
|
+
throw new Error(`sandbox upload: path is not a regular file: ${localPath}`);
|
|
169
|
+
}
|
|
170
|
+
const content = readFileSync(localPath);
|
|
171
|
+
const base64 = content.toString('base64');
|
|
172
|
+
const expectedMd5 = createHash('md5').update(content).digest('hex');
|
|
173
|
+
const chunks = splitBase64Chunks(base64);
|
|
174
|
+
const tmp = `${remotePath}.b64tmp`;
|
|
175
|
+
|
|
176
|
+
const reset = await execWithSession(workspaceId, `rm -f "${tmp}"`, username, timeoutMs);
|
|
177
|
+
if (reset.exitCode !== 0) {
|
|
178
|
+
throw new Error(`sandbox upload: failed to reset temp file: ${reset.stdout || reset.error || reset.exitCode}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (let batchStart = 0; batchStart < chunks.length; batchStart += UPLOAD_BATCH_SIZE) {
|
|
182
|
+
const batch = chunks.slice(batchStart, batchStart + UPLOAD_BATCH_SIZE);
|
|
183
|
+
const combinedChunk = batch.join('');
|
|
184
|
+
const batchNum = Math.floor(batchStart / UPLOAD_BATCH_SIZE) + 1;
|
|
185
|
+
const totalBatches = Math.ceil(chunks.length / UPLOAD_BATCH_SIZE);
|
|
186
|
+
const cmd = `printf '%s' '${combinedChunk}' >> "${tmp}"`;
|
|
187
|
+
const res = await execWithSession(workspaceId, cmd, username, timeoutMs);
|
|
188
|
+
if (res.exitCode !== 0) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`sandbox upload: failed writing batch ${batchNum}/${totalBatches}: ${res.stdout || res.error || res.exitCode}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (batchNum % 10 === 0 || batchNum === totalBatches) {
|
|
194
|
+
console.error(` upload progress: batch ${batchNum}/${totalBatches}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const decode = await execWithSession(
|
|
199
|
+
workspaceId,
|
|
200
|
+
`base64 -d "${tmp}" > "${remotePath}" && rm -f "${tmp}"`,
|
|
201
|
+
username,
|
|
202
|
+
timeoutMs,
|
|
203
|
+
);
|
|
204
|
+
if (decode.exitCode !== 0) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`sandbox upload: failed decoding to ${remotePath}: ${decode.stdout || decode.error || decode.exitCode}`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const verify = await execWithSession(workspaceId, `md5sum "${remotePath}"`, username, timeoutMs);
|
|
211
|
+
let md5Verified = false;
|
|
212
|
+
if (verify.exitCode === 0) {
|
|
213
|
+
const remoteMd5 = String(verify.stdout || '')
|
|
214
|
+
// eslint-disable-next-line no-control-regex
|
|
215
|
+
.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')
|
|
216
|
+
// eslint-disable-next-line no-control-regex
|
|
217
|
+
.replace(/\x1b\][^\x07]*\x07/g, '')
|
|
218
|
+
.trim()
|
|
219
|
+
.split(/\s+/)[0];
|
|
220
|
+
md5Verified = remoteMd5 === expectedMd5;
|
|
221
|
+
if (!md5Verified) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`sandbox upload: md5 mismatch for ${remotePath} (expected ${expectedMd5}, got ${remoteMd5 || 'none'})`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
ok: true,
|
|
230
|
+
localPath,
|
|
231
|
+
remotePath,
|
|
232
|
+
bytes: content.length,
|
|
233
|
+
chunks: chunks.length,
|
|
234
|
+
md5: expectedMd5,
|
|
235
|
+
md5Verified,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const SANDBOX_FILE_SERVER_SCRIPT = readFileSync(join(__dirname, 'sandbox-file-server.py'), 'utf8');
|
|
240
|
+
|
|
241
|
+
const TUNNEL_READY_TIMEOUT_MS = 30000;
|
|
242
|
+
const SERVER_HEALTH_MAX_RETRIES = 30;
|
|
243
|
+
const SERVER_HEALTH_INTERVAL_MS = 1000;
|
|
244
|
+
const UPLOAD_LOG_PATH = join(tmpdir(), 'sandbox-upload.log');
|
|
245
|
+
|
|
246
|
+
function uploadLog(message) {
|
|
247
|
+
const ts = new Date().toISOString();
|
|
248
|
+
const line = `[${ts}] ${message}\n`;
|
|
249
|
+
console.error(line.trimEnd());
|
|
250
|
+
try {
|
|
251
|
+
appendFileSync(UPLOAD_LOG_PATH, line);
|
|
252
|
+
} catch {}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function generateUploadToken() {
|
|
256
|
+
return randomBytes(16).toString('hex');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function createTarGz(localDir, exclude = []) {
|
|
260
|
+
const archiveName = `${basename(localDir)}.tar.gz`;
|
|
261
|
+
const archiveDir = join(tmpdir(), `sandbox-upload-${Date.now()}`);
|
|
262
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
263
|
+
const archivePath = join(archiveDir, archiveName);
|
|
264
|
+
|
|
265
|
+
const args = [
|
|
266
|
+
...exclude.flatMap((p) => ['--exclude', p]),
|
|
267
|
+
'-czf',
|
|
268
|
+
archivePath,
|
|
269
|
+
'-C',
|
|
270
|
+
dirname(localDir),
|
|
271
|
+
basename(localDir),
|
|
272
|
+
];
|
|
273
|
+
await execFileAsync('tar', args);
|
|
274
|
+
return archivePath;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function computeMd5(filePath) {
|
|
278
|
+
return new Promise((resolve, reject) => {
|
|
279
|
+
const hash = createHash('md5');
|
|
280
|
+
createReadStream(filePath)
|
|
281
|
+
.on('data', (chunk) => hash.update(chunk))
|
|
282
|
+
.on('end', () => resolve(hash.digest('hex')))
|
|
283
|
+
.on('error', reject);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function cleanupLocalArchive(archivePath) {
|
|
288
|
+
try {
|
|
289
|
+
rmSync(dirname(archivePath), { recursive: true, force: true });
|
|
290
|
+
} catch {}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function deployFileServer(workspaceId, username, port = 8888, token = '') {
|
|
294
|
+
const scriptPath = '/tmp/sandbox-file-server.py';
|
|
295
|
+
const pidFile = '/tmp/sandbox-file-server.pid';
|
|
296
|
+
uploadLog(`deployFileServer: killing old server (pidFile=${pidFile})`);
|
|
297
|
+
await execWithSession(workspaceId, `kill $(cat ${pidFile} 2>/dev/null) 2>/dev/null; rm -f ${pidFile}`, username);
|
|
298
|
+
const b64 = Buffer.from(SANDBOX_FILE_SERVER_SCRIPT).toString('base64');
|
|
299
|
+
uploadLog(`deployFileServer: writing script (${b64.length} b64 chars)`);
|
|
300
|
+
await execWithSession(workspaceId, `echo '${b64}' | base64 -d > ${scriptPath}`, username);
|
|
301
|
+
const cmd = token
|
|
302
|
+
? `python3 ${scriptPath} ${port} ${token} & echo $! > ${pidFile}`
|
|
303
|
+
: `python3 ${scriptPath} ${port} & echo $! > ${pidFile}`;
|
|
304
|
+
uploadLog(`deployFileServer: starting server on port ${port}`);
|
|
305
|
+
const startResult = await execWithSession(workspaceId, cmd, username);
|
|
306
|
+
uploadLog(
|
|
307
|
+
`deployFileServer: start result exitCode=${startResult.exitCode} stdout=${JSON.stringify(startResult.stdout?.slice(0, 200))}`,
|
|
308
|
+
);
|
|
309
|
+
return startResult;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function uploadViaTunnel(localPort, archivePath, archiveSize, archiveRemotePath, uploadToken, timeoutMs) {
|
|
313
|
+
const archiveBuffer = readFileSync(archivePath);
|
|
314
|
+
const headers = [
|
|
315
|
+
'POST /upload HTTP/1.1',
|
|
316
|
+
'Host: localhost',
|
|
317
|
+
'Content-Type: application/octet-stream',
|
|
318
|
+
`Content-Length: ${archiveSize}`,
|
|
319
|
+
`X-Target-Path: ${archiveRemotePath}`,
|
|
320
|
+
`X-Upload-Token: ${uploadToken}`,
|
|
321
|
+
'Connection: close',
|
|
322
|
+
'',
|
|
323
|
+
'',
|
|
324
|
+
].join('\r\n');
|
|
325
|
+
return new Promise((resolve, reject) => {
|
|
326
|
+
let settled = false;
|
|
327
|
+
const done = (fn) => {
|
|
328
|
+
if (!settled) {
|
|
329
|
+
settled = true;
|
|
330
|
+
clearTimeout(timer);
|
|
331
|
+
sock.destroy();
|
|
332
|
+
fn();
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const timer = setTimeout(() => done(() => reject(new Error(`upload timeout after ${timeoutMs}ms`))), timeoutMs);
|
|
336
|
+
const sock = netConnect({ host: '127.0.0.1', port: localPort }, () => {
|
|
337
|
+
sock.write(headers);
|
|
338
|
+
sock.write(archiveBuffer);
|
|
339
|
+
});
|
|
340
|
+
let respBuf = Buffer.alloc(0);
|
|
341
|
+
let contentLength = -1;
|
|
342
|
+
let headerEnd = -1;
|
|
343
|
+
sock.on('data', (chunk) => {
|
|
344
|
+
respBuf = Buffer.concat([respBuf, chunk]);
|
|
345
|
+
if (contentLength < 0) {
|
|
346
|
+
const resp = respBuf.toString();
|
|
347
|
+
headerEnd = resp.indexOf('\r\n\r\n');
|
|
348
|
+
if (headerEnd > 0) {
|
|
349
|
+
const headerBlock = resp.slice(0, headerEnd);
|
|
350
|
+
const clMatch = headerBlock.match(/Content-Length:\s*(\d+)/i);
|
|
351
|
+
if (clMatch) contentLength = parseInt(clMatch[1], 10);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (contentLength >= 0 && headerEnd > 0) {
|
|
355
|
+
const bodyReceived = respBuf.length - (headerEnd + 4);
|
|
356
|
+
if (bodyReceived >= contentLength) {
|
|
357
|
+
done(() => {
|
|
358
|
+
const resp = respBuf.toString();
|
|
359
|
+
const statusLine = resp.split('\r\n')[0] || '';
|
|
360
|
+
const statusCode = parseInt(statusLine.split(' ')[1], 10);
|
|
361
|
+
const body = resp.slice(headerEnd + 4, headerEnd + 4 + contentLength);
|
|
362
|
+
if (!statusCode || statusCode < 200 || statusCode >= 300) {
|
|
363
|
+
uploadLog(`uploadViaTunnel: POST failed with HTTP ${statusCode}: ${body.slice(0, 200)}`);
|
|
364
|
+
reject(new Error(`upload HTTP ${statusCode}: ${body}`));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
resolve(JSON.parse(body));
|
|
369
|
+
} catch (e) {
|
|
370
|
+
reject(new Error(`invalid JSON response: ${body.slice(0, 200)}`));
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
sock.on('close', () => {
|
|
377
|
+
done(() => {
|
|
378
|
+
const resp = respBuf.toString();
|
|
379
|
+
const statusLine = resp.split('\r\n')[0] || '';
|
|
380
|
+
const statusCode = parseInt(statusLine.split(' ')[1], 10);
|
|
381
|
+
const he = resp.indexOf('\r\n\r\n');
|
|
382
|
+
const body = he > 0 ? resp.slice(he + 4) : '';
|
|
383
|
+
if (!statusCode || statusCode < 200 || statusCode >= 300) {
|
|
384
|
+
uploadLog(`uploadViaTunnel: POST failed with HTTP ${statusCode}: ${body.slice(0, 200)}`);
|
|
385
|
+
reject(new Error(`upload HTTP ${statusCode}: ${body}`));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
resolve(JSON.parse(body));
|
|
390
|
+
} catch (e) {
|
|
391
|
+
reject(new Error(`invalid JSON response: ${body.slice(0, 200)}`));
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
sock.on('error', (err) => {
|
|
396
|
+
done(() => {
|
|
397
|
+
uploadLog(`uploadViaTunnel: socket error: ${err.message}`);
|
|
398
|
+
reject(err);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function waitForServerReady(localPort) {
|
|
405
|
+
for (let i = 0; i < SERVER_HEALTH_MAX_RETRIES; i++) {
|
|
406
|
+
try {
|
|
407
|
+
uploadLog(`waitForServerReady: attempt ${i + 1}, checking http://localhost:${localPort}/health`);
|
|
408
|
+
const ok = await new Promise((resolve) => {
|
|
409
|
+
let settled = false;
|
|
410
|
+
const done = (val) => {
|
|
411
|
+
if (!settled) {
|
|
412
|
+
settled = true;
|
|
413
|
+
sock.destroy();
|
|
414
|
+
resolve(val);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const sock = netConnect({ host: '127.0.0.1', port: localPort }, () => {
|
|
418
|
+
sock.write('GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n');
|
|
419
|
+
});
|
|
420
|
+
let resp = '';
|
|
421
|
+
sock.on('data', (c) => {
|
|
422
|
+
resp += c.toString();
|
|
423
|
+
if (resp.includes('200 OK')) done(true);
|
|
424
|
+
});
|
|
425
|
+
sock.on('close', () => done(resp.includes('200 OK')));
|
|
426
|
+
sock.on('error', () => done(false));
|
|
427
|
+
setTimeout(() => done(false), 3000);
|
|
428
|
+
});
|
|
429
|
+
if (ok) {
|
|
430
|
+
uploadLog(`waitForServerReady: server ready on port ${localPort} after ${i} retries`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
uploadLog(`waitForServerReady: health check returned non-200 (retry ${i + 1})`);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
uploadLog(`waitForServerReady: health check failed: ${err.message} (retry ${i + 1})`);
|
|
436
|
+
}
|
|
437
|
+
await new Promise((r) => setTimeout(r, SERVER_HEALTH_INTERVAL_MS));
|
|
438
|
+
}
|
|
439
|
+
throw new Error(
|
|
440
|
+
`sandbox file server not ready after ${SERVER_HEALTH_MAX_RETRIES * SERVER_HEALTH_INTERVAL_MS}ms (log: ${UPLOAD_LOG_PATH})`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function cleanupFileServer(workspaceId, username) {
|
|
445
|
+
const pidFile = '/tmp/sandbox-file-server.pid';
|
|
446
|
+
const scriptPath = '/tmp/sandbox-file-server.py';
|
|
447
|
+
await execWithSession(workspaceId, `kill $(cat ${pidFile}) 2>/dev/null; rm -f ${pidFile} ${scriptPath}`, username);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function uploadViaHttpTunnel(workspaceId, archivePath, archiveRemotePath, username, timeoutMs, options) {
|
|
451
|
+
const sandboxPort = options.sandboxPort || 8888;
|
|
452
|
+
const uploadToken = generateUploadToken();
|
|
453
|
+
const archiveSize = statSync(archivePath).size;
|
|
454
|
+
|
|
455
|
+
uploadLog(
|
|
456
|
+
`uploadViaHttpTunnel: start (archive=${archivePath}, size=${archiveSize}, remotePath=${archiveRemotePath})`,
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
uploadLog(`uploadViaHttpTunnel: deploying file server on sandbox port ${sandboxPort}`);
|
|
460
|
+
await deployFileServer(workspaceId, username, sandboxPort, uploadToken);
|
|
461
|
+
|
|
462
|
+
uploadLog(`uploadViaHttpTunnel: creating dedicated tunnel session`);
|
|
463
|
+
const tunnelSession = await createTunnelSession(workspaceId, username);
|
|
464
|
+
|
|
465
|
+
uploadLog(`uploadViaHttpTunnel: creating tunnel channel (localPort=0, remotePort=${sandboxPort})`);
|
|
466
|
+
const { HwlinkTunnelChannel } = await import(WS_EXEC_INDEX_URL);
|
|
467
|
+
const tunnel = new HwlinkTunnelChannel({
|
|
468
|
+
localPort: 0,
|
|
469
|
+
remotePort: sandboxPort,
|
|
470
|
+
});
|
|
471
|
+
tunnel.attach(tunnelSession.mux);
|
|
472
|
+
|
|
473
|
+
uploadLog(`uploadViaHttpTunnel: waiting for tunnel ready (timeout=${TUNNEL_READY_TIMEOUT_MS}ms)`);
|
|
474
|
+
try {
|
|
475
|
+
await Promise.race([
|
|
476
|
+
tunnel.ready,
|
|
477
|
+
new Promise((_, reject) =>
|
|
478
|
+
setTimeout(
|
|
479
|
+
() => reject(new Error(`tunnel ready timeout after ${TUNNEL_READY_TIMEOUT_MS}ms`)),
|
|
480
|
+
TUNNEL_READY_TIMEOUT_MS,
|
|
481
|
+
),
|
|
482
|
+
),
|
|
483
|
+
]);
|
|
484
|
+
uploadLog(`uploadViaHttpTunnel: tunnel ready, localPort=${tunnel.localPort}`);
|
|
485
|
+
} catch (tunnelReadyError) {
|
|
486
|
+
uploadLog(`uploadViaHttpTunnel: TUNNEL READY FAILED: ${tunnelReadyError.message}`);
|
|
487
|
+
tunnel.close();
|
|
488
|
+
throw new Error(
|
|
489
|
+
`HTTP tunnel failed to establish: ${tunnelReadyError.message}. ` +
|
|
490
|
+
`This means the WebSocket port-forwarding channel to sandbox port ${sandboxPort} could not be opened. ` +
|
|
491
|
+
`Common causes: (1) Python file server not running on sandbox, (2) sandbox port ${sandboxPort} blocked, ` +
|
|
492
|
+
`(3) hwlink multiplexer channel rejected. ` +
|
|
493
|
+
`Diagnostic log: ${UPLOAD_LOG_PATH}`,
|
|
494
|
+
{ cause: tunnelReadyError },
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
uploadLog(`uploadViaHttpTunnel: waiting for server health on localhost:${tunnel.localPort}`);
|
|
500
|
+
await waitForServerReady(tunnel.localPort);
|
|
501
|
+
|
|
502
|
+
uploadLog(`uploadViaHttpTunnel: sending POST with ${archiveSize} bytes`);
|
|
503
|
+
const result = await uploadViaTunnel(
|
|
504
|
+
tunnel.localPort,
|
|
505
|
+
archivePath,
|
|
506
|
+
archiveSize,
|
|
507
|
+
archiveRemotePath,
|
|
508
|
+
uploadToken,
|
|
509
|
+
timeoutMs,
|
|
510
|
+
);
|
|
511
|
+
uploadLog(`uploadViaHttpTunnel: upload complete (bytes=${result.bytes}, md5=${result.md5})`);
|
|
512
|
+
return result;
|
|
513
|
+
} catch (uploadError) {
|
|
514
|
+
uploadLog(`uploadViaHttpTunnel: UPLOAD FAILED: ${uploadError.message}`);
|
|
515
|
+
throw uploadError;
|
|
516
|
+
} finally {
|
|
517
|
+
tunnel.close();
|
|
518
|
+
tunnelSession.close();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export async function uploadProjectWithSession(
|
|
523
|
+
workspaceId,
|
|
524
|
+
localDir,
|
|
525
|
+
remoteDir,
|
|
526
|
+
username = 'root',
|
|
527
|
+
timeoutMs = 300000,
|
|
528
|
+
options = {},
|
|
529
|
+
) {
|
|
530
|
+
if (!existsSync(localDir)) {
|
|
531
|
+
throw new Error(`sandbox upload project: local directory not found: ${localDir}`);
|
|
532
|
+
}
|
|
533
|
+
if (!statSync(localDir).isDirectory()) {
|
|
534
|
+
throw new Error(`sandbox upload project: path is not a directory: ${localDir}`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const projectName = basename(localDir);
|
|
538
|
+
const targetParentDir = remoteDir || '/workspace';
|
|
539
|
+
const archiveRemotePath = `${targetParentDir}/${projectName}.tar.gz`;
|
|
540
|
+
|
|
541
|
+
const archivePath = await createTarGz(localDir, options.exclude);
|
|
542
|
+
const archiveSize = statSync(archivePath).size;
|
|
543
|
+
const expectedMd5 = await computeMd5(archivePath);
|
|
544
|
+
|
|
545
|
+
uploadLog(`uploadProject: ${localDir} -> ${archiveRemotePath} (archive=${archiveSize} bytes, md5=${expectedMd5})`);
|
|
546
|
+
|
|
547
|
+
let result;
|
|
548
|
+
try {
|
|
549
|
+
result = await uploadViaHttpTunnel(workspaceId, archivePath, archiveRemotePath, username, timeoutMs, options);
|
|
550
|
+
} catch (tunnelError) {
|
|
551
|
+
uploadLog(`uploadProject: HTTP tunnel upload failed: ${tunnelError.message}`);
|
|
552
|
+
uploadLog(`uploadProject: NOT falling back to base64 (removed). Rethrowing with diagnostics.`);
|
|
553
|
+
cleanupLocalArchive(archivePath);
|
|
554
|
+
throw new Error(
|
|
555
|
+
`sandbox upload failed: HTTP tunnel could not transfer the project archive. ` +
|
|
556
|
+
`Archive size: ${(archiveSize / 1024).toFixed(1)}KB. ` +
|
|
557
|
+
`Root cause: ${tunnelError.message}. ` +
|
|
558
|
+
`Diagnostic log: ${UPLOAD_LOG_PATH}`,
|
|
559
|
+
{ cause: tunnelError },
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (options.verify !== false && result.md5 && result.md5 !== expectedMd5) {
|
|
564
|
+
throw new Error(`md5 mismatch: expected ${expectedMd5}, got ${result.md5}`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (options.extract !== false) {
|
|
568
|
+
await execWithSession(
|
|
569
|
+
workspaceId,
|
|
570
|
+
`mkdir -p "${targetParentDir}" && tar -xzf "${archiveRemotePath}" -C "${targetParentDir}" && rm -f "${archiveRemotePath}"`,
|
|
571
|
+
username,
|
|
572
|
+
timeoutMs,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
await cleanupFileServer(workspaceId, username);
|
|
578
|
+
} catch {}
|
|
579
|
+
cleanupLocalArchive(archivePath);
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
ok: true,
|
|
583
|
+
localDir,
|
|
584
|
+
remotePath: options.extract !== false ? `${targetParentDir}/${projectName}` : archiveRemotePath,
|
|
585
|
+
bytes: result.bytes || 0,
|
|
586
|
+
md5: result.md5 || expectedMd5,
|
|
587
|
+
md5Verified: result.md5 ? result.md5 === expectedMd5 : true,
|
|
588
|
+
extracted: options.extract !== false,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
96
592
|
export async function closeSession(workspaceId, username) {
|
|
97
593
|
const key = `${workspaceId}:${username}`;
|
|
98
594
|
const session = sessions.get(key);
|
|
99
595
|
if (!session) return false;
|
|
100
596
|
sessions.delete(key);
|
|
101
|
-
try {
|
|
597
|
+
try {
|
|
598
|
+
session.close();
|
|
599
|
+
} catch {}
|
|
102
600
|
return true;
|
|
103
601
|
}
|
|
104
602
|
|
|
105
603
|
export async function closeAllSessions() {
|
|
106
604
|
for (const [key, session] of sessions) {
|
|
107
605
|
sessions.delete(key);
|
|
108
|
-
try {
|
|
606
|
+
try {
|
|
607
|
+
session.close();
|
|
608
|
+
} catch {}
|
|
109
609
|
}
|
|
110
610
|
}
|
|
111
611
|
|
|
112
|
-
export {
|
|
612
|
+
export { currentWorkspaceId, setWorkspaceId, runNodeExec };
|
|
@@ -1,17 +1,45 @@
|
|
|
1
1
|
import { getProxyDispatcher } from './proxy/proxy-agent.mjs';
|
|
2
2
|
|
|
3
|
-
const INDEX_URL =
|
|
4
|
-
|
|
3
|
+
const INDEX_URL =
|
|
4
|
+
'https://gitcode.com/api/v5/repos/2501_91318609/skills-for-index/contents/skills-index/index.json?ref=main';
|
|
5
|
+
const CN_EN_MAP_URL =
|
|
6
|
+
'https://gitcode.com/api/v5/repos/2501_91318609/skills-for-index/contents/skills-index/cn-en-map.json?ref=main';
|
|
5
7
|
const HTTP_TIMEOUT_MS = 10000;
|
|
6
8
|
|
|
7
9
|
const GENERIC_KEYWORDS = new Set([
|
|
8
|
-
'华为云',
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
10
|
+
'华为云',
|
|
11
|
+
'huawei',
|
|
12
|
+
'huawei cloud',
|
|
13
|
+
'云',
|
|
14
|
+
'cloud',
|
|
15
|
+
'技能',
|
|
16
|
+
'skill',
|
|
17
|
+
'skills',
|
|
18
|
+
'所有',
|
|
19
|
+
'all',
|
|
20
|
+
'全部',
|
|
21
|
+
'有什么',
|
|
22
|
+
'有哪些',
|
|
23
|
+
'相关',
|
|
24
|
+
'列表',
|
|
25
|
+
'list',
|
|
26
|
+
'查找',
|
|
27
|
+
'搜索',
|
|
28
|
+
'发现',
|
|
29
|
+
'浏览',
|
|
30
|
+
'find',
|
|
31
|
+
'search',
|
|
32
|
+
'discover',
|
|
33
|
+
'browse',
|
|
34
|
+
'show',
|
|
35
|
+
'explore',
|
|
36
|
+
'agent',
|
|
37
|
+
'市场',
|
|
38
|
+
'market',
|
|
39
|
+
'类目',
|
|
40
|
+
'category',
|
|
41
|
+
'安装',
|
|
42
|
+
'install',
|
|
15
43
|
]);
|
|
16
44
|
|
|
17
45
|
let cachedIndex = null;
|
|
@@ -35,7 +63,7 @@ async function fetchJson(url, label = '') {
|
|
|
35
63
|
}
|
|
36
64
|
return data;
|
|
37
65
|
} catch (e) {
|
|
38
|
-
throw new Error(`Failed to fetch ${label}: ${e.message}
|
|
66
|
+
throw new Error(`Failed to fetch ${label}: ${e.message}`, { cause: e });
|
|
39
67
|
} finally {
|
|
40
68
|
clearTimeout(timer);
|
|
41
69
|
}
|
|
@@ -94,7 +122,10 @@ function scoreSkill(skill, specificKws, genericKws) {
|
|
|
94
122
|
else if (triggers.some((t) => t.includes(k))) s += 8;
|
|
95
123
|
else if (descLower.includes(k)) s += 5;
|
|
96
124
|
else if (serviceLower.includes(k)) s += 3;
|
|
97
|
-
if (s > 0) {
|
|
125
|
+
if (s > 0) {
|
|
126
|
+
total += s;
|
|
127
|
+
matched.push(kw);
|
|
128
|
+
}
|
|
98
129
|
}
|
|
99
130
|
for (const kw of genericKws) {
|
|
100
131
|
const k = kw.toLowerCase();
|
|
@@ -103,7 +134,10 @@ function scoreSkill(skill, specificKws, genericKws) {
|
|
|
103
134
|
else if (triggers.some((t) => t.includes(k))) s += 4;
|
|
104
135
|
else if (descLower.includes(k)) s += 2;
|
|
105
136
|
else if (serviceLower.includes(k)) s += 1;
|
|
106
|
-
if (s > 0) {
|
|
137
|
+
if (s > 0) {
|
|
138
|
+
total += s;
|
|
139
|
+
matched.push(kw);
|
|
140
|
+
}
|
|
107
141
|
}
|
|
108
142
|
if (!specificKws.length && total === 0) {
|
|
109
143
|
total = 1;
|