huaweicloud-devkit 1.0.3-dev.0 → 1.0.3-next.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.
Files changed (96) hide show
  1. package/.agents/plugins/marketplace.json +18 -18
  2. package/README.md +99 -41
  3. package/README.zh-CN.md +99 -41
  4. package/bin/setup.cjs +5 -3
  5. package/integrations/opencode/opencode.json +1 -4
  6. package/package.json +61 -42
  7. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +4 -17
  8. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +4 -17
  9. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +4 -17
  10. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +30 -0
  11. package/plugins/huaweicloud-core/.mcp.json +2 -5
  12. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +4 -18
  13. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +32 -9
  14. package/plugins/huaweicloud-core/openclaw.plugin.json +20 -0
  15. package/plugins/huaweicloud-core/safety/policy.json +75 -99
  16. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +41 -13
  17. package/plugins/huaweicloud-core/skills/huawei-apig/SKILL.md +34 -34
  18. package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +18 -18
  19. package/plugins/huaweicloud-core/skills/huawei-cbr/SKILL.md +19 -19
  20. package/plugins/huaweicloud-core/skills/huawei-cce/SKILL.md +34 -32
  21. package/plugins/huaweicloud-core/skills/huawei-cloud-eye/SKILL.md +30 -30
  22. package/plugins/huaweicloud-core/skills/huawei-cts/SKILL.md +20 -20
  23. package/plugins/huaweicloud-core/skills/huawei-dds-dcs/SKILL.md +31 -31
  24. package/plugins/huaweicloud-core/skills/huawei-deployment/SKILL.md +21 -20
  25. package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +39 -30
  26. package/plugins/huaweicloud-core/skills/huawei-dew/references/csms-usage.md +8 -1
  27. package/plugins/huaweicloud-core/skills/huawei-dew/references/kms-usage.md +6 -0
  28. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +39 -36
  29. package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +15 -4
  30. package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +11 -9
  31. package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +13 -13
  32. package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +40 -39
  33. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/apig-event-format.md +12 -10
  34. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/create-function.md +21 -20
  35. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/triggers.md +19 -19
  36. package/plugins/huaweicloud-core/skills/huawei-gaussdb/SKILL.md +20 -20
  37. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +27 -23
  38. package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +27 -27
  39. package/plugins/huaweicloud-core/skills/huawei-iam/references/policy-examples.md +65 -39
  40. package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +9 -9
  41. package/plugins/huaweicloud-core/skills/huawei-modelarts/SKILL.md +33 -25
  42. package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +51 -44
  43. package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-create.md +19 -19
  44. package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-lifecycle.md +12 -7
  45. package/plugins/huaweicloud-core/skills/huawei-obs/references/common-pitfalls.md +13 -12
  46. package/plugins/huaweicloud-core/skills/huawei-obs/references/single-file-share.md +4 -4
  47. package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +63 -58
  48. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +128 -42
  49. package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +26 -26
  50. package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +39 -39
  51. package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +13 -12
  52. package/plugins/huaweicloud-core/skills/huawei-vpc/references/security-group.md +20 -19
  53. package/plugins/huaweicloud-core/skills/huawei-vpc/references/subnet.md +15 -15
  54. package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +18 -18
  55. package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/SKILL.md +13 -13
  56. package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/agents/openai.yaml +3 -3
  57. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +25 -2
  58. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/agents/openai.yaml +3 -3
  59. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +27 -1
  60. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/agents/openai.yaml +3 -3
  61. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +65 -47
  62. package/plugins/huaweicloud-core/skills/huaweicloud-core/agents/openai.yaml +3 -3
  63. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/report-issue.md +6 -6
  64. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/select.md +15 -1
  65. package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +0 -1
  66. package/plugins/huaweicloud-core/skills/huaweicloud-safety/agents/openai.yaml +3 -3
  67. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +33 -33
  68. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/agents/openai.yaml +3 -3
  69. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +123 -2
  70. package/plugins/huaweicloud-core/src/auth/credentials.mjs +77 -4
  71. package/plugins/huaweicloud-core/src/auth/service.mjs +1 -1
  72. package/plugins/huaweicloud-core/src/data/icons-manifest.v1.json +3517 -0
  73. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +25 -7
  74. package/plugins/huaweicloud-core/src/icon-library.mjs +171 -0
  75. package/plugins/huaweicloud-core/src/mcp-server.mjs +30 -3
  76. package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +73 -0
  77. package/plugins/huaweicloud-core/src/proxy/proxy-config.mjs +94 -0
  78. package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +4 -1
  79. package/plugins/huaweicloud-core/src/safety-policy.mjs +122 -68
  80. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +14 -10
  81. package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +34 -27
  82. package/plugins/huaweicloud-core/src/sandbox/sandbox-file-server.py +45 -0
  83. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +521 -8
  84. package/plugins/huaweicloud-core/src/search-market.mjs +53 -14
  85. package/plugins/huaweicloud-core/src/setup-cli.mjs +1741 -215
  86. package/plugins/huaweicloud-core/src/tools.mjs +667 -123
  87. package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +72 -38
  88. package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +10 -5
  89. package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +12 -15
  90. package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +12 -16
  91. package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +46 -31
  92. package/plugins/huaweicloud-core/src/ws-exec/hwlink-tunnel-channel.mjs +267 -0
  93. package/plugins/huaweicloud-core/src/ws-exec/index.js +3 -0
  94. package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +9 -5
  95. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/SKILL.md +0 -190
  96. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/scripts/search-skills.mjs +0 -124
@@ -1,12 +1,25 @@
1
- import { spawn } from 'node:child_process';
2
- import { join, dirname } from 'node:path';
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';
10
+ import { getWebSocketImpl } from '../proxy/proxy-agent.mjs';
11
+
12
+ const execFileAsync = promisify(execFile);
5
13
 
6
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
15
  export const WS_EXEC_INDEX_URL = pathToFileURL(join(__dirname, '..', 'ws-exec', 'index.js')).href;
8
16
 
9
- const DEFAULT_WORKSPACE_ID = process.env.HW_WORKSPACE_ID || '0107bd9997aa4287bd2b4890b49af07d';
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
+ }
10
23
 
11
24
  function resolveEnv() {
12
25
  const env = { ...process.env };
@@ -20,8 +33,12 @@ async function runNodeExec(args, timeoutMs = 30000) {
20
33
  const proc = spawn('node', args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
21
34
  let stdout = '';
22
35
  let stderr = '';
23
- proc.stdout.on('data', (d) => { stdout += d.toString(); });
24
- proc.stderr.on('data', (d) => { stderr += d.toString(); });
36
+ proc.stdout.on('data', (d) => {
37
+ stdout += d.toString();
38
+ });
39
+ proc.stderr.on('data', (d) => {
40
+ stderr += d.toString();
41
+ });
25
42
 
26
43
  const timer = setTimeout(() => {
27
44
  proc.kill();
@@ -55,22 +72,66 @@ async function getSession(workspaceId, username, timeoutMs) {
55
72
  const { ak, sk, securitytoken } = getCredentials();
56
73
  const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
57
74
 
75
+ const WebSocketImpl = await getWebSocketImpl(wsUrl);
76
+
58
77
  const { connectHwlinkTerminalSession } = await import(WS_EXEC_INDEX_URL);
59
78
  const session = await connectHwlinkTerminalSession({
60
79
  url: wsUrl,
61
80
  source,
62
81
  username,
63
82
  timeoutMs,
83
+ WebSocketImpl,
64
84
  });
65
85
 
66
86
  sessions.set(key, session);
67
87
  return session;
68
88
  }
69
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
+
70
129
  export async function execOneShot(workspaceId, command, username, timeoutMs) {
71
130
  const { ak, sk, securitytoken } = getCredentials();
72
131
  const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
73
132
 
133
+ const WebSocketImpl = await getWebSocketImpl(wsUrl);
134
+
74
135
  const { executeHwlinkCommand } = await import(WS_EXEC_INDEX_URL);
75
136
  return await executeHwlinkCommand({
76
137
  url: wsUrl,
@@ -78,6 +139,7 @@ export async function execOneShot(workspaceId, command, username, timeoutMs) {
78
139
  username,
79
140
  command,
80
141
  timeoutMs,
142
+ WebSocketImpl,
81
143
  });
82
144
  }
83
145
 
@@ -86,20 +148,471 @@ export async function execWithSession(workspaceId, command, username, timeoutMs)
86
148
  return await session.exec(command, { timeoutMs });
87
149
  }
88
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 (!workspaceId) {
531
+ throw new Error(
532
+ 'sandbox upload project: workspace_id is required. ' +
533
+ 'Set HW_WORKSPACE_ID env var or ensure huaweicloud_sandbox_connect was called first.',
534
+ );
535
+ }
536
+ if (!existsSync(localDir)) {
537
+ throw new Error(`sandbox upload project: local directory not found: ${localDir}`);
538
+ }
539
+ if (!statSync(localDir).isDirectory()) {
540
+ throw new Error(`sandbox upload project: path is not a directory: ${localDir}`);
541
+ }
542
+
543
+ const projectName = basename(localDir);
544
+ const targetParentDir = remoteDir || '/workspace';
545
+ const archiveRemotePath = `${targetParentDir}/${projectName}.tar.gz`;
546
+
547
+ const archivePath = await createTarGz(localDir, options.exclude);
548
+ const archiveSize = statSync(archivePath).size;
549
+ const expectedMd5 = await computeMd5(archivePath);
550
+
551
+ uploadLog(`uploadProject: ${localDir} -> ${archiveRemotePath} (archive=${archiveSize} bytes, md5=${expectedMd5})`);
552
+
553
+ let result;
554
+ try {
555
+ result = await uploadViaHttpTunnel(workspaceId, archivePath, archiveRemotePath, username, timeoutMs, options);
556
+ } catch (tunnelError) {
557
+ uploadLog(`uploadProject: HTTP tunnel upload failed: ${tunnelError.message}`);
558
+ uploadLog(`uploadProject: NOT falling back to base64 (removed). Rethrowing with diagnostics.`);
559
+ cleanupLocalArchive(archivePath);
560
+ throw new Error(
561
+ `sandbox upload failed: HTTP tunnel could not transfer the project archive. ` +
562
+ `Archive size: ${(archiveSize / 1024).toFixed(1)}KB. ` +
563
+ `Root cause: ${tunnelError.message}. ` +
564
+ `Diagnostic log: ${UPLOAD_LOG_PATH}`,
565
+ { cause: tunnelError },
566
+ );
567
+ }
568
+
569
+ if (options.verify !== false && result.md5 && result.md5 !== expectedMd5) {
570
+ throw new Error(`md5 mismatch: expected ${expectedMd5}, got ${result.md5}`);
571
+ }
572
+
573
+ if (options.extract !== false) {
574
+ await execWithSession(
575
+ workspaceId,
576
+ `mkdir -p "${targetParentDir}" && tar -xzf "${archiveRemotePath}" -C "${targetParentDir}" && rm -f "${archiveRemotePath}"`,
577
+ username,
578
+ timeoutMs,
579
+ );
580
+ }
581
+
582
+ try {
583
+ await cleanupFileServer(workspaceId, username);
584
+ } catch {}
585
+ cleanupLocalArchive(archivePath);
586
+
587
+ return {
588
+ ok: true,
589
+ localDir,
590
+ remotePath: options.extract !== false ? `${targetParentDir}/${projectName}` : archiveRemotePath,
591
+ bytes: result.bytes || 0,
592
+ md5: result.md5 || expectedMd5,
593
+ md5Verified: result.md5 ? result.md5 === expectedMd5 : true,
594
+ extracted: options.extract !== false,
595
+ };
596
+ }
597
+
89
598
  export async function closeSession(workspaceId, username) {
90
599
  const key = `${workspaceId}:${username}`;
91
600
  const session = sessions.get(key);
92
601
  if (!session) return false;
93
602
  sessions.delete(key);
94
- try { session.close(); } catch {}
603
+ try {
604
+ session.close();
605
+ } catch {}
95
606
  return true;
96
607
  }
97
608
 
98
609
  export async function closeAllSessions() {
99
610
  for (const [key, session] of sessions) {
100
611
  sessions.delete(key);
101
- try { session.close(); } catch {}
612
+ try {
613
+ session.close();
614
+ } catch {}
102
615
  }
103
616
  }
104
617
 
105
- export { DEFAULT_WORKSPACE_ID, runNodeExec };
618
+ export { currentWorkspaceId, setWorkspaceId, runNodeExec };
@@ -1,15 +1,45 @@
1
- const INDEX_URL = 'https://gitcode.com/api/v5/repos/2501_91318609/skills-for-index/contents/skills-index/index.json?ref=main';
2
- const CN_EN_MAP_URL = 'https://gitcode.com/api/v5/repos/2501_91318609/skills-for-index/contents/skills-index/cn-en-map.json?ref=main';
1
+ import { getProxyDispatcher } from './proxy/proxy-agent.mjs';
2
+
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';
3
7
  const HTTP_TIMEOUT_MS = 10000;
4
8
 
5
9
  const GENERIC_KEYWORDS = new Set([
6
- '华为云', 'huawei', 'huawei cloud', '云', 'cloud',
7
- '技能', 'skill', 'skills', '所有', 'all', '全部',
8
- '有什么', '有哪些', '相关', '列表', 'list',
9
- '查找', '搜索', '发现', '浏览', 'find', 'search',
10
- 'discover', 'browse', 'show', 'explore',
11
- 'agent', '市场', 'market', '类目', 'category',
12
- '安装', 'install',
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',
13
43
  ]);
14
44
 
15
45
  let cachedIndex = null;
@@ -19,10 +49,13 @@ async function fetchJson(url, label = '') {
19
49
  const controller = new AbortController();
20
50
  const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
21
51
  try {
22
- const resp = await fetch(url, {
52
+ const dispatcher = await getProxyDispatcher(url);
53
+ const fetchOpts = {
23
54
  headers: { 'User-Agent': 'huaweicloud-devkit/1.0' },
24
55
  signal: controller.signal,
25
- });
56
+ };
57
+ if (dispatcher) fetchOpts.dispatcher = dispatcher;
58
+ const resp = await fetch(url, fetchOpts);
26
59
  const data = await resp.json();
27
60
  if (data && data.encoding === 'base64' && data.content) {
28
61
  const decoded = Buffer.from(data.content, 'base64').toString('utf8');
@@ -30,7 +63,7 @@ async function fetchJson(url, label = '') {
30
63
  }
31
64
  return data;
32
65
  } catch (e) {
33
- throw new Error(`Failed to fetch ${label}: ${e.message}`);
66
+ throw new Error(`Failed to fetch ${label}: ${e.message}`, { cause: e });
34
67
  } finally {
35
68
  clearTimeout(timer);
36
69
  }
@@ -89,7 +122,10 @@ function scoreSkill(skill, specificKws, genericKws) {
89
122
  else if (triggers.some((t) => t.includes(k))) s += 8;
90
123
  else if (descLower.includes(k)) s += 5;
91
124
  else if (serviceLower.includes(k)) s += 3;
92
- if (s > 0) { total += s; matched.push(kw); }
125
+ if (s > 0) {
126
+ total += s;
127
+ matched.push(kw);
128
+ }
93
129
  }
94
130
  for (const kw of genericKws) {
95
131
  const k = kw.toLowerCase();
@@ -98,7 +134,10 @@ function scoreSkill(skill, specificKws, genericKws) {
98
134
  else if (triggers.some((t) => t.includes(k))) s += 4;
99
135
  else if (descLower.includes(k)) s += 2;
100
136
  else if (serviceLower.includes(k)) s += 1;
101
- if (s > 0) { total += s; matched.push(kw); }
137
+ if (s > 0) {
138
+ total += s;
139
+ matched.push(kw);
140
+ }
102
141
  }
103
142
  if (!specificKws.length && total === 0) {
104
143
  total = 1;