huaweicloud-devkit 1.1.4-next.3 → 1.1.4-next.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +2 -2
- package/plugins/huaweicloud-core/skills/huawei-obs/references/static-website.md +3 -7
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +48 -23
- package/plugins/huaweicloud-core/src/auth/reconcile.mjs +9 -2
- package/plugins/huaweicloud-core/src/tools.mjs +148 -4
- package/plugins/huaweicloud-core/src/update-check.mjs +7 -3
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-next.
|
|
4
|
+
"version": "1.1.4-next.5",
|
|
5
5
|
"kooCliVersion": "7.2.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.4-next.
|
|
20
|
+
"version": "1.1.4-next.5",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"mcpServers": "./.mcp.json",
|
|
21
21
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
22
22
|
"skills": "./skills/",
|
|
23
|
-
"version": "1.1.4-next.
|
|
23
|
+
"version": "1.1.4-next.5",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.4-next.
|
|
20
|
+
"version": "1.1.4-next.5",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.4-next.
|
|
3
|
+
"version": "1.1.4-next.5",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.4-next.
|
|
3
|
+
"version": "1.1.4-next.5",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"id": "huaweicloud-devkit",
|
|
4
4
|
"displayName": "HuaweiCloud DevKit",
|
|
5
|
-
"version": "1.1.4-next.
|
|
5
|
+
"version": "1.1.4-next.5",
|
|
6
6
|
"family": "bundle-plugin",
|
|
7
7
|
"bundleFormat": "codex",
|
|
8
8
|
"description": "Guide coding agents to use Huawei Cloud safely — KooCLI, APIs, SDKs, 28 MCP tools, skills, and safety guardrails.",
|
|
@@ -41,7 +41,7 @@ Domain expertise for Huawei Cloud Object Storage Service (OBS). Covers bucket/ob
|
|
|
41
41
|
| Three-layer permissions | IAM > Bucket Policy > ACL. Most restrictive wins |
|
|
42
42
|
| Versioning is irreversible | Once enabled, cannot be disabled, only suspended |
|
|
43
43
|
| OBS uses AK/SK directly | NOT IAM tokens. Auth errors mean check AK/SK validity |
|
|
44
|
-
| Static website via CLI missing | KooCLI OBS lacks website config. Use
|
|
44
|
+
| Static website via CLI missing | KooCLI OBS lacks website config. Use `huaweicloud_obs_set_website_config` MCP tool (handles AWS4 signing internally) or console |
|
|
45
45
|
| **OBS needs separate cred config** | `hcloud configure` is NOT enough for OBS. Before any OBS operation, call `huaweicloud_setup_obs_config` to sync credentials from hcloud profile. |
|
|
46
46
|
| **obsutil interactive prompts** | `cp`/`rm` without `-f` causes "Please input (y/n)" → Agent hangs (TIMEOUT). Always use `-f` for non-interactive. |
|
|
47
47
|
| **Directory upload adds prefix** | `cp <dir>/ obs://<bucket>/ -r` puts files under `bucket/<dir>/...`. Use `-flat` for root-level files (static sites). Preview with `-dryRun` first. |
|
|
@@ -83,7 +83,7 @@ hcloud OBS config -e=<endpoint> -i=<AK> -k=<SK> -t=token
|
|
|
83
83
|
See `references/static-website.md` for the full end-to-end workflow:
|
|
84
84
|
Build → Create bucket → Upload → Set bucket ACL → Set object ACL → Configure website (REST API/console)
|
|
85
85
|
|
|
86
|
-
> KooCLI OBS does NOT support `SetBucketWebsite`.
|
|
86
|
+
> KooCLI OBS does NOT support `SetBucketWebsite`. Use the `huaweicloud_obs_set_website_config` MCP tool (handles AWS4 signing internally, no manual signature needed) or the Huawei Cloud console.
|
|
87
87
|
|
|
88
88
|
## Single-File Quick Share
|
|
89
89
|
|
|
@@ -31,13 +31,9 @@ hcloud OBS chattri obs://my-static-site -acl=public-read
|
|
|
31
31
|
hcloud OBS chattri obs://my-static-site/ -r -f -acl=public-read
|
|
32
32
|
|
|
33
33
|
# 5. Configure static website hosting
|
|
34
|
-
# KooCLI OBS does NOT support this. Use
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# Host: my-static-site.obs.cn-north-4.myhuaweicloud.com
|
|
38
|
-
# Body: {"IndexDocument": {"Suffix": "index.html"}, "ErrorDocument": {"Key": "error.html"}}
|
|
39
|
-
# (requires AK/SK signature)
|
|
40
|
-
# Option B — Huawei Cloud Console:
|
|
34
|
+
# KooCLI OBS does NOT support this. Use the MCP tool (handles AWS4 signing internally):
|
|
35
|
+
# action=set, bucket=<bucket>, region=<region>, indexDocument=index.html, errorDocument=404.html
|
|
36
|
+
# Or use Huawei Cloud Console:
|
|
41
37
|
# Console → OBS → Bucket → Basic Settings → Static Website Hosting
|
|
42
38
|
|
|
43
39
|
# 6. Verify
|
|
@@ -58,6 +58,33 @@ export function readGlobalCredentials() {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// R11: Placeholder / masked credential values are treated as "not configured".
|
|
62
|
+
// A vendor/IDE marketplace that pre-fills mcp_settings env with template text
|
|
63
|
+
// (e.g. "<HW_ACCESS_KEY>" or "${SECRET_KEY}") must not shadow the user's real
|
|
64
|
+
// S1 vault. Real AK/SK are >=20 char alphanumeric strings, so none of the
|
|
65
|
+
// template/masked patterns below can match a genuine credential.
|
|
66
|
+
export function isPlaceholder(value) {
|
|
67
|
+
if (typeof value !== 'string' || value.length === 0) return false;
|
|
68
|
+
// <HW_ACCESS_KEY> / <your-ak> / <...>
|
|
69
|
+
if (/^<[^>]*>$/.test(value)) return true;
|
|
70
|
+
// ${HW_ACCESS_KEY}
|
|
71
|
+
if (/^\$\{[^}]*\}$/.test(value)) return true;
|
|
72
|
+
// YOUR_AK / ACCESS_KEY / SECRET_KEY / SECURITY_TOKEN / REGION (bare keyword)
|
|
73
|
+
if (/^(YOUR_)?(AK|SK|ACCESS[_-]?KEY|SECRET[_-]?KEY|TOKEN|SECURITY[_-]?TOKEN|REGION)$/i.test(value)) return true;
|
|
74
|
+
// placeholder / replace_me / change.me
|
|
75
|
+
if (/\b(placeholder|replace[._-]?me|change[._-]?me)\b/i.test(value)) return true;
|
|
76
|
+
// masked value: short alnum prefix + asterisk run (abc**** / token****) — the
|
|
77
|
+
// S4 mirror writes the SK slot this way so the secret never lands in plaintext.
|
|
78
|
+
if (/^[A-Za-z0-9]{3,8}\*+$/.test(value)) return true;
|
|
79
|
+
// bare asterisks
|
|
80
|
+
if (/^\*+$/.test(value)) return true;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function present(v) {
|
|
85
|
+
return typeof v === 'string' && v.length > 0 && !isPlaceholder(v);
|
|
86
|
+
}
|
|
87
|
+
|
|
61
88
|
export function writeGlobalCredentials(credentials = {}) {
|
|
62
89
|
const path = globalCredentialsPath();
|
|
63
90
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -99,9 +126,10 @@ export function writeObsConfig(credentials = {}) {
|
|
|
99
126
|
}
|
|
100
127
|
|
|
101
128
|
export function resolveCredentials(options = {}) {
|
|
102
|
-
|
|
103
|
-
let
|
|
104
|
-
let
|
|
129
|
+
// R11: placeholder/masked env values are "not configured", never credentials.
|
|
130
|
+
let ak = present(process.env.HW_ACCESS_KEY) ? process.env.HW_ACCESS_KEY : '';
|
|
131
|
+
let sk = present(process.env.HW_SECRET_KEY) ? process.env.HW_SECRET_KEY : '';
|
|
132
|
+
let securityToken = present(process.env.HW_SECURITY_TOKEN) ? process.env.HW_SECURITY_TOKEN : '';
|
|
105
133
|
let region = process.env.HW_REGION || process.env.HUAWEICLOUD_REGION || '';
|
|
106
134
|
|
|
107
135
|
const codeartsCreds = isCodeArtsContext() ? readCodeArtsCredentials() : null;
|
|
@@ -113,36 +141,33 @@ export function resolveCredentials(options = {}) {
|
|
|
113
141
|
|
|
114
142
|
const stored = readGlobalCredentials();
|
|
115
143
|
if (stored) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!
|
|
144
|
+
// R11: skip placeholder/masked values from the vault too (templates must
|
|
145
|
+
// not win the truthiness check and shadow another store).
|
|
146
|
+
if (!ak && present(stored.ak)) ak = stored.ak;
|
|
147
|
+
if (!sk && present(stored.sk)) sk = stored.sk;
|
|
148
|
+
if (!securityToken && present(stored.securityToken)) securityToken = stored.securityToken;
|
|
119
149
|
if (!region && stored.region) region = stored.region;
|
|
120
150
|
}
|
|
121
151
|
|
|
122
152
|
// R9: S1 written by `auth_switch persist` (configuredBySession) is the session's
|
|
123
153
|
// source of truth and wins over env-injected defaults. Plain S1 (`auth init`)
|
|
124
154
|
// still yields to env so devspace-style env injection keeps working.
|
|
125
|
-
if (stored && stored.configuredBySession === true && stored.ak && stored.sk) {
|
|
155
|
+
if (stored && stored.configuredBySession === true && present(stored.ak) && present(stored.sk)) {
|
|
126
156
|
ak = stored.ak;
|
|
127
157
|
sk = stored.sk;
|
|
128
|
-
if (!securityToken) securityToken = stored.securityToken
|
|
158
|
+
if (!securityToken) securityToken = present(stored.securityToken) ? stored.securityToken : '';
|
|
129
159
|
if (!region) region = stored.region || '';
|
|
130
160
|
}
|
|
131
161
|
|
|
132
162
|
// Sandbox/platform-injected temporary STS credentials (env vars carrying a
|
|
133
163
|
// security token) must not shadow the user's explicit permanent credentials
|
|
134
164
|
// from `auth init`. Prefer the stored file when both exist.
|
|
135
|
-
|
|
136
|
-
process.env.HW_ACCESS_KEY &&
|
|
137
|
-
|
|
138
|
-
process.env.HW_SECURITY_TOKEN &&
|
|
139
|
-
stored &&
|
|
140
|
-
stored.ak &&
|
|
141
|
-
stored.sk
|
|
142
|
-
) {
|
|
165
|
+
const envHasFullTriplet =
|
|
166
|
+
present(process.env.HW_ACCESS_KEY) && present(process.env.HW_SECRET_KEY) && present(process.env.HW_SECURITY_TOKEN);
|
|
167
|
+
if (envHasFullTriplet && stored && present(stored.ak) && present(stored.sk)) {
|
|
143
168
|
ak = stored.ak;
|
|
144
169
|
sk = stored.sk;
|
|
145
|
-
securityToken = stored.securityToken
|
|
170
|
+
securityToken = present(stored.securityToken) ? stored.securityToken : '';
|
|
146
171
|
region = stored.region || region;
|
|
147
172
|
}
|
|
148
173
|
|
|
@@ -189,16 +214,16 @@ export function readCodeArtsCredentials() {
|
|
|
189
214
|
try {
|
|
190
215
|
if (!existsSync(path)) continue;
|
|
191
216
|
const config = JSON.parse(readFileSync(path, 'utf8'));
|
|
192
|
-
const server = config?.mcpServers?.['huaweicloud-devkit'];
|
|
217
|
+
const server = config?.mcpServers?.['huaweicloud-devkit'] || config?.mcpServers?.['HuaweiCloud DevKit'];
|
|
193
218
|
if (!server?.env) continue;
|
|
194
219
|
|
|
195
220
|
const ak = server.env.HW_ACCESS_KEY;
|
|
196
221
|
const sk = server.env.HW_SECRET_KEY;
|
|
197
|
-
if (ak && sk) {
|
|
222
|
+
if (present(ak) && present(sk)) {
|
|
198
223
|
return {
|
|
199
224
|
ak,
|
|
200
225
|
sk,
|
|
201
|
-
securityToken: server.env.HW_SECURITY_TOKEN
|
|
226
|
+
securityToken: present(server.env.HW_SECURITY_TOKEN) ? server.env.HW_SECURITY_TOKEN : '',
|
|
202
227
|
region: server.env.HW_REGION || server.env.HUAWEICLOUD_REGION || '',
|
|
203
228
|
};
|
|
204
229
|
}
|
|
@@ -213,15 +238,15 @@ export function readCodeArtsCredentials() {
|
|
|
213
238
|
try {
|
|
214
239
|
if (existsSync(path)) {
|
|
215
240
|
const config = JSON.parse(readFileSync(path, 'utf8'));
|
|
216
|
-
const server = config?.mcp?.['huaweicloud-devkit'];
|
|
241
|
+
const server = config?.mcp?.['huaweicloud-devkit'] || config?.mcp?.['HuaweiCloud DevKit'];
|
|
217
242
|
if (server?.environment) {
|
|
218
243
|
const ak = server.environment.HW_ACCESS_KEY;
|
|
219
244
|
const sk = server.environment.HW_SECRET_KEY;
|
|
220
|
-
if (ak && sk) {
|
|
245
|
+
if (present(ak) && present(sk)) {
|
|
221
246
|
return {
|
|
222
247
|
ak,
|
|
223
248
|
sk,
|
|
224
|
-
securityToken: server.environment.HW_SECURITY_TOKEN
|
|
249
|
+
securityToken: present(server.environment.HW_SECURITY_TOKEN) ? server.environment.HW_SECURITY_TOKEN : '',
|
|
225
250
|
region: server.environment.HW_REGION || server.environment.HUAWEICLOUD_REGION || '',
|
|
226
251
|
};
|
|
227
252
|
}
|
|
@@ -6,6 +6,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
hasRuntimeCredentials,
|
|
9
|
+
isPlaceholder,
|
|
9
10
|
obsConfigPath,
|
|
10
11
|
readGlobalCredentials,
|
|
11
12
|
readLastSync,
|
|
@@ -104,10 +105,13 @@ function s2CurrentMatchesLastDevkitSync(kooCli, s1, s1Fingerprint) {
|
|
|
104
105
|
|
|
105
106
|
export function scanState() {
|
|
106
107
|
const s1 = readGlobalCredentials() || {};
|
|
107
|
-
const envAk = process.env.HW_ACCESS_KEY || '';
|
|
108
|
-
const envSk = process.env.HW_SECRET_KEY || '';
|
|
108
|
+
const envAk = isPlaceholder(process.env.HW_ACCESS_KEY) ? '' : process.env.HW_ACCESS_KEY || '';
|
|
109
|
+
const envSk = isPlaceholder(process.env.HW_SECRET_KEY) ? '' : process.env.HW_SECRET_KEY || '';
|
|
109
110
|
const kooCli = readKooCliProfiles();
|
|
110
111
|
const inconsistencies = [];
|
|
112
|
+
const envIsPlaceholder =
|
|
113
|
+
Boolean(process.env.HW_ACCESS_KEY && isPlaceholder(process.env.HW_ACCESS_KEY)) ||
|
|
114
|
+
Boolean(process.env.HW_SECRET_KEY && isPlaceholder(process.env.HW_SECRET_KEY));
|
|
111
115
|
|
|
112
116
|
const s1Fingerprint = fingerprint(s1.ak, s1.sk);
|
|
113
117
|
const currentFp = currentFingerprintFromHcloud(kooCli);
|
|
@@ -153,6 +157,9 @@ export function scanState() {
|
|
|
153
157
|
runtimeFingerprint,
|
|
154
158
|
},
|
|
155
159
|
kooCliCurrent: kooCli.error ? null : kooCli.current,
|
|
160
|
+
// R11: env carries placeholder/masked values → surface that explicitly so
|
|
161
|
+
// diagnostics don't show a bogus fingerprint derived from template text.
|
|
162
|
+
envIsPlaceholder,
|
|
156
163
|
// S2 is encrypted storage (authEncrypt) → its fingerprint is unavailable,
|
|
157
164
|
// so the S2-current drift check is intentionally skipped (#533).
|
|
158
165
|
s2Encrypted: Boolean(!kooCli.error && kooCli.authEncrypt),
|
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { homedir, tmpdir } from 'node:os';
|
|
5
5
|
import { spawnSync, execFile } from 'node:child_process';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
|
+
import { createHmac, createHash } from 'node:crypto';
|
|
7
8
|
|
|
8
9
|
import { evaluateArtifacts, evaluateCommandRisk, evaluateDeployPlan } from './risk-rule-engine.mjs';
|
|
9
10
|
import { classifyTextCommand, redactSecrets } from './safety-policy.mjs';
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
resolveCredentialsWithRuntime,
|
|
49
50
|
} from './auth/credentials.mjs';
|
|
50
51
|
import { trackToolInvoke, trackSkillRetrieve, clearUserHash } from './telemetry/telemetry.mjs';
|
|
52
|
+
import { fetchWithProxy } from './proxy/proxy-agent.mjs';
|
|
51
53
|
import { fingerprint, runHcloudConfigure, resolveManagedProfile } from './auth/reconcile.mjs';
|
|
52
54
|
import {
|
|
53
55
|
getCachedUpdateInfo,
|
|
@@ -846,6 +848,32 @@ export const TOOL_DEFINITIONS = [
|
|
|
846
848
|
},
|
|
847
849
|
},
|
|
848
850
|
},
|
|
851
|
+
{
|
|
852
|
+
name: 'huaweicloud_obs_set_website_config',
|
|
853
|
+
description:
|
|
854
|
+
'配置 OBS 桶的静态网站托管。KooCLI OBS 不支持 SetBucketWebsite API,此工具内部实现 AWS4 签名调用 OBS REST API,屏蔽签名细节。支持 set(配置)、get(查询)、delete(删除)三种操作。操作前需确保桶已创建且已设置 public-read ACL。',
|
|
855
|
+
inputSchema: {
|
|
856
|
+
type: 'object',
|
|
857
|
+
properties: {
|
|
858
|
+
action: {
|
|
859
|
+
type: 'string',
|
|
860
|
+
enum: ['set', 'get', 'delete'],
|
|
861
|
+
description: '操作类型:set=配置静态网站托管,get=查询当前配置,delete=删除配置',
|
|
862
|
+
},
|
|
863
|
+
bucket: { type: 'string', description: 'OBS 桶名称' },
|
|
864
|
+
region: { type: 'string', description: 'OBS 桶所在区域,如 cn-north-4' },
|
|
865
|
+
indexDocument: {
|
|
866
|
+
type: 'string',
|
|
867
|
+
description: '首页文件名(action=set 时必填),如 index.html',
|
|
868
|
+
},
|
|
869
|
+
errorDocument: {
|
|
870
|
+
type: 'string',
|
|
871
|
+
description: '错误页面文件名(action=set 时可选),如 404.html 或 error.html',
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
required: ['action', 'bucket', 'region'],
|
|
875
|
+
},
|
|
876
|
+
},
|
|
849
877
|
];
|
|
850
878
|
|
|
851
879
|
function toolInvokeValue(name, args) {
|
|
@@ -1448,19 +1476,26 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
|
|
|
1448
1476
|
case 'huaweicloud_voucher_claim':
|
|
1449
1477
|
return await hdkitVoucherClaim(args.domain_id);
|
|
1450
1478
|
case 'huaweicloud_check_update':
|
|
1451
|
-
return await handleCheckUpdate(args, { sessionId: opts?.sessionId });
|
|
1479
|
+
return await handleCheckUpdate(args, { sessionId: opts?.sessionId, doQuery: opts?.doQuery });
|
|
1452
1480
|
case 'huaweicloud_upgrade':
|
|
1453
1481
|
return await handleUpgrade(args, { sessionId: opts?.sessionId });
|
|
1482
|
+
case 'huaweicloud_obs_set_website_config':
|
|
1483
|
+
return await handleObsWebsiteConfig(args);
|
|
1454
1484
|
default:
|
|
1455
1485
|
throw new Error(`Unknown tool: ${name}`);
|
|
1456
1486
|
}
|
|
1457
1487
|
}
|
|
1458
1488
|
|
|
1459
1489
|
async function handleCheckUpdate(args = {}, opts = {}) {
|
|
1460
|
-
const sessionId =
|
|
1490
|
+
const { sessionId = null, doQuery } = opts;
|
|
1461
1491
|
const current = readInstalledVersion() || '0.0.0';
|
|
1462
1492
|
if (args.dismiss === true) {
|
|
1463
|
-
const distTags = await getUpdateDistTags(current, { sessionId });
|
|
1493
|
+
const distTags = await getUpdateDistTags(current, { sessionId, doQuery });
|
|
1494
|
+
if (!distTags) {
|
|
1495
|
+
// 查询失败:不降级为 current 冷却、不写 skip,返回 check_failed。
|
|
1496
|
+
// 不调 invalidateUpdateCache(),以保留 failedAt 的 5 分钟失败节流。
|
|
1497
|
+
return judgeUpdate(current, null, null);
|
|
1498
|
+
}
|
|
1464
1499
|
const target = determineTarget(current, distTags);
|
|
1465
1500
|
const dismissedVersion =
|
|
1466
1501
|
typeof args.dismissVersion === 'string' && args.dismissVersion ? args.dismissVersion : target || current;
|
|
@@ -1468,7 +1503,7 @@ async function handleCheckUpdate(args = {}, opts = {}) {
|
|
|
1468
1503
|
invalidateUpdateCache();
|
|
1469
1504
|
return judgeUpdate(current, distTags, state);
|
|
1470
1505
|
}
|
|
1471
|
-
return getCachedUpdateInfo(current, { sessionId });
|
|
1506
|
+
return getCachedUpdateInfo(current, { sessionId, doQuery });
|
|
1472
1507
|
}
|
|
1473
1508
|
|
|
1474
1509
|
async function handleUpgrade(args = {}, opts = {}) {
|
|
@@ -2255,3 +2290,112 @@ async function getRegionalAvailability(service, region) {
|
|
|
2255
2290
|
export function classifyRawCommand(command) {
|
|
2256
2291
|
return classifyTextCommand(command);
|
|
2257
2292
|
}
|
|
2293
|
+
|
|
2294
|
+
// ── OBS Static Website Hosting (AWS4 signed REST API) ──
|
|
2295
|
+
|
|
2296
|
+
async function handleObsWebsiteConfig(args) {
|
|
2297
|
+
const { action, bucket, region, indexDocument, errorDocument } = args;
|
|
2298
|
+
if (!bucket || !region) {
|
|
2299
|
+
throw new Error('bucket and region are required');
|
|
2300
|
+
}
|
|
2301
|
+
const creds = resolveCredentialsWithRuntime({});
|
|
2302
|
+
if (!creds?.ak || !creds?.sk) {
|
|
2303
|
+
throw new Error('OBS website config requires AK/SK credentials. Run huaweicloud_auth_init first.');
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
const host = `${bucket}.obs.${region}.myhuaweicloud.com`;
|
|
2307
|
+
const endpoint = `https://${host}`;
|
|
2308
|
+
|
|
2309
|
+
if (action === 'get') {
|
|
2310
|
+
const res = await obsSignedRequest('GET', endpoint, '/?website', '', creds, region);
|
|
2311
|
+
return { ok: res.status === 200, status: res.status, body: res.body };
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
if (action === 'delete') {
|
|
2315
|
+
const res = await obsSignedRequest('DELETE', endpoint, '/?website', '', creds, region);
|
|
2316
|
+
return { ok: res.status === 204, status: res.status };
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
if (action === 'set') {
|
|
2320
|
+
if (!indexDocument) {
|
|
2321
|
+
throw new Error('indexDocument is required for action=set');
|
|
2322
|
+
}
|
|
2323
|
+
const xmlParts = ['<WebsiteConfiguration>', ` <IndexDocument><Suffix>${indexDocument}</Suffix></IndexDocument>`];
|
|
2324
|
+
if (errorDocument) {
|
|
2325
|
+
xmlParts.push(` <ErrorDocument><Key>${errorDocument}</Key></ErrorDocument>`);
|
|
2326
|
+
}
|
|
2327
|
+
xmlParts.push('</WebsiteConfiguration>');
|
|
2328
|
+
const body = xmlParts.join('\n');
|
|
2329
|
+
const res = await obsSignedRequest('PUT', endpoint, '/?website', body, creds, region);
|
|
2330
|
+
const websiteUrl = `http://${bucket}.obs-website.${region}.myhuaweicloud.com`;
|
|
2331
|
+
return {
|
|
2332
|
+
ok: res.status === 200,
|
|
2333
|
+
status: res.status,
|
|
2334
|
+
websiteUrl,
|
|
2335
|
+
message:
|
|
2336
|
+
res.status === 200
|
|
2337
|
+
? `Static website hosting configured. Website URL: ${websiteUrl} (may take ~1 min to propagate)`
|
|
2338
|
+
: `Failed to configure website: HTTP ${res.status}`,
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
throw new Error(`Unknown action: ${action}. Use set, get, or delete.`);
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
async function obsSignedRequest(method, endpoint, pathAndQuery, body, creds, region) {
|
|
2346
|
+
const now = new Date();
|
|
2347
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
2348
|
+
const amzDate = dateStamp + 'T' + now.toISOString().slice(11, 19).replace(/:/g, '') + 'Z';
|
|
2349
|
+
const payloadHash = createHash('sha256').update(body).digest('hex');
|
|
2350
|
+
|
|
2351
|
+
const url = new URL(endpoint + pathAndQuery);
|
|
2352
|
+
const canonicalUri = '/';
|
|
2353
|
+
const canonicalQueryString = 'website=';
|
|
2354
|
+
const canonicalHeaders = `host:${url.host}\n` + `x-amz-content-sha256:${payloadHash}\n` + `x-amz-date:${amzDate}\n`;
|
|
2355
|
+
const signedHeaders = 'host;x-amz-content-sha256;x-amz-date';
|
|
2356
|
+
const canonicalRequest = [
|
|
2357
|
+
method,
|
|
2358
|
+
canonicalUri,
|
|
2359
|
+
canonicalQueryString,
|
|
2360
|
+
canonicalHeaders,
|
|
2361
|
+
signedHeaders,
|
|
2362
|
+
payloadHash,
|
|
2363
|
+
].join('\n');
|
|
2364
|
+
|
|
2365
|
+
const credentialScope = `${dateStamp}/${region}/s3/aws4_request`;
|
|
2366
|
+
const stringToSign = [
|
|
2367
|
+
'AWS4-HMAC-SHA256',
|
|
2368
|
+
amzDate,
|
|
2369
|
+
credentialScope,
|
|
2370
|
+
createHash('sha256').update(canonicalRequest).digest('hex'),
|
|
2371
|
+
].join('\n');
|
|
2372
|
+
|
|
2373
|
+
const kDate = createHmac('sha256', 'AWS4' + creds.sk)
|
|
2374
|
+
.update(dateStamp)
|
|
2375
|
+
.digest();
|
|
2376
|
+
const kRegion = createHmac('sha256', kDate).update(region).digest();
|
|
2377
|
+
const kService = createHmac('sha256', kRegion).update('s3').digest();
|
|
2378
|
+
const kSigning = createHmac('sha256', kService).update('aws4_request').digest();
|
|
2379
|
+
const signature = createHmac('sha256', kSigning).update(stringToSign).digest('hex');
|
|
2380
|
+
|
|
2381
|
+
const authorization =
|
|
2382
|
+
`AWS4-HMAC-SHA256 Credential=${creds.ak}/${credentialScope}, ` +
|
|
2383
|
+
`SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
2384
|
+
|
|
2385
|
+
const headers = {
|
|
2386
|
+
Host: url.host,
|
|
2387
|
+
'x-amz-content-sha256': payloadHash,
|
|
2388
|
+
'x-amz-date': amzDate,
|
|
2389
|
+
Authorization: authorization,
|
|
2390
|
+
};
|
|
2391
|
+
if (body) headers['Content-Type'] = 'application/xml';
|
|
2392
|
+
if (creds.securityToken) headers['x-amz-security-token'] = creds.securityToken;
|
|
2393
|
+
|
|
2394
|
+
const res = await fetchWithProxy(endpoint + pathAndQuery, {
|
|
2395
|
+
method,
|
|
2396
|
+
headers,
|
|
2397
|
+
body: body || undefined,
|
|
2398
|
+
});
|
|
2399
|
+
const resBody = await res.text();
|
|
2400
|
+
return { status: res.status, body: resBody };
|
|
2401
|
+
}
|
|
@@ -114,6 +114,10 @@ export function judgeUpdate(current, distTags, skipState, now = Date.now()) {
|
|
|
114
114
|
if (!distTags) {
|
|
115
115
|
return buildResult(current, null, { result: 'check_failed', note: '检测失败,不影响使用' });
|
|
116
116
|
}
|
|
117
|
+
// truthy 但无有效版本(如 failure 折叠后的 { latest:null, next:null })同样视为检测失败
|
|
118
|
+
if (!distTags.latest && !distTags.next) {
|
|
119
|
+
return buildResult(current, null, { result: 'check_failed', note: '检测失败,不影响使用' });
|
|
120
|
+
}
|
|
117
121
|
const target = determineTarget(current, distTags);
|
|
118
122
|
if (!target || semverCompare(target, current) <= 0) {
|
|
119
123
|
return buildResult(current, distTags, { result: 'up_to_date', target: target ?? null });
|
|
@@ -348,9 +352,9 @@ export function peekCachedUpdateInfo() {
|
|
|
348
352
|
return lastHint && lastHint.updateAvailable && lastHint.targetVersion ? lastHint : null;
|
|
349
353
|
}
|
|
350
354
|
|
|
351
|
-
export async function getUpdateDistTags(current, { sessionId = null } = {}) {
|
|
352
|
-
const result = await getCachedUpdateInfo(current, { sessionId });
|
|
353
|
-
if (!result) return null;
|
|
355
|
+
export async function getUpdateDistTags(current, { sessionId = null, doQuery } = {}) {
|
|
356
|
+
const result = await getCachedUpdateInfo(current, { sessionId, doQuery });
|
|
357
|
+
if (!result || result.result === 'check_failed') return null;
|
|
354
358
|
return { latest: result.latestStable ?? null, next: result.latestNext ?? null };
|
|
355
359
|
}
|
|
356
360
|
|