huaweicloud-devkit 1.1.12-next.0 → 1.1.13-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +2 -2
- 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 +21 -3
- package/plugins/huaweicloud-core/skills/huawei-sandbox/references/fullstack-postgrest.md +178 -17
- package/plugins/huaweicloud-core/src/mcp-protocol.mjs +8 -1
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +3 -0
- package/plugins/huaweicloud-core/src/setup-cli.mjs +12 -5
- package/plugins/huaweicloud-core/src/telemetry/agent-detect.mjs +20 -15
- package/plugins/huaweicloud-core/src/telemetry/agent-registry.mjs +4 -0
- package/plugins/huaweicloud-core/src/telemetry/telemetry.mjs +105 -31
- package/plugins/huaweicloud-core/src/tools.mjs +12 -5
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/huaweicloud-devkit)
|
|
7
|
-
[](https://github.com/huaweicloud/huaweicloud-devkit)
|
|
8
8
|
|
|
9
9
|
**[中文](README.zh-CN.md) | English**
|
|
10
10
|
|
package/README.zh-CN.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/huaweicloud-devkit)
|
|
7
|
-
[](https://github.com/huaweicloud/huaweicloud-devkit)
|
|
8
8
|
|
|
9
9
|
**中文 | [English](README.md)**
|
|
10
10
|
|
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.
|
|
4
|
+
"version": "1.1.13-next.0",
|
|
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",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"postinstall": "node ./bin/dsh-postinstall.cjs"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"undici": "^8.
|
|
46
|
+
"undici": "^8.12.3"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@eslint/js": "^10.0.1",
|
|
@@ -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.
|
|
20
|
+
"version": "1.1.13-next.0",
|
|
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.
|
|
23
|
+
"version": "1.1.13-next.0",
|
|
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.
|
|
20
|
+
"version": "1.1.13-next.0",
|
|
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.
|
|
3
|
+
"version": "1.1.13-next.0",
|
|
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.
|
|
3
|
+
"version": "1.1.13-next.0",
|
|
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.
|
|
5
|
+
"version": "1.1.13-next.0",
|
|
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.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: huawei-sandbox
|
|
3
|
-
description: 'Use when creating, connecting, or managing Huawei Cloud Sandbox instances and workspace terminals, or when a task needs a temporary runtime to deploy, run, or preview a web application. Covers sandbox lifecycle (check-user, sign-agreement, connect, release), session-based terminal execution, and credential injection. Triggers on: sandbox, workspace, terminal, web app deployment, deploy web app, preview app, deploy github, github repo, deploy repo, website, static site, web application, hwlink, devstation, hdkitservice, remote exec. NOT for: ECS instances (use huawei-ecs), CCE clusters (use huawei-cce).'
|
|
3
|
+
description: 'Use when creating, connecting, or managing Huawei Cloud Sandbox instances and workspace terminals, or when a task needs a temporary runtime to deploy, run, or preview a web application, backend service, or full-stack workbench. Covers sandbox lifecycle (check-user, sign-agreement, connect, release), session-based terminal execution, and credential injection. Triggers on: sandbox, workspace, terminal, web app deployment, deploy web app, preview app, deploy github, github repo, deploy repo, website, static site, web application, backend service, 工作台, workbench, full-stack, 全栈, CRUD 应用, database-backed REST API, PostgREST, postgres, hwlink, devstation, hdkitservice, remote exec. NOT for: ECS instances (use huawei-ecs), CCE clusters (use huawei-cce).'
|
|
4
4
|
version: 1
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -27,6 +27,22 @@ Hard rules:
|
|
|
27
27
|
- Include the OBS domain reminder and the ECS EIP pairing reminder in the options as presented.
|
|
28
28
|
- After the choice: sandbox → continue with this skill; OBS → `huawei-obs`; ECS → `huawei-ecs` / `huawei-iac`; CCE → `huawei-cce`.
|
|
29
29
|
|
|
30
|
+
## Stack-Selection Gate (MANDATORY — do not skip)
|
|
31
|
+
|
|
32
|
+
When the request involves a **workbench / 工作台 / 全栈 / full-stack project** — front-end plus back-end, CRUD with persistence, or a database-backed app — you MUST treat it as a **full-stack workbench deployment**: the back end defaults to **PostgreSQL + PostgREST** even when the developer never mentions 数据库/postgres/CRUD. Do NOT silently fall through to the SPA/nginx-only path.
|
|
33
|
+
|
|
34
|
+
Use the `question` tool to confirm the stack whenever the backend is ambiguous, presenting the PostgREST default first:
|
|
35
|
+
|
|
36
|
+
1. **PostgreSQL + PostgREST 全栈 (recommended)** — schema + read-only `api_user` role, nginx `/api/` reverse proxy; the REST layer needs zero application code
|
|
37
|
+
2. **应用自带后端** — the project ships its own server (Express/Next API/Nuxt server routes); deploy it inside the sandbox per its framework
|
|
38
|
+
3. **纯静态/SPA** — no persistence; nginx-only
|
|
39
|
+
|
|
40
|
+
Hard rules:
|
|
41
|
+
|
|
42
|
+
- **NEVER infer or default the stack to SPA** when persistence is implied: 工作台/workbench/全栈/full-stack/CRUD/数据库/持久化 ⇒ full-stack (PostgREST default) unless the developer explicitly picks otherwise.
|
|
43
|
+
- **Pre-code checkpoint (MANDATORY)**: before uploading or writing ANY application code, verify the backend choice matches this gate. If an SPA-only deployment already started and persistence turns out to be implied, STOP, tell the developer, and redirect to the full-stack path — do not paper over it with a bolted-on static backend.
|
|
44
|
+
- After the choice: PostgREST → follow [Full-Stack: REST API over PostgreSQL](#full-stack-rest-api-over-postgresql-postgrest) **FIRST** (backend before any frontend code), then continue with the web deployment steps; 自带后端 / 纯静态 → continue with [Web Application Deployment](#web-application-deployment).
|
|
45
|
+
|
|
30
46
|
## Overview
|
|
31
47
|
|
|
32
48
|
Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace terminal execution. Covers sandbox lifecycle via hdkitservice API and remote terminal command execution via hwlink protocol.
|
|
@@ -117,6 +133,8 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
117
133
|
|
|
118
134
|
**Step 0 — Target-Selection Gate (MANDATORY)**: before any sandbox lifecycle tool, confirm the target. If the developer asked to deploy/host/publish a site without naming a target, STOP and run the [Target-Selection Gate](#target-selection-gate-mandatory--do-not-skip) first; do NOT call `huaweicloud_sandbox_connect` until it is resolved. Proceed with this workflow only if the target is already confirmed as sandbox (or a temporary runtime).
|
|
119
135
|
|
|
136
|
+
**Step 0.5 — Stack-Selection Gate (MANDATORY)**: before uploading or writing any application code, confirm the backend stack via the [Stack-Selection Gate](#stack-selection-gate-mandatory--do-not-skip). 工作台/workbench/全栈/full-stack/CRUD/持久化 ⇒ full-stack (PostgreSQL + PostgREST default) — do NOT start an SPA-only deployment for a workbench request.
|
|
137
|
+
|
|
120
138
|
Setup is a **plugin-side preflight** — the developer should be asked a question only once, when the agreement actually needs signing:
|
|
121
139
|
|
|
122
140
|
1. **Check user** (transparent): `huaweicloud_sandbox_check_user` — returns `realnameVerified`/`agreementSigned` (200) when all good, OR throws a 403 error with one of these codes:
|
|
@@ -462,13 +480,13 @@ When deploying a **full-stack workbench project** (前后端工作台) or asking
|
|
|
462
480
|
3. **Nginx single-origin** — `/` serves the frontend, `/api/` reverse-proxies to PostgREST (no CORS, same origin). The frontend dir needs `chmod o+x` on the whole path chain or nginx returns 403.
|
|
463
481
|
4. **Expose + keep alive (automatic)** — `huaweicloud_sandbox_expose_tunnel` with the nginx port; it AUTO-enrolls the tunnel into the keepalive watchdog (merged into existing coverage). Prefer `expiry_hours: 720` for long-lived workbench deployments.
|
|
464
482
|
|
|
465
|
-
Complete measured procedure (
|
|
483
|
+
Complete measured procedure (full command chain) plus the 2026-10 field notes (Aliyun mirror URLs, cross-mirror resume corruption, `upload_file` 640 → PGRST002, exec-channel escaping → upload files and run `psql -f` / `curl -d @file`): see `references/fullstack-postgrest.md`. For the sandbox restart recovery checklist (what survives on `/workspace` vs what is lost on the 系统盘, plus the write-GRANT rollback trap): see the `Sandbox Restart Recovery Checklist` section in the same file.
|
|
466
484
|
|
|
467
485
|
## Web Application Deployment
|
|
468
486
|
|
|
469
487
|
When deploying a web application to the sandbox, build the app inside the sandbox before exposing it. Source code is uploaded, dependencies installed, and the framework built in the sandbox environment.
|
|
470
488
|
|
|
471
|
-
**Backend
|
|
489
|
+
**Backend selection (Stack-Selection Gate)**: the backend choice was already confirmed by the [Stack-Selection Gate](#stack-selection-gate-mandatory--do-not-skip) before any code was uploaded. If the project includes a PostgreSQL + PostgREST backend (or the developer asked for one), set up the backend FIRST by following [Full-Stack: REST API over PostgreSQL](#full-stack-rest-api-over-postgresql-postgrest) — install PostgreSQL/PostgREST, create the schema, and point the frontend's nginx `/api/` location at PostgREST — then continue with the steps below. Every tunnel exposed afterwards is auto-enrolled into the keepalive watchdog.
|
|
472
490
|
|
|
473
491
|
**沙箱无 systemd(实测 #935 报告3 问题3)**:沙箱为无 systemd 的容器环境,`systemctl` 全部失效(`System has not been booted with systemd`)—— 不要用 `systemctl start/reload/restart nginx`。Nginx 配置变更直接使用 `nginx -t` 验证 + `nginx -s reload` 热加载,或直接执行 nginx 二进制(`sudo nginx` 启动 / `nginx -s stop` 停止)。`huaweicloud_sandbox_deploy_nginx` 工具返回的 nextStep 文案与该语义一致(配置写入后自动回验存在性与 `nginx -t`,通过后即生效,无需 systemctl)。
|
|
474
492
|
|
|
@@ -25,10 +25,12 @@ least 14.0"). PGDG is the source for PG 16 — but HCE is NOT el8-lib-compatible
|
|
|
25
25
|
```bash
|
|
26
26
|
# PGDG repo RPM fails on HCE ("nothing provides /etc/redhat-release") — write the repo file by hand.
|
|
27
27
|
# The el8 path is rhel-8-aarch64 WITH A HYPHEN: rhel8-aarch64 (no hyphen) 404s.
|
|
28
|
+
# Use the Aliyun mirror: download.postgresql.org / repo.almalinux.org measure ~8KB/s from
|
|
29
|
+
# sandbox CN egress (a 6.8MB server RPM times out inside 240s); the mirror returns in seconds.
|
|
28
30
|
cat > /etc/yum.repos.d/pgdg16.repo << 'PGDG_EOF'
|
|
29
31
|
[pgdg16]
|
|
30
|
-
name=PostgreSQL 16 for RHEL8 aarch64
|
|
31
|
-
baseurl=https://
|
|
32
|
+
name=PostgreSQL 16 for RHEL8 aarch64 (Aliyun mirror)
|
|
33
|
+
baseurl=https://mirrors.aliyun.com/postgresql/repos/yum/16/redhat/rhel-8-aarch64/
|
|
32
34
|
enabled=1
|
|
33
35
|
gpgcheck=0
|
|
34
36
|
PGDG_EOF
|
|
@@ -39,10 +41,13 @@ differently (they want `libldap-2.5.so.0`). Install the el8 RPMs with
|
|
|
39
41
|
`--nodeps` and shim the sonames:
|
|
40
42
|
|
|
41
43
|
```bash
|
|
42
|
-
|
|
43
|
-
# Download with resume (--max-time + -C -)
|
|
44
|
-
#
|
|
45
|
-
#
|
|
44
|
+
# Aliyun mirror (measured 2026-10: seconds per RPM vs ~8KB/s timeouts on the official source).
|
|
45
|
+
# Download with resume (--max-time + -C -) WITHIN THE SAME MIRROR only: a partial file
|
|
46
|
+
# resumed from a DIFFERENT mirror fails rpm verification ("Payload SHA256 digest: BAD") —
|
|
47
|
+
# when switching sources, rm the partial file and re-download in full (see field notes).
|
|
48
|
+
# A corrupt RPM also ABORTS the whole transaction, silently skipping the other RPMs in the
|
|
49
|
+
# same rpm -ivh call. Install one RPM per call.
|
|
50
|
+
BASE="https://mirrors.aliyun.com/postgresql/repos/yum/16/redhat/rhel-8-aarch64"
|
|
46
51
|
curl -fsSL --http1.1 --max-time 240 -C - "$BASE/postgresql16-libs-16.15-1PGDG.rhel8.10.aarch64.rpm" -o /tmp/pg16-libs.rpm
|
|
47
52
|
rpm -ivh --nodeps /tmp/pg16-libs.rpm
|
|
48
53
|
curl -fsSL --http1.1 --max-time 240 -C - "$BASE/postgresql16-16.15-1PGDG.rhel8.10.aarch64.rpm" -o /tmp/pg16.rpm
|
|
@@ -56,9 +61,10 @@ Soname shims (measured 2026-10):
|
|
|
56
61
|
```bash
|
|
57
62
|
# ICU: versioned symbols (u_strToLower_60) make symlinks IMPOSSIBLE — a
|
|
58
63
|
# libicu.so.60 -> .69 symlink loads but crashes with "undefined symbol".
|
|
59
|
-
# Install the REAL el8 libicu60 (different sonames, coexists with HCE's icu69)
|
|
64
|
+
# Install the REAL el8 libicu60 (different sonames, coexists with HCE's icu69);
|
|
65
|
+
# AlmaLinux official repo is also ~8KB/s from the sandbox — use the Aliyun mirror:
|
|
60
66
|
curl -fsSL --http1.1 --max-time 240 -C - \
|
|
61
|
-
"https://
|
|
67
|
+
"https://mirrors.aliyun.com/almalinux/8/BaseOS/aarch64/os/Packages/libicu-60.3-2.el8_1.aarch64.rpm" -o /tmp/libicu60.rpm
|
|
62
68
|
rpm -ivh --nodeps --force /tmp/libicu60.rpm # --force: license file conflicts with icu69
|
|
63
69
|
|
|
64
70
|
# LDAP: postgres only uses it for LDAP auth (not needed here) — symlinks suffice.
|
|
@@ -72,12 +78,16 @@ ln -sf /usr/lib64/libreadline.so.8 /usr/lib64/libreadline.so.7
|
|
|
72
78
|
ldconfig # registers /usr/pgsql-16/lib (libpq.so.5)
|
|
73
79
|
```
|
|
74
80
|
|
|
75
|
-
Init and start (PG refuses root; no systemd on HCE)
|
|
81
|
+
Init and start (PG refuses root; no systemd on HCE). **The data directory
|
|
82
|
+
MUST live on the data disk** (`/workspace`), not `/var/lib/pgsql` —
|
|
83
|
+
`/var/lib/pgsql` is on the 系统盘 (system disk) and a sandbox reboot rolls it
|
|
84
|
+
back to the image state, destroying all databases. `/workspace` is the data
|
|
85
|
+
disk and survives reboots (#999):
|
|
76
86
|
|
|
77
87
|
```bash
|
|
78
|
-
mkdir -p /
|
|
79
|
-
su - postgres -c "/usr/pgsql-16/bin/initdb -D /
|
|
80
|
-
su - postgres -c "/usr/pgsql-16/bin/pg_ctl -D /
|
|
88
|
+
mkdir -p /workspace/pgsql/16/data && chown postgres:postgres /workspace/pgsql/16/data
|
|
89
|
+
su - postgres -c "/usr/pgsql-16/bin/initdb -D /workspace/pgsql/16/data"
|
|
90
|
+
su - postgres -c "/usr/pgsql-16/bin/pg_ctl -D /workspace/pgsql/16/data -l /workspace/pgsql/16/logfile start"
|
|
81
91
|
```
|
|
82
92
|
|
|
83
93
|
## Step 2: PostgREST (static build) + schema
|
|
@@ -87,8 +97,30 @@ static). **Dynamic Ubuntu builds do NOT work** — HCE glibc 2.34 is older than
|
|
|
87
97
|
Ubuntu 22.04's 2.35 and the binaries fail on load.
|
|
88
98
|
|
|
89
99
|
```bash
|
|
90
|
-
#
|
|
91
|
-
|
|
100
|
+
# Asset name has NO patch version: it is postgrest-v16.5-linux-static-aarch64.tar.xz
|
|
101
|
+
# (v16.4 likewise) — writing "v16.5.0" 404s and is easily misdiagnosed as a broken proxy.
|
|
102
|
+
# Verify the real asset name via the GitHub API before downloading:
|
|
103
|
+
curl -fsSL https://api.github.com/repos/PostgREST/postgrest/releases \
|
|
104
|
+
| grep -o '"name": *"postgrest-v16[^"]*aarch64[^"]*"'
|
|
105
|
+
# GitHub direct is slow from the sandbox; prefix proxies measure ~500KB/s (2026-10):
|
|
106
|
+
# https://ghfast.top/https://github.com/... or https://gh-proxy.com/https://github.com/...
|
|
107
|
+
# ⚠️ Those proxies are THIRD-PARTY: never extract their bytes unverified (supply-chain
|
|
108
|
+
# risk). Pull the official asset sha256 digest from the GitHub releases API, download
|
|
109
|
+
# through the proxy, and verify with sha256sum BEFORE extracting — this also catches
|
|
110
|
+
# partial downloads (a truncated tar.xz fails the check instead of a cryptic tar error).
|
|
111
|
+
PGRST_SHA256="$(curl -fsSL https://api.github.com/repos/PostgREST/postgrest/releases/tags/v16.5 \
|
|
112
|
+
| grep -A30 '"name": *"postgrest-v16.5-linux-static-aarch64.tar.xz"' \
|
|
113
|
+
| grep -o '"digest": *"sha256:[a-f0-9]*"' | head -1 \
|
|
114
|
+
| sed 's/.*sha256://; s/"$//')"
|
|
115
|
+
# ⚠️ The sed strips both the `sha256:` prefix and the closing quote.
|
|
116
|
+
# Using `cut` by delimiter instead would leave a trailing double-quote on the
|
|
117
|
+
# hash, making `sha256sum -c -` fail with "no properly formatted checksum
|
|
118
|
+
# lines found" even when the download is correct.
|
|
119
|
+
test -n "$PGRST_SHA256" # empty digest = API lookup failed; do NOT proceed unverified
|
|
120
|
+
curl -fsSL --http1.1 --max-time 240 -C - -o /tmp/pgrst.tar.xz \
|
|
121
|
+
"https://ghfast.top/https://github.com/PostgREST/postgrest/releases/download/v16.5/postgrest-v16.5-linux-static-aarch64.tar.xz"
|
|
122
|
+
echo "$PGRST_SHA256 /tmp/pgrst.tar.xz" | sha256sum -c - # must print ": OK" before tar
|
|
123
|
+
mkdir -p /opt/postgrest && tar -xJf /tmp/pgrst.tar.xz -C /opt/postgrest
|
|
92
124
|
|
|
93
125
|
cat > /opt/postgrest/pgrst.conf << 'PGRST_EOF'
|
|
94
126
|
db-uri = "postgres://api_user:<db-password>@localhost:5432/postgres"
|
|
@@ -97,8 +129,11 @@ db-anon-role = "api_user"
|
|
|
97
129
|
server-host = "127.0.0.1"
|
|
98
130
|
server-port = 3000
|
|
99
131
|
PGRST_EOF
|
|
132
|
+
# ⚠️ Via the MCP exec channel, do NOT build pgrst.conf / SQL inline (escaping breaks
|
|
133
|
+
# them — see field notes below): write the file locally and huaweicloud_sandbox_upload_file it.
|
|
100
134
|
|
|
101
|
-
# Schema + read-only API role (the anon role gates all writes)
|
|
135
|
+
# Schema + read-only API role (the anon role gates all writes).
|
|
136
|
+
# Same escaping trap: prefer a local schema.sql → upload_file → psql -f (field notes).
|
|
102
137
|
su - postgres -c "/usr/pgsql-16/bin/psql -c \"CREATE SCHEMA api; CREATE TABLE api.todos (id serial primary key, done boolean not null default false, task text not null); CREATE ROLE api_user LOGIN PASSWORD '<db-password>'; GRANT USAGE ON SCHEMA api TO api_user; GRANT SELECT ON ALL TABLES IN SCHEMA api TO api_user;\""
|
|
103
138
|
|
|
104
139
|
setsid nohup /opt/postgrest/postgrest /opt/postgrest/pgrst.conf > /tmp/pgrst.log 2>&1 < /dev/null &
|
|
@@ -145,5 +180,131 @@ devbridge host -p <port> -e 720
|
|
|
145
180
|
```
|
|
146
181
|
|
|
147
182
|
Verify end-to-end through the public URL: `GET /` (page, 200), `GET /api/todos`
|
|
148
|
-
(data, 200), `POST /api/todos` (401 — the anon role has SELECT
|
|
149
|
-
|
|
183
|
+
(data, 200), `POST /api/todos` (**401 by default** — the anon role has SELECT
|
|
184
|
+
only). To enable writes on one table, `GRANT INSERT ON api.todos TO api_user`
|
|
185
|
+
(never a broad grant to anon); after that targeted grant the same POST returns
|
|
186
|
+
**201**.
|
|
187
|
+
|
|
188
|
+
## Field notes (2026-10 retest): 5 measured traps
|
|
189
|
+
|
|
190
|
+
Re-measured 2026-10-23 on HCE 2.0 (aarch64, no systemd) with PG 16.15 +
|
|
191
|
+
PostgREST 16.5; all five traps reproduced and solved, end-to-end verified
|
|
192
|
+
(page 200 / API 200 / POST 401 anon-default, 201 after `GRANT INSERT` on
|
|
193
|
+
`api.todos`).
|
|
194
|
+
|
|
195
|
+
### 1. RPM sources: use the Aliyun mirror
|
|
196
|
+
|
|
197
|
+
`download.postgresql.org` and `repo.almalinux.org` measure **~8KB/s** from the
|
|
198
|
+
sandbox CN egress (the 6.8MB server RPM times out mid-transfer at 240s). The
|
|
199
|
+
Aliyun mirror completes in seconds:
|
|
200
|
+
|
|
201
|
+
- PG 16: `https://mirrors.aliyun.com/postgresql/repos/yum/16/redhat/rhel-8-aarch64/`
|
|
202
|
+
- libicu60: `https://mirrors.aliyun.com/almalinux/8/BaseOS/aarch64/os/Packages/`
|
|
203
|
+
- PostgREST: `ghfast.top` / `gh-proxy.com` prefix proxies (~500KB/s) — but see
|
|
204
|
+
trap 3 first: the asset name must be exact, and the proxy is third-party:
|
|
205
|
+
fetch the official GitHub asset digest and `sha256sum -c` the tarball before
|
|
206
|
+
extracting (see the Step 2 download block).
|
|
207
|
+
|
|
208
|
+
### 2. Cross-mirror resume corrupts RPMs
|
|
209
|
+
|
|
210
|
+
Resuming a partial download with `-C -` **from a different mirror** produces a
|
|
211
|
+
file that fails rpm verification (`Payload SHA256 digest: BAD`). When switching
|
|
212
|
+
sources, `rm` the partial file and re-download it in full — never resume across
|
|
213
|
+
mirrors.
|
|
214
|
+
|
|
215
|
+
### 3. PostgREST 404 is usually the asset name, not the proxy
|
|
216
|
+
|
|
217
|
+
The release asset is `postgrest-v16.5-linux-static-aarch64.tar.xz` — the version
|
|
218
|
+
has **no patch number** (v16.4 likewise); guessing `v16.5.0` returns 404 and
|
|
219
|
+
gets misdiagnosed as "proxy broken". Check the real name first:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
curl -fsSL https://api.github.com/repos/PostgREST/postgrest/releases \
|
|
223
|
+
| grep -o '"name": *"postgrest-v16[^"]*aarch64[^"]*"'
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### 4. `upload_file` lands as 640 → silent schema failure → PGRST002
|
|
227
|
+
|
|
228
|
+
Files uploaded by root via `huaweicloud_sandbox_upload_file` arrive as
|
|
229
|
+
`rw-r-----` (640). `su - postgres -c "psql -f /tmp/schema.sql"` then fails with
|
|
230
|
+
`Permission denied`, the schema/role are **silently not created**, and
|
|
231
|
+
PostgREST starts but keeps logging
|
|
232
|
+
`PGRST002: Could not query the database for the schema cache`.
|
|
233
|
+
|
|
234
|
+
Fix: `chmod o+r` the uploaded file before `psql -f`. When troubleshooting
|
|
235
|
+
PGRST002, first confirm the schema/role actually exist (`\dn`, `\du`) — the
|
|
236
|
+
`psql -f` error may have scrolled out of the output tail you looked at.
|
|
237
|
+
|
|
238
|
+
### 5. MCP exec channel mangles nested quotes — upload files instead
|
|
239
|
+
|
|
240
|
+
Nested quotes / inline JSON (pgrst.conf, `psql -c` SQL, `curl -d '{...}'`) get
|
|
241
|
+
corrupted by the exec channel's multi-layer escaping (symptoms: POST 400, SQL
|
|
242
|
+
written wrong). The heredoc/inline `printf` snippets above are for a direct
|
|
243
|
+
shell; over MCP, always use the file-upload pattern:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# locally: write schema.sql / pgrst.conf / payload.json, then upload_file them, then on the sandbox:
|
|
247
|
+
chmod o+r /tmp/schema.sql && su - postgres -c "/usr/pgsql-16/bin/psql -f /tmp/schema.sql"
|
|
248
|
+
curl -s -X POST -H "Content-Type: application/json" -d @/tmp/payload.json http://127.0.0.1:3000/todos
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Sandbox Restart Recovery Checklist (2026-10)
|
|
252
|
+
|
|
253
|
+
A sandbox reboot rolls the **系统盘 (system disk)** back to the image state —
|
|
254
|
+
everything installed under `/`, `/usr`, `/etc`, `$HOME` is lost. Only
|
|
255
|
+
`/workspace` (the data disk) survives. Below is the measured recovery
|
|
256
|
+
checklist, assuming PG data was initialized on `/workspace/pgsql/16/data` per
|
|
257
|
+
Step 1 above.
|
|
258
|
+
|
|
259
|
+
### Lost on reboot (系统盘 — must rebuild)
|
|
260
|
+
|
|
261
|
+
| Component | Location | Recovery action |
|
|
262
|
+
| ---------------------- | ------------------------- | -------------------------------------------------------------------------------- |
|
|
263
|
+
| PostgreSQL 16 RPMs | `/usr/pgsql-16/` | Re-run the Step 1 `rpm -ivh --nodeps` block |
|
|
264
|
+
| Soname shims | `/usr/lib64/lib*.so.*` | Re-run the Step 1 symlink + `ldconfig` block |
|
|
265
|
+
| `postgres` user | `/etc/passwd` entry | `useradd -r -m postgres` then `chown postgres:postgres /workspace/pgsql/16/data` |
|
|
266
|
+
| PostgREST binary | `/opt/postgrest/` | Re-download + sha256-verify + extract (Step 2) |
|
|
267
|
+
| pgrst.conf | `/opt/postgrest/` | Re-upload via `huaweicloud_sandbox_upload_file` |
|
|
268
|
+
| nginx config | `/etc/nginx/conf.d/` | Re-create via `huaweicloud_sandbox_deploy_nginx` |
|
|
269
|
+
| DevBridge 0.2.x binary | `~/.huawei/bin/` (系统盘) | Re-install to `/workspace/.local/bin` (see #999) |
|
|
270
|
+
| DevBridge tunnel | in-memory | Re-run `huaweicloud_sandbox_expose_tunnel` |
|
|
271
|
+
| `/tmp` credentials | `/tmp/hw_creds.sh` etc. | Re-run `huaweicloud_sandbox_credentials` |
|
|
272
|
+
|
|
273
|
+
### Survives reboot (/workspace — no rebuild needed)
|
|
274
|
+
|
|
275
|
+
| Component | Location | Notes |
|
|
276
|
+
| ---------------------- | -------------------------- | -------------------------------------------- |
|
|
277
|
+
| Frontend pages | `/workspace/<repo>/` | Uploaded project persists |
|
|
278
|
+
| PG data directory | `/workspace/pgsql/16/data` | Schema, roles, tables, seed data ALL survive |
|
|
279
|
+
| DevBridge 0.2.x binary | `/workspace/.local/bin/` | If installed per #999 guidance |
|
|
280
|
+
|
|
281
|
+
Because PG data lives on `/workspace`, a restart does **NOT** require rebuilding
|
|
282
|
+
the schema (`schema.sql`) or re-seeding data — `initdb` is also unnecessary.
|
|
283
|
+
Only the PG server RPMs and the `postgres` user need rebuilding; then
|
|
284
|
+
`pg_ctl -D /workspace/pgsql/16/data start` resumes the surviving cluster
|
|
285
|
+
immediately.
|
|
286
|
+
|
|
287
|
+
### Write-permission GRANT rollback trap (POST 401 → GRANT → 201)
|
|
288
|
+
|
|
289
|
+
The `postgres` system user and PG RPMs live on the 系统盘, so after a reboot
|
|
290
|
+
you must recreate the user and re-install PG before `pg_ctl` can start the
|
|
291
|
+
surviving data directory. Once PG starts against the surviving
|
|
292
|
+
`/workspace/pgsql/16/data`, all roles and grants in the PG catalog are intact
|
|
293
|
+
— no re-grant is needed.
|
|
294
|
+
|
|
295
|
+
**However**, if you rebuild the schema from a `schema.sql` that only contains
|
|
296
|
+
`GRANT SELECT` (the read-only anon default), writes are blocked: `POST
|
|
297
|
+
/api/todos` returns **401**. The write grant (`GRANT INSERT, UPDATE, DELETE`)
|
|
298
|
+
was stored in the PG catalog; re-running a SELECT-only script overwrites it.
|
|
299
|
+
The measured fix (2026-10):
|
|
300
|
+
|
|
301
|
+
```sql
|
|
302
|
+
GRANT INSERT, UPDATE, DELETE ON api.todos TO api_user;
|
|
303
|
+
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA api TO api_user;
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
After this targeted re-grant the same `POST /api/todos` returns **201**
|
|
307
|
+
(measured 2026-10: POST 401 → `GRANT INSERT, UPDATE, DELETE` → POST 201).
|
|
308
|
+
This mirrors the Step 4 anon-role gating: `db-anon-role` is read-only by
|
|
309
|
+
default; targeted write grants must be re-applied after any schema rebuild.
|
|
310
|
+
Never grant broad write access to the anon role.
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { TOOL_DEFINITIONS, callTool } from './tools.mjs';
|
|
2
2
|
import { peekCachedUpdateInfo, applyUpdateHint, readInstalledVersion } from './update-check.mjs';
|
|
3
|
-
import { initTelemetry } from './telemetry/telemetry.mjs';
|
|
3
|
+
import { initTelemetry, flushEvents } from './telemetry/telemetry.mjs';
|
|
4
4
|
import { detectAgent } from './telemetry/agent-detect.mjs';
|
|
5
5
|
|
|
6
|
+
// #1014 P0: 领券类工具同步上报——callTool 完毕后 await flushEvents(),
|
|
7
|
+
// 防止进程提前退出导致领券遥测丢失。
|
|
8
|
+
const P0_SYNC_FLUSH_TOOLS = new Set(['huaweicloud_voucher_claim', 'huaweicloud_competition_voucher_claim']);
|
|
9
|
+
|
|
6
10
|
const pkgVersion = readInstalledVersion() || '0.0.0';
|
|
7
11
|
|
|
8
12
|
// 会话内首个非 check/upgrade 工具调用附加 _updateInfo,只消费一次。
|
|
@@ -78,6 +82,9 @@ export async function dispatch(method, params, opts = {}) {
|
|
|
78
82
|
throw invalidParamsError;
|
|
79
83
|
}
|
|
80
84
|
const result = await callTool(params.name, params.arguments || {});
|
|
85
|
+
if (P0_SYNC_FLUSH_TOOLS.has(params.name)) {
|
|
86
|
+
await flushEvents().catch(() => {});
|
|
87
|
+
}
|
|
81
88
|
const decorated = _decorateResult(sessionId, params.name, result);
|
|
82
89
|
return {
|
|
83
90
|
content: [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getCredentials } from './hwlink-api.mjs';
|
|
2
2
|
import { getProxyDispatcher } from '../proxy/proxy-agent.mjs';
|
|
3
3
|
import { cacheUserHash } from '../telemetry/telemetry.mjs';
|
|
4
|
+
import { detectAgent } from '../telemetry/agent-detect.mjs';
|
|
4
5
|
import { readInstalledVersion } from '../update-check.mjs';
|
|
5
6
|
|
|
6
7
|
function getHdkitBaseUrl() {
|
|
@@ -21,6 +22,8 @@ async function hdkitRequest(method, path, body, timeoutMs = 300000) {
|
|
|
21
22
|
'X-HW-AK': ak,
|
|
22
23
|
'X-HW-SK': sk,
|
|
23
24
|
'X-HW-Client-Version': readInstalledVersion() || '0.0.0',
|
|
25
|
+
// #1040: 对外请求统一携带 X-HW-Agent(值=detectAgent 结果 harness)
|
|
26
|
+
'X-HW-Agent': detectAgent().harness,
|
|
24
27
|
};
|
|
25
28
|
if (securitytoken) {
|
|
26
29
|
headers['X-HW-Security-Token'] = securitytoken;
|
|
@@ -75,6 +75,13 @@ function configRoot(target = 'opencode') {
|
|
|
75
75
|
return join(home, '.config', target);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// Must match telemetry.mjs GLOBAL_TELEMETRY_DIR root: HUAWEICLOUD_DEVKIT_HOME
|
|
79
|
+
// overrides homedir() so sandbox/multi-HOME environments stay consistent (#1014).
|
|
80
|
+
function devkitTelemetryDir() {
|
|
81
|
+
const home = (process.env.HUAWEICLOUD_DEVKIT_HOME || '').trim() || homedir();
|
|
82
|
+
return join(home, '.huaweicloud-devkit', 'telemetry');
|
|
83
|
+
}
|
|
84
|
+
|
|
78
85
|
function opencodeSkillsDir() {
|
|
79
86
|
return join(configRoot('opencode'), 'skills');
|
|
80
87
|
}
|
|
@@ -1814,7 +1821,7 @@ function deployWorkBuddyHooks() {
|
|
|
1814
1821
|
|
|
1815
1822
|
// 4. Ensure telemetry output directories exist
|
|
1816
1823
|
mkdirSync(join(workbuddyPluginsDir(), 'telemetry'), { recursive: true });
|
|
1817
|
-
mkdirSync(
|
|
1824
|
+
mkdirSync(devkitTelemetryDir(), { recursive: true });
|
|
1818
1825
|
}
|
|
1819
1826
|
|
|
1820
1827
|
// ── AtomCode hooks deployment ────────────────────────────────
|
|
@@ -1982,10 +1989,10 @@ function uninstallWorkBuddy() {
|
|
|
1982
1989
|
console.log(` Removed hook tracker: ${trackerPath}`);
|
|
1983
1990
|
}
|
|
1984
1991
|
|
|
1985
|
-
const
|
|
1986
|
-
if (existsSync(
|
|
1987
|
-
rmSync(
|
|
1988
|
-
console.log(` Removed telemetry cache: ${
|
|
1992
|
+
const devkitTelemetryPath = devkitTelemetryDir();
|
|
1993
|
+
if (existsSync(devkitTelemetryPath)) {
|
|
1994
|
+
rmSync(devkitTelemetryPath, { recursive: true, force: true });
|
|
1995
|
+
console.log(` Removed telemetry cache: ${devkitTelemetryPath}`);
|
|
1989
1996
|
}
|
|
1990
1997
|
}
|
|
1991
1998
|
|
|
@@ -1,24 +1,31 @@
|
|
|
1
|
-
import { AGENTS, matchAgent, detectVersion
|
|
1
|
+
import { AGENTS, matchAgent, detectVersion } from './agent-registry.mjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* 仅返回 harness
|
|
5
|
-
*
|
|
4
|
+
* 仅返回 harness 字符串,不返回 version。
|
|
5
|
+
* #1040: 白名单 / AGENT_HARNESS 未命中时一律返回 'unknown'——
|
|
6
|
+
* 不再透传 clientInfo.name(startup、voucher-claim*、test* 等探针类 clientName 同样归 unknown),
|
|
7
|
+
* 也不拼接 installSegment,避免 agent 名出现 '|' 污染。
|
|
6
8
|
*/
|
|
7
9
|
export function detectAgentHarness(clientInfo = {}) {
|
|
8
|
-
|
|
10
|
+
const envHarness = process.env.AGENT_HARNESS;
|
|
11
|
+
if (envHarness && AGENTS.some((a) => a.id === envHarness)) return envHarness;
|
|
9
12
|
for (const agent of AGENTS) {
|
|
10
13
|
if (matchAgent(agent, clientInfo)) return agent.id;
|
|
11
14
|
}
|
|
12
|
-
return
|
|
15
|
+
return 'unknown';
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
export function detectAgent(clientInfo = {}) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
// AGENT_HARNESS 仅在命中白名单时生效;未命中视为不可信输入,不透传任意环境值。
|
|
20
|
+
const envHarness = process.env.AGENT_HARNESS;
|
|
21
|
+
if (envHarness) {
|
|
22
|
+
const cfg = AGENTS.find((a) => a.id === envHarness) || null;
|
|
23
|
+
if (cfg) {
|
|
24
|
+
return {
|
|
25
|
+
harness: envHarness,
|
|
26
|
+
version: detectVersion(cfg.version) || clientInfo.version || '0.0.0',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
for (const agent of AGENTS) {
|
|
@@ -30,11 +37,9 @@ export function detectAgent(clientInfo = {}) {
|
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
const base = clientInfo.name || 'unknown';
|
|
35
|
-
const harness = seg ? `${base}|${seg}` : base;
|
|
40
|
+
// #1040: 兜底收敛 'unknown'——安装位置信息走遥测独立字段 installSegment,不进入 agent 名。
|
|
36
41
|
return {
|
|
37
|
-
harness:
|
|
42
|
+
harness: 'unknown',
|
|
38
43
|
version: clientInfo.version || '0.0.0',
|
|
39
44
|
};
|
|
40
45
|
}
|
|
@@ -10,6 +10,10 @@ const selfPath = (() => {
|
|
|
10
10
|
}
|
|
11
11
|
})();
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* #1040: 安装位置段——仅用于遥测的独立安装位置字段(telemetry 事件 installSegment),
|
|
15
|
+
* 不进入 agent 名/harness;AGENTS 白名单与 matchAgent 规则不受安装位置影响。
|
|
16
|
+
*/
|
|
13
17
|
export function installSegment() {
|
|
14
18
|
const parts = selfPath.split('/').filter(Boolean);
|
|
15
19
|
const srcIdx = parts.lastIndexOf('src');
|
|
@@ -14,6 +14,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
|
|
16
16
|
import { fetchWithProxy } from '../proxy/proxy-agent.mjs';
|
|
17
|
+
import { installSegment } from './agent-registry.mjs';
|
|
17
18
|
|
|
18
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
20
|
const __dirname = dirname(__filename);
|
|
@@ -68,7 +69,7 @@ const MAX_RETRIES = 3;
|
|
|
68
69
|
const DEFAULT_ENDPOINT = 'https://devkit.huaweicloud.com/rest/developer/server/hdkitservice/telemetry/events';
|
|
69
70
|
|
|
70
71
|
let eventQueue = [];
|
|
71
|
-
let
|
|
72
|
+
let currentFlush = null;
|
|
72
73
|
let flushTimer = null;
|
|
73
74
|
let lastCheckedDate = '';
|
|
74
75
|
let installId = null;
|
|
@@ -207,6 +208,9 @@ function buildEvent(raw) {
|
|
|
207
208
|
os: osTypeStr,
|
|
208
209
|
osVersion: osVersionStr,
|
|
209
210
|
};
|
|
211
|
+
// #1040: 安装位置独立字段——原拼接进 harness 的 installSegment 改为单列上报。
|
|
212
|
+
const seg = installSegment();
|
|
213
|
+
if (seg) event.installSegment = sanitizeValue(seg);
|
|
210
214
|
const cap = raw.capability || capabilityFromKey(raw.key);
|
|
211
215
|
if (cap) event.capability = cap;
|
|
212
216
|
return event;
|
|
@@ -214,7 +218,9 @@ function buildEvent(raw) {
|
|
|
214
218
|
|
|
215
219
|
export function enqueueEvent(raw) {
|
|
216
220
|
if (!isTelemetryEnabled()) return;
|
|
217
|
-
|
|
221
|
+
// #1014: installId 未初始化时(如未走 initialize 直接 tools/call)不再丢弃事件,
|
|
222
|
+
// 惰性恢复,避免 telemetry_event 缺记录。
|
|
223
|
+
if (!installId) installId = generateOrRecoverInstallId();
|
|
218
224
|
|
|
219
225
|
ingestHookEvents();
|
|
220
226
|
|
|
@@ -324,10 +330,37 @@ export function clearUserHash() {
|
|
|
324
330
|
} catch {}
|
|
325
331
|
}
|
|
326
332
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
333
|
+
// #1014: flush 前兜底补齐队列事件的缺失身份字段——
|
|
334
|
+
// userHash 在 enqueue 之后才解析到(领券响应 / user-hash 文件)时,
|
|
335
|
+
// 已入队事件的 userHash 仍为 null,这里统一回填,降低 user_hash null 占比。
|
|
336
|
+
function backfillIdentityFields() {
|
|
337
|
+
try {
|
|
338
|
+
if (!installId) installId = generateOrRecoverInstallId();
|
|
339
|
+
if (!userHash) loadUserHash();
|
|
340
|
+
for (const event of eventQueue) {
|
|
341
|
+
if (!event.installId && installId) event.installId = installId;
|
|
342
|
+
if (event.userHash == null && userHash) event.userHash = userHash;
|
|
343
|
+
}
|
|
344
|
+
} catch (_) {
|
|
345
|
+
debugLog('BACKFILL identity fields failed');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// #1014: flush 可被 await(P0 领券类工具同步上报);
|
|
350
|
+
// 并发调用复用同一次 in-flight flush(currentFlush 声明于模块顶部)。
|
|
351
|
+
export function flushEvents() {
|
|
352
|
+
if (currentFlush) return currentFlush;
|
|
353
|
+
if (eventQueue.length === 0) return Promise.resolve();
|
|
354
|
+
currentFlush = performFlush().finally(() => {
|
|
355
|
+
currentFlush = null;
|
|
356
|
+
});
|
|
357
|
+
return currentFlush;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function performFlush() {
|
|
361
|
+
// flush 前统一回填身份字段(含 userHash 重解析,#1014):
|
|
362
|
+
// 入队时身份未解析的事件此前会永久携带 null userHash。
|
|
363
|
+
backfillIdentityFields();
|
|
331
364
|
|
|
332
365
|
const batch = eventQueue.splice(0, BATCH_SIZE);
|
|
333
366
|
const keys = batch.map((e) => e.key).join(',');
|
|
@@ -339,34 +372,72 @@ function flushEvents() {
|
|
|
339
372
|
const controller = new AbortController();
|
|
340
373
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
341
374
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
} else if (resp.status >= 400 && resp.status < 500) {
|
|
358
|
-
debugLog(`POST status=${resp.status} dropping ${batch.length} events (client error)`);
|
|
359
|
-
} else {
|
|
360
|
-
requeueEvents(batch);
|
|
375
|
+
try {
|
|
376
|
+
const resp = await fetchWithProxy(endpoint, {
|
|
377
|
+
method: 'POST',
|
|
378
|
+
// #1040: 对外上报统一携带 X-HW-Agent(值=detectAgent 结果 harness,initTelemetry 缓存)
|
|
379
|
+
headers: { 'Content-Type': 'application/json', 'X-HW-Agent': agentHarness || 'unknown' },
|
|
380
|
+
body: JSON.stringify(batch.map(({ _retries, ...rest }) => rest)),
|
|
381
|
+
signal: controller.signal,
|
|
382
|
+
});
|
|
383
|
+
clearTimeout(timer);
|
|
384
|
+
debugLog(`POST status=${resp.status} events=${batch.length}`);
|
|
385
|
+
if (resp.ok) {
|
|
386
|
+
for (const event of batch) {
|
|
387
|
+
if (event.key === 'dau:active_today') touchFile(dauStampPath());
|
|
388
|
+
if (event.key === 'plugin:install') touchFile(installStampPath());
|
|
389
|
+
if (event.key === 'plugin:first_use') touchFile(firstUseStampPath());
|
|
361
390
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
clearTimeout(timer);
|
|
366
|
-
debugLog(`POST FAIL err=${error.message} events=${batch.length}`);
|
|
391
|
+
} else if (resp.status >= 400 && resp.status < 500) {
|
|
392
|
+
debugLog(`POST status=${resp.status} dropping ${batch.length} events (client error)`);
|
|
393
|
+
} else {
|
|
367
394
|
requeueEvents(batch);
|
|
368
|
-
|
|
395
|
+
}
|
|
396
|
+
} catch (error) {
|
|
397
|
+
clearTimeout(timer);
|
|
398
|
+
debugLog(`POST FAIL err=${error.message} events=${batch.length}`);
|
|
399
|
+
requeueEvents(batch);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// #1014 P1: 排空整个队列(退出兜底用,单次 flush 只发一个 batch)。
|
|
404
|
+
export async function flushAllEvents() {
|
|
405
|
+
const maxBatches = Math.ceil(MAX_QUEUE_SIZE / BATCH_SIZE) + 1;
|
|
406
|
+
for (let i = 0; i < maxBatches && eventQueue.length > 0; i++) {
|
|
407
|
+
await flushEvents();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const EXIT_FLUSH_TIMEOUT_MS = 3000;
|
|
412
|
+
let exitFlushRegistered = false;
|
|
413
|
+
|
|
414
|
+
// #1014 P1: SIGTERM/SIGINT 退出兜底 flush(有界超时,避免阻塞退出)。
|
|
415
|
+
// 返回反注册函数(仅测试使用)。
|
|
416
|
+
export function registerExitFlush() {
|
|
417
|
+
if (exitFlushRegistered) return () => {};
|
|
418
|
+
exitFlushRegistered = true;
|
|
419
|
+
const flushAndExit = () => {
|
|
420
|
+
if (eventQueue.length === 0) {
|
|
421
|
+
// eslint-disable-next-line n/no-process-exit -- signal shutdown with nothing to flush
|
|
422
|
+
process.exit(0);
|
|
423
|
+
}
|
|
424
|
+
const cap = new Promise((resolve) => {
|
|
425
|
+
const t = setTimeout(resolve, EXIT_FLUSH_TIMEOUT_MS);
|
|
426
|
+
t.unref?.();
|
|
427
|
+
});
|
|
428
|
+
const done = flushAllEvents().catch(() => {});
|
|
429
|
+
Promise.race([done, cap]).finally(() => {
|
|
430
|
+
// eslint-disable-next-line n/no-process-exit -- signal shutdown after bounded exit flush
|
|
431
|
+
process.exit(0);
|
|
369
432
|
});
|
|
433
|
+
};
|
|
434
|
+
process.once('SIGTERM', flushAndExit);
|
|
435
|
+
process.once('SIGINT', flushAndExit);
|
|
436
|
+
return () => {
|
|
437
|
+
process.removeListener('SIGTERM', flushAndExit);
|
|
438
|
+
process.removeListener('SIGINT', flushAndExit);
|
|
439
|
+
exitFlushRegistered = false;
|
|
440
|
+
};
|
|
370
441
|
}
|
|
371
442
|
|
|
372
443
|
function requeueEvents(batch) {
|
|
@@ -390,6 +461,9 @@ export function initTelemetry({ harness, version }) {
|
|
|
390
461
|
|
|
391
462
|
loadUserHash();
|
|
392
463
|
|
|
464
|
+
// #1014 P1: SIGTERM/SIGINT 退出兜底 flush(队列空时立即退出,零开销)。
|
|
465
|
+
registerExitFlush();
|
|
466
|
+
|
|
393
467
|
if (!isTelemetryEnabled()) return;
|
|
394
468
|
|
|
395
469
|
if (!stampExists(installStampPath()) && !existsSync(installCounterPath())) {
|
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
parseStsExpiry,
|
|
55
55
|
resolveCredentialsWithRuntime,
|
|
56
56
|
} from './auth/credentials.mjs';
|
|
57
|
-
import { trackToolInvoke, trackSkillRetrieve, clearUserHash } from './telemetry/telemetry.mjs';
|
|
57
|
+
import { trackToolInvoke, trackSkillRetrieve, clearUserHash, cacheUserHash } from './telemetry/telemetry.mjs';
|
|
58
58
|
import { fetchWithProxy } from './proxy/proxy-agent.mjs';
|
|
59
59
|
import { fingerprint, runHcloudConfigure, runHcloudProfileDelete, resolveManagedProfile } from './auth/reconcile.mjs';
|
|
60
60
|
import {
|
|
@@ -1785,10 +1785,17 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1785
1785
|
}
|
|
1786
1786
|
case 'huaweicloud_voucher_status':
|
|
1787
1787
|
return await hdkitVoucherStatus(args.domain_id);
|
|
1788
|
-
case 'huaweicloud_voucher_claim':
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1788
|
+
case 'huaweicloud_voucher_claim': {
|
|
1789
|
+
const result = await hdkitVoucherClaim(args.domain_id);
|
|
1790
|
+
// #1014 P0: 领券成功后补缓存 userHash(响应携带时),flush 前兜底补齐队列缺失字段。
|
|
1791
|
+
if (result?.userHash) cacheUserHash(result.userHash);
|
|
1792
|
+
return result;
|
|
1793
|
+
}
|
|
1794
|
+
case 'huaweicloud_competition_voucher_claim': {
|
|
1795
|
+
const result = await hdkitCompetitionVoucherClaim(args.domain_id);
|
|
1796
|
+
if (result?.userHash) cacheUserHash(result.userHash);
|
|
1797
|
+
return result;
|
|
1798
|
+
}
|
|
1792
1799
|
case 'huaweicloud_check_update':
|
|
1793
1800
|
return await handleCheckUpdate(args, { sessionId: opts?.sessionId, doQuery: opts?.doQuery });
|
|
1794
1801
|
case 'huaweicloud_upgrade':
|