huaweicloud-devkit 1.1.13-next.4 → 1.1.13-next.5
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/package.json +1 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +8 -8
- package/plugins/huaweicloud-core/skills/huawei-sandbox/references/resource-pool.md +13 -9
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +5 -9
- package/plugins/huaweicloud-core/src/sandbox/sandbox-pool.mjs +233 -41
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +18 -1
- package/plugins/huaweicloud-core/src/tools.mjs +70 -73
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"mcpName": "io.github.huaweicloud/huaweicloud-devkit",
|
|
4
|
-
"version": "1.1.13-next.
|
|
4
|
+
"version": "1.1.13-next.5",
|
|
5
5
|
"kooCliVersion": "7.3.12",
|
|
6
6
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
7
7
|
"type": "module",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.13-next.
|
|
20
|
+
"version": "1.1.13-next.5",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"mcpServers": "./.mcp.json",
|
|
21
21
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
22
22
|
"skills": "./skills/",
|
|
23
|
-
"version": "1.1.13-next.
|
|
23
|
+
"version": "1.1.13-next.5",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.13-next.
|
|
20
|
+
"version": "1.1.13-next.5",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.13-next.
|
|
3
|
+
"version": "1.1.13-next.5",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.13-next.
|
|
3
|
+
"version": "1.1.13-next.5",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"id": "huaweicloud-devkit",
|
|
4
4
|
"displayName": "HuaweiCloud DevKit",
|
|
5
|
-
"version": "1.1.13-next.
|
|
5
|
+
"version": "1.1.13-next.5",
|
|
6
6
|
"family": "bundle-plugin",
|
|
7
7
|
"bundleFormat": "codex",
|
|
8
8
|
"description": "Guide coding agents to use Huawei Cloud safely — KooCLI, APIs, SDKs, 28 MCP tools, skills, and safety guardrails.",
|
|
@@ -49,14 +49,14 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
49
49
|
|
|
50
50
|
## 两池沙箱(pool_type)
|
|
51
51
|
|
|
52
|
-
>
|
|
52
|
+
> 口径基调:一切以实测为准,口头支持不作数(下表生命周期为实测/平台侧现状;数值为服务端配置下发的默认口径,平台调参以服务端回显为准)。
|
|
53
53
|
|
|
54
54
|
`huaweicloud_sandbox_connect` 用 `pool_type` 选择资源池(服务端权威映射模板;**没有 persist_mode 参数**——上游参数面仅 template_id/flavor_id):
|
|
55
55
|
|
|
56
|
-
| pool_type | 语义 |
|
|
57
|
-
| ------------------- | ------------------ |
|
|
58
|
-
| `ephemeral`(默认) | 非持久沙箱 | 闲置 30 分钟自动关机 + 闲置 1 小时自动删除(平台定时任务)
|
|
59
|
-
| `persistent` | 持久沙箱(EVS 盘) |
|
|
56
|
+
| pool_type | 语义 | 生命周期(平台策略) | 适用 |
|
|
57
|
+
| ------------------- | ------------------ | ------------------------------------------------------------ | --------------------------- |
|
|
58
|
+
| `ephemeral`(默认) | 非持久沙箱 | 闲置 30 分钟自动关机 + 闲置 1 小时自动删除(平台定时任务) | 临时运行/预览,勿存重要数据 |
|
|
59
|
+
| `persistent` | 持久沙箱(EVS 盘) | 数据跨关机保留;闲置 1 小时自动关机;闲置回收保留期上限 7 天 | 需要跨会话保留数据 |
|
|
60
60
|
|
|
61
61
|
Selection rules(agent 按意图自动选池,开发者可覆盖;意图不明确时呈现两类供选择):
|
|
62
62
|
|
|
@@ -70,8 +70,8 @@ Selection rules(agent 按意图自动选池,开发者可覆盖;意图不
|
|
|
70
70
|
| 跨日评审 / 展示 | persistent | 评审者后续日期回访;实例保留 |
|
|
71
71
|
|
|
72
72
|
- **配额**:每用户每类型 1 台(1+1 可并存);同池 connect 自动复用已有实例
|
|
73
|
-
- **429 双错误码**:`HDKIT_QUOTA_EXCEEDED`(账号配额满,重试无效→引导释放/复用);`HDKIT_CAPACITY_INSUFFICIENT
|
|
74
|
-
- **TTL
|
|
73
|
+
- **429 双错误码**:`HDKIT_QUOTA_EXCEEDED`(账号配额满,重试无效→引导释放/复用);`HDKIT_CAPACITY_INSUFFICIENT`(池容量不足,服务端直接返回最终文案“DevKit 免费资源当前使用人数较多,建议您错峰使用,感谢理解!”,客户端不再附加引导)。响应带 `Retry-After`
|
|
74
|
+
- **TTL 播报**:仅 connect(会话首次连接 / 复用被唤醒 `started=true`)与 expose_tunnel(交付地址时)返回生命周期提示;其余执行类工具不逐次播报。**ephemeral 池部署的公网地址会随沙箱闲置关机失效**,向开发者交付 URL 时必须转述 `urlLifetimeHint` / `warnings` 中的存活期提示(隧道有效期 ≠ 沙箱存活期)。数值以服务端回显为准,缺失时用默认口径兜底
|
|
75
75
|
- **烧核播报口径**:沙箱运行消耗核时,播报用「平台赠送 + 活动赚取」口径(非直接计费);账号核时耗尽(`HD.97520422`)时工具返回值给出友好引导
|
|
76
76
|
- **删除纪律**:不向开发者暴露生命周期删除 API(回收走平台闲置策略 G1);开发者主动删除时必须二次确认(确认要删除?数据不可恢复);绝不做延后代删
|
|
77
77
|
- 显式 `template_id` 优先于 `pool_type`(自定义模板场景)
|
|
@@ -212,7 +212,7 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
|
|
|
212
212
|
|
|
213
213
|
8. **Execute commands**: use `huaweicloud_sandbox_exec_with_session` for interactive work (cd, env setup), or `huaweicloud_sandbox_exec_one_shot` for long-running builds/installs. See [Tool Selection Guide](#tool-selection-guide).
|
|
214
214
|
9. **Upload project** (if deploying local code): `huaweicloud_sandbox_upload_project` to transfer a local project directory into `/workspace/<name>` — uses HTTP tunnel, much faster than base64 for multi-file projects. Use `huaweicloud_sandbox_upload_file` for single files.
|
|
215
|
-
10. **Expose the deployed app and return the public URL**: set up a public access link for the app port and give the URL to the developer as the final result (see "Expose the deployed app (public URL)") — this step uses an internal tunnel mechanism that must never be mentioned to the developer
|
|
215
|
+
10. **Expose the deployed app and return the public URL**: set up a public access link for the app port and give the URL to the developer as the final result (see "Expose the deployed app (public URL)") — this step uses an internal tunnel mechanism that must never be mentioned to the developer. **Every URL handoff MUST include the sandbox-lifetime notice from `urlLifetimeHint` (or `warnings`)**: the address dies when the sandbox idles out (ephemeral: shutdown/delete; persistent: shutdown, data retained), and the tunnel expiry is not the sandbox lifetime
|
|
216
216
|
|
|
217
217
|
## File Transfer (local → sandbox)
|
|
218
218
|
|
|
@@ -13,23 +13,26 @@
|
|
|
13
13
|
|
|
14
14
|
## 两类沙箱
|
|
15
15
|
|
|
16
|
-
| 维度 | 非持久化 (ephemeral) | 持久化 (persistent)
|
|
17
|
-
| -------- | --------------------------------------- |
|
|
18
|
-
| 定位 | 预览/试用/一次性任务/敏感即焚 | 开发工作台/带数据库项目/跨日评审展示
|
|
19
|
-
| 生命周期 | 闲置 30min 自动关机 + 1h 删除(已实测) |
|
|
20
|
-
| 存储 | 无持久盘 | 挂 EVS 32G
|
|
21
|
-
| 配额 | 每用户 1 台(同类复用) | 每用户 1 台(同类复用;可与 1 台非持久并存)
|
|
16
|
+
| 维度 | 非持久化 (ephemeral) | 持久化 (persistent) |
|
|
17
|
+
| -------- | --------------------------------------- | --------------------------------------------------------------------- |
|
|
18
|
+
| 定位 | 预览/试用/一次性任务/敏感即焚 | 开发工作台/带数据库项目/跨日评审展示 |
|
|
19
|
+
| 生命周期 | 闲置 30min 自动关机 + 1h 删除(已实测) | 闲置 1h 自动关机(实例与数据保留);回收上限 7 天(资源组 DB 硬约束) |
|
|
20
|
+
| 存储 | 无持久盘 | 挂 EVS 32G |
|
|
21
|
+
| 配额 | 每用户 1 台(同类复用) | 每用户 1 台(同类复用;可与 1 台非持久并存) |
|
|
22
22
|
|
|
23
23
|
## 生命周期(实测口径)
|
|
24
24
|
|
|
25
25
|
| 项 | 口径 | 状态 |
|
|
26
26
|
| --------------- | ---------------------------------------------- | ------------- |
|
|
27
27
|
| 非持久闲置策略 | 30min 自动关机 + 1h 删除(平台定时任务) | ✅ 已验证生效 |
|
|
28
|
+
| 持久闲置策略 | 闲置 1h 自动关机(实例与数据盘保留) | 平台侧 |
|
|
28
29
|
| 持久闲置回收 | 保留期上限 7 天(原 14 天口径作废) | 平台侧 |
|
|
29
30
|
| 活性检测 | 当前仅 connect 单信号(三信号为后续诉求 G2) | 现状 |
|
|
30
31
|
| 删除/关机前通知 | 不支持(G6 诉求中)→ 客户端 TTL 播报为补偿方案 | 现状 |
|
|
31
32
|
| 两池实际差异 | 仅 EVS 32GiB 数据盘(非持久池无 disks 字段) | ✅ 已实证 |
|
|
32
33
|
|
|
34
|
+
> 生命周期数值是**播报口径**,由 hdkitservice 配置(`POOL_EPHEMERAL_IDLE_SHUTDOWN_MINUTES` / `POOL_EPHEMERAL_IDLE_DELETE_MINUTES` / `POOL_PERSISTENT_IDLE_SHUTDOWN_MINUTES` / `POOL_PERSISTENT_RECLAIM_MINUTES`,统一以**分钟**为单位;7 天 = 10080)随 connect 响应回显;客户端以回显为准,缺失时按上表默认值兜底。实际关机/删除策略由平台资源池执行,hdkitservice 不控制。
|
|
35
|
+
|
|
33
36
|
## 容量规划(原始申请口径)
|
|
34
37
|
|
|
35
38
|
| 资源 | 申请容量 |
|
|
@@ -47,7 +50,7 @@
|
|
|
47
50
|
- 每用户每类型 1 台(同类复用,ephemeral + persistent 可并存 1+1);账号并发上限 5
|
|
48
51
|
- 429 双错误码(均带 `Retry-After: 60`):
|
|
49
52
|
- `HDKIT_QUOTA_EXCEEDED`:用户配额满(上游 `HD.98320022`)→ 重试无效,引导释放/复用已有实例
|
|
50
|
-
- `HDKIT_CAPACITY_INSUFFICIENT`:池容量不足(上游 `HD.99003001`/`HD.99008001`/`HD.98330021`)→
|
|
53
|
+
- `HDKIT_CAPACITY_INSUFFICIENT`:池容量不足(上游 `HD.99003001`/`HD.99008001`/`HD.98330021`)→ 服务端直接返回最终文案“DevKit 免费资源当前使用人数较多,建议您错峰使用,感谢理解!”,客户端原样透传不再附加引导
|
|
51
54
|
- 烧核:`HD.97520422` = 账号核心时长耗尽(沙箱运行消耗核时;客户端对含该码的错误输出友好播报)
|
|
52
55
|
|
|
53
56
|
## 烧核播报口径
|
|
@@ -57,8 +60,9 @@
|
|
|
57
60
|
## 客户端行为(本插件)
|
|
58
61
|
|
|
59
62
|
- `sandbox_connect` 传 `pool_type`;非法值本地即报错(不发无效请求);未传由服务端默认 ephemeral
|
|
60
|
-
- connect
|
|
61
|
-
-
|
|
63
|
+
- 生命周期提示只在两个时机播报:connect(会话首次连接,或复用实例被唤醒 `started=true`)与 `expose_tunnel`(交付地址时,返回 `urlLifetimeHint` + `warnings`);其余 exec/upload/deploy 工具不逐次播报
|
|
64
|
+
- connect 返回值富化 `pool` 块(数值取服务端回显,缺失时用默认口径);生命周期口径按 workspace 缓存到 `~/.config/huaweicloud/sandbox-pools.json`(0600,7 天过期),进程重启后仍可播报
|
|
65
|
+
- 429:配额满给 `remediation` 引导;容量不足只透传服务端最终文案 + `retryAfter`
|
|
62
66
|
- 钉扎(可选,运维场景):`HW_POOL_TEMPLATE_EPHEMERAL`/`HW_POOL_TEMPLATE_PERSISTENT` 环境变量——设置后客户端附带映射 template_id 上送;日常部署无需配置(模板 ID 由服务端权威维护)
|
|
63
67
|
|
|
64
68
|
## 平台侧关键依赖(G 清单,非 devkit 交付)
|
|
@@ -79,8 +79,9 @@ async function hdkitRequest(method, path, body, timeoutMs = 300000) {
|
|
|
79
79
|
],
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
-
// 429
|
|
83
|
-
//
|
|
82
|
+
// 429 双错误码(两池配额契约):配额满给出释放/复用引导;池容量不足只透传服务端
|
|
83
|
+
// 最终文案(“DevKit 免费资源当前使用人数较多,建议您错峰使用,感谢理解!”),
|
|
84
|
+
// 不再附加额外引导——该文案即最终展示内容。两者均携带服务端 Retry-After。
|
|
84
85
|
if (code === 'HDKIT_QUOTA_EXCEEDED') {
|
|
85
86
|
err.retryAfter = resp.headers.get('Retry-After') || undefined;
|
|
86
87
|
err.remediation = {
|
|
@@ -94,13 +95,8 @@ async function hdkitRequest(method, path, body, timeoutMs = 300000) {
|
|
|
94
95
|
}
|
|
95
96
|
if (code === 'HDKIT_CAPACITY_INSUFFICIENT') {
|
|
96
97
|
err.retryAfter = resp.headers.get('Retry-After') || undefined;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
steps: [
|
|
100
|
-
'稍后错峰重试(devkit提供的免费资源紧张,建议错峰使用)',
|
|
101
|
-
'急需使用时可尝试另一 pool_type(另一池可能有余量)',
|
|
102
|
-
],
|
|
103
|
-
};
|
|
98
|
+
// 该文案即最终展示内容(不带 "CODE: " 前缀、不附加引导)
|
|
99
|
+
err.message = data.message || err.message;
|
|
104
100
|
}
|
|
105
101
|
// 烧核播报(缺口#5 代码层):账号核心时长耗尽——上游码 HD.97520422 经
|
|
106
102
|
// hdkitservice 以 HDKIT_UPSTREAM_ERROR 包装透传,客户端按消息内错误码识别。
|
|
@@ -1,14 +1,35 @@
|
|
|
1
1
|
// 两池沙箱(pool_type)客户端支持。
|
|
2
2
|
// 机制(定稿):池 = 平台预置模板,客户端传 pool_type(ephemeral/persistent),
|
|
3
3
|
// 服务端权威映射 template_id;显式 template_id 优先(运维钉扎场景)。
|
|
4
|
-
// -
|
|
4
|
+
// - 非持久池(默认):闲置自动关机 + 自动删除(平台策略)
|
|
5
5
|
// - 持久池:EVS 数据盘跨关机保留,闲置回收保留期上限 7 天
|
|
6
|
+
// - 生命周期数值由 hdkitservice 配置回显(仅播报口径,实际策略由平台执行)
|
|
6
7
|
// - 配额:每用户每类型 1 台(同类复用),账号并发上限 5
|
|
7
8
|
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
10
|
+
import { join, dirname } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
|
|
8
13
|
const POOL_TYPES = new Set(['ephemeral', 'persistent']);
|
|
9
14
|
|
|
10
|
-
/**
|
|
15
|
+
/** 默认生命周期口径:与服务端默认一致;服务端回显优先,缺失时兜底 */
|
|
16
|
+
export const DEFAULT_POOL_LIFECYCLE = {
|
|
17
|
+
ephemeral: { idleShutdownMinutes: 30, idleDeleteMinutes: 60, reclaimMinutes: null },
|
|
18
|
+
// 7 天 = 10080 分钟(与服务端一致,统一以分钟为单位)
|
|
19
|
+
persistent: { idleShutdownMinutes: 60, idleDeleteMinutes: null, reclaimMinutes: 7 * 24 * 60 },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
/** workspaceId → { poolType, lifecycle, updatedAt }:connect 回显后缓存 */
|
|
11
25
|
const workspacePool = new Map();
|
|
26
|
+
/** 本进程内已播报过 connect 提示的 workspace(会话级,不落盘) */
|
|
27
|
+
const announcedWorkspaces = new Set();
|
|
28
|
+
let cacheLoaded = false;
|
|
29
|
+
|
|
30
|
+
function cachePath() {
|
|
31
|
+
return process.env.HW_SANDBOX_POOL_CACHE_PATH || join(homedir(), '.config', 'huaweicloud', 'sandbox-pools.json');
|
|
32
|
+
}
|
|
12
33
|
|
|
13
34
|
/**
|
|
14
35
|
* 校验并归一化 pool_type;未传时返回 null(交由服务端默认 ephemeral,不在客户端伪造语义)。
|
|
@@ -20,7 +41,7 @@ export function validatePoolType(raw) {
|
|
|
20
41
|
if (!POOL_TYPES.has(normalized)) {
|
|
21
42
|
throw new Error(
|
|
22
43
|
`invalid pool_type "${raw}" (expected ephemeral or persistent). ` +
|
|
23
|
-
'ephemeral =
|
|
44
|
+
'ephemeral = 非持久沙箱(闲置自动关机/删除);persistent = 持久沙箱(数据盘保留,闲置回收上限 7 天)',
|
|
24
45
|
);
|
|
25
46
|
}
|
|
26
47
|
return normalized;
|
|
@@ -37,68 +58,239 @@ export function pinnedTemplateFor(poolType) {
|
|
|
37
58
|
return '';
|
|
38
59
|
}
|
|
39
60
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
61
|
+
function positiveNumber(value) {
|
|
62
|
+
const n = Number(value);
|
|
63
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
43
64
|
}
|
|
44
65
|
|
|
45
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* 归一化生命周期口径:服务端字段(snake_case 官方契约)+ camelCase 双读兜底;
|
|
68
|
+
* 缺失/非法时回退客户端默认值(与服务端默认一致)。
|
|
69
|
+
*/
|
|
70
|
+
function normalizeLifecycle(poolType, raw = {}) {
|
|
71
|
+
const defaults = DEFAULT_POOL_LIFECYCLE[poolType];
|
|
72
|
+
if (!defaults) return null;
|
|
73
|
+
const pick = (...keys) => {
|
|
74
|
+
for (const key of keys) {
|
|
75
|
+
const value = positiveNumber(raw[key]);
|
|
76
|
+
if (value !== null) return value;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
};
|
|
80
|
+
const idleShutdownMinutes =
|
|
81
|
+
pick('pool_idle_shutdown_minutes', 'poolIdleShutdownMinutes', 'idleShutdownMinutes') ??
|
|
82
|
+
defaults.idleShutdownMinutes;
|
|
83
|
+
const idleDeleteMinutes =
|
|
84
|
+
poolType === 'ephemeral'
|
|
85
|
+
? (pick('pool_idle_delete_minutes', 'poolIdleDeleteMinutes', 'idleDeleteMinutes') ?? defaults.idleDeleteMinutes)
|
|
86
|
+
: null;
|
|
87
|
+
const reclaimMinutes =
|
|
88
|
+
poolType === 'persistent'
|
|
89
|
+
? (pick('pool_reclaim_minutes', 'poolReclaimMinutes', 'reclaimMinutes') ?? defaults.reclaimMinutes)
|
|
90
|
+
: null;
|
|
91
|
+
return { idleShutdownMinutes, idleDeleteMinutes, reclaimMinutes };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ensureCacheLoaded() {
|
|
95
|
+
if (cacheLoaded) return;
|
|
96
|
+
cacheLoaded = true;
|
|
97
|
+
try {
|
|
98
|
+
const raw = JSON.parse(readFileSync(cachePath(), 'utf8'));
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
for (const [wsId, entry] of Object.entries(raw?.workspaces || {})) {
|
|
101
|
+
if (!entry || !POOL_TYPES.has(entry.poolType)) continue;
|
|
102
|
+
if (!Number.isFinite(entry.updatedAt) || now - entry.updatedAt > CACHE_TTL_MS) continue;
|
|
103
|
+
workspacePool.set(wsId, {
|
|
104
|
+
poolType: entry.poolType,
|
|
105
|
+
lifecycle: normalizeLifecycle(entry.poolType, entry.lifecycle || {}),
|
|
106
|
+
updatedAt: entry.updatedAt,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// 首次使用 / 文件损坏:按空缓存处理
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function persistCache() {
|
|
115
|
+
try {
|
|
116
|
+
ensureCacheLoaded();
|
|
117
|
+
const file = cachePath();
|
|
118
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
119
|
+
const workspaces = {};
|
|
120
|
+
for (const [wsId, entry] of workspacePool) workspaces[wsId] = entry;
|
|
121
|
+
const payload = JSON.stringify({ workspaces }, null, 2);
|
|
122
|
+
const tmp = `${file}.tmp`;
|
|
123
|
+
writeFileSync(tmp, payload, { mode: 0o600 });
|
|
124
|
+
try {
|
|
125
|
+
renameSync(tmp, file);
|
|
126
|
+
} catch {
|
|
127
|
+
writeFileSync(file, payload, { mode: 0o600 });
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// 缓存失败不影响主流程(仅丢失跨进程提示能力)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 记录 workspace 的池归属与生命周期口径(来自服务端 connect 回显)。
|
|
136
|
+
* 无 pool_type 回显(自定义模板 / 老服务端)不记录,避免编造池语义。
|
|
137
|
+
*/
|
|
138
|
+
export function rememberWorkspacePool(workspaceId, poolType, lifecycleRaw) {
|
|
139
|
+
if (!workspaceId || !POOL_TYPES.has(poolType)) return;
|
|
140
|
+
workspacePool.set(workspaceId, {
|
|
141
|
+
poolType,
|
|
142
|
+
lifecycle: normalizeLifecycle(poolType, lifecycleRaw || {}),
|
|
143
|
+
updatedAt: Date.now(),
|
|
144
|
+
});
|
|
145
|
+
persistCache();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 清理 workspace 池缓存(测试用 / 会话关闭时) */
|
|
46
149
|
export function forgetWorkspacePool(workspaceId) {
|
|
47
|
-
if (workspaceId === undefined)
|
|
48
|
-
|
|
150
|
+
if (workspaceId === undefined) {
|
|
151
|
+
workspacePool.clear();
|
|
152
|
+
announcedWorkspaces.clear();
|
|
153
|
+
} else {
|
|
154
|
+
workspacePool.delete(workspaceId);
|
|
155
|
+
announcedWorkspaces.delete(workspaceId);
|
|
156
|
+
}
|
|
157
|
+
persistCache();
|
|
49
158
|
}
|
|
50
159
|
|
|
51
160
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
161
|
+
* connect 生命周期提示的播报判定:会话首次 connect 播报;
|
|
162
|
+
* 复用实例被本次 connect 唤醒(started=true)时再次播报;其余复用静默。
|
|
163
|
+
*/
|
|
164
|
+
export function shouldAnnounceConnect(workspaceId, result = {}) {
|
|
165
|
+
const started = result.started === true || result.startedFromStopped === true;
|
|
166
|
+
if (!workspaceId) return true;
|
|
167
|
+
if (!announcedWorkspaces.has(workspaceId)) {
|
|
168
|
+
announcedWorkspaces.add(workspaceId);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return started;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 分钟数 → 中文口径(60 的整数倍显示为小时) */
|
|
175
|
+
function formatMinutes(minutes, fallback) {
|
|
176
|
+
const n = Number(minutes) > 0 ? Number(minutes) : fallback;
|
|
177
|
+
if (n % 60 === 0 && n >= 60) return `${n / 60} 小时`;
|
|
178
|
+
return `${n} 分钟`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 回收保留期:分钟 → 中文口径(1440 的整数倍显示为天) */
|
|
182
|
+
function formatReclaim(minutes, fallback) {
|
|
183
|
+
const n = Number(minutes) > 0 ? Number(minutes) : fallback;
|
|
184
|
+
if (n % 1440 === 0 && n >= 1440) return `${n / 1440} 天`;
|
|
185
|
+
return `${n} 分钟`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* exec/upload/deploy 等工具的内部池上下文(不再默认并入工具返回,避免逐次刷屏)。
|
|
190
|
+
* expose_tunnel 用 poolLifecycle 生成交付地址提示;poolMode 用于 DevBridge key 分池。
|
|
54
191
|
*/
|
|
55
192
|
export function poolContextFor(workspaceId) {
|
|
56
|
-
|
|
193
|
+
ensureCacheLoaded();
|
|
194
|
+
const entry = workspacePool.get(workspaceId);
|
|
195
|
+
if (!entry) return undefined;
|
|
196
|
+
const { poolType, lifecycle } = entry;
|
|
197
|
+
const shutdown = formatMinutes(lifecycle.idleShutdownMinutes, DEFAULT_POOL_LIFECYCLE[poolType].idleShutdownMinutes);
|
|
57
198
|
if (poolType === 'ephemeral') {
|
|
199
|
+
const del = formatMinutes(lifecycle.idleDeleteMinutes, DEFAULT_POOL_LIFECYCLE.ephemeral.idleDeleteMinutes);
|
|
58
200
|
return {
|
|
59
201
|
poolMode: 'ephemeral',
|
|
60
|
-
|
|
202
|
+
poolLifecycle: { poolType, ...lifecycle },
|
|
203
|
+
persistenceHint: `非持久沙箱:闲置 ${shutdown}自动关机、${del}自动删除,重要数据请及时保存或迁出`,
|
|
61
204
|
};
|
|
62
205
|
}
|
|
63
|
-
|
|
206
|
+
const reclaim = formatReclaim(lifecycle.reclaimMinutes, DEFAULT_POOL_LIFECYCLE.persistent.reclaimMinutes);
|
|
207
|
+
return {
|
|
208
|
+
poolMode: 'persistent',
|
|
209
|
+
poolLifecycle: { poolType, ...lifecycle },
|
|
210
|
+
persistenceHint: `持久沙箱:闲置 ${shutdown}自动关机(数据盘保留);闲置回收保留期最长 ${reclaim}`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildConnectTtlHint(poolType, lifecycle) {
|
|
215
|
+
if (poolType === 'ephemeral') {
|
|
216
|
+
const shutdown = formatMinutes(lifecycle.idleShutdownMinutes, DEFAULT_POOL_LIFECYCLE.ephemeral.idleShutdownMinutes);
|
|
217
|
+
const del = formatMinutes(lifecycle.idleDeleteMinutes, DEFAULT_POOL_LIFECYCLE.ephemeral.idleDeleteMinutes);
|
|
64
218
|
return {
|
|
65
|
-
|
|
66
|
-
|
|
219
|
+
lifecycle: `idle shutdown ${lifecycle.idleShutdownMinutes}min + auto-delete ${lifecycle.idleDeleteMinutes}min (platform policy)`,
|
|
220
|
+
ttl_hint: `非持久沙箱:闲置 ${shutdown}自动关机、${del}自动删除(平台策略)。重要数据请及时保存或迁出。`,
|
|
67
221
|
};
|
|
68
222
|
}
|
|
69
|
-
|
|
223
|
+
const shutdown = formatMinutes(lifecycle.idleShutdownMinutes, DEFAULT_POOL_LIFECYCLE.persistent.idleShutdownMinutes);
|
|
224
|
+
const reclaim = formatReclaim(lifecycle.reclaimMinutes, DEFAULT_POOL_LIFECYCLE.persistent.reclaimMinutes);
|
|
225
|
+
return {
|
|
226
|
+
lifecycle: `idle shutdown ${lifecycle.idleShutdownMinutes}min; EVS data disk retained; idle reclaim within ${lifecycle.reclaimMinutes}min max`,
|
|
227
|
+
ttl_hint: `持久沙箱:闲置 ${shutdown}自动关机(实例与数据盘保留,重新连接可继续使用);闲置回收保留期最长 ${reclaim},到期实例与数据将被删除,长期不用请自行导出。`,
|
|
228
|
+
};
|
|
70
229
|
}
|
|
71
230
|
|
|
72
231
|
/**
|
|
73
|
-
* connect
|
|
74
|
-
* 仅当服务端回显 pool_type
|
|
75
|
-
*
|
|
76
|
-
*
|
|
232
|
+
* connect 结果池富化(生命周期播报)。
|
|
233
|
+
* - 仅当服务端回显 pool_type 才生成提示(自定义模板不编造池语义)
|
|
234
|
+
* - announce=false 时不播报(会话内重复复用)
|
|
235
|
+
* - 数值以服务端回显为准,缺失时用客户端默认口径兜底
|
|
77
236
|
*/
|
|
78
|
-
export function enrichConnectResult(result) {
|
|
237
|
+
export function enrichConnectResult(result, options = {}) {
|
|
79
238
|
if (!result || typeof result !== 'object') return result;
|
|
80
239
|
const poolType = result.pool_type || result.poolType;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
ttl_hint:
|
|
86
|
-
'非持久沙箱:闲置 30 分钟将自动关机、闲置 1 小时将自动删除(平台定时任务已生效)。' +
|
|
87
|
-
'重要数据请及时保存或迁出;需要跨会话保留数据请改用 pool_type=persistent。',
|
|
88
|
-
};
|
|
89
|
-
} else if (poolType === 'persistent') {
|
|
90
|
-
result.pool = {
|
|
91
|
-
pool_type: 'persistent',
|
|
92
|
-
lifecycle: 'EVS data disk retained; idle reclaim within 7 days max',
|
|
93
|
-
ttl_hint: '持久沙箱:数据盘跨关机保留,闲置回收保留期最长 7 天;长期不用请自行导出数据。',
|
|
94
|
-
};
|
|
240
|
+
const announce = options.announce !== false;
|
|
241
|
+
if (!announce || !POOL_TYPES.has(poolType)) {
|
|
242
|
+
delete result.pool;
|
|
243
|
+
return result;
|
|
95
244
|
}
|
|
245
|
+
const lifecycle = normalizeLifecycle(poolType, result);
|
|
246
|
+
result.pool = {
|
|
247
|
+
pool_type: poolType,
|
|
248
|
+
...buildConnectTtlHint(poolType, lifecycle),
|
|
249
|
+
};
|
|
96
250
|
return result;
|
|
97
251
|
}
|
|
98
252
|
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
253
|
+
/**
|
|
254
|
+
* 交付公网地址时的沙箱存活期提示(expose_tunnel 专用)。
|
|
255
|
+
* 返回 undefined 表示池语义未知(自定义模板 / 无缓存),不编造提示。
|
|
256
|
+
*/
|
|
257
|
+
export function buildExposeLifetimeNotice(poolContext, expiryHours) {
|
|
258
|
+
const lifecycle = poolContext?.poolLifecycle;
|
|
259
|
+
if (!lifecycle) return undefined;
|
|
260
|
+
const poolType = lifecycle.poolType;
|
|
261
|
+
const tunnelHours = Number(expiryHours) > 0 ? Number(expiryHours) : 72;
|
|
262
|
+
if (poolType === 'ephemeral') {
|
|
263
|
+
const shutdown = formatMinutes(lifecycle.idleShutdownMinutes, DEFAULT_POOL_LIFECYCLE.ephemeral.idleShutdownMinutes);
|
|
264
|
+
const del = formatMinutes(lifecycle.idleDeleteMinutes, DEFAULT_POOL_LIFECYCLE.ephemeral.idleDeleteMinutes);
|
|
265
|
+
return {
|
|
266
|
+
sandboxLifetime: {
|
|
267
|
+
poolType,
|
|
268
|
+
idleShutdownMinutes: lifecycle.idleShutdownMinutes,
|
|
269
|
+
idleDeleteMinutes: lifecycle.idleDeleteMinutes,
|
|
270
|
+
},
|
|
271
|
+
urlLifetimeHint:
|
|
272
|
+
`该公网地址随沙箱存亡:非持久沙箱闲置 ${shutdown}自动关机(地址随即失效)、` +
|
|
273
|
+
`闲置 ${del}自动删除(地址与数据永久消失)。隧道有效期 ${tunnelHours} 小时不等于沙箱存活期;` +
|
|
274
|
+
'自动续期只保隧道,不能阻止沙箱关机。',
|
|
275
|
+
warning:
|
|
276
|
+
`URL 存活期提示:非持久沙箱闲置 ${shutdown}自动关机、${del}自动删除,此地址届时失效` +
|
|
277
|
+
`(隧道 ${tunnelHours} 小时有效期不等于沙箱存活期)`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const shutdown = formatMinutes(lifecycle.idleShutdownMinutes, DEFAULT_POOL_LIFECYCLE.persistent.idleShutdownMinutes);
|
|
281
|
+
const reclaim = formatReclaim(lifecycle.reclaimMinutes, DEFAULT_POOL_LIFECYCLE.persistent.reclaimMinutes);
|
|
282
|
+
return {
|
|
283
|
+
sandboxLifetime: {
|
|
284
|
+
poolType,
|
|
285
|
+
idleShutdownMinutes: lifecycle.idleShutdownMinutes,
|
|
286
|
+
reclaimMinutes: lifecycle.reclaimMinutes,
|
|
287
|
+
},
|
|
288
|
+
urlLifetimeHint:
|
|
289
|
+
`该公网地址随沙箱存亡:持久沙箱闲置 ${shutdown}自动关机(地址失效,但实例与数据盘保留,` +
|
|
290
|
+
`重新连接沙箱后可重新暴露端口恢复访问);闲置回收保留期最长 ${reclaim},到期实例与数据将被删除。` +
|
|
291
|
+
`隧道有效期 ${tunnelHours} 小时不等于沙箱存活期;自动续期只保隧道,不能阻止沙箱关机。`,
|
|
292
|
+
warning:
|
|
293
|
+
`URL 存活期提示:持久沙箱闲置 ${shutdown}自动关机,此地址届时失效(实例与数据保留,` +
|
|
294
|
+
`重新连接后可恢复);闲置回收上限 ${reclaim}`,
|
|
295
|
+
};
|
|
104
296
|
}
|
|
@@ -19,6 +19,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
19
19
|
import { createConnection, getCredentials } from './hwlink-api.mjs';
|
|
20
20
|
import { getWebSocketImpl } from '../proxy/proxy-agent.mjs';
|
|
21
21
|
import { trackSandboxConnect, trackSandboxDisconnect } from '../telemetry/telemetry.mjs';
|
|
22
|
+
import { buildExposeLifetimeNotice } from './sandbox-pool.mjs';
|
|
22
23
|
|
|
23
24
|
const execFileAsync = promisify(execFile);
|
|
24
25
|
|
|
@@ -1862,7 +1863,16 @@ export async function installKeepaliveWatchdog(workspaceId, { tunnels }, usernam
|
|
|
1862
1863
|
// session-surviving URL from one that dies with the deploying session.
|
|
1863
1864
|
export async function exposeTunnel(
|
|
1864
1865
|
workspaceId,
|
|
1865
|
-
{
|
|
1866
|
+
{
|
|
1867
|
+
port,
|
|
1868
|
+
expiryHours,
|
|
1869
|
+
keepalive,
|
|
1870
|
+
poolType,
|
|
1871
|
+
poolContext,
|
|
1872
|
+
ensureDevbridgeKey,
|
|
1873
|
+
buildKeyringInjectScript,
|
|
1874
|
+
listKeysForQuotaHint,
|
|
1875
|
+
},
|
|
1866
1876
|
username = 'root',
|
|
1867
1877
|
timeoutMs = 90000,
|
|
1868
1878
|
) {
|
|
@@ -2140,10 +2150,17 @@ export async function exposeTunnel(
|
|
|
2140
2150
|
}
|
|
2141
2151
|
}
|
|
2142
2152
|
|
|
2153
|
+
// 交付地址时必须播报沙箱存活期(隧道有效期 ≠ 沙箱存活期;keepalive 只保隧道不保沙箱)
|
|
2154
|
+
const lifetimeNotice = buildExposeLifetimeNotice(poolContext, parsed.expiryHours);
|
|
2155
|
+
if (lifetimeNotice) warnings.push(lifetimeNotice.warning);
|
|
2156
|
+
|
|
2143
2157
|
return {
|
|
2144
2158
|
ok: true,
|
|
2145
2159
|
publicUrl: parsed.tunnelUrl || undefined,
|
|
2146
2160
|
port: validatedPort,
|
|
2161
|
+
...(lifetimeNotice
|
|
2162
|
+
? { sandboxLifetime: lifetimeNotice.sandboxLifetime, urlLifetimeHint: lifetimeNotice.urlLifetimeHint }
|
|
2163
|
+
: {}),
|
|
2147
2164
|
tunnelId: parsed.tunnelId || undefined,
|
|
2148
2165
|
authMode,
|
|
2149
2166
|
httpCode: parsed.httpCode,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
rememberWorkspacePool,
|
|
33
33
|
enrichConnectResult,
|
|
34
34
|
poolContextFor,
|
|
35
|
-
|
|
35
|
+
shouldAnnounceConnect,
|
|
36
36
|
forgetWorkspacePool,
|
|
37
37
|
} from './sandbox/sandbox-pool.mjs';
|
|
38
38
|
import { ensureDevbridgeKey, buildKeyringInjectScript, listKeysForQuotaHint } from './sandbox/devbridge-key.mjs';
|
|
@@ -776,7 +776,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
776
776
|
{
|
|
777
777
|
name: 'huaweicloud_sandbox_expose_tunnel',
|
|
778
778
|
description:
|
|
779
|
-
'Expose a port on the sandbox to a public DevBridge tunnel URL (devbridge 0.2.x). Authenticates automatically (probing AK/SK vs API-Key builds, using /tmp/hw_creds.sh and /tmp/hw_api_key as injected by huaweicloud_sandbox_credentials), pre-cleans stale tunnels to avoid quota errors, starts the host, extracts the public URL (https://<id>-<port>.devbridge-s2.hwtunnel.com), and health-checks it before returning. AUTO-ENROLLS the tunnel into the keepalive watchdog (host re-attach + daily 720h renewal + health check) by default, merged into existing coverage — pass keepalive:false to opt out. Use the ACTUAL port reported by huaweicloud_sandbox_deploy_nginx (its "port" field auto-increments on conflict) — never the originally requested port. Returns ok, publicUrl, port, tunnelId, keepalive.enrolled, and
|
|
779
|
+
'Expose a port on the sandbox to a public DevBridge tunnel URL (devbridge 0.2.x). Authenticates automatically (probing AK/SK vs API-Key builds, using /tmp/hw_creds.sh and /tmp/hw_api_key as injected by huaweicloud_sandbox_credentials), pre-cleans stale tunnels to avoid quota errors, starts the host, extracts the public URL (https://<id>-<port>.devbridge-s2.hwtunnel.com), and health-checks it before returning. AUTO-ENROLLS the tunnel into the keepalive watchdog (host re-attach + daily 720h renewal + health check) by default, merged into existing coverage — pass keepalive:false to opt out. Use the ACTUAL port reported by huaweicloud_sandbox_deploy_nginx (its "port" field auto-increments on conflict) — never the originally requested port. Returns ok, publicUrl, port, tunnelId, keepalive.enrolled, warnings, sandboxLifetime and urlLifetimeHint. The urlLifetimeHint MUST be relayed to the developer together with the URL: the tunnel expiry is NOT the sandbox lifetime — an idle sandbox shuts down (ephemeral also auto-deletes), which kills the URL. If the API Key is missing on a release build it returns ok:false with instructions to create one.',
|
|
780
780
|
inputSchema: {
|
|
781
781
|
type: 'object',
|
|
782
782
|
required: ['port'],
|
|
@@ -886,7 +886,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
886
886
|
{
|
|
887
887
|
name: 'huaweicloud_sandbox_connect',
|
|
888
888
|
description:
|
|
889
|
-
'Connect to a sandbox via hdkitservice. One instance per user per pool — reuses the same-pool sandbox if available, otherwise creates a new one. Quota: 1 instance per pool type (a user may hold 1 ephemeral AND 1 persistent simultaneously; same-type requests reuse the existing instance). On quota exhaustion the API returns HTTP 429 with a Retry-After header and a dual error code: HDKIT_QUOTA_EXCEEDED (retry useless — release or reuse the existing instance) vs HDKIT_CAPACITY_INSUFFICIENT (pool capacity; retry later / off-peak). Returns session_id, dev_stage_id, connection_id, connection_address, expiresAt (STS credential expiry),
|
|
889
|
+
'Connect to a sandbox via hdkitservice. One instance per user per pool — reuses the same-pool sandbox if available, otherwise creates a new one. Quota: 1 instance per pool type (a user may hold 1 ephemeral AND 1 persistent simultaneously; same-type requests reuse the existing instance). On quota exhaustion the API returns HTTP 429 with a Retry-After header and a dual error code: HDKIT_QUOTA_EXCEEDED (retry useless — release or reuse the existing instance) vs HDKIT_CAPACITY_INSUFFICIENT (pool capacity; retry later / off-peak). Returns session_id, dev_stage_id, connection_id, connection_address, expiresAt (STS credential expiry), pool_type/template_id echo, server-configured lifecycle numbers (pool_idle_shutdown_minutes / pool_idle_delete_minutes / pool_reclaim_minutes) and started (true when a reused instance was woken from shutdown). Lifecycle hints are announced only on the first connect of a session or when started=true. Ephemeral pool (default): idle auto-shutdown after 30min, auto-delete after 1h — do not keep important data there. Persistent pool: data disk retained (EVS 32G), idle shutdown after 1h, idle reclaim capped at 7 days. Numbers are configured by hdkitservice; the text follows the server echo and falls back to these defaults.',
|
|
890
890
|
inputSchema: {
|
|
891
891
|
type: 'object',
|
|
892
892
|
properties: {
|
|
@@ -1499,10 +1499,10 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1499
1499
|
const sandboxUser2 = args.username || 'root';
|
|
1500
1500
|
const sandboxTimeout2 = args.timeout_ms || 120000;
|
|
1501
1501
|
const sandboxResult2 = await execWithSession(sandboxWsId2, args.command, sandboxUser2, sandboxTimeout2);
|
|
1502
|
-
return
|
|
1502
|
+
return {
|
|
1503
1503
|
stdout: sandboxResult2.stdout,
|
|
1504
1504
|
exitCode: sandboxResult2.exitCode,
|
|
1505
|
-
}
|
|
1505
|
+
};
|
|
1506
1506
|
}
|
|
1507
1507
|
case 'huaweicloud_sandbox_exec_one_shot': {
|
|
1508
1508
|
const sandboxWsId3 = args.workspace_id || getCurrentWorkspaceId();
|
|
@@ -1515,10 +1515,10 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1515
1515
|
const sandboxUser3 = args.username || 'root';
|
|
1516
1516
|
const sandboxTimeout3 = args.timeout_ms || 120000;
|
|
1517
1517
|
const sandboxResult3 = await execOneShot(sandboxWsId3, args.command, sandboxUser3, sandboxTimeout3);
|
|
1518
|
-
return
|
|
1518
|
+
return {
|
|
1519
1519
|
stdout: sandboxResult3.stdout,
|
|
1520
1520
|
exitCode: sandboxResult3.exitCode,
|
|
1521
|
-
}
|
|
1521
|
+
};
|
|
1522
1522
|
}
|
|
1523
1523
|
case 'huaweicloud_sandbox_close_session': {
|
|
1524
1524
|
const sandboxWsId4 = args.workspace_id || getCurrentWorkspaceId();
|
|
@@ -1549,9 +1549,12 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1549
1549
|
}
|
|
1550
1550
|
const sandboxUser5 = args.username || 'root';
|
|
1551
1551
|
const sandboxTimeout5 = args.timeout_ms || 120000;
|
|
1552
|
-
return
|
|
1552
|
+
return await uploadFileWithSession(
|
|
1553
1553
|
sandboxWsId5,
|
|
1554
|
-
|
|
1554
|
+
args.local_path,
|
|
1555
|
+
args.remote_path,
|
|
1556
|
+
sandboxUser5,
|
|
1557
|
+
sandboxTimeout5,
|
|
1555
1558
|
);
|
|
1556
1559
|
}
|
|
1557
1560
|
case 'huaweicloud_sandbox_upload_project': {
|
|
@@ -1567,12 +1570,16 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1567
1570
|
}
|
|
1568
1571
|
const sandboxUser6 = args.username || 'root';
|
|
1569
1572
|
const sandboxTimeout6 = args.timeout_ms || 120000;
|
|
1570
|
-
return
|
|
1573
|
+
return await uploadProjectWithSession(
|
|
1571
1574
|
sandboxWsId6,
|
|
1572
|
-
|
|
1575
|
+
args.local_dir,
|
|
1576
|
+
args.remote_dir,
|
|
1577
|
+
sandboxUser6,
|
|
1578
|
+
sandboxTimeout6,
|
|
1579
|
+
{
|
|
1573
1580
|
exclude: args.exclude,
|
|
1574
1581
|
extract: args.extract,
|
|
1575
|
-
}
|
|
1582
|
+
},
|
|
1576
1583
|
);
|
|
1577
1584
|
}
|
|
1578
1585
|
case 'huaweicloud_sandbox_deploy_nginx': {
|
|
@@ -1588,22 +1595,19 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1588
1595
|
}
|
|
1589
1596
|
const sandboxUser7 = args.username || 'root';
|
|
1590
1597
|
const sandboxTimeout7 = args.timeout_ms || 60000;
|
|
1591
|
-
return
|
|
1598
|
+
return await deployNginx(
|
|
1592
1599
|
sandboxWsId7,
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
sandboxUser7,
|
|
1605
|
-
sandboxTimeout7,
|
|
1606
|
-
),
|
|
1600
|
+
{
|
|
1601
|
+
nginxType: args.nginx_type,
|
|
1602
|
+
port: args.port,
|
|
1603
|
+
project: args.project,
|
|
1604
|
+
outputDir: args.output_dir,
|
|
1605
|
+
nodePort: args.node_port,
|
|
1606
|
+
publicPort: args.public_port,
|
|
1607
|
+
configName: args.config_name,
|
|
1608
|
+
},
|
|
1609
|
+
sandboxUser7,
|
|
1610
|
+
sandboxTimeout7,
|
|
1607
1611
|
);
|
|
1608
1612
|
}
|
|
1609
1613
|
case 'huaweicloud_sandbox_deploy_check': {
|
|
@@ -1619,19 +1623,16 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1619
1623
|
}
|
|
1620
1624
|
const sandboxUser8 = args.username || 'root';
|
|
1621
1625
|
const sandboxTimeout8 = args.timeout_ms || 30000;
|
|
1622
|
-
return
|
|
1626
|
+
return await deployCheck(
|
|
1623
1627
|
sandboxWsId8,
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
sandboxUser8,
|
|
1633
|
-
sandboxTimeout8,
|
|
1634
|
-
),
|
|
1628
|
+
{
|
|
1629
|
+
port: args.port,
|
|
1630
|
+
project: args.project,
|
|
1631
|
+
outputDir: args.output_dir,
|
|
1632
|
+
frameworkType: args.framework_type,
|
|
1633
|
+
},
|
|
1634
|
+
sandboxUser8,
|
|
1635
|
+
sandboxTimeout8,
|
|
1635
1636
|
);
|
|
1636
1637
|
}
|
|
1637
1638
|
case 'huaweicloud_sandbox_expose_tunnel': {
|
|
@@ -1649,23 +1650,22 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1649
1650
|
}
|
|
1650
1651
|
const sandboxUser9 = args.username || 'root';
|
|
1651
1652
|
const sandboxTimeout9 = args.timeout_ms || 90000;
|
|
1652
|
-
|
|
1653
|
+
const exposePoolContext = poolContextFor(sandboxWsId9);
|
|
1654
|
+
return await exposeTunnel(
|
|
1653
1655
|
sandboxWsId9,
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
sandboxTimeout9,
|
|
1668
|
-
),
|
|
1656
|
+
{
|
|
1657
|
+
port: args.port,
|
|
1658
|
+
expiryHours: args.expiry_hours,
|
|
1659
|
+
keepalive: args.keepalive,
|
|
1660
|
+
// DevBridge API Key 自动创建兜底(认证固定 apikey,AK/SK 已移除)
|
|
1661
|
+
poolType: exposePoolContext?.poolMode || 'ephemeral',
|
|
1662
|
+
poolContext: exposePoolContext,
|
|
1663
|
+
ensureDevbridgeKey,
|
|
1664
|
+
buildKeyringInjectScript,
|
|
1665
|
+
listKeysForQuotaHint,
|
|
1666
|
+
},
|
|
1667
|
+
sandboxUser9,
|
|
1668
|
+
sandboxTimeout9,
|
|
1669
1669
|
);
|
|
1670
1670
|
}
|
|
1671
1671
|
case 'huaweicloud_sandbox_renew_tunnel': {
|
|
@@ -1681,14 +1681,11 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1681
1681
|
'or set HW_WORKSPACE_ID environment variable before starting the agent.',
|
|
1682
1682
|
);
|
|
1683
1683
|
}
|
|
1684
|
-
return
|
|
1684
|
+
return await renewTunnel(
|
|
1685
1685
|
sandboxWsId10,
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
args.username || 'root',
|
|
1690
|
-
args.timeout_ms || 60000,
|
|
1691
|
-
),
|
|
1686
|
+
{ tunnelId: args.tunnel_id, expiryHours: args.expiry_hours },
|
|
1687
|
+
args.username || 'root',
|
|
1688
|
+
args.timeout_ms || 60000,
|
|
1692
1689
|
);
|
|
1693
1690
|
}
|
|
1694
1691
|
case 'huaweicloud_sandbox_keepalive_watchdog': {
|
|
@@ -1705,14 +1702,11 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1705
1702
|
);
|
|
1706
1703
|
}
|
|
1707
1704
|
const tunnels11 = args.tunnels.map((t) => ({ tunnelId: t.tunnel_id || t.tunnelId, url: t.url }));
|
|
1708
|
-
return
|
|
1705
|
+
return await installKeepaliveWatchdog(
|
|
1709
1706
|
sandboxWsId11,
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
args.username || 'root',
|
|
1714
|
-
args.timeout_ms || 60000,
|
|
1715
|
-
),
|
|
1707
|
+
{ tunnels: tunnels11 },
|
|
1708
|
+
args.username || 'root',
|
|
1709
|
+
args.timeout_ms || 60000,
|
|
1716
1710
|
);
|
|
1717
1711
|
}
|
|
1718
1712
|
case 'huaweicloud_sandbox_check_user':
|
|
@@ -1754,11 +1748,14 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1754
1748
|
throw error;
|
|
1755
1749
|
}
|
|
1756
1750
|
const devStageId = connectResult?.dev_stage_id || connectResult?.devStageId;
|
|
1751
|
+
let announceConnect = true;
|
|
1757
1752
|
if (devStageId) {
|
|
1758
1753
|
setWorkspaceId(devStageId);
|
|
1759
|
-
//
|
|
1754
|
+
// 播报判定:会话首次 connect 或复用实例被唤醒(started=true)才提示,其余复用静默
|
|
1755
|
+
announceConnect = shouldAnnounceConnect(devStageId, connectResult || {});
|
|
1756
|
+
// 池归属 + 生命周期口径缓存(服务端回显优先,缺失时客户端默认兜底)。
|
|
1760
1757
|
// 双读兜底与 devStageId 同型(防服务端 wire 命名漂移,QA #1082 配对复审教训)
|
|
1761
|
-
rememberWorkspacePool(devStageId, connectResult?.pool_type || connectResult?.poolType);
|
|
1758
|
+
rememberWorkspacePool(devStageId, connectResult?.pool_type || connectResult?.poolType, connectResult || {});
|
|
1762
1759
|
try {
|
|
1763
1760
|
await execOneShot(devStageId, 'devbridge delete-all 2>/dev/null || true', 'root', 15000);
|
|
1764
1761
|
} catch {}
|
|
@@ -1772,8 +1769,8 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1772
1769
|
} catch {}
|
|
1773
1770
|
await transferGitRepo(args, devStageId, connectResult);
|
|
1774
1771
|
}
|
|
1775
|
-
//
|
|
1776
|
-
return enrichConnectResult(connectResult);
|
|
1772
|
+
// 生命周期提示:仅首次连接 / 关机唤醒时播报(服务端回显 pool_type 才生成)
|
|
1773
|
+
return enrichConnectResult(connectResult, { announce: announceConnect });
|
|
1777
1774
|
}
|
|
1778
1775
|
case 'huaweicloud_sandbox_credentials': {
|
|
1779
1776
|
const devStageId = args.dev_stage_id || getCurrentWorkspaceId();
|