huaweicloud-devkit 1.1.0-next.4 → 1.1.0-next.6
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 +284 -49
- 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 -11
- package/plugins/huaweicloud-core/src/tools.mjs +15 -4
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.6",
|
|
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.6",
|
|
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.6",
|
|
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.6",
|
|
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.6",
|
|
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.6",
|
|
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.0-next.
|
|
5
|
+
"version": "1.1.0-next.6",
|
|
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.",
|
|
@@ -102,14 +102,27 @@ Setup is a **plugin-side preflight** — the developer should be asked a questio
|
|
|
102
102
|
"local_dir": "/path/to/local/project",
|
|
103
103
|
"remote_dir": "/workspace",
|
|
104
104
|
"extract": true,
|
|
105
|
-
"exclude": [
|
|
105
|
+
"exclude": [
|
|
106
|
+
"node_modules",
|
|
107
|
+
".git",
|
|
108
|
+
"__pycache__",
|
|
109
|
+
".next",
|
|
110
|
+
".nuxt",
|
|
111
|
+
".output",
|
|
112
|
+
".turbo",
|
|
113
|
+
".cache",
|
|
114
|
+
".swc",
|
|
115
|
+
"dist",
|
|
116
|
+
"coverage",
|
|
117
|
+
"*.pyc"
|
|
118
|
+
]
|
|
106
119
|
}
|
|
107
120
|
```
|
|
108
121
|
|
|
109
122
|
- `local_dir` (required): local project directory
|
|
110
123
|
- `remote_dir` (optional, default `/workspace`): parent directory on sandbox
|
|
111
124
|
- `extract` (optional, default `true`): extract tar.gz after upload
|
|
112
|
-
- `exclude` (optional): patterns to exclude from archive
|
|
125
|
+
- `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
126
|
- Result includes `md5` and `md5Verified` for integrity check
|
|
114
127
|
|
|
115
128
|
### upload_file (for single files)
|
|
@@ -170,11 +183,28 @@ devbridge auth login --huaweicloud --access-key "$AK" --secret-key "$SK"
|
|
|
170
183
|
**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
184
|
|
|
172
185
|
```bash
|
|
173
|
-
|
|
174
|
-
|
|
186
|
+
# 0. Pre-cleanup: remove stale tunnels to avoid quota exceeded
|
|
187
|
+
devbridge delete-all 2>/dev/null || true
|
|
188
|
+
|
|
189
|
+
# 1. Start tunnel
|
|
190
|
+
nohup devbridge host -p <port> -e 8 > /tmp/host.log 2>&1 &
|
|
175
191
|
sleep 10 && cat /tmp/host.log
|
|
176
192
|
```
|
|
177
193
|
|
|
194
|
+
**Quota recovery**: if the tunnel creation fails with `10006: quota exceeded`:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Step A: List all tunnels (both active and stale)
|
|
198
|
+
devbridge ls --all
|
|
199
|
+
# Step B: Remove all stale tunnels
|
|
200
|
+
devbridge delete-all
|
|
201
|
+
# Step C: Retry tunnel creation
|
|
202
|
+
nohup devbridge host -p <port> -e 8 > /tmp/host.log 2>&1 &
|
|
203
|
+
sleep 10 && cat /tmp/host.log
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
This eliminates the most common deployment failure — historical tunnels from previous sessions accumulating past the max=10 quota.
|
|
207
|
+
|
|
178
208
|
- The public URL has the form `https://<id>-<port>.cn-north-4-bridge.myhuaweicloud.com` (from the `Tunnel URL:` line).
|
|
179
209
|
- **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
210
|
- Internal docs: https://huaweicloud.github.io/devspace-devbridge/
|
|
@@ -208,14 +238,80 @@ Follow the standard [Workflow](#workflow) steps 1-6 to connect to the sandbox, t
|
|
|
208
238
|
```json
|
|
209
239
|
{
|
|
210
240
|
"local_dir": "<projectPath>",
|
|
211
|
-
"remote_dir": "/workspace"
|
|
241
|
+
"remote_dir": "/workspace",
|
|
242
|
+
"exclude": [
|
|
243
|
+
"node_modules",
|
|
244
|
+
".git",
|
|
245
|
+
"__pycache__",
|
|
246
|
+
".next",
|
|
247
|
+
".nuxt",
|
|
248
|
+
".output",
|
|
249
|
+
".turbo",
|
|
250
|
+
".cache",
|
|
251
|
+
".swc",
|
|
252
|
+
"dist",
|
|
253
|
+
"coverage",
|
|
254
|
+
"*.pyc"
|
|
255
|
+
]
|
|
212
256
|
}
|
|
213
257
|
```
|
|
214
258
|
|
|
215
|
-
|
|
259
|
+
**Always exclude build artifacts and dependency directories** — they will be re-installed/built inside the sandbox:
|
|
260
|
+
|
|
261
|
+
| Pattern | Why excluded |
|
|
262
|
+
| -------------- | ---------------------------------------------- |
|
|
263
|
+
| `node_modules` | Dependencies — reinstall in sandbox |
|
|
264
|
+
| `.git` | Version control — not needed for deployment |
|
|
265
|
+
| `__pycache__` | Python bytecode cache |
|
|
266
|
+
| `.next` | Next.js build output — rebuild in sandbox |
|
|
267
|
+
| `.nuxt` | Nuxt build cache — rebuild in sandbox |
|
|
268
|
+
| `.output` | Nuxt production output — rebuild in sandbox |
|
|
269
|
+
| `.turbo` | Turborepo cache — re-run in sandbox |
|
|
270
|
+
| `.cache` | Generic tool cache (Parcel, Storybook, etc.) |
|
|
271
|
+
| `.swc` | Taro/Webpack SWC cache — regenerate in sandbox |
|
|
272
|
+
| `dist` | Build output — rebuild in sandbox |
|
|
273
|
+
| `coverage` | Test coverage reports — not needed for deploy |
|
|
274
|
+
| `*.pyc` | Python compiled files |
|
|
275
|
+
|
|
276
|
+
**Post-upload permission fix**: after `upload_project` extracts the project, fix file permissions lost during transfer (native binaries from other platforms, .bin symlinks):
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# Fix executable permissions on node_modules/.bin (lost during cross-platform transfer)
|
|
280
|
+
chmod -R +x /workspace/<dirname>/node_modules/.bin 2>/dev/null || true
|
|
281
|
+
# Fix world-read on all files (sandbox default umask may restrict)
|
|
282
|
+
chmod -R o+rX /workspace/<dirname> 2>/dev/null || true
|
|
283
|
+
```
|
|
216
284
|
|
|
217
285
|
### Step 3: Sandbox Environment Readiness
|
|
218
286
|
|
|
287
|
+
Install OS-level dependencies **before** uploading the project (independent of project code, can run in parallel if desired).
|
|
288
|
+
|
|
289
|
+
#### 3a: Detect OS and package manager
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
source /etc/os-release 2>/dev/null
|
|
293
|
+
echo "OS_DETECTED=${ID:-unknown}|${ID_LIKE:-}"
|
|
294
|
+
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
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Use the detected `PKG_MGR` for all package installations below.
|
|
298
|
+
|
|
299
|
+
#### 3b: Install nginx (before project upload)
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
# Use the detected PKG_MGR from step 3a
|
|
303
|
+
case "$PKG_MGR" in
|
|
304
|
+
apt) sudo apt-get update -qq && sudo apt-get install -y -qq nginx ;;
|
|
305
|
+
yum) sudo yum install -y nginx ;;
|
|
306
|
+
dnf) sudo dnf install -y nginx ;;
|
|
307
|
+
esac
|
|
308
|
+
sudo nginx -t && echo "nginx: ready"
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
If nginx cannot be installed, skip to Python HTTP server fallback (see `references/nginx-templates.md`).
|
|
312
|
+
|
|
313
|
+
#### 3c: Verify remaining tools
|
|
314
|
+
|
|
219
315
|
Before installing project dependencies, verify the sandbox has the required runtime tools. Run this pre-flight check via `exec_one_shot`:
|
|
220
316
|
|
|
221
317
|
```bash
|
|
@@ -228,7 +324,6 @@ if command -v git >/dev/null 2>&1; then git --version; else echo "MISSING: git";
|
|
|
228
324
|
if command -v python3 >/dev/null 2>&1; then python3 --version; else echo "MISSING: python3"; fi
|
|
229
325
|
if command -v curl >/dev/null 2>&1; then curl --version | head -1; else echo "MISSING: curl"; fi
|
|
230
326
|
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
327
|
if command -v make >/dev/null 2>&1; then make --version | head -1; else echo "MISSING: make"; fi
|
|
233
328
|
|
|
234
329
|
# Framework-specific tools (install on demand)
|
|
@@ -239,41 +334,113 @@ if command -v hugo >/dev/null 2>&1; then hugo version; else echo "MISSING: hugo"
|
|
|
239
334
|
if command -v devbridge >/dev/null 2>&1; then devbridge version; else echo "MISSING: devbridge"; fi
|
|
240
335
|
```
|
|
241
336
|
|
|
242
|
-
**Install only missing tools** — parse the pre-flight output and install only tools reported as `MISSING`.
|
|
337
|
+
**Install only missing tools** — parse the pre-flight output and install only tools reported as `MISSING`. Use OS-aware commands:
|
|
243
338
|
|
|
244
|
-
| Missing Tool
|
|
245
|
-
|
|
|
246
|
-
| Node.js
|
|
247
|
-
| nginx
|
|
248
|
-
| curl
|
|
249
|
-
| wget
|
|
250
|
-
|
|
|
251
|
-
|
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
|
|
|
255
|
-
| DevBridge | `curl -fsSL https://res-hd.hc-cdn.cn/sharedata/hdspace/devbridge/install.sh \| bash && export PATH=$PATH:$HOME/.huawei/bin` |
|
|
256
|
-
|
|
257
|
-
**If nginx cannot be installed**, skip to Python HTTP server fallback (see `references/nginx-templates.md`).
|
|
339
|
+
| Missing Tool | Install Command (apt) | Install Command (yum/dnf) |
|
|
340
|
+
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
|
|
341
|
+
| Node.js | Follow [Node.js in the sandbox](#nodejs-in-the-sandbox) | Same |
|
|
342
|
+
| nginx | `sudo apt-get update -qq && sudo apt-get install -y -qq nginx` | `sudo yum install -y nginx` |
|
|
343
|
+
| curl | `sudo apt-get update -qq && sudo apt-get install -y -qq curl` | `sudo yum install -y curl` |
|
|
344
|
+
| wget | `sudo apt-get update -qq && sudo apt-get install -y -qq wget` | `sudo yum install -y wget` |
|
|
345
|
+
| make | `sudo apt-get update -qq && sudo apt-get install -y -qq make` | `sudo yum install -y make` |
|
|
346
|
+
| pnpm | `npm i -g pnpm` | Same |
|
|
347
|
+
| yarn | `npm i -g yarn` | Same |
|
|
348
|
+
| 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 |
|
|
349
|
+
| DevBridge | `curl -fsSL https://res-hd.hc-cdn.cn/sharedata/hdspace/devbridge/install.sh \| bash && export PATH=$PATH:$HOME/.huawei/bin` | Same |
|
|
258
350
|
|
|
259
351
|
**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
352
|
|
|
261
353
|
### Step 4: Install and Build
|
|
262
354
|
|
|
263
|
-
|
|
355
|
+
#### 4a: Inject Environment Variables
|
|
356
|
+
|
|
357
|
+
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:
|
|
264
358
|
|
|
265
|
-
|
|
359
|
+
```bash
|
|
360
|
+
cd /workspace/<dirname>
|
|
361
|
+
# Load env files if present (most specific first)
|
|
362
|
+
for f in .env.local .env.development.local .env.development .env; do
|
|
363
|
+
if [ -f "$f" ]; then
|
|
364
|
+
set -a && source "$f" 2>/dev/null; set +a
|
|
365
|
+
echo "Loaded env: $f"
|
|
366
|
+
fi
|
|
367
|
+
done
|
|
368
|
+
# Verify key variables for common tools
|
|
369
|
+
echo "DATABASE_URL=${DATABASE_URL:-<NOT SET>}"
|
|
370
|
+
echo "NODE_ENV=${NODE_ENV:-development}"
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
This must run via `exec_with_session` so the exported variables persist for subsequent build commands in the same session.
|
|
374
|
+
|
|
375
|
+
#### 4b: Install Dependencies
|
|
376
|
+
|
|
377
|
+
Use `exec_one_shot` for install (no shared state needed). Skip if `node_modules` already exists:
|
|
266
378
|
|
|
267
379
|
```bash
|
|
268
380
|
cd /workspace/<dirname> && [ -d node_modules ] && echo "SKIP: node_modules exists" || <installCmd>
|
|
269
381
|
```
|
|
270
382
|
|
|
271
|
-
Wait for install to complete,
|
|
383
|
+
Wait for install to complete. For large projects on aarch64 sandboxes (1000+ packages), set `timeout_ms` to 180000 (3 min).
|
|
384
|
+
|
|
385
|
+
#### 4c: Build
|
|
386
|
+
|
|
387
|
+
**Timeout strategy by framework type:**
|
|
388
|
+
|
|
389
|
+
| Type | timeout_ms | Rationale |
|
|
390
|
+
| ------------------------------ | --------------- | ------------------------------------- |
|
|
391
|
+
| SPA / SSG | 300000 (5 min) | Vite/Webpack builds typically < 3 min |
|
|
392
|
+
| Cross-platform (Taro, uni-app) | 900000 (15 min) | Webpack5 H5 slow on aarch64, 7-8 min |
|
|
393
|
+
| SSR (Next.js, Nuxt) | 600000 (10 min) | Full-stack compilation + SSG pages |
|
|
394
|
+
| Monorepo | 600000 (10 min) | Multiple apps, shared packages |
|
|
395
|
+
| `null` (no build) | N/A | Skip |
|
|
272
396
|
|
|
273
|
-
**Build
|
|
397
|
+
**Build with `exec_one_shot`:**
|
|
274
398
|
|
|
275
399
|
```bash
|
|
276
|
-
cd /workspace/<dirname> && [ -d <outputDir> ] && echo "SKIP: <outputDir> exists" || (umask 022 && <buildCmd>)
|
|
400
|
+
cd /workspace/<dirname> && [ -d <outputDir> ] && echo "SKIP: <outputDir> exists" || (umask 022 && <buildCmd> 2>&1 | tee /tmp/build.log)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Always pipe build output through `tee /tmp/build.log` — captures stderr+stdout so diagnostics are available even if the command times out.
|
|
404
|
+
|
|
405
|
+
**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'`):
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
# Check for custom outDir in common config files
|
|
409
|
+
grep -r "outDir\|outputDir\|dest\|distDir" /workspace/<dirname>/.vitepress/config.* 2>/dev/null || true
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
If a custom outDir is found, use that instead of the framework-detected default for all subsequent checks.
|
|
413
|
+
|
|
414
|
+
**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:
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
# If timeout occurred, show captured output and verify build
|
|
418
|
+
if timeout_error; then
|
|
419
|
+
echo "=== Build log (tail) ==="
|
|
420
|
+
tail -30 /tmp/build.log 2>/dev/null
|
|
421
|
+
echo "=== Checking output ==="
|
|
422
|
+
if [ -d <outputDir> ] && [ "$(ls -A <outputDir> 2>/dev/null)" ]; then
|
|
423
|
+
echo "Build output detected despite timeout — continuing with deployment"
|
|
424
|
+
else
|
|
425
|
+
echo "ERROR: Build did not complete. Output directory empty or missing."
|
|
426
|
+
echo "Full log: /tmp/build.log"
|
|
427
|
+
exit 1
|
|
428
|
+
fi
|
|
429
|
+
fi
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
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`.
|
|
433
|
+
|
|
434
|
+
**Build progress visibility**: for very large builds, touch a marker file before starting and use `exec_with_session` to poll intermediate logs:
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
# Before build:
|
|
438
|
+
touch /tmp/build-start && echo "Build started at $(date)"
|
|
439
|
+
|
|
440
|
+
# During build via exec_with_session (separate call for polling):
|
|
441
|
+
cat .next/trace 2>/dev/null | tail -5 # Next.js build trace
|
|
442
|
+
# or
|
|
443
|
+
tail -5 /tmp/build.log 2>/dev/null
|
|
277
444
|
```
|
|
278
445
|
|
|
279
446
|
- `cd /workspace/<dirname>/<subAppPath>` for Monorepo sub-apps.
|
|
@@ -281,11 +448,55 @@ cd /workspace/<dirname> && [ -d <outputDir> ] && echo "SKIP: <outputDir> exists"
|
|
|
281
448
|
- For Hugo/static sites where `installCmd` is `null`, skip install entirely.
|
|
282
449
|
- For static sites where `buildCmd` is `null`, skip build entirely.
|
|
283
450
|
|
|
284
|
-
|
|
451
|
+
### Step 5: Port Availability Check
|
|
452
|
+
|
|
453
|
+
**Before configuring nginx or starting the app**, verify the target ports are free. Port conflicts from previous deployments cause silent failures:
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
# Check ports from framework detection
|
|
457
|
+
check_port() {
|
|
458
|
+
PORT=$1
|
|
459
|
+
# Prefer lsof (most portable), fallback to netstat, then ss
|
|
460
|
+
if command -v lsof >/dev/null 2>&1; then
|
|
461
|
+
PID=$(lsof -ti :$PORT 2>/dev/null)
|
|
462
|
+
if [ -n "$PID" ]; then
|
|
463
|
+
echo "PORT_IN_USE:$PORT (PID=$PID)"
|
|
464
|
+
kill -9 $PID 2>/dev/null && echo "Killed PID $PID on port $PORT"
|
|
465
|
+
else
|
|
466
|
+
echo "PORT_FREE:$PORT"
|
|
467
|
+
fi
|
|
468
|
+
elif command -v netstat >/dev/null 2>&1; then
|
|
469
|
+
PID=$(netstat -tlnp 2>/dev/null | grep ":$PORT " | awk '{print $NF}' | sed 's|/.*||')
|
|
470
|
+
if [ -n "$PID" ] && [ "$PID" != "-" ]; then
|
|
471
|
+
echo "PORT_IN_USE:$PORT (PID=$PID)"
|
|
472
|
+
kill -9 $PID 2>/dev/null && echo "Killed PID $PID on port $PORT"
|
|
473
|
+
else
|
|
474
|
+
echo "PORT_FREE:$PORT"
|
|
475
|
+
fi
|
|
476
|
+
else
|
|
477
|
+
# Last resort: ss (iproute2)
|
|
478
|
+
PID=$(ss -tlnp 2>/dev/null | grep ":$PORT " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
|
|
479
|
+
if [ -n "$PID" ]; then
|
|
480
|
+
echo "PORT_IN_USE:$PORT (PID=$PID)"
|
|
481
|
+
kill -9 $PID 2>/dev/null && echo "Killed PID $PID on port $PORT"
|
|
482
|
+
else
|
|
483
|
+
echo "PORT_FREE:$PORT"
|
|
484
|
+
fi
|
|
485
|
+
fi
|
|
486
|
+
}
|
|
487
|
+
check_port <port>
|
|
488
|
+
# For SSR, also check the Node port
|
|
489
|
+
check_port <nodePort>
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
| Scenario | Port | Action if occupied |
|
|
493
|
+
| ---------------------- | ------------------------------------ | -------------------------------------- |
|
|
494
|
+
| SPA/SSG/Cross-platform | nginx port (from `framework.detect`) | Kill old process, then configure nginx |
|
|
495
|
+
| SSR | nginx public port + Node app port | Kill old processes on both ports |
|
|
285
496
|
|
|
286
|
-
|
|
497
|
+
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
498
|
|
|
288
|
-
|
|
499
|
+
#### Configure Nginx
|
|
289
500
|
|
|
290
501
|
Check `references/nginx-templates.md` for the correct template based on `nginxType`:
|
|
291
502
|
|
|
@@ -311,7 +522,7 @@ If the status code is not 2xx/3xx:
|
|
|
311
522
|
- **000 (connection refused)** — nginx not listening: check `sudo nginx -t` for config errors
|
|
312
523
|
- **Other** — check nginx error log: `sudo tail -20 /var/log/nginx/error.log`
|
|
313
524
|
|
|
314
|
-
> If `curl` is unavailable, check port
|
|
525
|
+
> If `curl` is unavailable, check port via `lsof -i :<port>` or `netstat -tlnp | grep :<port>`
|
|
315
526
|
|
|
316
527
|
### Step 6: Start the App
|
|
317
528
|
|
|
@@ -324,9 +535,31 @@ Follow the standard [Expose the deployed app](#expose-the-deployed-app-public-ur
|
|
|
324
535
|
|
|
325
536
|
Use `exec_with_session` to background DevBridge. For SSR, DevBridge tunnels the nginx public port (not the Node port directly).
|
|
326
537
|
|
|
538
|
+
**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.
|
|
539
|
+
|
|
327
540
|
### Step 8: Return URL
|
|
328
541
|
|
|
329
|
-
Extract the tunnel URL from DevBridge output and return it to the developer.
|
|
542
|
+
Extract the tunnel URL from DevBridge output and return it to the developer.
|
|
543
|
+
|
|
544
|
+
**For cross-platform H5 apps** (Taro, uni-app), also generate a QR code for mobile scanning:
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
TUNNEL_URL="<extracted-tunnel-url>"
|
|
548
|
+
|
|
549
|
+
# Method 1 (preferred): PNG via curl API (works on all terminals)
|
|
550
|
+
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
|
|
551
|
+
chmod o+r /workspace/qr.png
|
|
552
|
+
echo "QR code saved. Scan QR to access on mobile."
|
|
553
|
+
echo "Desktop URL: $TUNNEL_URL"
|
|
554
|
+
|
|
555
|
+
# Method 2 (fallback): terminal ANSI QR (requires qrencode, may not render on all terminals)
|
|
556
|
+
# apt-get install -y qrencode || yum install -y qrencode
|
|
557
|
+
# qrencode -t ANSI256 -m 1 -s 2 "$TUNNEL_URL"
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
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.
|
|
561
|
+
|
|
562
|
+
Return both the QR code and the URL to the developer. For cross-platform apps, mention: "手机扫描二维码即可访问".
|
|
330
563
|
|
|
331
564
|
## References
|
|
332
565
|
|
|
@@ -335,23 +568,25 @@ Extract the tunnel URL from DevBridge output and return it to the developer. For
|
|
|
335
568
|
|
|
336
569
|
## Critical Warnings
|
|
337
570
|
|
|
338
|
-
| Trap | Why
|
|
339
|
-
| ------------------------------------ |
|
|
340
|
-
| Agreement required first | `sandbox_connect` fails if the agreement isn't signed; the `sandbox_check_user` preflight detects this, so surface it to the developer only when signing is needed
|
|
341
|
-
| Real-name required | `sandbox_connect` fails if `realnameVerified=false`; tell the developer once and stop, don't loop on connect
|
|
342
|
-
| Never expose tunnel details | Do not mention "DevBridge"/"tunnel"/"devbridge" to the developer — say "正在生成访问地址..." and hand over only the URL
|
|
343
|
-
| Login needs `--huaweicloud` | `devbridge auth login --access-key/--secret-key` without `--huaweicloud` falls back to interactive browser login, which fails in the sandbox
|
|
344
|
-
| CLI PATH | The installer only writes `~/.bashrc`; run `export PATH=$PATH:$HOME/.huawei/bin` in the session before using `devbridge`
|
|
345
|
-
| Never install tunnel tooling locally | If the sandbox cannot install it, report a generic error and stop — installing on the developer's machine defeats sandbox deployment
|
|
346
|
-
| Return the deployment URL | Always hand the public URL from the host log to the developer as the final result
|
|
347
|
-
| Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls
|
|
348
|
-
| Long commands prefer one-shot | `exec_one_shot` creates a fresh connection per call — more stable for builds, installs, and scripts >30s. See [Tool Selection Guide](#tool-selection-guide).
|
|
349
|
-
| Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy
|
|
350
|
-
| Workspace ID = dev_stage_id | Use `dev_stage_id` from `sandbox_connect` as `workspace_id` for terminal exec
|
|
351
|
-
| Projects live in `/workspace` | Clone/install project code under `/workspace/<repo-name>` (filesystem-root workspace mount, not `$HOME/workspace`), never in `/tmp` — ephemeral locations lose the project when the sandbox session restarts
|
|
352
|
-
| Upload project for local code | Use `sandbox_upload_project` to transfer local projects — packages as tar.gz, uploads via HTTP tunnel, extracts on sandbox. Much faster than base64 for multi-file projects
|
|
353
|
-
| Upload file for single files | Use `sandbox_upload_file` for individual files — base64 chunked, reliable for small files (<1MB)
|
|
354
|
-
| Node.js >= 22 required | Sandbox terminal uses built-in WebSocket (globalThis.WebSocket); if Node.js is missing, install it from the Huawei Cloud mirror (see "Node.js in the sandbox")
|
|
571
|
+
| Trap | Why |
|
|
572
|
+
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
573
|
+
| Agreement required first | `sandbox_connect` fails if the agreement isn't signed; the `sandbox_check_user` preflight detects this, so surface it to the developer only when signing is needed |
|
|
574
|
+
| Real-name required | `sandbox_connect` fails if `realnameVerified=false`; tell the developer once and stop, don't loop on connect |
|
|
575
|
+
| Never expose tunnel details | Do not mention "DevBridge"/"tunnel"/"devbridge" to the developer — say "正在生成访问地址..." and hand over only the URL |
|
|
576
|
+
| Login needs `--huaweicloud` | `devbridge auth login --access-key/--secret-key` without `--huaweicloud` falls back to interactive browser login, which fails in the sandbox |
|
|
577
|
+
| CLI PATH | The installer only writes `~/.bashrc`; run `export PATH=$PATH:$HOME/.huawei/bin` in the session before using `devbridge` |
|
|
578
|
+
| Never install tunnel tooling locally | If the sandbox cannot install it, report a generic error and stop — installing on the developer's machine defeats sandbox deployment |
|
|
579
|
+
| Return the deployment URL | Always hand the public URL from the host log to the developer as the final result |
|
|
580
|
+
| Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls |
|
|
581
|
+
| Long commands prefer one-shot | `exec_one_shot` creates a fresh connection per call — more stable for builds, installs, and scripts >30s. See [Tool Selection Guide](#tool-selection-guide). |
|
|
582
|
+
| Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy |
|
|
583
|
+
| Workspace ID = dev_stage_id | Use `dev_stage_id` from `sandbox_connect` as `workspace_id` for terminal exec |
|
|
584
|
+
| Projects live in `/workspace` | Clone/install project code under `/workspace/<repo-name>` (filesystem-root workspace mount, not `$HOME/workspace`), never in `/tmp` — ephemeral locations lose the project when the sandbox session restarts |
|
|
585
|
+
| Upload project for local code | Use `sandbox_upload_project` to transfer local projects — packages as tar.gz, uploads via HTTP tunnel, extracts on sandbox. Much faster than base64 for multi-file projects |
|
|
586
|
+
| Upload file for single files | Use `sandbox_upload_file` for individual files — base64 chunked, reliable for small files (<1MB) |
|
|
587
|
+
| Node.js >= 22 required | Sandbox terminal uses built-in WebSocket (globalThis.WebSocket); if Node.js is missing, install it from the Huawei Cloud mirror (see "Node.js in the sandbox") |
|
|
588
|
+
| Sandbox restart kills processes | After sandbox restarts, all user processes (nginx, Node.js, Python servers) are stopped. Re-run startup commands and verify ports are listening before proceeding. |
|
|
589
|
+
| Cross-platform binaries incompatible | The sandbox runs Linux. Native binaries built on Windows/macOS (e.g., Prisma client, `node_modules/.prisma/`, platform-specific native addons) will not execute. Always install and build dependencies inside the sandbox, not locally. |
|
|
355
590
|
|
|
356
591
|
## Node.js in the sandbox
|
|
357
592
|
|
|
@@ -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
|
|
|
@@ -155,7 +155,9 @@ export async function execWithSession(workspaceId, command, username, timeoutMs)
|
|
|
155
155
|
|
|
156
156
|
export const UPLOAD_CHUNK_SIZE = 30000;
|
|
157
157
|
|
|
158
|
-
export const UPLOAD_BATCH_SIZE =
|
|
158
|
+
export const UPLOAD_BATCH_SIZE = 2;
|
|
159
|
+
|
|
160
|
+
export const UPLOAD_MAX_RETRIES = 3;
|
|
159
161
|
|
|
160
162
|
export function splitBase64Chunks(base64, chunkSize = UPLOAD_CHUNK_SIZE) {
|
|
161
163
|
const chunks = [];
|
|
@@ -189,11 +191,21 @@ export async function uploadFileWithSession(workspaceId, localPath, remotePath,
|
|
|
189
191
|
const batchNum = Math.floor(batchStart / UPLOAD_BATCH_SIZE) + 1;
|
|
190
192
|
const totalBatches = Math.ceil(chunks.length / UPLOAD_BATCH_SIZE);
|
|
191
193
|
const cmd = `printf '%s' '${combinedChunk}' >> "${tmp}"`;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
194
|
+
|
|
195
|
+
let batchOk = false;
|
|
196
|
+
let lastError;
|
|
197
|
+
for (let retry = 0; retry < UPLOAD_MAX_RETRIES; retry++) {
|
|
198
|
+
const res = await execWithSession(workspaceId, cmd, username, timeoutMs);
|
|
199
|
+
if (res.exitCode === 0) {
|
|
200
|
+
batchOk = true;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
lastError = res.stdout || res.error || res.exitCode;
|
|
204
|
+
console.error(` upload retry ${retry + 1}/${UPLOAD_MAX_RETRIES} for batch ${batchNum}/${totalBatches}`);
|
|
205
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
206
|
+
}
|
|
207
|
+
if (!batchOk) {
|
|
208
|
+
throw new Error(`sandbox upload: failed writing batch ${batchNum}/${totalBatches}: ${lastError}`);
|
|
197
209
|
}
|
|
198
210
|
if (batchNum % 10 === 0 || batchNum === totalBatches) {
|
|
199
211
|
console.error(` upload progress: batch ${batchNum}/${totalBatches}`);
|
|
@@ -556,14 +568,25 @@ export async function uploadProjectWithSession(
|
|
|
556
568
|
uploadLog(`uploadProject: ${localDir} -> ${archiveRemotePath} (archive=${archiveSize} bytes, md5=${expectedMd5})`);
|
|
557
569
|
|
|
558
570
|
let result;
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
571
|
+
let tunnelError;
|
|
572
|
+
for (let attempt = 0; attempt < UPLOAD_MAX_RETRIES; attempt++) {
|
|
573
|
+
try {
|
|
574
|
+
await cleanupFileServer(workspaceId, username).catch(() => {});
|
|
575
|
+
result = await uploadViaHttpTunnel(workspaceId, archivePath, archiveRemotePath, username, timeoutMs, options);
|
|
576
|
+
tunnelError = null;
|
|
577
|
+
break;
|
|
578
|
+
} catch (error) {
|
|
579
|
+
tunnelError = error;
|
|
580
|
+
uploadLog(`uploadProject: attempt ${attempt + 1}/${UPLOAD_MAX_RETRIES} failed: ${error.message}`);
|
|
581
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (tunnelError) {
|
|
585
|
+
uploadLog(`uploadProject: all ${UPLOAD_MAX_RETRIES} attempts failed: ${tunnelError.message}`);
|
|
563
586
|
uploadLog(`uploadProject: NOT falling back to base64 (removed). Rethrowing with diagnostics.`);
|
|
564
587
|
cleanupLocalArchive(archivePath);
|
|
565
588
|
throw new Error(
|
|
566
|
-
`sandbox upload failed: HTTP tunnel could not transfer the project archive. ` +
|
|
589
|
+
`sandbox upload failed after ${UPLOAD_MAX_RETRIES} attempts: HTTP tunnel could not transfer the project archive. ` +
|
|
567
590
|
`Archive size: ${(archiveSize / 1024).toFixed(1)}KB. ` +
|
|
568
591
|
`Root cause: ${tunnelError.message}. ` +
|
|
569
592
|
`Diagnostic log: ${UPLOAD_LOG_PATH}`,
|
|
@@ -648,21 +648,27 @@ 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: {
|
|
655
|
-
domain_id: {
|
|
655
|
+
domain_id: {
|
|
656
|
+
type: 'string',
|
|
657
|
+
description: 'Optional. Leave empty in production — account is resolved from IAM automatically.',
|
|
658
|
+
},
|
|
656
659
|
},
|
|
657
660
|
},
|
|
658
661
|
},
|
|
659
662
|
{
|
|
660
663
|
name: 'huaweicloud_voucher_claim',
|
|
661
|
-
description: '
|
|
664
|
+
description: '领取代金券(一人一次)。重复领取会返回已领取。',
|
|
662
665
|
inputSchema: {
|
|
663
666
|
type: 'object',
|
|
664
667
|
properties: {
|
|
665
|
-
domain_id: {
|
|
668
|
+
domain_id: {
|
|
669
|
+
type: 'string',
|
|
670
|
+
description: 'Optional. Leave empty in production — account is resolved from IAM automatically.',
|
|
671
|
+
},
|
|
666
672
|
},
|
|
667
673
|
},
|
|
668
674
|
},
|
|
@@ -1184,6 +1190,11 @@ function serviceCatalog(intent = '') {
|
|
|
1184
1190
|
skills: ['huawei-dds-dcs'],
|
|
1185
1191
|
services: ['DDS', 'DCS'],
|
|
1186
1192
|
},
|
|
1193
|
+
{
|
|
1194
|
+
keywords: ['voucher', 'coupon', 'incentive', 'credit', '领券', '代金券', '优惠券', '激励金', '领取'],
|
|
1195
|
+
skills: ['huawei-voucher'],
|
|
1196
|
+
services: ['Incentive Voucher'],
|
|
1197
|
+
},
|
|
1187
1198
|
];
|
|
1188
1199
|
const matched = [];
|
|
1189
1200
|
const tokens = new Set(it.split(/[\s,./-]+/).filter((t) => t.length > 0));
|