huaweicloud-devkit 1.1.8-next.3 → 1.1.9-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 +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/references/common-pitfalls.md +30 -3
- package/plugins/huaweicloud-core/skills/huawei-voucher/SKILL.md +1 -0
- package/plugins/huaweicloud-core/src/auth/credential-validator.mjs +37 -2
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +14 -27
- package/plugins/huaweicloud-core/src/auth/guidance.mjs +112 -0
- package/plugins/huaweicloud-core/src/auth/reconcile.mjs +10 -2
- package/plugins/huaweicloud-core/src/auth/service.mjs +54 -46
- package/plugins/huaweicloud-core/src/hcloud-probe.mjs +26 -6
- package/plugins/huaweicloud-core/src/tools.mjs +2 -1
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.9-next.0",
|
|
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.
|
|
20
|
+
"version": "1.1.9-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.9-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.9-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.9-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.9-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.9-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.",
|
|
@@ -54,10 +54,37 @@ hcloud OBS config -i
|
|
|
54
54
|
|
|
55
55
|
KooCLI OBS **不支持** `SetBucketWebsite` 命令。配置静态网站托管需要:
|
|
56
56
|
|
|
57
|
-
1. **
|
|
58
|
-
2.
|
|
57
|
+
1. **MCP 工具**: `huaweicloud_obs_set_website_config`(插件内置,AWS4 签名处理,action=set/get/delete,实测可用)
|
|
58
|
+
2. **REST API**: `PUT /?website` 到桶的 endpoint
|
|
59
|
+
3. **控制台**: OBS 控制台 → 桶 → 静态网站托管 → 开启
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
## Content-Disposition 强制下载陷阱(2022-01-01 后创建的桶)
|
|
62
|
+
|
|
63
|
+
**实测发现(2026-10,cn-north-4)**:2022 年 1 月 1 日之后创建的 OBS 桶,对象 **GET 响应被 OBS 强制注入 `Content-Disposition: attachment`**——浏览器访问 HTML 一律下载而不是渲染,静态网站托管端点(`<bucket>.obs-website.<region>.myhuaweicloud.com`)也不例外,与对象元数据无关。
|
|
64
|
+
|
|
65
|
+
| 请求方式 | 返回的 Content-Disposition | 说明 |
|
|
66
|
+
| ----------------------- | ---------------------------- | -------------------------- |
|
|
67
|
+
| GET(浏览器、在线预览) | `attachment`(OBS 强制注入) | 网页被下载,无法渲染 |
|
|
68
|
+
| HEAD | 元数据中的值(如 inline) | 会误导诊断,不是浏览器行为 |
|
|
69
|
+
|
|
70
|
+
**避坑方法**:
|
|
71
|
+
|
|
72
|
+
1. **验证网页行为必须用 GET 直连**(`curl -s -D - -o NUL <url>` 或浏览器)——HEAD 和代理层会给出假象
|
|
73
|
+
2. **官方解法**:绑定自定义域名接入 **CDN**,在"CDN 控制台 > 域名管理 > 高级设置 > HTTP header 配置"设置 `Content-Disposition: inline`(参见华为云 CDN 文档《如何在浏览器中在线预览 OBS 中的对象?》)
|
|
74
|
+
3. 跨云对照:阿里云 OSS 同样策略(Bucket 域名访问 HTML 强制下载,绑定自定义域名即可在线预览)
|
|
75
|
+
|
|
76
|
+
**对静态托管的结论**:2022 年后创建的桶,裸 OBS(含 obs-website 端点)**无法交付可渲染的 web 站点**;自定义域名 + CDN(+ header 配置 inline)是**必要条件**,不是增强项。
|
|
77
|
+
|
|
78
|
+
## obsutil 元数据大小写陷阱
|
|
79
|
+
|
|
80
|
+
`cp -meta=content-disposition:inline`(小写)会被存为用户自定义元数据 `x-amz-meta-content-disposition`,**不命中标准头**;标准 HTTP 头(Content-Type、Content-Disposition、Cache-Control 等)必须按标准大小写书写:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 错误:存为 x-amz-meta-content-disposition,浏览器不识别
|
|
84
|
+
hcloud OBS cp index.html obs://<bucket>/ -meta=content-disposition:inline
|
|
85
|
+
# 正确:命中标准 Content-Disposition 头
|
|
86
|
+
hcloud OBS cp index.html obs://<bucket>/ -meta=Content-Disposition:inline
|
|
87
|
+
```
|
|
61
88
|
|
|
62
89
|
## 权限模型(三层)
|
|
63
90
|
|
|
@@ -74,6 +74,7 @@ version: 2
|
|
|
74
74
|
| `code=HDKIT_NOT_SIGNED_UP`(您尚未报名…) | 未报名该赛事 | 告知需先完成赛事报名后再来领取 |
|
|
75
75
|
| `已领取过` / `已领取` | 该账号已领过赛事券 | 告知已领取,不打扰 |
|
|
76
76
|
| `code=HDKIT_COMPETITION_NOT_FOUND`(赛事不存在或已下线) | 赛事配置异常 | 告知赛事暂不可用,稍后再试 |
|
|
77
|
+
| `code=HDKIT_COMPETITION_NOT_CONFIGURED`(赛事代金券服务配置缺失…) | 服务端活动参数未配置 | 告知赛事代金券暂未开放,稍后再试 |
|
|
77
78
|
| `本月代金券总额度已用完,所有账号均无法领取` | 全局额度用尽(HD.60630042) | 告知下月再试 |
|
|
78
79
|
| `请先完成实名认证` | 账号未实名(HD.60630022) | 引导去控制台实名认证 |
|
|
79
80
|
| `code=HDKIT_CRED_*` / 含「未配置 / not configured / AK/SK 无效 / 限额」 | 凭证未配置、无效或达限额 | 引导用户检查并配置 AK/SK |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import { getProxyDispatcher } from '../proxy/proxy-agent.mjs';
|
|
4
|
+
import { errorFromTable } from './guidance.mjs';
|
|
4
5
|
|
|
5
6
|
function iamBaseUrl() {
|
|
6
7
|
return process.env.HW_IAM_ENDPOINT || 'https://iam.myhuaweicloud.com';
|
|
@@ -81,15 +82,28 @@ function projectForRegion(projects, region) {
|
|
|
81
82
|
* SDK-HMAC-SHA256 request signing. A wrong SK produces an invalid signature,
|
|
82
83
|
* which IAM rejects with HTTP 401 before any project data is returned.
|
|
83
84
|
*
|
|
84
|
-
* Returns { valid, projectId, error, warning }:
|
|
85
|
+
* Returns { valid, projectId, error, warning, code, messageZh, action, nextStep }:
|
|
85
86
|
* - valid: true when IAM verified the signature (or the rejection is not
|
|
86
87
|
* authentication-related), false when the credentials are unusable.
|
|
87
88
|
* - projectId: first project matching `region`, else the first visible project.
|
|
88
89
|
* - warning: set when credentials passed but project discovery was denied.
|
|
90
|
+
* - code/messageZh/action/nextStep: unified #900 shape from the shared
|
|
91
|
+
* ERROR_TABLE (guidance.mjs). The English `error`/`warning` detail text is
|
|
92
|
+
* intentionally kept — existing consumers (tools.mjs sandbox_credentials,
|
|
93
|
+
* external string-matching tests) rely on it.
|
|
89
94
|
*/
|
|
90
95
|
export async function validateIamCredentials({ ak, sk, securityToken, region, timeoutMs = 15000 } = {}) {
|
|
91
96
|
if (!ak || !sk) {
|
|
92
|
-
|
|
97
|
+
const e = errorFromTable('HDKIT_CRED_MISSING');
|
|
98
|
+
return {
|
|
99
|
+
valid: false,
|
|
100
|
+
projectId: null,
|
|
101
|
+
error: 'AK and SK are both required for credential validation.',
|
|
102
|
+
code: e.code,
|
|
103
|
+
messageZh: e.messageZh,
|
|
104
|
+
action: e.action,
|
|
105
|
+
nextStep: e.nextStep,
|
|
106
|
+
};
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
const base = iamBaseUrl();
|
|
@@ -115,11 +129,16 @@ export async function validateIamCredentials({ ak, sk, securityToken, region, ti
|
|
|
115
129
|
resp = await fetch(url, fetchOpts);
|
|
116
130
|
}
|
|
117
131
|
} catch (error) {
|
|
132
|
+
const e = errorFromTable('HDKIT_CRED_UNEXPECTED', error.message);
|
|
118
133
|
return {
|
|
119
134
|
valid: false,
|
|
120
135
|
projectId: null,
|
|
121
136
|
skipped: true,
|
|
122
137
|
error: `IAM validation request failed (treating credentials as unverified): ${error.message}`,
|
|
138
|
+
code: e.code,
|
|
139
|
+
messageZh: e.messageZh,
|
|
140
|
+
action: e.action,
|
|
141
|
+
nextStep: e.nextStep,
|
|
123
142
|
};
|
|
124
143
|
} finally {
|
|
125
144
|
clearTimeout(timer);
|
|
@@ -138,26 +157,42 @@ export async function validateIamCredentials({ ak, sk, securityToken, region, ti
|
|
|
138
157
|
|
|
139
158
|
if (resp.status === 401) {
|
|
140
159
|
const msg = data?.error?.message || text.slice(0, 200);
|
|
160
|
+
const e = errorFromTable('HDKIT_CRED_INVALID', `HTTP 401: ${msg}`);
|
|
141
161
|
return {
|
|
142
162
|
valid: false,
|
|
143
163
|
projectId: null,
|
|
144
164
|
error: `IAM rejected the credentials (HTTP 401: ${msg}). The AK/SK is invalid - check the SK for typos or expired security tokens.`,
|
|
165
|
+
code: e.code,
|
|
166
|
+
messageZh: e.messageZh,
|
|
167
|
+
action: e.action,
|
|
168
|
+
nextStep: e.nextStep,
|
|
145
169
|
};
|
|
146
170
|
}
|
|
147
171
|
|
|
148
172
|
if (resp.status === 403) {
|
|
173
|
+
const msg = data?.error?.message || text.slice(0, 200);
|
|
174
|
+
const e = errorFromTable('HDKIT_CRED_FORBIDDEN', `HTTP 403: ${msg}`);
|
|
149
175
|
return {
|
|
150
176
|
valid: true,
|
|
151
177
|
projectId: null,
|
|
152
178
|
error: null,
|
|
153
179
|
warning: `Credentials signed successfully but project listing was denied (HTTP 403). Continuing without project_id.`,
|
|
180
|
+
code: e.code,
|
|
181
|
+
messageZh: e.messageZh,
|
|
182
|
+
action: e.action,
|
|
183
|
+
nextStep: e.nextStep,
|
|
154
184
|
};
|
|
155
185
|
}
|
|
156
186
|
|
|
187
|
+
const e = errorFromTable('HDKIT_CRED_UNEXPECTED', `HTTP ${resp.status}`);
|
|
157
188
|
return {
|
|
158
189
|
valid: false,
|
|
159
190
|
projectId: null,
|
|
160
191
|
skipped: true,
|
|
161
192
|
error: `Unexpected IAM response (HTTP ${resp.status}): ${text.slice(0, 200)}`,
|
|
193
|
+
code: e.code,
|
|
194
|
+
messageZh: e.messageZh,
|
|
195
|
+
action: e.action,
|
|
196
|
+
nextStep: e.nextStep,
|
|
162
197
|
};
|
|
163
198
|
}
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
import { dirname, join } from 'node:path';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
|
|
14
|
+
import { buildOnboardingSteps, makeError } from './guidance.mjs';
|
|
15
|
+
|
|
14
16
|
// Verify a credential file ended up with 0600. On Windows-mounted drives inside WSL
|
|
15
17
|
// (drvfs/9p) chmod is silently ignored, so the file can be world-readable (0777).
|
|
16
18
|
// Native Windows has no POSIX modes (statSync always reports 0666), so skip the check there.
|
|
@@ -203,7 +205,8 @@ export function writeObsConfig(credentials = {}) {
|
|
|
203
205
|
const sk = String(credentials.sk || '');
|
|
204
206
|
const securityToken = String(credentials.securityToken || '');
|
|
205
207
|
if (!region || !ak || !sk) {
|
|
206
|
-
|
|
208
|
+
// #900: unified error shape from the shared ERROR_TABLE.
|
|
209
|
+
throw makeError('HDKIT_CRED_REGION_MISSING');
|
|
207
210
|
}
|
|
208
211
|
const path = obsConfigPath();
|
|
209
212
|
const endpoint = credentials.endpoint || `https://obs.${region}.myhuaweicloud.com`;
|
|
@@ -263,43 +266,25 @@ export function resolveCredentials(options = {}) {
|
|
|
263
266
|
|
|
264
267
|
if (!ak || !sk) {
|
|
265
268
|
if (options.allowMissing) return null;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
)
|
|
269
|
-
err.code = 'HDKIT_CRED_MISSING';
|
|
270
|
-
// Lightweight onboarding hint. HDKIT_CRED_MISSING fires when no credential
|
|
271
|
-
// resolved: with env empty/masked and S1 absent, the scenario is 3 by
|
|
272
|
-
// default. Scenario 4 (import a config-provided account) is only reachable
|
|
273
|
-
// in the narrow case where CodeArts S4 offers real creds that the resolver
|
|
274
|
-
// still couldn't adopt (e.g. CODEARTS_PROJECT_DIR set but the marker dir is
|
|
275
|
-
// absent). Full guidance comes from getAuthStatus(); keep this branch cheap.
|
|
269
|
+
// #900: unified guidance — one steps builder shared with computeOnboarding,
|
|
270
|
+
// and the error shape from the single ERROR_TABLE. Zero IO beyond the
|
|
271
|
+
// readCodeArtsCredentials() probe we already did above for scenario routing.
|
|
276
272
|
const codeartsCreds = readCodeArtsCredentials();
|
|
277
273
|
const injectedAvailable = present(codeartsCreds?.ak) && present(codeartsCreds?.sk);
|
|
274
|
+
const codearts = isCodeArtsContext();
|
|
275
|
+
const err = makeError('HDKIT_CRED_MISSING');
|
|
278
276
|
err.onboarding = injectedAvailable
|
|
279
277
|
? {
|
|
280
278
|
scenario: 4,
|
|
281
279
|
reason: 'import-injected',
|
|
282
280
|
message: '检测到配置中已有有效账号,可导入为正式凭证。',
|
|
283
|
-
steps:
|
|
284
|
-
? [{ action: 'auth_switch', args: { mode: 'mcp-config', action: 'persist' }, label: '导入配置中的账号' }]
|
|
285
|
-
: [
|
|
286
|
-
{
|
|
287
|
-
action: 'auth_switch',
|
|
288
|
-
args: { mode: 'import', action: 'persist' },
|
|
289
|
-
label: '通过 creds-import.json 导入',
|
|
290
|
-
},
|
|
291
|
-
],
|
|
281
|
+
steps: buildOnboardingSteps(4, { codearts }),
|
|
292
282
|
}
|
|
293
283
|
: {
|
|
294
284
|
scenario: 3,
|
|
295
285
|
reason: 's1-missing',
|
|
296
286
|
message: '未配置华为云凭证,需要完成登录后才能使用云能力。',
|
|
297
|
-
steps:
|
|
298
|
-
? [
|
|
299
|
-
{ action: 'write-import', target: join(homedir(), '.config', 'huaweicloud', 'creds-import.json') },
|
|
300
|
-
{ action: 'auth_switch', args: { mode: 'import', action: 'persist' } },
|
|
301
|
-
]
|
|
302
|
-
: [{ action: 'auth-init', args: {} }],
|
|
287
|
+
steps: buildOnboardingSteps(3, { codearts }),
|
|
303
288
|
};
|
|
304
289
|
throw err;
|
|
305
290
|
}
|
|
@@ -320,7 +305,9 @@ export function getParentCwd() {
|
|
|
320
305
|
}
|
|
321
306
|
}
|
|
322
307
|
|
|
323
|
-
|
|
308
|
+
// Exported for tests: the failure path and the tests must agree on which
|
|
309
|
+
// onboarding variant (codearts vs plain) the current environment selects.
|
|
310
|
+
export function isCodeArtsContext() {
|
|
324
311
|
return (
|
|
325
312
|
existsSync(join(process.cwd(), '.codeartsdoer')) ||
|
|
326
313
|
existsSync(join(homedir(), '.codeartsdoer')) ||
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// plugins/huaweicloud-core/src/auth/guidance.mjs
|
|
2
|
+
// Single Source of Truth for onboarding guidance steps and the error surface.
|
|
3
|
+
// Leaf module: imports NOTHING from auth/ siblings. Zero IO — pure builders.
|
|
4
|
+
// Callers: credentials.mjs (failure path), service.mjs (computeOnboarding),
|
|
5
|
+
// credential-validator / reconcile / hcloud-probe (error shapes).
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
export function buildOnboardingSteps(scenario, { codearts = false } = {}) {
|
|
10
|
+
switch (scenario) {
|
|
11
|
+
case 3: {
|
|
12
|
+
const base = [{ order: 1, action: 'obtain-aksk', target: 'console', label: '获取 AK/SK(华为云控制台)' }];
|
|
13
|
+
if (codearts) {
|
|
14
|
+
return [
|
|
15
|
+
...base,
|
|
16
|
+
{
|
|
17
|
+
order: 2,
|
|
18
|
+
action: 'write-import',
|
|
19
|
+
target: join(homedir(), '.config', 'huaweicloud', 'creds-import.json'),
|
|
20
|
+
label: '把 AK/SK 写入 creds-import.json',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
order: 3,
|
|
24
|
+
action: 'auth_switch',
|
|
25
|
+
// QA #905 Required: scenario-3 codearts flow writes creds-import.json
|
|
26
|
+
// in step 2, so the import must READ that file (mode:'import').
|
|
27
|
+
// mode:'mcp-config' reads the CodeArts injected environment, which
|
|
28
|
+
// is empty/placeholder in scenario 3 — the channel would not close.
|
|
29
|
+
args: { mode: 'import', action: 'persist' },
|
|
30
|
+
label: '执行导入完成配置',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
return [...base, { order: 2, action: 'auth-init', args: {}, label: '运行 npx huaweicloud-devkit auth init' }];
|
|
35
|
+
}
|
|
36
|
+
case 4:
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
order: 1,
|
|
40
|
+
action: 'auth_switch',
|
|
41
|
+
args: { mode: codearts ? 'mcp-config' : 'import', action: 'persist' },
|
|
42
|
+
label: '导入该账号',
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
default:
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// #900 error contract table. messageZh: Chinese, human-readable, no English
|
|
51
|
+
// sentences, no internal codenames. action: directly executable. nextStep is
|
|
52
|
+
// kept as a derived compatibility field for existing consumers.
|
|
53
|
+
export const ERROR_TABLE = {
|
|
54
|
+
HDKIT_CRED_MISSING: {
|
|
55
|
+
messageZh: '未配置华为云凭证,需要先完成登录。',
|
|
56
|
+
action: 'npx huaweicloud-devkit auth init',
|
|
57
|
+
},
|
|
58
|
+
HDKIT_CRED_INVALID: {
|
|
59
|
+
messageZh: '凭证已被华为云拒绝(AK/SK 无效或已过期),请重新生成。',
|
|
60
|
+
action: 'npx huaweicloud-devkit auth status',
|
|
61
|
+
},
|
|
62
|
+
HDKIT_CRED_FORBIDDEN: {
|
|
63
|
+
messageZh: '凭证有效但无权限列出项目,请检查账号授权。',
|
|
64
|
+
action: 'hcloud IAM KeystoneListProjects --cli-region=<region>',
|
|
65
|
+
},
|
|
66
|
+
HDKIT_CRED_REGION_MISSING: {
|
|
67
|
+
messageZh: '缺少 region(区域),登录配置不完整。',
|
|
68
|
+
action: 'hcloud configure set --cli-region=<region>',
|
|
69
|
+
},
|
|
70
|
+
HDKIT_KOOCLI_NOT_READY: {
|
|
71
|
+
messageZh: 'KooCLI(hcloud)未安装或未就绪。',
|
|
72
|
+
action: 'npx huaweicloud-devkit install-hcloud',
|
|
73
|
+
},
|
|
74
|
+
HDKIT_PROJECT_ID_MISSING: {
|
|
75
|
+
messageZh: 'KooCLI 配置缺少 project_id,导致同步失败。',
|
|
76
|
+
action: 'hcloud configure set --cli-project-id=<id>',
|
|
77
|
+
},
|
|
78
|
+
// Runtime credentials (in-process, set via auth_init/auth_switch) suppress
|
|
79
|
+
// auto-sync (rule R10). No `auth sync --force` flag exists; the real unblock
|
|
80
|
+
// is clearing the runtime store first, then re-running the sync.
|
|
81
|
+
HDKIT_SYNC_SKIPPED_RUNTIME: {
|
|
82
|
+
messageZh: '运行时凭证生效,自动同步已跳过。',
|
|
83
|
+
action: 'huaweicloud_auth_switch action=clear',
|
|
84
|
+
},
|
|
85
|
+
// IAM returned something other than 200/401/403 (or the request itself
|
|
86
|
+
// failed). The canonical debug hint lives in the cli-and-auth skill.
|
|
87
|
+
HDKIT_CRED_UNEXPECTED: {
|
|
88
|
+
messageZh: '凭证校验遇到异常响应,请开启调试排查。',
|
|
89
|
+
action: 'hcloud <Service> <Op> --cli-debug=true',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export function errorFromTable(code, detail) {
|
|
94
|
+
const e = ERROR_TABLE[code];
|
|
95
|
+
if (!e) return { code, messageZh: detail || String(code), action: '', nextStep: detail || '' };
|
|
96
|
+
return {
|
|
97
|
+
code,
|
|
98
|
+
messageZh: detail ? `${e.messageZh}(${detail})` : e.messageZh,
|
|
99
|
+
action: e.action,
|
|
100
|
+
nextStep: e.action,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function makeError(code, detail) {
|
|
105
|
+
const shape = errorFromTable(code, detail);
|
|
106
|
+
const err = new Error(shape.messageZh);
|
|
107
|
+
err.code = shape.code;
|
|
108
|
+
err.action = shape.action;
|
|
109
|
+
err.nextStep = shape.nextStep;
|
|
110
|
+
err.messageZh = shape.messageZh;
|
|
111
|
+
return err;
|
|
112
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
readLastSync,
|
|
13
13
|
resolveCredentialsWithRuntime,
|
|
14
14
|
} from './credentials.mjs';
|
|
15
|
+
import { errorFromTable } from './guidance.mjs';
|
|
15
16
|
import { resolveHcloudCommand } from '../hcloud-probe.mjs';
|
|
16
17
|
import { redactSecrets } from '../safety-policy.mjs';
|
|
17
18
|
|
|
@@ -43,7 +44,12 @@ export function isManualModified(path) {
|
|
|
43
44
|
|
|
44
45
|
export function readKooCliProfiles() {
|
|
45
46
|
const configPath = kooCliConfigPath();
|
|
46
|
-
if (!existsSync(configPath))
|
|
47
|
+
if (!existsSync(configPath)) {
|
|
48
|
+
// #900: bare string replaced by the unified ERROR_TABLE shape; the old
|
|
49
|
+
// English text survives only as the distinguishing detail argument.
|
|
50
|
+
const e = errorFromTable('HDKIT_KOOCLI_NOT_READY', 'KooCLI config not found');
|
|
51
|
+
return { error: e.messageZh, code: e.code, messageZh: e.messageZh, action: e.action, nextStep: e.nextStep };
|
|
52
|
+
}
|
|
47
53
|
try {
|
|
48
54
|
const raw = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
49
55
|
const current = String(raw.current || 'default');
|
|
@@ -72,7 +78,9 @@ export function readKooCliProfiles() {
|
|
|
72
78
|
),
|
|
73
79
|
};
|
|
74
80
|
} catch {
|
|
75
|
-
|
|
81
|
+
// #900: bare string replaced by the unified ERROR_TABLE shape.
|
|
82
|
+
const e = errorFromTable('HDKIT_KOOCLI_NOT_READY', 'KooCLI config parse failed');
|
|
83
|
+
return { error: e.messageZh, code: e.code, messageZh: e.messageZh, action: e.action, nextStep: e.nextStep };
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
86
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
2
|
|
|
5
3
|
import { getAgentRegistrationStatuses } from './agent-registration.mjs';
|
|
6
4
|
import { resolveAndApplyProjectId } from './project-id.mjs';
|
|
7
5
|
import {
|
|
8
6
|
globalCredentialsPath,
|
|
7
|
+
isCodeArtsContext,
|
|
9
8
|
isPlaceholder,
|
|
10
9
|
obsConfigPath,
|
|
11
10
|
parseStsExpiry,
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
writeLastSync,
|
|
16
15
|
writeObsConfig,
|
|
17
16
|
} from './credentials.mjs';
|
|
17
|
+
import { buildOnboardingSteps, errorFromTable } from './guidance.mjs';
|
|
18
18
|
import {
|
|
19
19
|
fingerprint,
|
|
20
20
|
exportStateForStatus,
|
|
@@ -22,15 +22,7 @@ import {
|
|
|
22
22
|
resolveManagedProfile,
|
|
23
23
|
runHcloudConfigure,
|
|
24
24
|
} from './reconcile.mjs';
|
|
25
|
-
import { hcloudProbeNextStep, probeHcloud } from '../hcloud-probe.mjs';
|
|
26
|
-
|
|
27
|
-
function isCodeArtsHome() {
|
|
28
|
-
return (
|
|
29
|
-
existsSync(join(process.cwd(), '.codeartsdoer')) ||
|
|
30
|
-
existsSync(join(homedir(), '.codeartsdoer')) ||
|
|
31
|
-
existsSync(join(homedir(), '.codeartswork'))
|
|
32
|
-
);
|
|
33
|
-
}
|
|
25
|
+
import { hcloudProbeNextStep, hcloudProbeRaw, probeHcloud } from '../hcloud-probe.mjs';
|
|
34
26
|
|
|
35
27
|
function envHasRealTriplet() {
|
|
36
28
|
return (
|
|
@@ -57,7 +49,9 @@ export function computeOnboarding({ credentials, reconciled } = {}) {
|
|
|
57
49
|
const envRealAk = !isPlaceholder(process.env.HW_ACCESS_KEY) && Boolean(process.env.HW_ACCESS_KEY);
|
|
58
50
|
const envRealSk = !isPlaceholder(process.env.HW_SECRET_KEY) && Boolean(process.env.HW_SECRET_KEY);
|
|
59
51
|
const envHasCreds = envRealAk && envRealSk;
|
|
60
|
-
|
|
52
|
+
// Same context detector the resolveCredentials failure path uses (#900): the
|
|
53
|
+
// codearts flag must select the identical builder variant on both paths.
|
|
54
|
+
const codeArts = isCodeArtsContext();
|
|
61
55
|
const accountHint = s1Has ? fingerprint(creds.ak, creds.sk) : null;
|
|
62
56
|
|
|
63
57
|
let scenario;
|
|
@@ -133,32 +127,12 @@ export function computeOnboarding({ credentials, reconciled } = {}) {
|
|
|
133
127
|
scenario = 4;
|
|
134
128
|
reason = 'import-injected';
|
|
135
129
|
message = '检测到配置中已有有效账号,可导入为正式凭证。';
|
|
136
|
-
steps =
|
|
137
|
-
{
|
|
138
|
-
order: 1,
|
|
139
|
-
action: 'switch-persist',
|
|
140
|
-
args: { mode: codeArts ? 'mcp-config' : 'import', action: 'persist' },
|
|
141
|
-
label: '导入该账号',
|
|
142
|
-
},
|
|
143
|
-
];
|
|
130
|
+
steps = buildOnboardingSteps(4, { codearts: codeArts });
|
|
144
131
|
} else {
|
|
145
132
|
scenario = 3;
|
|
146
133
|
reason = 's1-missing';
|
|
147
134
|
message = '未配置华为云凭证,需要完成登录后才能使用云能力。';
|
|
148
|
-
steps =
|
|
149
|
-
{ order: 1, action: 'obtain-aksk', target: 'console', label: '获取 AK/SK(华为云控制台)' },
|
|
150
|
-
...(codeArts
|
|
151
|
-
? [
|
|
152
|
-
{
|
|
153
|
-
order: 2,
|
|
154
|
-
action: 'write-import',
|
|
155
|
-
target: join(homedir(), '.config', 'huaweicloud', 'creds-import.json'),
|
|
156
|
-
label: '把 AK/SK 写入 creds-import.json',
|
|
157
|
-
},
|
|
158
|
-
{ order: 3, action: 'auth_switch', args: { mode: 'import', action: 'persist' }, label: '执行导入完成配置' },
|
|
159
|
-
]
|
|
160
|
-
: [{ order: 2, action: 'auth-init', args: {}, label: '运行 npx huaweicloud-devkit auth init' }]),
|
|
161
|
-
];
|
|
135
|
+
steps = buildOnboardingSteps(3, { codearts: codeArts });
|
|
162
136
|
}
|
|
163
137
|
|
|
164
138
|
return { needsSetup: true, scenario, reason, message, steps, accountHint };
|
|
@@ -215,6 +189,7 @@ export function getAuthStatus(target = 'all') {
|
|
|
215
189
|
kooCliInstalled: hcloud.installed,
|
|
216
190
|
kooCliStatus: hcloud.status,
|
|
217
191
|
kooCliNextStep: hcloudProbeNextStep(hcloud),
|
|
192
|
+
kooCliRaw: hcloudProbeRaw(hcloud),
|
|
218
193
|
reconciled,
|
|
219
194
|
onboarding,
|
|
220
195
|
agents: getAgentRegistrationStatuses(target).agents,
|
|
@@ -224,17 +199,28 @@ export function getAuthStatus(target = 'all') {
|
|
|
224
199
|
export function syncAuth(target = 'all') {
|
|
225
200
|
const credentials = readGlobalCredentials();
|
|
226
201
|
if (!credentials?.ak || !credentials?.sk) {
|
|
202
|
+
// #900: unified error shape from the shared ERROR_TABLE.
|
|
203
|
+
const e = errorFromTable('HDKIT_CRED_MISSING');
|
|
227
204
|
return {
|
|
228
205
|
ok: false,
|
|
229
|
-
error:
|
|
230
|
-
|
|
206
|
+
error: e.messageZh,
|
|
207
|
+
code: e.code,
|
|
208
|
+
messageZh: e.messageZh,
|
|
209
|
+
action: e.action,
|
|
210
|
+
nextStep: e.nextStep,
|
|
231
211
|
};
|
|
232
212
|
}
|
|
233
213
|
if (hasRuntimeCredentials()) {
|
|
214
|
+
// R10: runtime credentials suppress auto-sync. The codename stays in code
|
|
215
|
+
// comments only, never in user-facing strings (#900).
|
|
216
|
+
const e = errorFromTable('HDKIT_SYNC_SKIPPED_RUNTIME');
|
|
234
217
|
return {
|
|
235
218
|
ok: false,
|
|
236
|
-
error:
|
|
237
|
-
|
|
219
|
+
error: e.messageZh,
|
|
220
|
+
code: e.code,
|
|
221
|
+
messageZh: e.messageZh,
|
|
222
|
+
action: e.action,
|
|
223
|
+
nextStep: e.nextStep,
|
|
238
224
|
};
|
|
239
225
|
}
|
|
240
226
|
|
|
@@ -264,25 +250,47 @@ export function syncAuth(target = 'all') {
|
|
|
264
250
|
|
|
265
251
|
const hcloud = probeHcloud();
|
|
266
252
|
if (!hcloud.ok) {
|
|
253
|
+
// #900: three probe states share the table code; the detail distinguishes.
|
|
254
|
+
const detail =
|
|
255
|
+
hcloud.status === 'sandbox_home_failure'
|
|
256
|
+
? '无法解析用户主目录'
|
|
257
|
+
: hcloud.status === 'privacy_pending'
|
|
258
|
+
? '隐私协议待同意'
|
|
259
|
+
: '';
|
|
260
|
+
const e = errorFromTable('HDKIT_KOOCLI_NOT_READY', detail);
|
|
267
261
|
return {
|
|
268
262
|
ok: false,
|
|
269
|
-
error:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
263
|
+
error: e.messageZh,
|
|
264
|
+
code: e.code,
|
|
265
|
+
messageZh: e.messageZh,
|
|
266
|
+
action: e.action,
|
|
267
|
+
// #900 issue comment: nextStep is the executable table action; the
|
|
268
|
+
// verbose English guidance survives only in the `raw` side-channel.
|
|
275
269
|
nextStep: hcloudProbeNextStep(hcloud),
|
|
270
|
+
raw: hcloudProbeRaw(hcloud),
|
|
276
271
|
obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
|
|
277
272
|
};
|
|
278
273
|
}
|
|
279
274
|
|
|
280
275
|
const { ok, error } = runHcloudConfigure(profile, credentials.ak, credentials.sk, credentials.region);
|
|
281
276
|
if (!ok) {
|
|
277
|
+
// #900: `hcloud configure set` failures are usually a missing/invalid
|
|
278
|
+
// project_id for the target region; anything else keeps the raw (already
|
|
279
|
+
// redacted) stderr as the detail argument.
|
|
280
|
+
const detail = String(error || '').trim();
|
|
281
|
+
const projectIdCause = /project[_\s-]?id/i.test(detail);
|
|
282
|
+
const e = projectIdCause
|
|
283
|
+
? errorFromTable('HDKIT_PROJECT_ID_MISSING', detail)
|
|
284
|
+
: errorFromTable('HDKIT_KOOCLI_NOT_READY', detail || 'KooCLI 配置同步失败');
|
|
282
285
|
return {
|
|
283
286
|
ok: false,
|
|
284
|
-
error:
|
|
285
|
-
|
|
287
|
+
error: e.messageZh,
|
|
288
|
+
code: e.code,
|
|
289
|
+
messageZh: e.messageZh,
|
|
290
|
+
action: e.action,
|
|
291
|
+
nextStep: projectIdCause
|
|
292
|
+
? e.nextStep
|
|
293
|
+
: 'Run "npx huaweicloud-devkit auth init" in a real terminal to refresh the KooCLI config.',
|
|
286
294
|
obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
|
|
287
295
|
};
|
|
288
296
|
}
|
|
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
|
+
import { errorFromTable } from './auth/guidance.mjs';
|
|
6
7
|
import { compareVersion, getKooCliVersion, parseHcloudVersion } from './koocli-version.mjs';
|
|
7
8
|
import { kooCliInstallLogPath } from './preflight.mjs';
|
|
8
9
|
import { redactSecrets } from './safety-policy.mjs';
|
|
@@ -140,19 +141,24 @@ export function probeHcloud(options = {}) {
|
|
|
140
141
|
};
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
// #900 (issue comment Required 1): every not-ready probe state routes through
|
|
145
|
+
// the shared ERROR_TABLE.HDKIT_KOOCLI_NOT_READY so nextStep is always a
|
|
146
|
+
// directly executable command (d11-41-nextstep-actionable). The verbose
|
|
147
|
+
// English guidance is preserved verbatim in hcloudProbeRaw() — a docs/debug
|
|
148
|
+
// side-channel — and must never leak back into the top-level nextStep.
|
|
149
|
+
export function hcloudProbeRaw(probe) {
|
|
150
|
+
if (probe?.status === 'ok')
|
|
145
151
|
return 'Use huaweicloud_show_profile_redacted to inspect the active KooCLI profile safely.';
|
|
146
|
-
if (probe
|
|
152
|
+
if (probe?.status === 'version_mismatch') {
|
|
147
153
|
return `KooCLI version mismatch: installed ${probe.installedVersion}, this plugin is paired with ${probe.requiredVersion}. Reinstall the pinned version (see huaweicloud-cli-and-auth skill) and restart the agent.`;
|
|
148
154
|
}
|
|
149
|
-
if (probe
|
|
155
|
+
if (probe?.status === 'sandbox_home_failure') {
|
|
150
156
|
return 'KooCLI was found, but this agent sandbox cannot resolve the Windows user home directory. Set HCLOUD_BIN to the full hcloud path, restart Codex, or verify from a normal terminal.';
|
|
151
157
|
}
|
|
152
|
-
if (probe
|
|
158
|
+
if (probe?.status === 'privacy_pending') {
|
|
153
159
|
return 'KooCLI requires accepting its one-time privacy agreement. Run hcloud version in a real terminal and accept the prompt, then restart the agent.';
|
|
154
160
|
}
|
|
155
|
-
if (probe
|
|
161
|
+
if (probe?.status === 'not_found') {
|
|
156
162
|
return (
|
|
157
163
|
'KooCLI is not installed yet. Background auto-install may be running — please retry shortly instead of assuming failure. To install now: npx huaweicloud-devkit install-hcloud. DevKit picks up the binary from its fixed install dirs (~/.local/bin on Linux, /usr/local/bin on macOS, ~/hcloud on Windows) or HCLOUD_BIN on the next call — no agent restart needed. Install progress: ' +
|
|
158
164
|
kooCliInstallLogPath()
|
|
@@ -160,3 +166,17 @@ export function hcloudProbeNextStep(probe) {
|
|
|
160
166
|
}
|
|
161
167
|
return 'Install Huawei Cloud KooCLI: npx huaweicloud-devkit install-hcloud. Configure credentials outside the agent conversation.';
|
|
162
168
|
}
|
|
169
|
+
|
|
170
|
+
export function hcloudProbeNextStep(probe) {
|
|
171
|
+
if (probe?.status === 'ok') return 'huaweicloud_show_profile_redacted';
|
|
172
|
+
const detail =
|
|
173
|
+
probe?.status === 'version_mismatch'
|
|
174
|
+
? `版本不匹配(已装 ${probe?.installedVersion ?? '未知'},插件配套 ${probe?.requiredVersion ?? '未知'})`
|
|
175
|
+
: probe?.status === 'sandbox_home_failure'
|
|
176
|
+
? '无法解析用户主目录'
|
|
177
|
+
: probe?.status === 'privacy_pending'
|
|
178
|
+
? '隐私协议待同意'
|
|
179
|
+
: '';
|
|
180
|
+
const e = errorFromTable('HDKIT_KOOCLI_NOT_READY', detail);
|
|
181
|
+
return e.nextStep;
|
|
182
|
+
}
|
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
resolveSkipFilePath,
|
|
66
66
|
upgradePackage,
|
|
67
67
|
} from './update-check.mjs';
|
|
68
|
-
import { hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
|
|
68
|
+
import { hcloudProbeNextStep, hcloudProbeRaw, probeHcloud } from './hcloud-probe.mjs';
|
|
69
69
|
import { isUsableOfficeaceRoot, readOfficeaceRootMarker } from './officeace-paths.mjs';
|
|
70
70
|
|
|
71
71
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1662,6 +1662,7 @@ export async function runVersionCheck(options = {}) {
|
|
|
1662
1662
|
installedVersion: result.installedVersion || undefined,
|
|
1663
1663
|
versionMismatch: Boolean(result.versionMismatch),
|
|
1664
1664
|
nextStep: hcloudProbeNextStep(result),
|
|
1665
|
+
raw: hcloudProbeRaw(result),
|
|
1665
1666
|
authHint:
|
|
1666
1667
|
'If hcloud is installed but commands fail with "配置文件中不存在配置项", run `npx huaweicloud-devkit auth init` outside agent chat to configure credentials.',
|
|
1667
1668
|
};
|