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 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.8-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.9-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.8-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.9-orange)](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.8-next.3",
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.8-next.3",
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.8-next.3",
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.8-next.3",
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.8-next.3",
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.8-next.3",
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.8-next.3",
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. **REST API**: `PUT /?website` 到桶的 endpoint
58
- 2. **控制台**: OBS 控制台 → 桶 → 静态网站托管 → 开启
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
- 插件只能完成"上传文件 + 设置 ACL",静态托管开关需用户手动完成。
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
- return { valid: false, projectId: null, error: 'AK and SK are both required for credential validation.' };
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
- throw new Error('region, ak, and sk are required to write OBS config');
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
- const err = new Error(
267
- 'Huawei Cloud credentials are not configured. Run "npx huaweicloud-devkit auth init" or set HW_ACCESS_KEY/HW_SECRET_KEY.',
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: isCodeArtsContext()
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: isCodeArtsContext()
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
- function isCodeArtsContext() {
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)) return { error: 'KooCLI config not found' };
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
- return { error: 'KooCLI config parse failed' };
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
- const codeArts = isCodeArtsHome();
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: 'Global credentials are not configured.',
230
- nextStep: 'Run "npx huaweicloud-devkit auth init" first.',
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: 'Runtime credentials are active; auto-sync suppressed (R10).',
237
- nextStep: 'Run huaweicloud_auth_switch action=clear or action=persist first.',
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
- hcloud.status === 'sandbox_home_failure'
271
- ? 'KooCLI detected but cannot resolve the user home directory in this agent sandbox.'
272
- : hcloud.status === 'privacy_pending'
273
- ? 'KooCLI privacy agreement is pending.'
274
- : 'KooCLI not ready.',
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: error || 'KooCLI config sync failed',
285
- nextStep: 'Run "npx huaweicloud-devkit auth init" in a real terminal to refresh the KooCLI config.',
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
- export function hcloudProbeNextStep(probe) {
144
- if (probe.status === 'ok')
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.status === 'version_mismatch') {
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.status === 'sandbox_home_failure') {
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.status === 'privacy_pending') {
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.status === 'not_found') {
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
  };