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.
Files changed (96) hide show
  1. package/.agents/plugins/marketplace.json +18 -18
  2. package/README.md +81 -51
  3. package/README.zh-CN.md +81 -49
  4. package/bin/setup.cjs +5 -3
  5. package/integrations/opencode/opencode.json +1 -4
  6. package/package.json +61 -44
  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-cloud-find-skills/SKILL.md +13 -12
  23. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/scripts/search-skills.mjs +64 -17
  24. package/plugins/huaweicloud-core/skills/huawei-cts/SKILL.md +20 -20
  25. package/plugins/huaweicloud-core/skills/huawei-dds-dcs/SKILL.md +31 -31
  26. package/plugins/huaweicloud-core/skills/huawei-deployment/SKILL.md +21 -20
  27. package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +39 -30
  28. package/plugins/huaweicloud-core/skills/huawei-dew/references/csms-usage.md +8 -1
  29. package/plugins/huaweicloud-core/skills/huawei-dew/references/kms-usage.md +6 -0
  30. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +39 -36
  31. package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +15 -4
  32. package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +11 -9
  33. package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +13 -13
  34. package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +40 -39
  35. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/apig-event-format.md +12 -10
  36. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/create-function.md +21 -20
  37. package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/triggers.md +19 -19
  38. package/plugins/huaweicloud-core/skills/huawei-gaussdb/SKILL.md +20 -20
  39. package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +27 -23
  40. package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +27 -27
  41. package/plugins/huaweicloud-core/skills/huawei-iam/references/policy-examples.md +65 -39
  42. package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +9 -9
  43. package/plugins/huaweicloud-core/skills/huawei-modelarts/SKILL.md +25 -25
  44. package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +45 -44
  45. package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-create.md +19 -19
  46. package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-lifecycle.md +12 -7
  47. package/plugins/huaweicloud-core/skills/huawei-obs/references/common-pitfalls.md +13 -12
  48. package/plugins/huaweicloud-core/skills/huawei-obs/references/single-file-share.md +4 -4
  49. package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +63 -58
  50. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +127 -42
  51. package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +26 -26
  52. package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +39 -39
  53. package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +13 -12
  54. package/plugins/huaweicloud-core/skills/huawei-vpc/references/security-group.md +20 -19
  55. package/plugins/huaweicloud-core/skills/huawei-vpc/references/subnet.md +15 -15
  56. package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +18 -18
  57. package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/SKILL.md +13 -13
  58. package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/agents/openai.yaml +3 -3
  59. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +17 -1
  60. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/agents/openai.yaml +3 -3
  61. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +27 -1
  62. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/agents/openai.yaml +3 -3
  63. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +56 -53
  64. package/plugins/huaweicloud-core/skills/huaweicloud-core/agents/openai.yaml +3 -3
  65. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/report-issue.md +6 -6
  66. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/select.md +15 -1
  67. package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +0 -1
  68. package/plugins/huaweicloud-core/skills/huaweicloud-safety/agents/openai.yaml +3 -3
  69. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +33 -33
  70. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/agents/openai.yaml +3 -3
  71. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +108 -5
  72. package/plugins/huaweicloud-core/src/auth/credentials.mjs +77 -4
  73. package/plugins/huaweicloud-core/src/auth/service.mjs +1 -1
  74. package/plugins/huaweicloud-core/src/data/icons-manifest.v1.json +3517 -0
  75. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +25 -7
  76. package/plugins/huaweicloud-core/src/icon-library.mjs +171 -0
  77. package/plugins/huaweicloud-core/src/mcp-server.mjs +30 -3
  78. package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +20 -102
  79. package/plugins/huaweicloud-core/src/proxy/proxy-config.mjs +11 -4
  80. package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +4 -1
  81. package/plugins/huaweicloud-core/src/safety-policy.mjs +122 -68
  82. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +9 -9
  83. package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +24 -26
  84. package/plugins/huaweicloud-core/src/sandbox/sandbox-file-server.py +45 -0
  85. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +510 -10
  86. package/plugins/huaweicloud-core/src/search-market.mjs +46 -12
  87. package/plugins/huaweicloud-core/src/setup-cli.mjs +1251 -181
  88. package/plugins/huaweicloud-core/src/tools.mjs +600 -122
  89. package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +72 -38
  90. package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +10 -5
  91. package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +12 -15
  92. package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +12 -16
  93. package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +46 -31
  94. package/plugins/huaweicloud-core/src/ws-exec/hwlink-tunnel-channel.mjs +267 -0
  95. package/plugins/huaweicloud-core/src/ws-exec/index.js +3 -0
  96. 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 { 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';
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
- 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
+ }
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) => { stdout += d.toString(); });
25
- 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
+ });
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 { session.close(); } catch {}
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 { session.close(); } catch {}
606
+ try {
607
+ session.close();
608
+ } catch {}
109
609
  }
110
610
  }
111
611
 
112
- export { DEFAULT_WORKSPACE_ID, runNodeExec };
612
+ export { currentWorkspaceId, setWorkspaceId, runNodeExec };
@@ -1,17 +1,45 @@
1
1
  import { getProxyDispatcher } from './proxy/proxy-agent.mjs';
2
2
 
3
- const INDEX_URL = 'https://gitcode.com/api/v5/repos/2501_91318609/skills-for-index/contents/skills-index/index.json?ref=main';
4
- 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';
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
- '华为云', 'huawei', 'huawei cloud', '云', 'cloud',
9
- '技能', 'skill', 'skills', '所有', 'all', '全部',
10
- '有什么', '有哪些', '相关', '列表', 'list',
11
- '查找', '搜索', '发现', '浏览', 'find', 'search',
12
- 'discover', 'browse', 'show', 'explore',
13
- 'agent', '市场', 'market', '类目', 'category',
14
- '安装', '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',
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) { total += s; matched.push(kw); }
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) { total += s; matched.push(kw); }
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;