huaweicloud-devkit 1.1.0-next.5 → 1.1.0-next.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/README.zh-CN.md +19 -1
- 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/hooks/huaweicloud-safety.py +23 -15
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +289 -36
- package/plugins/huaweicloud-core/skills/huawei-voucher/SKILL.md +52 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +2 -0
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +34 -2
- package/plugins/huaweicloud-core/src/setup-cli.mjs +89 -7
- package/plugins/huaweicloud-core/src/tools.mjs +7 -2
- package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +29 -8
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
Help AI coding agents use Huawei Cloud safely and accurately — a single integration that gives agents cloud knowledge, CLI tooling, and safety guardrails.
|
|
10
10
|
|
|
11
|
-
Supports OpenCode, CodeArts Agent, WorkBuddy, DeepSeek Harness (DSH), OfficeAce, OpenClaw, and AtomCode.
|
|
11
|
+
Supports OpenCode, CodeArts Agent, WorkBuddy, DeepSeek Harness (DSH), OfficeAce, Hermes, OpenClaw, and AtomCode.
|
|
12
12
|
|
|
13
13
|
## Prerequisites
|
|
14
14
|
|
|
@@ -98,6 +98,24 @@ npx --yes huaweicloud-devkit update --target officeace
|
|
|
98
98
|
npx --yes huaweicloud-devkit uninstall --target officeace
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
### Hermes
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx --yes huaweicloud-devkit install --target hermes
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Restart the Hermes session** after installation.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx --yes huaweicloud-devkit doctor --target hermes
|
|
111
|
+
npx --yes huaweicloud-devkit status --target hermes
|
|
112
|
+
npx --yes huaweicloud-devkit update --target hermes
|
|
113
|
+
npx --yes huaweicloud-devkit uninstall --target hermes
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> **Safety hooks**: The installer configures Hermes shell hooks (`config.yaml` → `hooks.pre_tool_call`) to intercept unsafe terminal commands such as credential file reads, environment variable dumps, and unapproved `hcloud` write operations. Hermes shows a consent prompt the first time; approve it or set `hooks_auto_accept: true` in `config.yaml` to auto-accept.
|
|
117
|
+
> **Windows**: Hermes on Windows stores data under `%LOCALAPPDATA%\hermes` instead of `~/.hermes`. The installer detects this automatically.
|
|
118
|
+
|
|
101
119
|
### OpenClaw
|
|
102
120
|
|
|
103
121
|
```bash
|
package/README.zh-CN.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
帮助 AI 编码助手安全、准确地使用华为云——一站式集成云知识、CLI 工具和安全护栏。
|
|
10
10
|
|
|
11
|
-
支持 OpenCode、码道(CodeArts Agent)、WorkBuddy、DeepSeek Harness(DSH)、OfficeAce、OpenClaw、AtomCode。
|
|
11
|
+
支持 OpenCode、码道(CodeArts Agent)、WorkBuddy、DeepSeek Harness(DSH)、OfficeAce、Hermes、OpenClaw、AtomCode。
|
|
12
12
|
|
|
13
13
|
## 前置条件
|
|
14
14
|
|
|
@@ -98,6 +98,24 @@ npx --yes huaweicloud-devkit update --target officeace
|
|
|
98
98
|
npx --yes huaweicloud-devkit uninstall --target officeace
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
### Hermes
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx --yes huaweicloud-devkit install --target hermes
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
安装后**重启 Hermes 会话**。
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx --yes huaweicloud-devkit doctor --target hermes
|
|
111
|
+
npx --yes huaweicloud-devkit status --target hermes
|
|
112
|
+
npx --yes huaweicloud-devkit update --target hermes
|
|
113
|
+
npx --yes huaweicloud-devkit uninstall --target hermes
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> **安全钩子(Safety hooks)**:安装器会在 `config.yaml` 中写入 shell hooks 配置(`hooks.pre_tool_call`),拦截不安全的终端命令,如读取凭据文件、导出环境变量、未审批的 `hcloud` 写操作。Hermes 首次使用时会弹出同意提示,可批准或设置 `hooks_auto_accept: true` 自动批准。
|
|
117
|
+
> **Windows**:Hermes 在 Windows 上数据存储在 `%LOCALAPPDATA%\hermes` 而非 `~/.hermes`,安装器会自动检测适配。
|
|
118
|
+
|
|
101
119
|
### OpenClaw
|
|
102
120
|
|
|
103
121
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.0-next.
|
|
3
|
+
"version": "1.1.0-next.7",
|
|
4
4
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -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.0-next.
|
|
20
|
+
"version": "1.1.0-next.7",
|
|
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.0-next.
|
|
23
|
+
"version": "1.1.0-next.7",
|
|
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.0-next.
|
|
20
|
+
"version": "1.1.0-next.7",
|
|
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.0-next.
|
|
3
|
+
"version": "1.1.0-next.7",
|
|
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.0-next.
|
|
3
|
+
"version": "1.1.0-next.7",
|
|
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",
|
|
@@ -53,16 +53,23 @@ READ_OPERATION_RE = re.compile(r"\b(List|Show|Get|Describe|NovaList|NovaShow)\w*
|
|
|
53
53
|
READ_OPERATION_RE = re.compile(r"\b(List|Show|Get|Describe|NovaList|NovaShow)\w*", re.I)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def deny(reason):
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"permissionDecisionReason": DENY_PREFIX + reason,
|
|
56
|
+
def deny(reason, hermes=False):
|
|
57
|
+
sys.stdout.write(
|
|
58
|
+
json.dumps(
|
|
59
|
+
{
|
|
60
|
+
"action": "block",
|
|
61
|
+
"message": DENY_PREFIX + reason,
|
|
63
62
|
}
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
if hermes
|
|
64
|
+
else {
|
|
65
|
+
"hookSpecificOutput": {
|
|
66
|
+
"hookEventName": "PreToolUse",
|
|
67
|
+
"permissionDecision": "deny",
|
|
68
|
+
"permissionDecisionReason": DENY_PREFIX + reason,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
+ "\n"
|
|
66
73
|
)
|
|
67
74
|
sys.exit(0)
|
|
68
75
|
|
|
@@ -139,18 +146,19 @@ def main():
|
|
|
139
146
|
|
|
140
147
|
tool_name = data.get("tool_name", "")
|
|
141
148
|
text = command_text(data.get("tool_input", {}))
|
|
149
|
+
hermes = "hook_event_name" in data
|
|
142
150
|
|
|
143
151
|
if CONFIG_FILE_RE and CONFIG_FILE_RE.search(text):
|
|
144
|
-
deny("reading Huawei Cloud credential/profile files can expose AK/SK or tokens. Use redacted toolkit tools.")
|
|
152
|
+
deny("reading Huawei Cloud credential/profile files can expose AK/SK or tokens. Use redacted toolkit tools.", hermes)
|
|
145
153
|
if ENV_DUMP_RE.search(text):
|
|
146
|
-
deny("dumping cloud credential environment variables is not allowed.")
|
|
154
|
+
deny("dumping cloud credential environment variables is not allowed.", hermes)
|
|
147
155
|
if SECRET_READ_RE and SECRET_READ_RE.search(text):
|
|
148
|
-
deny("direct secret value retrieval would put plaintext secrets into the agent context.")
|
|
156
|
+
deny("direct secret value retrieval would put plaintext secrets into the agent context.", hermes)
|
|
149
157
|
denied_rule = first_denied_command_rule(text)
|
|
150
158
|
if denied_rule:
|
|
151
|
-
deny(f"{denied_rule.get('message')} Remediation: {denied_rule.get('remediation')}")
|
|
152
|
-
if tool_name
|
|
153
|
-
deny("unapproved Huawei Cloud write operations must be planned first and explicitly approved by the user.")
|
|
159
|
+
deny(f"{denied_rule.get('message')} Remediation: {denied_rule.get('remediation')}", hermes)
|
|
160
|
+
if tool_name in ("Bash", "terminal") and HCLOUD_RE.search(text) and WRITE_OPERATION_RE.search(text) and not READ_OPERATION_RE.search(text):
|
|
161
|
+
deny("unapproved Huawei Cloud write operations must be planned first and explicitly approved by the user.", hermes)
|
|
154
162
|
|
|
155
163
|
allow()
|
|
156
164
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"id": "huaweicloud-devkit",
|
|
4
4
|
"displayName": "HuaweiCloud DevKit",
|
|
5
|
-
"version": "1.1.0-next.
|
|
5
|
+
"version": "1.1.0-next.7",
|
|
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.",
|
|
@@ -72,6 +72,8 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
72
72
|
{ "timeout_ms": 300000 }
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
**Session recovery**: if `exec_with_session` returns `session is not ready`, the WebSocket connection has dropped. Do NOT retry the same session — fall back to `exec_one_shot` for that command instead. To recover state (cd, env vars), reconstruct them explicitly in the one-shot command.
|
|
76
|
+
|
|
75
77
|
## Workflow
|
|
76
78
|
|
|
77
79
|
Setup is a **plugin-side preflight** — the developer should be asked a question only once, when the agreement actually needs signing:
|
|
@@ -84,10 +86,21 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
|
|
|
84
86
|
3. **Sign agreement only** (`HDKIT_NOT_AGREEMENT`): **STOP and do NOT sign on your own.** Ask the developer: "Huawei Cloud sandbox requires signing the latest developer service agreement. May I sign it for you?" Then **wait for the developer to explicitly agree** (e.g. "签署" / "确认" / "sign it"). Only after explicit consent call `huaweicloud_sandbox_sign_agreement` and return its result (`signed`/`signedCount`) to the developer. **Never sign a legal agreement on the developer's behalf without their explicit, unambiguous consent.** Do not expose the underlying sandbox/DevBridge service as a separate entity the developer must understand or sign up for
|
|
85
87
|
4. **Both missing** (`HDKIT_NOT_REALNAME_AND_AGREEMENT`): present **both** requirements together in one message — the real-name verification steps (console, step 2) **and** the agreement-signing request (step 3, wait for explicit consent) — so the developer can complete both at once
|
|
86
88
|
5. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`
|
|
87
|
-
6. **
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
6. **Cleanup previous deployments** (after first connect to a sandbox): nginx configs and DevBridge tunnels from previous deployments can cause port conflicts and quota errors. Run cleanup immediately after connect:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Remove stale nginx configs from previous deployments
|
|
93
|
+
sudo rm -f /etc/nginx/conf.d/app.conf /etc/nginx/conf.d/*.conf.bak 2>/dev/null
|
|
94
|
+
# Remove stale DevBridge tunnels
|
|
95
|
+
devbridge delete-all 2>/dev/null || true
|
|
96
|
+
# Reload nginx to apply
|
|
97
|
+
sudo nginx -s reload 2>/dev/null || true
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
7. **Inject credentials** (optional): `huaweicloud_sandbox_credentials` — enables cloud API access from sandbox
|
|
101
|
+
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).
|
|
102
|
+
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.
|
|
103
|
+
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
|
|
91
104
|
|
|
92
105
|
## File Transfer (local → sandbox)
|
|
93
106
|
|
|
@@ -102,14 +115,27 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
|
|
|
102
115
|
"local_dir": "/path/to/local/project",
|
|
103
116
|
"remote_dir": "/workspace",
|
|
104
117
|
"extract": true,
|
|
105
|
-
"exclude": [
|
|
118
|
+
"exclude": [
|
|
119
|
+
"**/node_modules",
|
|
120
|
+
".git",
|
|
121
|
+
"__pycache__",
|
|
122
|
+
".next",
|
|
123
|
+
".nuxt",
|
|
124
|
+
".output",
|
|
125
|
+
".turbo",
|
|
126
|
+
".cache",
|
|
127
|
+
".swc",
|
|
128
|
+
"dist",
|
|
129
|
+
"coverage",
|
|
130
|
+
"*.pyc"
|
|
131
|
+
]
|
|
106
132
|
}
|
|
107
133
|
```
|
|
108
134
|
|
|
109
135
|
- `local_dir` (required): local project directory
|
|
110
136
|
- `remote_dir` (optional, default `/workspace`): parent directory on sandbox
|
|
111
137
|
- `extract` (optional, default `true`): extract tar.gz after upload
|
|
112
|
-
- `exclude` (optional): patterns to exclude from archive
|
|
138
|
+
- `exclude` (optional): patterns to exclude from archive. **For web apps, always exclude dependency directories** (`node_modules`, `.next`, `.nuxt`, `.output`, `.turbo`, `.cache`) — these will be re-installed/built in the sandbox.
|
|
113
139
|
- Result includes `md5` and `md5Verified` for integrity check
|
|
114
140
|
|
|
115
141
|
### upload_file (for single files)
|
|
@@ -170,11 +196,28 @@ devbridge auth login --huaweicloud --access-key "$AK" --secret-key "$SK"
|
|
|
170
196
|
**Expose** (run the web server and the tunnel in the background, then read the URL from the log; the app lives in the workspace mount, e.g. `/workspace/<repo-name>`):
|
|
171
197
|
|
|
172
198
|
```bash
|
|
173
|
-
|
|
174
|
-
|
|
199
|
+
# 0. Pre-cleanup: remove stale tunnels to avoid quota exceeded
|
|
200
|
+
devbridge delete-all 2>/dev/null || true
|
|
201
|
+
|
|
202
|
+
# 1. Start tunnel
|
|
203
|
+
nohup devbridge host -p <port> -e 8 > /tmp/host.log 2>&1 &
|
|
175
204
|
sleep 10 && cat /tmp/host.log
|
|
176
205
|
```
|
|
177
206
|
|
|
207
|
+
**Quota recovery**: if the tunnel creation fails with `10006: quota exceeded`:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# Step A: List all tunnels (both active and stale)
|
|
211
|
+
devbridge ls --all
|
|
212
|
+
# Step B: Remove all stale tunnels
|
|
213
|
+
devbridge delete-all
|
|
214
|
+
# Step C: Retry tunnel creation
|
|
215
|
+
nohup devbridge host -p <port> -e 8 > /tmp/host.log 2>&1 &
|
|
216
|
+
sleep 10 && cat /tmp/host.log
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
This eliminates the most common deployment failure — historical tunnels from previous sessions accumulating past the max=10 quota.
|
|
220
|
+
|
|
178
221
|
- The public URL has the form `https://<id>-<port>.cn-north-4-bridge.myhuaweicloud.com` (from the `Tunnel URL:` line).
|
|
179
222
|
- **Return this URL to the developer as the deployment result link.** Keep the host process running (do not close the session before handing over the URL).
|
|
180
223
|
- Internal docs: https://huaweicloud.github.io/devspace-devbridge/
|
|
@@ -208,14 +251,80 @@ Follow the standard [Workflow](#workflow) steps 1-6 to connect to the sandbox, t
|
|
|
208
251
|
```json
|
|
209
252
|
{
|
|
210
253
|
"local_dir": "<projectPath>",
|
|
211
|
-
"remote_dir": "/workspace"
|
|
254
|
+
"remote_dir": "/workspace",
|
|
255
|
+
"exclude": [
|
|
256
|
+
"**/node_modules",
|
|
257
|
+
".git",
|
|
258
|
+
"__pycache__",
|
|
259
|
+
".next",
|
|
260
|
+
".nuxt",
|
|
261
|
+
".output",
|
|
262
|
+
".turbo",
|
|
263
|
+
".cache",
|
|
264
|
+
".swc",
|
|
265
|
+
"dist",
|
|
266
|
+
"coverage",
|
|
267
|
+
"*.pyc"
|
|
268
|
+
]
|
|
212
269
|
}
|
|
213
270
|
```
|
|
214
271
|
|
|
215
|
-
|
|
272
|
+
**Always exclude build artifacts and dependency directories** — they will be re-installed/built inside the sandbox:
|
|
273
|
+
|
|
274
|
+
| Pattern | Why excluded |
|
|
275
|
+
| ----------------- | ---------------------------------------------- |
|
|
276
|
+
| `**/node_modules` | Dependencies — reinstall in sandbox |
|
|
277
|
+
| `.git` | Version control — not needed for deployment |
|
|
278
|
+
| `__pycache__` | Python bytecode cache |
|
|
279
|
+
| `.next` | Next.js build output — rebuild in sandbox |
|
|
280
|
+
| `.nuxt` | Nuxt build cache — rebuild in sandbox |
|
|
281
|
+
| `.output` | Nuxt production output — rebuild in sandbox |
|
|
282
|
+
| `.turbo` | Turborepo cache — re-run in sandbox |
|
|
283
|
+
| `.cache` | Generic tool cache (Parcel, Storybook, etc.) |
|
|
284
|
+
| `.swc` | Taro/Webpack SWC cache — regenerate in sandbox |
|
|
285
|
+
| `dist` | Build output — rebuild in sandbox |
|
|
286
|
+
| `coverage` | Test coverage reports — not needed for deploy |
|
|
287
|
+
| `*.pyc` | Python compiled files |
|
|
288
|
+
|
|
289
|
+
**Post-upload permission fix**: after `upload_project` extracts the project, fix file permissions lost during transfer (native binaries from other platforms, .bin symlinks):
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# Fix executable permissions on node_modules/.bin (lost during cross-platform transfer)
|
|
293
|
+
chmod -R +x /workspace/<dirname>/node_modules/.bin 2>/dev/null || true
|
|
294
|
+
# Fix world-read on all files (sandbox default umask may restrict)
|
|
295
|
+
chmod -R o+rX /workspace/<dirname> 2>/dev/null || true
|
|
296
|
+
```
|
|
216
297
|
|
|
217
298
|
### Step 3: Sandbox Environment Readiness
|
|
218
299
|
|
|
300
|
+
Install OS-level dependencies **before** uploading the project (independent of project code, can run in parallel if desired).
|
|
301
|
+
|
|
302
|
+
#### 3a: Detect OS and package manager
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
source /etc/os-release 2>/dev/null
|
|
306
|
+
echo "OS_DETECTED=${ID:-unknown}|${ID_LIKE:-}"
|
|
307
|
+
if command -v apt-get >/dev/null 2>&1; then echo "PKG_MGR=apt"; elif command -v yum >/dev/null 2>&1; then echo "PKG_MGR=yum"; elif command -v dnf >/dev/null 2>&1; then echo "PKG_MGR=dnf"; elif command -v apk >/dev/null 2>&1; then echo "PKG_MGR=apk"; else echo "PKG_MGR=unknown"; fi
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Use the detected `PKG_MGR` for all package installations below.
|
|
311
|
+
|
|
312
|
+
#### 3b: Install nginx (before project upload)
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# Use the detected PKG_MGR from step 3a
|
|
316
|
+
case "$PKG_MGR" in
|
|
317
|
+
apt) sudo apt-get update -qq && sudo apt-get install -y -qq nginx ;;
|
|
318
|
+
yum) sudo yum install -y nginx ;;
|
|
319
|
+
dnf) sudo dnf install -y nginx ;;
|
|
320
|
+
esac
|
|
321
|
+
sudo nginx -t && echo "nginx: ready"
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
If nginx cannot be installed, skip to Python HTTP server fallback (see `references/nginx-templates.md`).
|
|
325
|
+
|
|
326
|
+
#### 3c: Verify remaining tools
|
|
327
|
+
|
|
219
328
|
Before installing project dependencies, verify the sandbox has the required runtime tools. Run this pre-flight check via `exec_one_shot`:
|
|
220
329
|
|
|
221
330
|
```bash
|
|
@@ -228,7 +337,6 @@ if command -v git >/dev/null 2>&1; then git --version; else echo "MISSING: git";
|
|
|
228
337
|
if command -v python3 >/dev/null 2>&1; then python3 --version; else echo "MISSING: python3"; fi
|
|
229
338
|
if command -v curl >/dev/null 2>&1; then curl --version | head -1; else echo "MISSING: curl"; fi
|
|
230
339
|
if command -v wget >/dev/null 2>&1; then wget --version | head -1; else echo "MISSING: wget"; fi
|
|
231
|
-
if dpkg -l build-essential >/dev/null 2>&1; then echo "build-essential: found"; else echo "MISSING: build-essential"; fi
|
|
232
340
|
if command -v make >/dev/null 2>&1; then make --version | head -1; else echo "MISSING: make"; fi
|
|
233
341
|
|
|
234
342
|
# Framework-specific tools (install on demand)
|
|
@@ -239,41 +347,120 @@ if command -v hugo >/dev/null 2>&1; then hugo version; else echo "MISSING: hugo"
|
|
|
239
347
|
if command -v devbridge >/dev/null 2>&1; then devbridge version; else echo "MISSING: devbridge"; fi
|
|
240
348
|
```
|
|
241
349
|
|
|
242
|
-
**Install only missing tools** — parse the pre-flight output and install only tools reported as `MISSING`.
|
|
243
|
-
|
|
244
|
-
| Missing Tool | Install Command |
|
|
245
|
-
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
246
|
-
| Node.js | Follow [Node.js in the sandbox](#nodejs-in-the-sandbox) |
|
|
247
|
-
| nginx | `sudo apt-get update -qq && sudo apt-get install -y -qq nginx` |
|
|
248
|
-
| curl | `sudo apt-get update -qq && sudo apt-get install -y -qq curl` |
|
|
249
|
-
| wget | `sudo apt-get update -qq && sudo apt-get install -y -qq wget` |
|
|
250
|
-
| build-essential | `sudo apt-get update -qq && sudo apt-get install -y -qq build-essential` |
|
|
251
|
-
| make | `sudo apt-get update -qq && sudo apt-get install -y -qq make` |
|
|
252
|
-
| pnpm | `npm i -g pnpm` |
|
|
253
|
-
| yarn | `npm i -g yarn` |
|
|
254
|
-
| Hugo | `curl -fsSL https://github.com/gohugoio/hugo/releases/download/v0.140.0/hugo_extended_0.140.0_linux-amd64.tar.gz -o /tmp/hugo.tar.gz && sudo tar -xzf /tmp/hugo.tar.gz -C /usr/local/bin hugo && rm /tmp/hugo.tar.gz` |
|
|
255
|
-
| DevBridge | `curl -fsSL https://res-hd.hc-cdn.cn/sharedata/hdspace/devbridge/install.sh \| bash && export PATH=$PATH:$HOME/.huawei/bin` |
|
|
350
|
+
**Install only missing tools** — parse the pre-flight output and install only tools reported as `MISSING`. Use OS-aware commands:
|
|
256
351
|
|
|
257
|
-
|
|
352
|
+
| Missing Tool | Install Command (apt) | Install Command (yum/dnf) |
|
|
353
|
+
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
|
|
354
|
+
| Node.js | Follow [Node.js in the sandbox](#nodejs-in-the-sandbox) | Same |
|
|
355
|
+
| nginx | `sudo apt-get update -qq && sudo apt-get install -y -qq nginx` | `sudo yum install -y nginx` |
|
|
356
|
+
| curl | `sudo apt-get update -qq && sudo apt-get install -y -qq curl` | `sudo yum install -y curl` |
|
|
357
|
+
| wget | `sudo apt-get update -qq && sudo apt-get install -y -qq wget` | `sudo yum install -y wget` |
|
|
358
|
+
| make | `sudo apt-get update -qq && sudo apt-get install -y -qq make` | `sudo yum install -y make` |
|
|
359
|
+
| pnpm | `npm i -g pnpm` | Same |
|
|
360
|
+
| yarn | `npm i -g yarn` | Same |
|
|
361
|
+
| Hugo | `curl -fsSL https://github.com/gohugoio/hugo/releases/download/v0.140.0/hugo_extended_0.140.0_linux-amd64.tar.gz -o /tmp/hugo.tar.gz && sudo tar -xzf /tmp/hugo.tar.gz -C /usr/local/bin hugo && rm /tmp/hugo.tar.gz` | Same |
|
|
362
|
+
| DevBridge | `curl -fsSL https://res-hd.hc-cdn.cn/sharedata/hdspace/devbridge/install.sh \| bash && export PATH=$PATH:$HOME/.huawei/bin` | Same |
|
|
258
363
|
|
|
259
364
|
**If Node.js is missing**, install it first — all build workflows depend on it. Stop and report to the developer if Node.js installation fails.
|
|
260
365
|
|
|
261
366
|
### Step 4: Install and Build
|
|
262
367
|
|
|
263
|
-
|
|
368
|
+
#### 4a: Inject Environment Variables
|
|
264
369
|
|
|
265
|
-
|
|
370
|
+
Before any project commands, parse `.env*` files and inject them into the shell environment. Prisma, Drizzle, and other ORM/database tools do NOT auto-read framework-level env files:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
cd /workspace/<dirname>
|
|
374
|
+
# Load env files if present (most specific first)
|
|
375
|
+
for f in .env.local .env.development.local .env.development .env; do
|
|
376
|
+
if [ -f "$f" ]; then
|
|
377
|
+
set -a && source "$f" 2>/dev/null; set +a
|
|
378
|
+
echo "Loaded env: $f"
|
|
379
|
+
fi
|
|
380
|
+
done
|
|
381
|
+
# Verify key variables for common tools
|
|
382
|
+
echo "DATABASE_URL=${DATABASE_URL:-<NOT SET>}"
|
|
383
|
+
echo "NODE_ENV=${NODE_ENV:-development}"
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
This must run via `exec_with_session` so the exported variables persist for subsequent build commands in the same session.
|
|
387
|
+
|
|
388
|
+
#### 4b: Install Dependencies
|
|
389
|
+
|
|
390
|
+
Use `exec_one_shot` for install (no shared state needed). Skip if `node_modules` already exists:
|
|
266
391
|
|
|
267
392
|
```bash
|
|
268
393
|
cd /workspace/<dirname> && [ -d node_modules ] && echo "SKIP: node_modules exists" || <installCmd>
|
|
269
394
|
```
|
|
270
395
|
|
|
271
|
-
Wait for install to complete,
|
|
396
|
+
Wait for install to complete. For large projects on aarch64 sandboxes (1000+ packages), set `timeout_ms` to 180000 (3 min).
|
|
397
|
+
|
|
398
|
+
#### 4c: Build
|
|
399
|
+
|
|
400
|
+
**Timeout strategy by framework type:**
|
|
401
|
+
|
|
402
|
+
| Type | timeout_ms | Rationale |
|
|
403
|
+
| ------------------------------ | --------------- | ------------------------------------- |
|
|
404
|
+
| SPA / SSG | 300000 (5 min) | Vite/Webpack builds typically < 3 min |
|
|
405
|
+
| Cross-platform (Taro, uni-app) | 900000 (15 min) | Webpack5 H5 slow on aarch64, 7-8 min |
|
|
406
|
+
| SSR (Next.js, Nuxt) | 600000 (10 min) | Full-stack compilation + SSG pages |
|
|
407
|
+
| Monorepo | 600000 (10 min) | Multiple apps, shared packages |
|
|
408
|
+
| `null` (no build) | N/A | Skip |
|
|
409
|
+
|
|
410
|
+
**Build with `exec_one_shot`:**
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
cd /workspace/<dirname> && [ -d <outputDir> ] && echo "SKIP: <outputDir> exists" || (umask 022 && <buildCmd> 2>&1 | tee /tmp/build.log)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Always pipe build output through `tee /tmp/build.log` — captures stderr+stdout so diagnostics are available even if the command times out.
|
|
272
417
|
|
|
273
|
-
**
|
|
418
|
+
**OutDir verification**: before building for the first time, check the project's actual output directory (not just the default from framework detection). Projects can override outDir in config (e.g., VitePress `outDir: '../dist'`):
|
|
274
419
|
|
|
275
420
|
```bash
|
|
276
|
-
|
|
421
|
+
# Check for custom outDir in common config files
|
|
422
|
+
grep -r "outDir\|outputDir\|dest\|distDir" /workspace/<dirname>/.vitepress/config.* 2>/dev/null || true
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
If a custom outDir is found, use that instead of the framework-detected default for all subsequent checks.
|
|
426
|
+
|
|
427
|
+
**Post-timeout recovery**: if `exec_one_shot` returns a timeout error (Request timed out), do NOT fail immediately. First dump any captured build log, then check the output directory:
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
# If timeout occurred, show captured output and verify build
|
|
431
|
+
if timeout_error; then
|
|
432
|
+
echo "=== Build log (tail) ==="
|
|
433
|
+
tail -30 /tmp/build.log 2>/dev/null
|
|
434
|
+
echo "=== Checking output ==="
|
|
435
|
+
if [ -d <outputDir> ] && [ "$(ls -A <outputDir> 2>/dev/null)" ]; then
|
|
436
|
+
# Verify at least one key output file exists (not just empty dir from broken build)
|
|
437
|
+
if [ -f <outputDir>/index.html ] || [ -f <outputDir>/server.js ] || [ -f <outputDir>/app.js ]; then
|
|
438
|
+
echo "Build output detected despite timeout — continuing with deployment"
|
|
439
|
+
else
|
|
440
|
+
echo "ERROR: Output directory exists but missing expected files (index.html/server.js). Build may have failed silently."
|
|
441
|
+
echo "Full log: /tmp/build.log"
|
|
442
|
+
exit 1
|
|
443
|
+
fi
|
|
444
|
+
else
|
|
445
|
+
echo "ERROR: Build did not complete. Output directory empty or missing."
|
|
446
|
+
echo "Full log: /tmp/build.log"
|
|
447
|
+
exit 1
|
|
448
|
+
fi
|
|
449
|
+
fi
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
For SSR frameworks, also verify the server entry point exists: `test -f <outputDir>/server.js || test -f node_modules/next/dist/server/next-server.js`.
|
|
453
|
+
|
|
454
|
+
**Build progress visibility**: for very large builds, touch a marker file before starting and use `exec_with_session` to poll intermediate logs:
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
# Before build:
|
|
458
|
+
touch /tmp/build-start && echo "Build started at $(date)"
|
|
459
|
+
|
|
460
|
+
# During build via exec_with_session (separate call for polling):
|
|
461
|
+
cat .next/trace 2>/dev/null | tail -5 # Next.js build trace
|
|
462
|
+
# or
|
|
463
|
+
tail -5 /tmp/build.log 2>/dev/null
|
|
277
464
|
```
|
|
278
465
|
|
|
279
466
|
- `cd /workspace/<dirname>/<subAppPath>` for Monorepo sub-apps.
|
|
@@ -281,11 +468,55 @@ cd /workspace/<dirname> && [ -d <outputDir> ] && echo "SKIP: <outputDir> exists"
|
|
|
281
468
|
- For Hugo/static sites where `installCmd` is `null`, skip install entirely.
|
|
282
469
|
- For static sites where `buildCmd` is `null`, skip build entirely.
|
|
283
470
|
|
|
284
|
-
|
|
471
|
+
### Step 5: Port Availability Check
|
|
472
|
+
|
|
473
|
+
**Before configuring nginx or starting the app**, verify the target ports are free. Port conflicts from previous deployments cause silent failures:
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
# Check ports from framework detection
|
|
477
|
+
check_port() {
|
|
478
|
+
PORT=$1
|
|
479
|
+
# Prefer lsof (most portable), fallback to netstat, then ss
|
|
480
|
+
if command -v lsof >/dev/null 2>&1; then
|
|
481
|
+
PID=$(lsof -ti :$PORT 2>/dev/null)
|
|
482
|
+
if [ -n "$PID" ]; then
|
|
483
|
+
echo "PORT_IN_USE:$PORT (PID=$PID)"
|
|
484
|
+
kill -9 $PID 2>/dev/null && echo "Killed PID $PID on port $PORT"
|
|
485
|
+
else
|
|
486
|
+
echo "PORT_FREE:$PORT"
|
|
487
|
+
fi
|
|
488
|
+
elif command -v netstat >/dev/null 2>&1; then
|
|
489
|
+
PID=$(netstat -tlnp 2>/dev/null | grep ":$PORT " | awk '{print $NF}' | sed 's|/.*||')
|
|
490
|
+
if [ -n "$PID" ] && [ "$PID" != "-" ]; then
|
|
491
|
+
echo "PORT_IN_USE:$PORT (PID=$PID)"
|
|
492
|
+
kill -9 $PID 2>/dev/null && echo "Killed PID $PID on port $PORT"
|
|
493
|
+
else
|
|
494
|
+
echo "PORT_FREE:$PORT"
|
|
495
|
+
fi
|
|
496
|
+
else
|
|
497
|
+
# Last resort: ss (iproute2)
|
|
498
|
+
PID=$(ss -tlnp 2>/dev/null | grep ":$PORT " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
|
|
499
|
+
if [ -n "$PID" ]; then
|
|
500
|
+
echo "PORT_IN_USE:$PORT (PID=$PID)"
|
|
501
|
+
kill -9 $PID 2>/dev/null && echo "Killed PID $PID on port $PORT"
|
|
502
|
+
else
|
|
503
|
+
echo "PORT_FREE:$PORT"
|
|
504
|
+
fi
|
|
505
|
+
fi
|
|
506
|
+
}
|
|
507
|
+
check_port <port>
|
|
508
|
+
# For SSR, also check the Node port
|
|
509
|
+
check_port <nodePort>
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
| Scenario | Port | Action if occupied |
|
|
513
|
+
| ---------------------- | ------------------------------------ | -------------------------------------- |
|
|
514
|
+
| SPA/SSG/Cross-platform | nginx port (from `framework.detect`) | Kill old process, then configure nginx |
|
|
515
|
+
| SSR | nginx public port + Node app port | Kill old processes on both ports |
|
|
285
516
|
|
|
286
|
-
|
|
517
|
+
If the port cannot be freed (different user/process), increment to the next available port: `<port>+1`, update all subsequent nginx config and DevBridge references accordingly.
|
|
287
518
|
|
|
288
|
-
|
|
519
|
+
#### Configure Nginx
|
|
289
520
|
|
|
290
521
|
Check `references/nginx-templates.md` for the correct template based on `nginxType`:
|
|
291
522
|
|
|
@@ -311,7 +542,7 @@ If the status code is not 2xx/3xx:
|
|
|
311
542
|
- **000 (connection refused)** — nginx not listening: check `sudo nginx -t` for config errors
|
|
312
543
|
- **Other** — check nginx error log: `sudo tail -20 /var/log/nginx/error.log`
|
|
313
544
|
|
|
314
|
-
> If `curl` is unavailable, check port
|
|
545
|
+
> If `curl` is unavailable, check port via `lsof -i :<port>` or `netstat -tlnp | grep :<port>`
|
|
315
546
|
|
|
316
547
|
### Step 6: Start the App
|
|
317
548
|
|
|
@@ -324,9 +555,31 @@ Follow the standard [Expose the deployed app](#expose-the-deployed-app-public-ur
|
|
|
324
555
|
|
|
325
556
|
Use `exec_with_session` to background DevBridge. For SSR, DevBridge tunnels the nginx public port (not the Node port directly).
|
|
326
557
|
|
|
558
|
+
**Pre-flight**: always run `devbridge delete-all` before creating a new tunnel to prevent `10006: quota exceeded` from accumulated stale tunnels. If you still get quota error, list tunnels with `devbridge ls --all`, delete stale ones, and retry.
|
|
559
|
+
|
|
327
560
|
### Step 8: Return URL
|
|
328
561
|
|
|
329
|
-
Extract the tunnel URL from DevBridge output and return it to the developer.
|
|
562
|
+
Extract the tunnel URL from DevBridge output and return it to the developer.
|
|
563
|
+
|
|
564
|
+
**For cross-platform H5 apps** (Taro, uni-app), also generate a QR code for mobile scanning:
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
TUNNEL_URL="<extracted-tunnel-url>"
|
|
568
|
+
|
|
569
|
+
# Method 1 (preferred): PNG via curl API (works on all terminals)
|
|
570
|
+
curl -s "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "$TUNNEL_URL")" -o /workspace/qr.png
|
|
571
|
+
chmod o+r /workspace/qr.png
|
|
572
|
+
echo "QR code saved. Scan QR to access on mobile."
|
|
573
|
+
echo "Desktop URL: $TUNNEL_URL"
|
|
574
|
+
|
|
575
|
+
# Method 2 (fallback): terminal ANSI QR (requires qrencode, may not render on all terminals)
|
|
576
|
+
# apt-get install -y qrencode || yum install -y qrencode
|
|
577
|
+
# qrencode -t ANSI256 -m 1 -s 2 "$TUNNEL_URL"
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
If the sandbox cannot reach `api.qrserver.com`, fall back to installing `qrencode` for terminal QR output. Always `chmod o+r` the generated QR image file.
|
|
581
|
+
|
|
582
|
+
Return both the QR code and the URL to the developer. For cross-platform apps, mention: "手机扫描二维码即可访问".
|
|
330
583
|
|
|
331
584
|
## References
|
|
332
585
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: huawei-voucher
|
|
3
|
+
description: '查询和领取华为云激励代金券(一人一次)。当用户询问插件能力、首次使用,或提到"领券/代金券/优惠券/激励金/领取"等话题时,先查询领取状态:未领取(claimed=false)则主动提示可领取,已领取(claimed=true)则不打扰。Triggers: voucher, coupon, incentive, 领券, 代金券, 优惠券, 激励金, 领取. NOT for: 账单/费用查询(用 huawei-billing)。'
|
|
4
|
+
version: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 华为云激励代金券
|
|
8
|
+
|
|
9
|
+
## 主动提示准则(核心)
|
|
10
|
+
|
|
11
|
+
此能力仅在用户尚未领取时才有价值,需避免已领取用户被反复打扰:
|
|
12
|
+
|
|
13
|
+
1. 当用户**询问插件能力 / 首次使用 / 提到福利领取类话题**时,先调用 `huaweicloud_voucher_status` 查询领取状态。
|
|
14
|
+
2. 若 `claimed=false` → 主动告知"可领取华为云代金券(一人一次)",并询问是否现在领取。
|
|
15
|
+
3. 若 `claimed=true` → 静默处理,不再提及代金券,不打扰用户。
|
|
16
|
+
|
|
17
|
+
## 流程
|
|
18
|
+
|
|
19
|
+
### 1. 查询状态
|
|
20
|
+
|
|
21
|
+
调用 `huaweicloud_voucher_status`(生产环境无需 `domain_id`,测试环境需传 `domain_id`)。
|
|
22
|
+
|
|
23
|
+
返回 `{ claimed, message }`:
|
|
24
|
+
|
|
25
|
+
- `claimed=true` + `message="已领取"` → 已领取,告知结果即可,不再引导
|
|
26
|
+
- `claimed=false` + `message="未领取"` → 未领取,可引导领取
|
|
27
|
+
|
|
28
|
+
### 2. 领取代金券
|
|
29
|
+
|
|
30
|
+
调用 `huaweicloud_voucher_claim`(生产环境无需 `domain_id`)。
|
|
31
|
+
|
|
32
|
+
返回 `{ claimed, voucherId, amount, message }`。
|
|
33
|
+
|
|
34
|
+
### 3. 结果解读(错误码 UX)
|
|
35
|
+
|
|
36
|
+
| 返回 message 关键特征 | 含义 | 对用户表述 |
|
|
37
|
+
| -------------------------------------------- | --------------------------- | -------------------- |
|
|
38
|
+
| `领取成功` | 发券成功 | 告知券已到账 |
|
|
39
|
+
| `已领取过` / `已领取` | 该账号已领过 | 告知已领取,不打扰 |
|
|
40
|
+
| `本月代金券总额度已用完,所有账号均无法领取` | 全局额度用尽(HD.60630042) | 告知下月再试 |
|
|
41
|
+
| `请先完成实名认证` | 账号未实名(HD.60630022) | 引导去控制台实名认证 |
|
|
42
|
+
| `发券失败: <原文>` | 其他错误 | 原样转达,不展开 |
|
|
43
|
+
|
|
44
|
+
## 环境注意
|
|
45
|
+
|
|
46
|
+
- 生产环境:`voucher_status` / `voucher_claim` 无需传 `domain_id`,后端从 IAM 自动解析账号。
|
|
47
|
+
- 测试环境:需传 `domain_id`(华为云账号 ID);未传时返回 `测试环境需提供 domain_id`,此时可用 `hcloud IAM KeystoneListAuthDomains` 查询账号 ID 后补传。
|
|
48
|
+
|
|
49
|
+
## 注意事项
|
|
50
|
+
|
|
51
|
+
- 每个账号(domain_id)仅可领取一次,重复调用返回已领取。
|
|
52
|
+
- 领取结果以激励服务返回为准,不要凭本地认知猜测。
|
|
@@ -43,6 +43,7 @@ Do not rely on training data for facts. Huawei Cloud services, pricing, quotas,
|
|
|
43
43
|
| troubleshoot | Troubleshooting Routing | error, bug, failed, AccessDenied, quota | Handoff to huaweicloud-troubleshooting |
|
|
44
44
|
| api-sdk | API/SDK Routing | API call, SDK, REST, integration, code example | Handoff to huaweicloud-api-and-sdk |
|
|
45
45
|
| report-issue | Issue Reporting | wrong service, you picked wrong, incorrect | Read references/report-issue.md |
|
|
46
|
+
| voucher | Voucher Routing | 领券, 代金券, 优惠券, 激励金, 领取, voucher, coupon, incentive | Handoff to huawei-voucher |
|
|
46
47
|
|
|
47
48
|
## Service Map
|
|
48
49
|
|
|
@@ -70,6 +71,7 @@ Do not rely on training data for facts. Huawei Cloud services, pricing, quotas,
|
|
|
70
71
|
| CI/CD pipeline | CloudDeploy | huawei-deployment |
|
|
71
72
|
| Temporary runtime / web app preview | Sandbox (DevStation) | huawei-sandbox |
|
|
72
73
|
| Getting started | Account setup | huaweicloud-cli-and-auth |
|
|
74
|
+
| Incentive voucher | Voucher | huawei-voucher |
|
|
73
75
|
|
|
74
76
|
**Web app scenario layering**: for "deploy a web app", prefer `huawei-sandbox` for free/quick try (hello world, prototype, demo, temporary preview — temporary runtime + public URL, ~8h validity, zero billed resources); route to `huawei-functiongraph` / `huawei-ecs` (or `huawei-cce`) for production / long-term / custom-domain / high-availability hosting.
|
|
75
77
|
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { spawn, execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { createConnection as netConnect } from 'node:net';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
statSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
createReadStream,
|
|
11
|
+
appendFileSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
} from 'node:fs';
|
|
5
14
|
import { createHash, randomBytes } from 'node:crypto';
|
|
6
15
|
import { join, dirname, basename } from 'node:path';
|
|
7
16
|
import { tmpdir } from 'node:os';
|
|
@@ -269,6 +278,17 @@ function uploadLog(message) {
|
|
|
269
278
|
} catch {}
|
|
270
279
|
}
|
|
271
280
|
|
|
281
|
+
function rotateUploadLog(maxBytes = 100 * 1024) {
|
|
282
|
+
try {
|
|
283
|
+
if (existsSync(UPLOAD_LOG_PATH)) {
|
|
284
|
+
const stat = statSync(UPLOAD_LOG_PATH);
|
|
285
|
+
if (stat.size > maxBytes) {
|
|
286
|
+
unlinkSync(UPLOAD_LOG_PATH);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch {}
|
|
290
|
+
}
|
|
291
|
+
|
|
272
292
|
function generateUploadToken() {
|
|
273
293
|
return randomBytes(16).toString('hex');
|
|
274
294
|
}
|
|
@@ -557,6 +577,8 @@ export async function uploadProjectWithSession(
|
|
|
557
577
|
throw new Error(`sandbox upload project: path is not a directory: ${localDir}`);
|
|
558
578
|
}
|
|
559
579
|
|
|
580
|
+
rotateUploadLog();
|
|
581
|
+
|
|
560
582
|
const projectName = basename(localDir);
|
|
561
583
|
const targetParentDir = remoteDir || '/workspace';
|
|
562
584
|
const archiveRemotePath = `${targetParentDir}/${projectName}.tar.gz`;
|
|
@@ -567,6 +589,15 @@ export async function uploadProjectWithSession(
|
|
|
567
589
|
|
|
568
590
|
uploadLog(`uploadProject: ${localDir} -> ${archiveRemotePath} (archive=${archiveSize} bytes, md5=${expectedMd5})`);
|
|
569
591
|
|
|
592
|
+
const SIZE_50MB = 50 * 1024 * 1024;
|
|
593
|
+
if (archiveSize > SIZE_50MB) {
|
|
594
|
+
uploadLog(
|
|
595
|
+
`uploadProject: archive size ${(archiveSize / (1024 * 1024)).toFixed(1)}MB exceeds 50MB. ` +
|
|
596
|
+
`Dependencies or platform binaries may have been included. ` +
|
|
597
|
+
`Ensure exclude list contains "**/node_modules" to match all nesting levels.`,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
570
601
|
let result;
|
|
571
602
|
let tunnelError;
|
|
572
603
|
for (let attempt = 0; attempt < UPLOAD_MAX_RETRIES; attempt++) {
|
|
@@ -577,7 +608,8 @@ export async function uploadProjectWithSession(
|
|
|
577
608
|
break;
|
|
578
609
|
} catch (error) {
|
|
579
610
|
tunnelError = error;
|
|
580
|
-
|
|
611
|
+
const errorType = error.code || error.name || 'unknown';
|
|
612
|
+
uploadLog(`uploadProject: attempt ${attempt + 1}/${UPLOAD_MAX_RETRIES} failed [${errorType}]: ${error.message}`);
|
|
581
613
|
await new Promise((r) => setTimeout(r, 5000));
|
|
582
614
|
}
|
|
583
615
|
}
|
|
@@ -2035,28 +2035,99 @@ function removeHermesMcpConfigBlock() {
|
|
|
2035
2035
|
console.log(' MCP config cleaned');
|
|
2036
2036
|
}
|
|
2037
2037
|
|
|
2038
|
+
function ensureHermesHooksConfig() {
|
|
2039
|
+
const configPath = hermesConfigFile();
|
|
2040
|
+
const pluginDest = hermesPluginsDir();
|
|
2041
|
+
const hookScript = join(pluginDest, 'hooks', 'huaweicloud-safety.py').replace(/\\/g, '/');
|
|
2042
|
+
|
|
2043
|
+
const blockLines = [
|
|
2044
|
+
'hooks:',
|
|
2045
|
+
' pre_tool_call:',
|
|
2046
|
+
' - matcher: "terminal"',
|
|
2047
|
+
` command: "python3 ${hookScript}"`,
|
|
2048
|
+
' timeout: 5',
|
|
2049
|
+
];
|
|
2050
|
+
const block = blockLines.join('\n');
|
|
2051
|
+
|
|
2052
|
+
let existing = '';
|
|
2053
|
+
if (existsSync(configPath)) {
|
|
2054
|
+
try {
|
|
2055
|
+
existing = readFileSync(configPath, 'utf8');
|
|
2056
|
+
} catch {}
|
|
2057
|
+
if (existing.includes('hooks:') && existing.includes('huaweicloud-safety.py')) {
|
|
2058
|
+
console.log(` Hooks config unchanged: ${configPath}`);
|
|
2059
|
+
return false;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
2063
|
+
const newContent = existing ? `${existing.trimEnd()}\n\n${block}\n` : `${block}\n`;
|
|
2064
|
+
writeFileSync(configPath, newContent);
|
|
2065
|
+
console.log(` Hooks config updated: ${configPath}`);
|
|
2066
|
+
return true;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function removeHermesHooksConfigBlock() {
|
|
2070
|
+
const configPath = hermesConfigFile();
|
|
2071
|
+
if (!existsSync(configPath)) return;
|
|
2072
|
+
const lines = readFileSync(configPath, 'utf8').split(/\r?\n/);
|
|
2073
|
+
const out = [];
|
|
2074
|
+
let skip = false;
|
|
2075
|
+
for (const line of lines) {
|
|
2076
|
+
if (/^\s*huaweicloud-safety\.py/.test(line.trim())) {
|
|
2077
|
+
skip = true;
|
|
2078
|
+
continue;
|
|
2079
|
+
}
|
|
2080
|
+
if (skip) {
|
|
2081
|
+
if (line.trim() === '') continue;
|
|
2082
|
+
if (/^\s{2,}/.test(line) && line.trim() !== '') continue;
|
|
2083
|
+
skip = false;
|
|
2084
|
+
}
|
|
2085
|
+
if (line.trim() === 'hooks:') {
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
if (line.trim() === 'pre_tool_call:') {
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
if (/^\s*- matcher:\s*"terminal"/.test(line)) {
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
out.push(line);
|
|
2095
|
+
}
|
|
2096
|
+
const cleaned = out.join('\n').trimEnd();
|
|
2097
|
+
writeFileSync(configPath, cleaned ? `${cleaned}\n` : '');
|
|
2098
|
+
console.log(' Hooks config cleaned');
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2038
2101
|
async function installHermes() {
|
|
2039
2102
|
const skillsSrc = join(PLUGIN_ROOT, 'skills');
|
|
2040
2103
|
const srcDir = join(PLUGIN_ROOT, 'src');
|
|
2041
2104
|
const safetyDir = join(PLUGIN_ROOT, 'safety');
|
|
2105
|
+
const hooksDir = join(PLUGIN_ROOT, 'hooks');
|
|
2042
2106
|
const pluginDest = hermesPluginsDir();
|
|
2107
|
+
const skipMcp = process.argv.includes('--skip-mcp-server');
|
|
2043
2108
|
|
|
2044
2109
|
copyDir(skillsSrc, hermesSkillsDir());
|
|
2045
2110
|
console.log(` Skills -> ${hermesSkillsDir()}`);
|
|
2046
2111
|
|
|
2047
|
-
|
|
2048
|
-
|
|
2112
|
+
if (!skipMcp) {
|
|
2113
|
+
copyDir(srcDir, join(pluginDest, 'src'));
|
|
2114
|
+
console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
|
|
2115
|
+
}
|
|
2049
2116
|
copyDir(safetyDir, join(pluginDest, 'safety'));
|
|
2050
2117
|
console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
|
|
2118
|
+
copyDir(hooksDir, join(pluginDest, 'hooks'));
|
|
2119
|
+
console.log(` Safety Hooks -> ${join(pluginDest, 'hooks')}`);
|
|
2051
2120
|
|
|
2052
|
-
ensureHermesMcpConfig();
|
|
2053
|
-
|
|
2121
|
+
if (!skipMcp) ensureHermesMcpConfig();
|
|
2122
|
+
ensureHermesHooksConfig();
|
|
2123
|
+
if (!skipMcp) installRuntimeDeps(pluginDest);
|
|
2054
2124
|
}
|
|
2055
2125
|
|
|
2056
2126
|
async function updateHermes() {
|
|
2057
2127
|
const skillsSrc = join(PLUGIN_ROOT, 'skills');
|
|
2058
2128
|
const srcDir = join(PLUGIN_ROOT, 'src');
|
|
2059
2129
|
const safetyDir = join(PLUGIN_ROOT, 'safety');
|
|
2130
|
+
const hooksDir = join(PLUGIN_ROOT, 'hooks');
|
|
2060
2131
|
const pluginDest = hermesPluginsDir();
|
|
2061
2132
|
|
|
2062
2133
|
copyDir(skillsSrc, hermesSkillsDir());
|
|
@@ -2066,7 +2137,10 @@ async function updateHermes() {
|
|
|
2066
2137
|
console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
|
|
2067
2138
|
copyDir(safetyDir, join(pluginDest, 'safety'));
|
|
2068
2139
|
console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
|
|
2140
|
+
copyDir(hooksDir, join(pluginDest, 'hooks'));
|
|
2141
|
+
console.log(` Safety Hooks updated -> ${join(pluginDest, 'hooks')}`);
|
|
2069
2142
|
ensureHermesMcpConfig();
|
|
2143
|
+
ensureHermesHooksConfig();
|
|
2070
2144
|
mkdirSync(pluginDest, { recursive: true });
|
|
2071
2145
|
writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
|
|
2072
2146
|
installRuntimeDeps(pluginDest);
|
|
@@ -2092,9 +2166,10 @@ function uninstallHermes() {
|
|
|
2092
2166
|
}
|
|
2093
2167
|
|
|
2094
2168
|
if (removeIfExists(hermesPluginsDir())) {
|
|
2095
|
-
console.log(' Removed MCP server
|
|
2169
|
+
console.log(' Removed MCP server, safety policy and hooks');
|
|
2096
2170
|
}
|
|
2097
2171
|
removeHermesMcpConfigBlock();
|
|
2172
|
+
removeHermesHooksConfigBlock();
|
|
2098
2173
|
}
|
|
2099
2174
|
|
|
2100
2175
|
function hermesStatus() {
|
|
@@ -2106,6 +2181,9 @@ function hermesStatus() {
|
|
|
2106
2181
|
console.log(
|
|
2107
2182
|
` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
|
|
2108
2183
|
);
|
|
2184
|
+
console.log(
|
|
2185
|
+
` Safety Hooks: ${existsSync(join(pluginDir, 'hooks', 'huaweicloud-safety.py')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
|
|
2186
|
+
);
|
|
2109
2187
|
let skillCount = 0;
|
|
2110
2188
|
if (existsSync(skillsDir)) {
|
|
2111
2189
|
skillCount = readdirSync(skillsDir, { withFileTypes: true }).filter(
|
|
@@ -2119,13 +2197,17 @@ function hermesStatus() {
|
|
|
2119
2197
|
if (existsSync(configPath)) {
|
|
2120
2198
|
try {
|
|
2121
2199
|
const config = readFileSync(configPath, 'utf8');
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2200
|
+
const mcpConfigured = config.includes('mcp_servers:') && config.includes('huaweicloud-devkit');
|
|
2201
|
+
const hooksConfigured = config.includes('hooks:') && config.includes('huaweicloud-safety.py');
|
|
2202
|
+
console.log(` MCP config: ${mcpConfigured ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
|
|
2203
|
+
console.log(` Hooks config: ${hooksConfigured ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
|
|
2124
2204
|
} catch {
|
|
2125
2205
|
console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
|
|
2206
|
+
console.log(` Hooks config: \x1b[31mInvalid\x1b[0m`);
|
|
2126
2207
|
}
|
|
2127
2208
|
} else {
|
|
2128
2209
|
console.log(` MCP config: \x1b[31mNot found\x1b[0m`);
|
|
2210
|
+
console.log(` Hooks config: \x1b[31mNot found\x1b[0m`);
|
|
2129
2211
|
}
|
|
2130
2212
|
}
|
|
2131
2213
|
|
|
@@ -648,7 +648,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
648
648
|
},
|
|
649
649
|
{
|
|
650
650
|
name: 'huaweicloud_voucher_status',
|
|
651
|
-
description: '
|
|
651
|
+
description: '查询代金券领取状态。',
|
|
652
652
|
inputSchema: {
|
|
653
653
|
type: 'object',
|
|
654
654
|
properties: {
|
|
@@ -661,7 +661,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
661
661
|
},
|
|
662
662
|
{
|
|
663
663
|
name: 'huaweicloud_voucher_claim',
|
|
664
|
-
description: '
|
|
664
|
+
description: '领取代金券(一人一次)。重复领取会返回已领取。',
|
|
665
665
|
inputSchema: {
|
|
666
666
|
type: 'object',
|
|
667
667
|
properties: {
|
|
@@ -1190,6 +1190,11 @@ function serviceCatalog(intent = '') {
|
|
|
1190
1190
|
skills: ['huawei-dds-dcs'],
|
|
1191
1191
|
services: ['DDS', 'DCS'],
|
|
1192
1192
|
},
|
|
1193
|
+
{
|
|
1194
|
+
keywords: ['voucher', 'coupon', 'incentive', 'credit', '领券', '代金券', '优惠券', '激励金', '领取'],
|
|
1195
|
+
skills: ['huawei-voucher'],
|
|
1196
|
+
services: ['Incentive Voucher'],
|
|
1197
|
+
},
|
|
1193
1198
|
];
|
|
1194
1199
|
const matched = [];
|
|
1195
1200
|
const tokens = new Set(it.split(/[\s,./-]+/).filter((t) => t.length > 0));
|
|
@@ -38,15 +38,36 @@ function closeQuietly(ws) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function escapeRegExp(text) {
|
|
42
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
function stripEchoedLine(text, line, options = {}) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
if (text.startsWith(line)) {
|
|
43
|
+
text = text.slice(line.length);
|
|
44
|
+
text = text.replace(/^\r?\n/, '');
|
|
45
|
+
if (!options.allowAttached) return text;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const nl of ['\r\n', '\n']) {
|
|
49
|
+
const idx = text.indexOf(nl + line);
|
|
50
|
+
if (idx !== -1) {
|
|
51
|
+
const before = text.slice(0, idx + nl.length);
|
|
52
|
+
const after = text.slice(idx + nl.length + line.length);
|
|
53
|
+
text = before + after.replace(/^\r?\n/, '');
|
|
54
|
+
if (!options.allowAttached) return text;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.allowAttached) {
|
|
60
|
+
for (let trail = 2; trail >= 0; trail--) {
|
|
61
|
+
for (const nl of ['\r\n', '\n']) {
|
|
62
|
+
const suffix = nl.repeat(trail);
|
|
63
|
+
if (text.endsWith(line + suffix)) {
|
|
64
|
+
return text.slice(0, text.length - line.length - suffix.length);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return text;
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
function buildMarkers(nonce = randomBytes(8).toString('hex')) {
|