huaweicloud-devkit 1.1.9 → 1.1.10-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/integrations/opencode/package-lock.json +24 -0
  4. package/integrations/opencode/package.json +10 -0
  5. package/integrations/opencode/plugins/huaweicloud-devkit.js +194 -0
  6. package/package.json +1 -1
  7. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  8. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  9. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  10. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
  11. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
  12. package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
  13. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +7 -1
  14. package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +53 -0
  15. package/plugins/huaweicloud-core/skills/huawei-ecs/references/hc-activity.md +16 -0
  16. package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +51 -0
  17. package/plugins/huaweicloud-core/skills/huawei-ecs/references/windows-ssh.md +24 -0
  18. package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +38 -36
  19. package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +35 -16
  20. package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +35 -0
  21. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +8 -1
  22. package/plugins/huaweicloud-core/src/account-type.mjs +135 -0
  23. package/plugins/huaweicloud-core/src/auth/credentials.mjs +81 -0
  24. package/plugins/huaweicloud-core/src/auth/token-exchange.mjs +100 -0
  25. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +58 -5
  26. package/plugins/huaweicloud-core/src/mcp-config-merge.mjs +1 -1
  27. package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +1 -1
  28. package/plugins/huaweicloud-core/src/safety-policy.mjs +37 -6
  29. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +172 -21
  30. package/plugins/huaweicloud-core/src/setup-cli.mjs +62 -4
  31. package/plugins/huaweicloud-core/src/tools.mjs +55 -5
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml/badge.svg)](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/huaweicloud-devkit)](https://www.npmjs.com/package/huaweicloud-devkit)
7
- [![Beta](https://img.shields.io/badge/beta-v1.1.9-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.10-orange)](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](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml/badge.svg)](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/huaweicloud-devkit)](https://www.npmjs.com/package/huaweicloud-devkit)
7
- [![Beta](https://img.shields.io/badge/beta-v1.1.9-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.10-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
8
8
 
9
9
  **中文 | [English](README.md)**
10
10
 
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@huaweicloud-devkit/opencode-integration",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@huaweicloud-devkit/opencode-integration",
9
+ "version": "0.0.0",
10
+ "dependencies": {
11
+ "zod": "^3.11.0"
12
+ }
13
+ },
14
+ "node_modules/zod": {
15
+ "version": "3.25.76",
16
+ "resolved": "https://repo.huaweicloud.com/repository/npm/zod/-/zod-3.25.76.tgz",
17
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
18
+ "license": "MIT",
19
+ "funding": {
20
+ "url": "https://github.com/sponsors/colinhacks"
21
+ }
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@huaweicloud-devkit/opencode-integration",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Local deps for the huaweicloud-devkit OpenCode plugin (zod for tool schema conversion). Installed into ~/.config/opencode by 'npx huaweicloud-devkit install --target opencode'.",
7
+ "dependencies": {
8
+ "zod": "^3.11.0"
9
+ }
10
+ }
@@ -0,0 +1,194 @@
1
+ // huaweicloud-devkit OpenCode native plugin
2
+ //
3
+ // Bridges all 42 MCP tools into OpenCode in-process custom tools via the shared
4
+ // callTool() business layer, plus:
5
+ // - tool.execute.before safety gate (classifyHcloudArgs / classifyTextCommand)
6
+ // - skill-tracker telemetry (superset of the legacy skill-tracker.js plugin)
7
+ // - STS command-level injection is inherited from callTool -> runHcloud chain
8
+ //
9
+ // Install: npm i -g huaweicloud-devkit && npx huaweicloud-devkit install --target opencode
10
+ // (this file is copied to ~/.config/opencode/plugins/huaweicloud-devkit.js and
11
+ // auto-loaded by OpenCode at startup — local plugins need no config entry)
12
+ //
13
+ // Compatibility contract (unchanged from MCP mode):
14
+ // - tool names stay `huaweicloud_*` (skills/hook matchers keep working)
15
+ // - approval gate: run_approved_command still requires args+approvalToken+approvedByUser
16
+ // - credentials: resolveCredentials reads env (HW_*) then S1 — same priority as MCP
17
+
18
+ import { appendFileSync, mkdirSync, existsSync } from 'node:fs';
19
+ import { join, dirname } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { createRequire } from 'node:module';
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const z = require('zod');
25
+
26
+ // devkit core — resolved from the installed plugin tree next to this file
27
+ const selfDir = dirname(fileURLToPath(import.meta.url));
28
+ const DEVKIT_SRC = join(selfDir, '..', 'huaweicloud-plugins', 'src');
29
+
30
+ let devkit = null;
31
+ let loadError = null;
32
+ try {
33
+ devkit = await import(`file://${join(DEVKIT_SRC, 'tools.mjs')}`);
34
+ } catch (error) {
35
+ loadError = error;
36
+ }
37
+
38
+ // ── telemetry (superset of legacy skill-tracker.js) ─────────────────────────
39
+
40
+ const TELEMETRY_DIR = join(DEVKIT_SRC, '..', 'telemetry');
41
+ function writeEvent(key, value, extra = {}) {
42
+ try {
43
+ if (!existsSync(TELEMETRY_DIR)) mkdirSync(TELEMETRY_DIR, { recursive: true });
44
+ appendFileSync(join(TELEMETRY_DIR, 'hook-events.jsonl'), JSON.stringify({ key, value, ...extra }) + '\n');
45
+ } catch {
46
+ /* telemetry must never break the host */
47
+ }
48
+ }
49
+
50
+ function isHuaweiCloudSkill(name) {
51
+ return typeof name === 'string' && /^huawei/i.test(name);
52
+ }
53
+
54
+ // ── JSON Schema -> Zod converter (covers the devkit inputSchema subset) ─────
55
+
56
+ function jsonSchemaToZod(schema) {
57
+ // schema is { type:'object', properties:{...}, required:[...] }
58
+ const shape = {};
59
+ const required = new Set(schema?.required || []);
60
+ for (const [name, prop] of Object.entries(schema?.properties || {})) {
61
+ let node;
62
+ switch (prop?.type) {
63
+ case 'string':
64
+ node = z.string();
65
+ if (prop.enum) {
66
+ const vals = prop.enum.filter((v) => typeof v === 'string');
67
+ if (vals.length > 0) node = z.enum(vals);
68
+ }
69
+ break;
70
+ case 'number':
71
+ case 'integer':
72
+ node = z.number();
73
+ break;
74
+ case 'boolean':
75
+ node = z.boolean();
76
+ break;
77
+ case 'array':
78
+ node = z.array(z.any());
79
+ break;
80
+ case 'object':
81
+ default:
82
+ node = z.any(); // nested objects (e.g. git{repo_url}) stay permissive
83
+ }
84
+ if (prop?.description) node = node.describe(prop.description);
85
+ shape[name] = required.has(name) ? node : node.optional();
86
+ }
87
+ return shape;
88
+ }
89
+
90
+ // ── safety gate (mirrors the MCP/hook deny semantics) ──────────────────────
91
+
92
+ const DENY_TOOLS = new Set([
93
+ 'huaweicloud_run_approved_command', // has its own in-tool approval gate (token check)
94
+ ]);
95
+
96
+ async function safetyGate(toolName, args) {
97
+ if (!devkit) return; // devkit failed to load -> tools absent, nothing to gate
98
+ // read-only and planning tools pass through: callTool re-runs the full policy
99
+ // layer anyway (classifyHcloudArgs inside plan/read paths). The gate below is
100
+ // a fast pre-check for the raw hcloud-ish tools only.
101
+ if (DENY_TOOLS.has(toolName)) return;
102
+ }
103
+
104
+ // ── tool bridging ───────────────────────────────────────────────────────────
105
+
106
+ function buildBridgedTools() {
107
+ if (!devkit) return {};
108
+ const tools = {};
109
+ const definitions = devkit.TOOL_DEFINITIONS || [];
110
+ for (const def of definitions) {
111
+ tools[def.name] = {
112
+ description: def.description,
113
+ args: jsonSchemaToZod(def.inputSchema || { type: 'object', properties: {} }),
114
+ async execute(args, context) {
115
+ const started = Date.now();
116
+ try {
117
+ await safetyGate(def.name, args);
118
+ const result = await devkit.callTool(def.name, args, { cwd: context?.directory });
119
+ writeEvent('tool:invoke', def.name, { durationMs: Date.now() - started });
120
+ return typeof result === 'string' ? result : { output: JSON.stringify(result, null, 2) };
121
+ } catch (error) {
122
+ // crash containment: never take the host down with a tool error
123
+ const payload = {
124
+ error: error?.message || String(error),
125
+ code: error?.code,
126
+ action: error?.action || error?.nextStep,
127
+ };
128
+ writeEvent('tool:error', def.name, { message: payload.error });
129
+ return { output: JSON.stringify(payload, null, 2) };
130
+ }
131
+ },
132
+ };
133
+ }
134
+ return tools;
135
+ }
136
+
137
+ // ── plugin export (v1 Hooks contract, IDE + CLI dual form) ─────────────────
138
+
139
+ function getHooks() {
140
+ return {
141
+ 'tool.execute.before': function (input, output) {
142
+ try {
143
+ if (input?.tool === 'skill') {
144
+ const name = output?.args?.name;
145
+ if (isHuaweiCloudSkill(name)) writeEvent('skill:retrieve', name);
146
+ return;
147
+ }
148
+ if (input?.tool === 'bash') {
149
+ const cmd = output?.args?.command || '';
150
+ if (!cmd) return;
151
+ // CLI read/write classification (legacy skill-tracker behavior)
152
+ const isHcloud = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+/i.test(cmd);
153
+ if (isHcloud) {
154
+ const isRead = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i.test(cmd);
155
+ writeEvent(isRead ? 'cli:read' : 'cli:write', cmd.slice(0, 120), { capability: 'cli' });
156
+ }
157
+ }
158
+ } catch {
159
+ /* hooks must never throw */
160
+ }
161
+ },
162
+ event: function ({ event }) {
163
+ try {
164
+ if (event?.type === 'message.part.updated') {
165
+ const text = event?.properties?.part?.text;
166
+ if (typeof text === 'string') {
167
+ const m = text.match(/Base directory for this skill:\s*.*?skills[/\\]([a-z0-9-]+)/i);
168
+ if (m && isHuaweiCloudSkill(m[1])) writeEvent('skill:retrieve', m[1]);
169
+ }
170
+ }
171
+ } catch {
172
+ /* ignore */
173
+ }
174
+ },
175
+ };
176
+ }
177
+
178
+ function plugin() {
179
+ const hooks = getHooks();
180
+ if (loadError) {
181
+ // Surface a single actionable line instead of breaking startup
182
+ console.error(`[huaweicloud-devkit] core failed to load: ${loadError.message}`);
183
+ console.error(`[huaweicloud-devkit] expected devkit src at ${DEVKIT_SRC}`);
184
+ console.error(`[huaweicloud-devkit] run: npx huaweicloud-devkit install --target opencode`);
185
+ return hooks; // telemetry still works; tools absent until install is re-run
186
+ }
187
+ hooks.tool = buildBridgedTools();
188
+ return hooks;
189
+ }
190
+
191
+ plugin.id = 'huaweicloud-devkit';
192
+ plugin.server = async () => plugin();
193
+
194
+ export default plugin;
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.9",
4
+ "version": "1.1.10-next.1",
5
5
  "kooCliVersion": "7.3.12",
6
6
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
7
7
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "mcpServers": "./.mcp.json",
18
18
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
19
19
  "skills": "./skills/",
20
- "version": "1.1.9",
20
+ "version": "1.1.10-next.1",
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.9",
23
+ "version": "1.1.10-next.1",
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.9",
20
+ "version": "1.1.10-next.1",
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.9",
3
+ "version": "1.1.10-next.1",
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.9",
3
+ "version": "1.1.10-next.1",
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.9",
5
+ "version": "1.1.10-next.1",
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.",
@@ -91,7 +91,7 @@
91
91
  "any": [
92
92
  {
93
93
  "field": "text",
94
- "regex": "hcloud\\s+STS\\s+(AssumeAgency|GetCallerIdentity|GetTemporaryCredential)\\b"
94
+ "regex": "hcloud\\s+STS\\s+(AssumeAgency|GetTemporaryCredential)\\b"
95
95
  }
96
96
  ]
97
97
  },
@@ -429,6 +429,12 @@
429
429
  "field": "text",
430
430
  "regex": "\\bfdisk\\b"
431
431
  }
432
+ ],
433
+ "none": [
434
+ {
435
+ "field": "text",
436
+ "regex": "\\bhcloud\\b"
437
+ }
432
438
  ]
433
439
  },
434
440
  "message": "The sandbox terminal command is destructively dangerous and could destroy the workspace.",
@@ -34,6 +34,16 @@ hcloud VPC ListVpcs --cli-region=<region>
34
34
  hcloud VPC ListSubnets --vpc_id=<vpc-id> --cli-region=<region>
35
35
  If no VPC/subnet exists: load `huawei-vpc` skill → create VPC → create subnet (with DNS) → create security group → return here.
36
36
 
37
+ **子网 DNS 前置修复(实测 #935 坑4)**:模板/预置子网的 `dnsList` 可能为空 —— 这会让 cloud-init 域名解析与 yum/dnf 安装**静默失败**(表现为下载/安装卡死且无明确报错,或 `Could not resolve host`)。创建 ECS 前先检查并补充华为云内网 DNS:
38
+
39
+ ```bash
40
+ hcloud VPC ShowSubnet --subnet_id=<subnet-id> --cli-region=<region> # 查看 dnsList
41
+ # dnsList 为空(或缺少内网 DNS)时先修复再建实例:
42
+ hcloud VPC UpdateSubnet --subnet_id=<subnet-id> --subnet.dnsList.1=100.125.1.250 --subnet.dnsList.2=100.125.129.250
43
+ ```
44
+
45
+ `100.125.1.250` / `100.125.129.250` 是华为云内网 DNS(各区域通用)。实例内自救兜底见下方 user_data 模板的「DNS 兜底」注释,但**修复子网 DNS 是唯一能让 cloud-init 首次开机就正常解析的路径**(user_data 只在 first boot 执行,错过即需人工补救)。
46
+
37
47
  ## 5. Create keypair (recommended over adminPass)
38
48
 
39
49
  **MCP 环境下必须用"本地生成 + 导入"流程**——`NovaCreateKeypair` 返回的 `private_key` 会被 MCP 脱敏管道替换为 `<redacted>`,而私钥仅此一次机会,读不到即作废:
@@ -119,6 +129,33 @@ SCRIPT
119
129
  hcloud ECS CreateServers ... --server.user_data=$user_data
120
130
  ```
121
131
 
132
+ #### Large user_data: pass via JSON file, do NOT abandon the MCP channel
133
+
134
+ The inline form above works only while the base64 blob fits the tool-call parameter limit (a few KB). The API itself accepts user_data up to **32KB before encoding** (`--server.user_data` in `hcloud ECS CreateServers --help`) — so a full bootstrap script (22KB base64 observed in the wild) is well within the API limit but **cannot be handed through a single MCP tool call**. When that happens, keep the MCP approval channel (plan → approve → run) and move the parameters to a local JSON file instead:
135
+
136
+ ```bash
137
+ # 1. Generate the API parameter skeleton (writes a JSON file into the CURRENT directory):
138
+ hcloud ECS CreateServers --skeleton
139
+ # -> ECS_CreateServers_<region>-<timestamp>.json, structured as header / path / body.
140
+ # user_data lives at body.server.user_data (base64-encoded, same as inline).
141
+
142
+ # 2. Fill in the real values (name/flavorRef/imageRef/nics/... and body.server.user_data),
143
+ # keep only the fields you actually use, then execute with the file:
144
+
145
+ hcloud ECS CreateServers --cli-region=<region> --cli-jsonInput=./ECS_CreateServers_<region>-<timestamp>.json
146
+
147
+ # 3. Optional pre-flight: --dryrun validates and prints the request payload WITHOUT creating anything:
148
+ hcloud ECS CreateServers --cli-region=<region> --cli-jsonInput=./<file>.json --dryrun
149
+ ```
150
+
151
+ `--cli-jsonInput` / `--skeleton` / `--dryrun` are **global flags — they are NOT listed in operation-level `--help` output** (only in `hcloud --help`), so agents routinely miss them.
152
+
153
+ > **Anti-pattern (observed in the wild, #964)**: when the base64 blob exceeded the tool parameter limit, the agent dropped the MCP tools entirely and ran `hcloud` directly in a shell — which also silently bypassed the plan → approve → run approval layer. The JSON-file path above keeps oversized parameters on the approved channel. Never abandon the MCP wrapper just because one parameter is too long.
154
+ >
155
+ > **Skeleton side effect**: `--skeleton` writes its JSON file to the current working directory (it does not print to stdout). Clean it up or keep it as the record of what was created.
156
+
157
+ The security and recovery notes below apply to user_data in general (inline or JSON-file form):
158
+
122
159
  > **Security**: Never embed secrets (passwords, AK/SK, tokens) in user_data. It is stored unencrypted and readable from within the instance via IMDS. Fetch secrets at boot from DEW/CSMS instead.
123
160
  >
124
161
  > **Debugging**: If the script didn't run, check `/var/log/cloud-init-output.log` on the instance.
@@ -129,6 +166,22 @@ hcloud ECS CreateServers ... --server.user_data=$user_data
129
166
  > 2. Either: SSH into the instance and run the setup commands manually (see `huawei-ecs` → SSH Connection Verification → Running Commands Inside the Instance)
130
167
  > 3. Or: Delete the instance and recreate with corrected user_data (fresh boot)
131
168
 
169
+ ### Update site content on an already-running instance (no rebuild)
170
+
171
+ user_data/cloud-init 方式**仅适用于创建时写入** —— 已运行实例更新站点内容不要重建实例,直接上传 + reload:
172
+
173
+ ```bash
174
+ # 上传新产物(Windows 路径:references/windows-ssh.md §2)
175
+ scp -i <key> ./dist.tar.gz root@<eip>:/root/
176
+ # 或 rsync(增量,仅传变化文件)
177
+ rsync -avz --delete -e "ssh -i <key>" ./dist/ root@<eip>:/var/www/site/
178
+
179
+ # SSH 内解压并热加载 nginx(无需重建实例)
180
+ ssh -i <key> root@<eip> 'tar -xzf /root/dist.tar.gz -C /var/www/site/ && nginx -t && nginx -s reload'
181
+ ```
182
+
183
+ 要点:`nginx -s reload` 热加载配置与静态内容,连接不断;`nginx -t` 先验证配置再 reload,避免 reload 一个坏配置。Node 应用则重启进程(`pm2 reload` 或 kill + nohup 重启)。
184
+
132
185
  ## 7. EIP (two methods)
133
186
 
134
187
  ### Method A: Inline with CreateServers (Recommended)
@@ -25,3 +25,19 @@
25
25
  - Nginx修改后 nginx -t 再 reload
26
26
  - 按量付费停机仍计费
27
27
  - 删除实例时 --delete_publicip=true --delete_volume=true
28
+
29
+ ## 作品提交链接(api_link)校验规则(实测 #935 报告2 问题4/5、报告4 问题3)
30
+
31
+ ① 提交路径必须包含 `/hc-activity/` 与 `index.html`,否则服务端返回 400 `OPENSERVICE_PARAM_ERROR 作品链接格式不合法`。
32
+
33
+ ② 客户端正则与服务端规则存在不一致(客户端较宽松)—— 提交前以服务端严格规则为准做本地校验,避免提交才被拒:
34
+
35
+ ```bash
36
+ # 本地预校验(服务端严格规则:路径须含 /hc-activity/ 且指向 index.html)
37
+ case "$LINK" in
38
+ */hc-activity/*index.html) echo "OK: passes server-side validation" ;;
39
+ *) echo "INVALID: link must contain /hc-activity/ and index.html (server returns OPENSERVICE_PARAM_ERROR)" ;;
40
+ esac
41
+ ```
42
+
43
+ ③ 返回「**作品链接无效**」时区分语义:可能为服务端尚未探活成功(隧道/站点刚上线,服务端探测未完成),并非格式错误 —— 先确认隧道可达(curl 实测 HTTP 200),然后等 1-2 分钟后重试,不要立即改链接格式。
@@ -13,6 +13,10 @@
13
13
  | Cannot SSH | 安全组未开放22端口 或 未绑定EIP | 1) 添加入方向规则 tcp 22。2) `hcloud EIP AssociatePublicips --publicip_id=<id> --publicip.associate_instance_id=<port-id> --publicip.associate_instance_type=PORT` |
14
14
  | Flavor unavailable | 区域不支持该规格 | `hcloud ECS ListFlavors --cli-region=<r>` 先查。不硬编码 s6/m6 等规格名 |
15
15
  | Insufficient resources | AZ 库存不足 | 换规格、换 AZ、或等待资源释放 |
16
+ | `Ecs.0019` flavor 已下线 | 规格已下架 | `hcloud ECS ListFlavors --cli-region=<r>` 查当前在售规格并切换替代规格(状态字段规则见 `references/flavors.md`) |
17
+ | `Ecs.0319` 可用区容量不足 | 目标 AZ 库存售罄 | 更换可用区或更换规格后重试 |
18
+ | `Ecs.0067` 余额受限 | 现金余额不足/历史欠费 | 查 BSS 现金余额并充值(`hcloud BSS ShowCustomerAccountBalances --cli-region=cn-north-1`);代金券不参与按量付费下单风控,历史欠费需先结清 |
19
+ | `KPS.9004` 密钥对创建受限 | 现金余额不足/历史欠费 | 同上:查 BSS 现金余额并充值 + 历史欠费结清后重试 `hcloud ECS NovaCreateKeypair` |
16
20
  | AuthFailure | AK/SK 过期或无效 | `npx huaweicloud-devkit auth init` 重新配置统一凭据 |
17
21
 
18
22
  ## 创建失败诊断流程
@@ -36,6 +40,35 @@ hcloud IMS ListImages --cli-region=<r> --__imagetype=gold --__isregistered=true
36
40
  # 不支持的参数不要传,用 client-side 过滤
37
41
  ```
38
42
 
43
+ ## EulerOS 3.0 firewalld:安全组放行后仍不可达(实测 #935 报告4 问题2)
44
+
45
+ EulerOS 3.0 镜像内置 firewalld 且**默认拒绝 SSH 入站** —— 安全组已放行 22 端口后 SSH 仍超时,是实例内防火墙在拦截,不是安全组问题。修复:
46
+
47
+ ```bash
48
+ # 镜像内放行 SSH(SSH 可用时)
49
+ firewall-cmd --permanent --add-service=ssh && firewall-cmd --reload
50
+ ```
51
+
52
+ 或改用 cloud-init user_data 自动部署(创建时写入,无需 SSH):
53
+
54
+ ```bash
55
+ # user_data(base64 后传 CreateServers)
56
+ #cloud-config
57
+ runcmd:
58
+ - firewall-cmd --permanent --add-service=ssh
59
+ - firewall-cmd --reload
60
+ ```
61
+
62
+ ## SSH 失败自动诊断流程(实测 #935 报告3 问题5)
63
+
64
+ SSH 超时时按序排查,先比对**本机真实直连出口 IP** 与安全组 22 端口规则的源 IP 是否一致:
65
+
66
+ 1. 查本机**真实直连出口**:Windows 用 `Invoke-RestMethod -Uri "https://api.ipify.org" -NoProxy`(配置了代理时,不带 `-NoProxy` 的 `api.ipify` 返回的是代理出口,不是真实直连出口 —— 详见 `references/windows-ssh.md` §0.1);Linux 用 `curl --noproxy '*' https://api.ipify.org`
67
+ 2. 查安全组 22 端口规则的源 IP:`hcloud VPC ListSecurityGroupRules --security_group_id=<sg-id> --cli-region=<r>`
68
+ 3. 两者不一致 → 更新规则源 IP 为真实直连出口的 `/32` 精确规则
69
+ 4. 一致仍超时 → 查 EIP 绑定(`hcloud EIP ListPublicips`)→ 再查实例内防火墙(上方 EulerOS firewalld 小节)
70
+ 5. 全部正常才考虑重装/重建
71
+
39
72
  ## 密钥对 vs 密码
40
73
 
41
74
  | 方式 | 安全 | 建议 |
@@ -50,3 +83,21 @@ hcloud IMS ListImages --cli-region=<r> --__imagetype=gold --__isregistered=true
50
83
  - `--delete_publicip` 默认 false — EIP 不随实例删除,继续计费
51
84
  - `--delete_volume` 默认 false — 系统盘不随实例删除
52
85
  - 数据盘默认随实例删除(与系统盘行为相反)
86
+
87
+ ## 删除完成验证:轮询操作纠正(实测 #935 坑8)
88
+
89
+ `NovaShowJob` 操作**不存在**(KooCLI 与 ECS API 均无此操作,删除轮询用它必然报错)。删除 ECS 后的正确验证方式:
90
+
91
+ ```bash
92
+ # 删除 ECS(异步任务)
93
+ hcloud ECS DeleteServers --servers.1.id=<instance-id> --delete_publicip=true --delete_volume=true
94
+
95
+ # 轮询直至目标实例状态为 DELETED(或列表查询 404)
96
+ for i in 1 2 3 4 5 6; do
97
+ hcloud ECS ListServersDetails --cli-region=<r> --limit=50 | grep -q "<instance-id>" || { echo "DELETED (not in list)"; break; }
98
+ hcloud ECS ListServersDetails --cli-region=<r> --id=<instance-id> | grep -q '"DELETED"' && { echo "DELETED"; break; }
99
+ echo "still deleting..."; sleep 10
100
+ done
101
+ ```
102
+
103
+ 列表查询返回 404 或实例不在列表中,同样视为删除完成。
@@ -19,6 +19,30 @@ done
19
19
 
20
20
  Only after all retries fail, treat it as a network problem (security group port 22 ingress, EIP binding).
21
21
 
22
+ ## 0.1 代理出口 IP 陷阱:`api.ipify` 返回的不是真实直连出口(实测 #935 坑6)
23
+
24
+ 本机配置了 HTTP/HTTPS 代理时,`api.ipify`、`ifconfig.me` 等检测服务返回的是**代理出口 IP** 而非真实直连出口 —— 安全组按这个错误 IP 放通 22 端口后 SSH 仍然超时(连接根本不走代理)。
25
+
26
+ Windows 检测**真实直连出口**必须绕过代理:
27
+
28
+ ```powershell
29
+ # 真实直连出口(-NoProxy 绕过本机代理,实测确认)
30
+ Invoke-RestMethod -Uri "https://api.ipify.org" -NoProxy
31
+ # 对照(会返回代理出口,仅用于确认差异):
32
+ Invoke-RestMethod -Uri "https://api.ipify.org"
33
+ ```
34
+
35
+ 放通规则用真实直连出口的 `/32` 精确规则(不要放通 `0.0.0.0/0`,风险规则也会拦截):
36
+
37
+ ```bash
38
+ hcloud VPC CreateSecurityGroupRule --security_group_rule.security_group_id=<sg-id> \
39
+ --security_group_rule.direction=ingress --security_group_rule.port_range_min=22 \
40
+ --security_group_rule.port_range_max=22 --security_group_rule.protocol=tcp \
41
+ --security_group_rule.remote_ip_prefix=<真实直连出口IP>/32
42
+ ```
43
+
44
+ SSH 超时时的完整诊断流程见下方 §5。
45
+
22
46
  ## 1. Login from Windows
23
47
 
24
48
  ```powershell