huaweicloud-devkit 1.0.2-dev.11 → 1.0.2-dev.12
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/bin/setup.cjs +0 -0
- 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/skills/huawei-obs/SKILL.md +6 -0
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +3 -2
- package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +4 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +18 -3
- package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +155 -0
- package/plugins/huaweicloud-core/src/proxy/proxy-config.mjs +87 -0
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +6 -2
- package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +12 -3
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +7 -0
- package/plugins/huaweicloud-core/src/search-market.mjs +7 -2
- package/plugins/huaweicloud-core/src/setup-cli.mjs +102 -0
- package/plugins/huaweicloud-core/src/tools.mjs +13 -3
package/bin/setup.cjs
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.0.2-dev.
|
|
3
|
+
"version": "1.0.2-dev.12",
|
|
4
4
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"mcpServers": "./.mcp.json",
|
|
21
21
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
22
22
|
"skills": "./skills/",
|
|
23
|
-
"version": "1.0.2-dev.
|
|
23
|
+
"version": "1.0.2-dev.12",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-core",
|
|
3
|
-
"version": "1.0.2-dev.
|
|
3
|
+
"version": "1.0.2-dev.12",
|
|
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",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"mcpServers": "./.mcp.json",
|
|
21
21
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
22
22
|
"skills": "./skills/",
|
|
23
|
-
"version": "1.0.2-dev.
|
|
23
|
+
"version": "1.0.2-dev.12",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|
|
@@ -8,6 +8,12 @@ version: 1
|
|
|
8
8
|
|
|
9
9
|
**STOP - Do not answer from general knowledge.** Follow the procedure below.
|
|
10
10
|
|
|
11
|
+
## Routing Guard: Deploy vs Store
|
|
12
|
+
|
|
13
|
+
- If the developer's goal is to **deploy, host, or preview a web app / static website** (temporary hosting, quick preview), do NOT default to OBS. Present deployment-target options with the sandbox first: ① huawei-sandbox (recommended), ② OBS static hosting, ③ ECS, ④ CCE. Ask: "建议优先部署到沙箱(临时运行环境,可预览访问),也可选 OBS 静态托管/ECS/CCE,你想部署到哪里?"
|
|
14
|
+
- Proceed with OBS only when the developer selects OBS, explicitly asks for OBS, or the intent is long-term static hosting / CDN / file storage.
|
|
15
|
+
- OBS is a storage service; it is not a general web-hosting default.
|
|
16
|
+
|
|
11
17
|
## Critical: OBS Command Syntax
|
|
12
18
|
|
|
13
19
|
KooCLI OBS uses **obsutil-style** commands, NOT API-style operations. Always run `hcloud OBS help` (no `--`) before constructing commands:
|
|
@@ -15,6 +15,7 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
15
15
|
## Activation
|
|
16
16
|
|
|
17
17
|
- **Proactive offering**: when the developer's task needs a temporary runtime (e.g. "deploy this web app", "run this app and preview it"), offer the sandbox proactively — the developer never has to say "use sandbox". Prompt: "This task can be satisfied by a sandbox — use it?"
|
|
18
|
+
- **Static website hosting — present options, sandbox first**: when the developer asks to deploy/host/publish a static website or web app without naming a target, do NOT silently pick a service. Present the target options in this order: ① 沙箱 (recommended — temporary runtime, instant preview URL) ② OBS 静态托管 (long-term static hosting/CDN) ③ ECS ④ CCE, then follow the developer's choice. Recommended prompt: "建议优先部署到沙箱(临时运行环境,可预览访问),也可选择 OBS 静态托管、ECS 或 CCE,你想部署到哪里?" If the developer has no preference, proceed with the sandbox.
|
|
18
19
|
- **Detect web apps in deploy-a-repo prompts**: prompts like "部署 GitHub - <owner>/<repo>" or "deploy <github repo>" do not say "web app". Clone or inspect the repository first (package.json with dev/serve/build scripts, index.html, frontend framework dependencies such as vue/react/angular/vite, static sites, Dockerfile running a web server). If it is a web application, offer the sandbox first: "检测到该项目是 Web 应用,建议优先使用沙箱部署(临时运行环境,可预览访问),是否使用?"
|
|
19
20
|
- **Clone into the sandbox workspace directory**: always put project code under `/workspace/<repo-name>` (create the directory if missing) — `/workspace` is the sandbox's dedicated workspace mount at the filesystem root, not `$HOME/workspace`. Never use `/tmp` or other ephemeral locations. This keeps the project with the sandbox session, is easy to reference for serving/exposing, and survives session-level restarts of the shell.
|
|
20
21
|
- **Deployment must end with a public URL**: after deploying and exposing the app with DevBridge, always return the tunnel URL to the developer as the final result — a deployment without an accessible link is incomplete.
|
|
@@ -48,9 +49,9 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
|
|
|
48
49
|
|
|
49
50
|
Setup is a **plugin-side preflight** — the developer should be asked a question only once, when the agreement actually needs signing:
|
|
50
51
|
|
|
51
|
-
1. **Check user** (transparent): `huaweicloud_sandbox_check_user` — verify `realnameVerified` and `agreementSigned`
|
|
52
|
+
1. **Check user** (transparent): `huaweicloud_sandbox_check_user` — verify `realnameVerified` and `agreementSigned`. Note: the tool may instead return an `HDKIT_NOT_AGREEMENT` error (e.g. `用户未签署最新版协议`) — treat that exactly the same as `agreementSigned=false`
|
|
52
53
|
2. **Real-name verification** (only if `realnameVerified=false`): tell the developer once, "Huawei Cloud requires real-name verification before using the sandbox." and stop — do not retry `connect` in a loop
|
|
53
|
-
3. **Sign agreement** (only if `agreementSigned=false`):
|
|
54
|
+
3. **Sign agreement** (only if `agreementSigned=false`, or when step 1 returned an `HDKIT_NOT_AGREEMENT` error): **STOP and do NOT sign.** Tell the developer the sandbox requires signing the latest Huawei Cloud developer service agreement, then **wait for the developer to explicitly agree to sign** (e.g. the developer says "签署" / "sign it"). Only then call `huaweicloud_sandbox_sign_agreement`. **Never call `sign_agreement` on your own initiative** — the signing decision always belongs to the developer, and you must never sign a legal agreement on the developer's behalf without their explicit, unambiguous consent. Do not expose the underlying sandbox/DevBridge service as a separate entity the developer must understand or sign up for
|
|
54
55
|
4. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`
|
|
55
56
|
5. **Inject credentials** (optional): `huaweicloud_sandbox_credentials` — enables cloud API access from sandbox
|
|
56
57
|
6. **Execute commands**: `huaweicloud_sandbox_exec_with_session` for interactive work
|
|
@@ -22,6 +22,10 @@ Use this skill to turn vague developer intent into a precise Huawei Cloud capabi
|
|
|
22
22
|
8. Treat Terraform as a secondary V1 path for reviewed IaC, not the default.
|
|
23
23
|
9. When no built-in devkit skill matches, browse the community skill marketplace at https://github.com/huaweicloud/huaweicloud-skills. Fetch the index from https://raw.githubusercontent.com/huaweicloud/huaweicloud-skills/master/skills-index/index.json.
|
|
24
24
|
|
|
25
|
+
## Deployment Target Options
|
|
26
|
+
|
|
27
|
+
When the intent is to **deploy, host, or preview a web app or static website** and the developer has not named a target, do NOT default to OBS or any single service. Present the options and let the developer choose, recommending the sandbox first: ① huawei-sandbox (recommended — temporary runtime, instant preview URL) ② huawei-obs (long-term static hosting/CDN) ③ huawei-ecs ④ huawei-cce. Only follow an explicit target when the developer names one.
|
|
28
|
+
|
|
25
29
|
## Region Intent
|
|
26
30
|
|
|
27
31
|
- Extract region intent from the user's words before querying. Examples: Singapore -> `ap-southeast-3` first, Hong Kong -> `ap-southeast-2`, Beijing -> `cn-north-4`, Shanghai -> `cn-east-3`.
|
|
@@ -26,7 +26,7 @@ Do not rely on training data for facts. Huawei Cloud services, pricing, quotas,
|
|
|
26
26
|
|----|------|----------------|------------|
|
|
27
27
|
| select | Service Selection | which service, what should I use, compare, recommend | Read references/select.md | select
|
|
28
28
|
| compute | Compute Routing | server, VM, instance, ECS, BMS, GPU, HPC | Handoff to huawei-ecs or huawei-cce |
|
|
29
|
-
| storage | Storage Routing | store files, bucket, OBS, backup, disk, EVS, SFS | Handoff to huawei-obs or huawei-cbr |
|
|
29
|
+
| storage | Storage Routing | store files, bucket, OBS, backup, disk, EVS, SFS (long-term storage/CDN intent only) | Handoff to huawei-obs or huawei-cbr |
|
|
30
30
|
| database | Database Routing | database, SQL, NoSQL, cache, RDS, GaussDB, DDS, DCS | Handoff to huawei-rds or huawei-gaussdb |
|
|
31
31
|
| network | Network Routing | VPC, subnet, security group, EIP, NAT, VPN | Handoff to huawei-vpc |
|
|
32
32
|
| serverless | Serverless Routing | function, Lambda, serverless, FunctionGraph, API gateway | Handoff to huawei-functiongraph or huawei-apig |
|
|
@@ -36,7 +36,7 @@ Do not rely on training data for facts. Huawei Cloud services, pricing, quotas,
|
|
|
36
36
|
| observability | Observability Routing | monitor, alarm, log, audit, trace, Cloud Eye | Handoff to huawei-cloud-eye or huawei-cts |
|
|
37
37
|
| billing | Billing Routing | cost, bill, budget, spending, invoice | Handoff to huawei-billing |
|
|
38
38
|
| iam | IAM Routing | permission, policy, role, user, group, AK/SK | Handoff to huawei-iam |
|
|
39
|
-
| deployment | Deployment Routing | deploy, CI/CD, pipeline, release | CI/CD pipeline -> huawei-deployment; deploy a web app or
|
|
39
|
+
| deployment | Deployment Routing | deploy, host, publish, CI/CD, pipeline, release | CI/CD pipeline -> huawei-deployment; deploy/host/preview a web app or static website (no CI/CD target) -> present deployment-target options first, see "Deployment Target Options" below |
|
|
40
40
|
| sandbox | Sandbox Routing | sandbox, DevStation, workspace, terminal, preview, temporary runtime | Handoff to huawei-sandbox |
|
|
41
41
|
| cli | CLI and Auth Routing | install hcloud, configure KooCLI, AK/SK setup | Handoff to huaweicloud-cli-and-auth |
|
|
42
42
|
| safety | Safety Routing | is this safe, approve command, risk review | Handoff to huaweicloud-safety |
|
|
@@ -50,7 +50,7 @@ Do not rely on training data for facts. Huawei Cloud services, pricing, quotas,
|
|
|
50
50
|
|----------|----------------|-------|
|
|
51
51
|
| Web application hosting | ECS | huawei-ecs |
|
|
52
52
|
| Containerized microservices | CCE | huawei-cce |
|
|
53
|
-
| Static file storage / CDN | OBS | huawei-obs |
|
|
53
|
+
| Static file storage / CDN (long-term) | OBS | huawei-obs |
|
|
54
54
|
| Relational database (MySQL/PG) | RDS | huawei-rds |
|
|
55
55
|
| Distributed SQL database | GaussDB | huawei-gaussdb |
|
|
56
56
|
| Document database (MongoDB API) | DDS | huawei-dds-dcs |
|
|
@@ -71,6 +71,21 @@ Do not rely on training data for facts. Huawei Cloud services, pricing, quotas,
|
|
|
71
71
|
| Temporary runtime / web app preview | Sandbox (DevStation) | huawei-sandbox |
|
|
72
72
|
| Getting started | Account setup | huaweicloud-cli-and-auth |
|
|
73
73
|
|
|
74
|
+
## Deployment Target Options
|
|
75
|
+
|
|
76
|
+
When the intent is to **deploy, host, or preview a web app or static website** (no explicit target named), NEVER default to a single service such as OBS. Ask the developer to choose a target and present the options in this order:
|
|
77
|
+
|
|
78
|
+
| # | Option | When to choose | Skill |
|
|
79
|
+
|---|--------|----------------|-------|
|
|
80
|
+
| 1 | **Sandbox (DevStation) — recommended** | Temporary runtime, quick preview, demo; instant public URL | huawei-sandbox |
|
|
81
|
+
| 2 | OBS static website hosting | Long-term static hosting / CDN, no backend | huawei-obs |
|
|
82
|
+
| 3 | ECS | Needs a server runtime (backend, custom server) | huawei-ecs |
|
|
83
|
+
| 4 | CCE | Containerized workloads, scaling | huawei-cce |
|
|
84
|
+
|
|
85
|
+
- Recommended prompt: "这个项目是 Web 应用/静态网站,建议优先部署到沙箱(临时运行环境,立即可预览)。也可选择 OBS 静态托管、ECS 或 CCE,你想部署到哪里?"
|
|
86
|
+
- If the developer explicitly names a target (e.g. "deploy to OBS"), follow that target and hand off to the corresponding service skill.
|
|
87
|
+
- If the developer has no preference, recommend the sandbox first.
|
|
88
|
+
|
|
74
89
|
## Capability Sources
|
|
75
90
|
|
|
76
91
|
- Huawei Cloud Skills: scenario workflows and recipes (check first)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { getProxyUrlForTarget } from './proxy-config.mjs';
|
|
2
|
+
|
|
3
|
+
let cachedDispatcher = undefined;
|
|
4
|
+
let cachedDispatcherProxyUrl = null;
|
|
5
|
+
|
|
6
|
+
export async function getProxyDispatcher(targetUrl) {
|
|
7
|
+
const proxyUrl = getProxyUrlForTarget(targetUrl);
|
|
8
|
+
if (!proxyUrl) return undefined;
|
|
9
|
+
|
|
10
|
+
if (cachedDispatcher && cachedDispatcherProxyUrl === proxyUrl) {
|
|
11
|
+
return cachedDispatcher;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { ProxyAgent } = await import('undici');
|
|
15
|
+
cachedDispatcher = new ProxyAgent(proxyUrl);
|
|
16
|
+
cachedDispatcherProxyUrl = proxyUrl;
|
|
17
|
+
return cachedDispatcher;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function clearProxyDispatcherCache() {
|
|
21
|
+
cachedDispatcher = undefined;
|
|
22
|
+
cachedDispatcherProxyUrl = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function createProxyWebSocket(url, protocols) {
|
|
26
|
+
const proxyUrl = getProxyUrlForTarget(url);
|
|
27
|
+
if (!proxyUrl) {
|
|
28
|
+
return new globalThis.WebSocket(url, protocols);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const dispatcher = await getProxyDispatcher(url);
|
|
32
|
+
const { WebSocket: UndiciWebSocket } = await import('undici');
|
|
33
|
+
|
|
34
|
+
const wsOptions = { dispatcher };
|
|
35
|
+
if (protocols) {
|
|
36
|
+
if (Array.isArray(protocols)) {
|
|
37
|
+
wsOptions.protocols = protocols;
|
|
38
|
+
} else {
|
|
39
|
+
wsOptions.protocols = [protocols];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return new UndiciWebSocket(url, wsOptions);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getWebSocketImpl(targetUrl) {
|
|
47
|
+
const proxyUrl = getProxyUrlForTarget(targetUrl);
|
|
48
|
+
if (!proxyUrl) return globalThis.WebSocket;
|
|
49
|
+
|
|
50
|
+
return (url, protocols) => {
|
|
51
|
+
const proxyUrlForTarget = getProxyUrlForTarget(url);
|
|
52
|
+
if (!proxyUrlForTarget) {
|
|
53
|
+
return new globalThis.WebSocket(url, protocols);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const wsPromise = (async () => {
|
|
57
|
+
const dispatcher = await getProxyDispatcher(url);
|
|
58
|
+
const { WebSocket: UndiciWebSocket } = await import('undici');
|
|
59
|
+
|
|
60
|
+
const wsOptions = { dispatcher };
|
|
61
|
+
if (protocols) {
|
|
62
|
+
if (Array.isArray(protocols)) {
|
|
63
|
+
wsOptions.protocols = protocols;
|
|
64
|
+
} else {
|
|
65
|
+
wsOptions.protocols = [protocols];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return new UndiciWebSocket(url, wsOptions);
|
|
70
|
+
})();
|
|
71
|
+
|
|
72
|
+
return createSyncProxyWebSocketWrapper(wsPromise);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createSyncProxyWebSocketWrapper(wsPromise) {
|
|
77
|
+
const pendingEvents = [];
|
|
78
|
+
const eventHandlers = new Map();
|
|
79
|
+
let ws = null;
|
|
80
|
+
let settled = false;
|
|
81
|
+
let readyState = 0;
|
|
82
|
+
let binaryType = 'arraybuffer';
|
|
83
|
+
|
|
84
|
+
const wrapper = {
|
|
85
|
+
get readyState() { return ws ? ws.readyState : readyState; },
|
|
86
|
+
get url() { return ws ? ws.url : ''; },
|
|
87
|
+
get protocol() { return ws ? ws.protocol : ''; },
|
|
88
|
+
get binaryType() { return ws ? ws.binaryType : binaryType; },
|
|
89
|
+
set binaryType(v) {
|
|
90
|
+
binaryType = v;
|
|
91
|
+
if (ws) ws.binaryType = v;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
send(data, callback) {
|
|
95
|
+
if (ws) {
|
|
96
|
+
if (ws.send.length >= 2) return ws.send(data, callback);
|
|
97
|
+
try { ws.send(data); if (callback) callback(null); } catch (e) { if (callback) callback(e); }
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
pendingEvents.push({ type: 'send', data, callback });
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
close(code, reason) {
|
|
104
|
+
if (ws) { ws.close(code, reason); return; }
|
|
105
|
+
readyState = 3;
|
|
106
|
+
pendingEvents.push({ type: 'close', code, reason });
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
addEventListener(type, handler) {
|
|
110
|
+
if (!eventHandlers.has(type)) eventHandlers.set(type, new Set());
|
|
111
|
+
eventHandlers.get(type).add(handler);
|
|
112
|
+
if (ws) ws.addEventListener(type, handler);
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
removeEventListener(type, handler) {
|
|
116
|
+
const handlers = eventHandlers.get(type);
|
|
117
|
+
if (handlers) handlers.delete(handler);
|
|
118
|
+
if (ws) ws.removeEventListener(type, handler);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
on(type, handler) { wrapper.addEventListener(type, handler); },
|
|
122
|
+
off(type, handler) { wrapper.removeEventListener(type, handler); },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function flushPendingEvents() {
|
|
126
|
+
for (const evt of pendingEvents) {
|
|
127
|
+
if (evt.type === 'send') {
|
|
128
|
+
if (ws.send.length >= 2) ws.send(evt.data, evt.callback);
|
|
129
|
+
else { try { ws.send(evt.data); if (evt.callback) evt.callback(null); } catch (e) { if (evt.callback) evt.callback(e); } }
|
|
130
|
+
} else if (evt.type === 'close') {
|
|
131
|
+
ws.close(evt.code, evt.reason);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
pendingEvents.length = 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
wsPromise.then((resolvedWs) => {
|
|
138
|
+
ws = resolvedWs;
|
|
139
|
+
if ('binaryType' in ws) ws.binaryType = binaryType;
|
|
140
|
+
for (const [type, handlers] of eventHandlers) {
|
|
141
|
+
for (const handler of handlers) {
|
|
142
|
+
ws.addEventListener(type, handler);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
flushPendingEvents();
|
|
146
|
+
}).catch((err) => {
|
|
147
|
+
readyState = 3;
|
|
148
|
+
const handlers = eventHandlers.get('error');
|
|
149
|
+
if (handlers) for (const h of handlers) h(err instanceof Error ? err : new Error(String(err)));
|
|
150
|
+
const closeHandlers = eventHandlers.get('close');
|
|
151
|
+
if (closeHandlers) for (const h of closeHandlers) h({ code: 1006, reason: err.message || String(err) });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return wrapper;
|
|
155
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
function baseHome() {
|
|
6
|
+
return process.env.HUAWEICLOUD_HOME || homedir();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function proxyConfigPath() {
|
|
10
|
+
return join(baseHome(), '.config', 'huaweicloud', 'proxy.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readProxyConfig() {
|
|
14
|
+
const path = proxyConfigPath();
|
|
15
|
+
if (!existsSync(path)) return null;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function writeProxyConfig(config = {}) {
|
|
24
|
+
const path = proxyConfigPath();
|
|
25
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
26
|
+
const payload = {
|
|
27
|
+
https_proxy: String(config.https_proxy || ''),
|
|
28
|
+
http_proxy: String(config.http_proxy || ''),
|
|
29
|
+
no_proxy: String(config.no_proxy || ''),
|
|
30
|
+
};
|
|
31
|
+
writeFileSync(path, JSON.stringify(payload, null, 2), { encoding: 'utf8' });
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function clearProxyConfig() {
|
|
36
|
+
const path = proxyConfigPath();
|
|
37
|
+
if (!existsSync(path)) return false;
|
|
38
|
+
rmSync(path);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function shouldBypassProxy(hostname, noProxyList) {
|
|
43
|
+
if (!noProxyList.length) return false;
|
|
44
|
+
const lower = hostname.toLowerCase();
|
|
45
|
+
for (const pattern of noProxyList) {
|
|
46
|
+
const p = pattern.trim().toLowerCase();
|
|
47
|
+
if (!p) continue;
|
|
48
|
+
if (p === lower) return true;
|
|
49
|
+
if (p === '*') return true;
|
|
50
|
+
if (p.startsWith('.') && lower.endsWith(p)) return true;
|
|
51
|
+
if (lower.endsWith('.' + p)) return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getProxySettings(targetUrl) {
|
|
57
|
+
const envHttps = process.env.HTTPS_PROXY || process.env.https_proxy || '';
|
|
58
|
+
const envHttp = process.env.HTTP_PROXY || process.env.http_proxy || '';
|
|
59
|
+
const envNoProxy = process.env.NO_PROXY || process.env.no_proxy || '';
|
|
60
|
+
|
|
61
|
+
const fileConfig = readProxyConfig();
|
|
62
|
+
|
|
63
|
+
const https_proxy = envHttps || (fileConfig?.https_proxy || '');
|
|
64
|
+
const http_proxy = envHttp || (fileConfig?.http_proxy || '');
|
|
65
|
+
const no_proxy = envNoProxy || (fileConfig?.no_proxy || '');
|
|
66
|
+
|
|
67
|
+
if (!https_proxy && !http_proxy) return null;
|
|
68
|
+
|
|
69
|
+
const noProxyList = no_proxy.split(',').map((s) => s.trim()).filter(Boolean);
|
|
70
|
+
|
|
71
|
+
if (targetUrl) {
|
|
72
|
+
const parsed = new URL(targetUrl);
|
|
73
|
+
if (shouldBypassProxy(parsed.hostname, noProxyList)) return null;
|
|
74
|
+
const isHttps = parsed.protocol === 'https:';
|
|
75
|
+
const proxyUrl = isHttps ? https_proxy : http_proxy;
|
|
76
|
+
if (!proxyUrl) return null;
|
|
77
|
+
return { proxyUrl, noProxyList, targetProtocol: parsed.protocol };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { https_proxy, http_proxy, no_proxy, noProxyList };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getProxyUrlForTarget(targetUrl) {
|
|
84
|
+
const settings = getProxySettings(targetUrl);
|
|
85
|
+
if (!settings) return null;
|
|
86
|
+
return settings.proxyUrl || null;
|
|
87
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getCredentials } from './hwlink-api.mjs';
|
|
2
|
+
import { getProxyDispatcher } from '../proxy/proxy-agent.mjs';
|
|
2
3
|
|
|
3
4
|
const HDKIT_BASE_URL =
|
|
4
5
|
process.env.HDKITSERVICE_ENDPOINT ||
|
|
@@ -22,12 +23,15 @@ async function hdkitRequest(method, path, body, timeoutMs = 300000) {
|
|
|
22
23
|
|
|
23
24
|
let resp;
|
|
24
25
|
try {
|
|
25
|
-
|
|
26
|
+
const dispatcher = await getProxyDispatcher(url);
|
|
27
|
+
const fetchOpts = {
|
|
26
28
|
method,
|
|
27
29
|
headers,
|
|
28
30
|
body: body ? JSON.stringify(body) : undefined,
|
|
29
31
|
signal: controller.signal,
|
|
30
|
-
}
|
|
32
|
+
};
|
|
33
|
+
if (dispatcher) fetchOpts.dispatcher = dispatcher;
|
|
34
|
+
resp = await fetch(url, fetchOpts);
|
|
31
35
|
} finally {
|
|
32
36
|
clearTimeout(timer);
|
|
33
37
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import { resolveCredentials } from '../auth/credentials.mjs';
|
|
3
|
+
import { getProxyDispatcher } from '../proxy/proxy-agent.mjs';
|
|
3
4
|
|
|
4
5
|
const BASE_URL = process.env.HWLINK_ENDPOINT || 'https://devstation.myhuaweicloud.com';
|
|
5
6
|
|
|
@@ -96,17 +97,25 @@ async function apiGet(path, query, ak, sk, securitytoken) {
|
|
|
96
97
|
const qs = sortedQs(query);
|
|
97
98
|
const fullPath = qs ? `${path}?${qs}` : path;
|
|
98
99
|
const headers = signRequest('GET', path, query, undefined, ak, sk, securitytoken);
|
|
99
|
-
const
|
|
100
|
+
const url = `${BASE_URL}${fullPath}`;
|
|
101
|
+
const dispatcher = await getProxyDispatcher(url);
|
|
102
|
+
const fetchOpts = { headers };
|
|
103
|
+
if (dispatcher) fetchOpts.dispatcher = dispatcher;
|
|
104
|
+
const resp = await fetch(url, fetchOpts);
|
|
100
105
|
return { status: resp.status, data: await resp.json() };
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
async function apiPost(path, body, ak, sk, securitytoken) {
|
|
104
109
|
const headers = signRequest('POST', path, {}, body, ak, sk, securitytoken);
|
|
105
|
-
const
|
|
110
|
+
const url = `${BASE_URL}${path}`;
|
|
111
|
+
const dispatcher = await getProxyDispatcher(url);
|
|
112
|
+
const fetchOpts = {
|
|
106
113
|
method: 'POST',
|
|
107
114
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
108
115
|
body: JSON.stringify(body),
|
|
109
|
-
}
|
|
116
|
+
};
|
|
117
|
+
if (dispatcher) fetchOpts.dispatcher = dispatcher;
|
|
118
|
+
const resp = await fetch(url, fetchOpts);
|
|
110
119
|
return { status: resp.status, data: await resp.json() };
|
|
111
120
|
}
|
|
112
121
|
|
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
4
|
import { createConnection, getCredentials } from './hwlink-api.mjs';
|
|
5
|
+
import { getWebSocketImpl } from '../proxy/proxy-agent.mjs';
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
export const WS_EXEC_INDEX_URL = pathToFileURL(join(__dirname, '..', 'ws-exec', 'index.js')).href;
|
|
@@ -55,12 +56,15 @@ async function getSession(workspaceId, username, timeoutMs) {
|
|
|
55
56
|
const { ak, sk, securitytoken } = getCredentials();
|
|
56
57
|
const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
|
|
57
58
|
|
|
59
|
+
const WebSocketImpl = getWebSocketImpl(wsUrl);
|
|
60
|
+
|
|
58
61
|
const { connectHwlinkTerminalSession } = await import(WS_EXEC_INDEX_URL);
|
|
59
62
|
const session = await connectHwlinkTerminalSession({
|
|
60
63
|
url: wsUrl,
|
|
61
64
|
source,
|
|
62
65
|
username,
|
|
63
66
|
timeoutMs,
|
|
67
|
+
WebSocketImpl,
|
|
64
68
|
});
|
|
65
69
|
|
|
66
70
|
sessions.set(key, session);
|
|
@@ -71,6 +75,8 @@ export async function execOneShot(workspaceId, command, username, timeoutMs) {
|
|
|
71
75
|
const { ak, sk, securitytoken } = getCredentials();
|
|
72
76
|
const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
|
|
73
77
|
|
|
78
|
+
const WebSocketImpl = getWebSocketImpl(wsUrl);
|
|
79
|
+
|
|
74
80
|
const { executeHwlinkCommand } = await import(WS_EXEC_INDEX_URL);
|
|
75
81
|
return await executeHwlinkCommand({
|
|
76
82
|
url: wsUrl,
|
|
@@ -78,6 +84,7 @@ export async function execOneShot(workspaceId, command, username, timeoutMs) {
|
|
|
78
84
|
username,
|
|
79
85
|
command,
|
|
80
86
|
timeoutMs,
|
|
87
|
+
WebSocketImpl,
|
|
81
88
|
});
|
|
82
89
|
}
|
|
83
90
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getProxyDispatcher } from './proxy/proxy-agent.mjs';
|
|
2
|
+
|
|
1
3
|
const INDEX_URL = 'https://gitcode.com/api/v5/repos/2501_91318609/skills-for-index/contents/skills-index/index.json?ref=main';
|
|
2
4
|
const CN_EN_MAP_URL = 'https://gitcode.com/api/v5/repos/2501_91318609/skills-for-index/contents/skills-index/cn-en-map.json?ref=main';
|
|
3
5
|
const HTTP_TIMEOUT_MS = 10000;
|
|
@@ -19,10 +21,13 @@ async function fetchJson(url, label = '') {
|
|
|
19
21
|
const controller = new AbortController();
|
|
20
22
|
const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
|
21
23
|
try {
|
|
22
|
-
const
|
|
24
|
+
const dispatcher = await getProxyDispatcher(url);
|
|
25
|
+
const fetchOpts = {
|
|
23
26
|
headers: { 'User-Agent': 'huaweicloud-devkit/1.0' },
|
|
24
27
|
signal: controller.signal,
|
|
25
|
-
}
|
|
28
|
+
};
|
|
29
|
+
if (dispatcher) fetchOpts.dispatcher = dispatcher;
|
|
30
|
+
const resp = await fetch(url, fetchOpts);
|
|
26
31
|
const data = await resp.json();
|
|
27
32
|
if (data && data.encoding === 'base64' && data.content) {
|
|
28
33
|
const decoded = Buffer.from(data.content, 'base64').toString('utf8');
|
|
@@ -8,6 +8,7 @@ import { createInterface } from 'node:readline';
|
|
|
8
8
|
import { spawnSync } from 'node:child_process';
|
|
9
9
|
import { getAuthStatus, syncAuth } from './auth/service.mjs';
|
|
10
10
|
import { globalCredentialsPath, readGlobalCredentials, writeGlobalCredentials, writeObsConfig } from './auth/credentials.mjs';
|
|
11
|
+
import { proxyConfigPath, readProxyConfig, writeProxyConfig, clearProxyConfig, getProxySettings } from './proxy/proxy-config.mjs';
|
|
11
12
|
|
|
12
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const PLUGIN_ROOT = resolve(__dirname, '..');
|
|
@@ -1064,6 +1065,14 @@ async function cmdDoctor() {
|
|
|
1064
1065
|
warn++;
|
|
1065
1066
|
}
|
|
1066
1067
|
|
|
1068
|
+
const proxyConfig = readProxyConfig();
|
|
1069
|
+
const proxyEnv = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
|
|
1070
|
+
if (proxyConfig || proxyEnv) {
|
|
1071
|
+
const source = proxyEnv ? 'env' : 'file';
|
|
1072
|
+
const proxyUrl = proxyEnv || proxyConfig.https_proxy || proxyConfig.http_proxy;
|
|
1073
|
+
console.log(` \x1b[36m[INFO]\x1b[0m Proxy configured (${source}): ${proxyUrl}`);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1067
1076
|
console.log(`\nResults: ${pass} pass, ${warn} warn, ${fail} fail`);
|
|
1068
1077
|
|
|
1069
1078
|
if (mcpConfigured && !hcloudOk) {
|
|
@@ -1524,6 +1533,93 @@ async function cmdAuth() {
|
|
|
1524
1533
|
return cmdAuthStatus();
|
|
1525
1534
|
}
|
|
1526
1535
|
|
|
1536
|
+
async function cmdProxyInit() {
|
|
1537
|
+
console.log(BANNER);
|
|
1538
|
+
console.log('HuaweiCloud DevKit Proxy Configuration\n');
|
|
1539
|
+
console.log('Configure HTTP/HTTPS proxy for connections to Huawei Cloud services.');
|
|
1540
|
+
console.log('Proxy settings are saved to ~/.config/huaweicloud/proxy.json\n');
|
|
1541
|
+
|
|
1542
|
+
const existing = readProxyConfig() || {};
|
|
1543
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
1544
|
+
|
|
1545
|
+
if (!interactive) {
|
|
1546
|
+
console.error('\x1b[31mNon-interactive session. Set proxy via environment variables:\x1b[0m');
|
|
1547
|
+
console.error(' HTTPS_PROXY=http://proxy:port');
|
|
1548
|
+
console.error(' HTTP_PROXY=http://proxy:port');
|
|
1549
|
+
console.error(' NO_PROXY=localhost,127.0.0.1');
|
|
1550
|
+
console.error('\nOr run "npx huaweicloud-devkit proxy init" in a real terminal.');
|
|
1551
|
+
process.exitCode = 1;
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const httpsProxy = await readLineQuestion(`HTTPS proxy [${existing.https_proxy || 'none'}]: `);
|
|
1556
|
+
const httpProxy = await readLineQuestion(`HTTP proxy [${existing.http_proxy || 'none'}]: `);
|
|
1557
|
+
const noProxy = await readLineQuestion(`NO_PROXY hosts [${existing.no_proxy || 'localhost,127.0.0.1'}]: `);
|
|
1558
|
+
|
|
1559
|
+
const config = {
|
|
1560
|
+
https_proxy: httpsProxy || existing.https_proxy || '',
|
|
1561
|
+
http_proxy: httpProxy || existing.http_proxy || '',
|
|
1562
|
+
no_proxy: noProxy || existing.no_proxy || 'localhost,127.0.0.1',
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
const path = writeProxyConfig(config);
|
|
1566
|
+
console.log(`\nProxy configuration saved to ${path}`);
|
|
1567
|
+
console.log('\nEffective settings:');
|
|
1568
|
+
console.log(` HTTPS_PROXY: ${config.https_proxy || '(none)'}`);
|
|
1569
|
+
console.log(` HTTP_PROXY: ${config.http_proxy || '(none)'}`);
|
|
1570
|
+
console.log(` NO_PROXY: ${config.no_proxy || '(none)'}`);
|
|
1571
|
+
console.log('\nEnvironment variables (HTTPS_PROXY, HTTP_PROXY, NO_PROXY) override file settings.');
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
async function cmdProxyShow() {
|
|
1575
|
+
console.log(BANNER);
|
|
1576
|
+
console.log('HuaweiCloud DevKit Proxy Configuration\n');
|
|
1577
|
+
|
|
1578
|
+
const config = readProxyConfig();
|
|
1579
|
+
const configPath = proxyConfigPath();
|
|
1580
|
+
|
|
1581
|
+
console.log(`Config file: ${configPath}`);
|
|
1582
|
+
console.log(`File exists: ${config ? 'yes' : 'no'}\n`);
|
|
1583
|
+
|
|
1584
|
+
if (config) {
|
|
1585
|
+
console.log('File settings:');
|
|
1586
|
+
console.log(` https_proxy: ${config.https_proxy || '(empty)'}`);
|
|
1587
|
+
console.log(` http_proxy: ${config.http_proxy || '(empty)'}`);
|
|
1588
|
+
console.log(` no_proxy: ${config.no_proxy || '(empty)'}`);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
console.log('\nEnvironment variables:');
|
|
1592
|
+
console.log(` HTTPS_PROXY: ${process.env.HTTPS_PROXY || process.env.https_proxy || '(not set)'}`);
|
|
1593
|
+
console.log(` HTTP_PROXY: ${process.env.HTTP_PROXY || process.env.http_proxy || '(not set)'}`);
|
|
1594
|
+
console.log(` NO_PROXY: ${process.env.NO_PROXY || process.env.no_proxy || '(not set)'}`);
|
|
1595
|
+
|
|
1596
|
+
const effective = getProxySettings();
|
|
1597
|
+
console.log('\nEffective (env > file):');
|
|
1598
|
+
if (effective) {
|
|
1599
|
+
console.log(` https_proxy: ${effective.https_proxy || '(none)'}`);
|
|
1600
|
+
console.log(` http_proxy: ${effective.http_proxy || '(none)'}`);
|
|
1601
|
+
console.log(` no_proxy: ${effective.no_proxy || '(none)'}`);
|
|
1602
|
+
} else {
|
|
1603
|
+
console.log(' (no proxy configured)');
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
async function cmdProxyClear() {
|
|
1608
|
+
const removed = clearProxyConfig();
|
|
1609
|
+
if (removed) {
|
|
1610
|
+
console.log('Proxy configuration removed.');
|
|
1611
|
+
} else {
|
|
1612
|
+
console.log('No proxy configuration file found.');
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
async function cmdProxy() {
|
|
1617
|
+
const sub = (process.argv[3] || 'show').toLowerCase();
|
|
1618
|
+
if (sub === 'init' || sub === 'setup') return cmdProxyInit();
|
|
1619
|
+
if (sub === 'clear' || sub === 'remove' || sub === 'reset') return cmdProxyClear();
|
|
1620
|
+
return cmdProxyShow();
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1527
1623
|
async function main() {
|
|
1528
1624
|
const cmd = process.argv[2] || 'help';
|
|
1529
1625
|
|
|
@@ -1557,6 +1653,9 @@ async function main() {
|
|
|
1557
1653
|
case 'auth':
|
|
1558
1654
|
await cmdAuth();
|
|
1559
1655
|
break;
|
|
1656
|
+
case 'proxy':
|
|
1657
|
+
await cmdProxy();
|
|
1658
|
+
break;
|
|
1560
1659
|
case 'help':
|
|
1561
1660
|
case '--help':
|
|
1562
1661
|
case '-h':
|
|
@@ -1572,6 +1671,7 @@ async function main() {
|
|
|
1572
1671
|
console.log(' doctor Self-check: hcloud, MCP, skills, auth');
|
|
1573
1672
|
console.log(' install-hcloud Show KooCLI install commands for your OS');
|
|
1574
1673
|
console.log(' auth Manage unified auth: init | sync | status');
|
|
1674
|
+
console.log(' proxy Manage proxy config: init | show | clear');
|
|
1575
1675
|
console.log(' help Show this help');
|
|
1576
1676
|
console.log('\nOptions:');
|
|
1577
1677
|
console.log(' --target Target agent: opencode (default), codex, codearts, workbuddy, all');
|
|
@@ -1584,6 +1684,8 @@ async function main() {
|
|
|
1584
1684
|
console.log(' npx huaweicloud-devkit auth init');
|
|
1585
1685
|
console.log(' npx huaweicloud-devkit auth sync --target all');
|
|
1586
1686
|
console.log(' npx huaweicloud-devkit auth status --target all');
|
|
1687
|
+
console.log(' npx huaweicloud-devkit proxy init');
|
|
1688
|
+
console.log(' npx huaweicloud-devkit proxy show');
|
|
1587
1689
|
break;
|
|
1588
1690
|
}
|
|
1589
1691
|
}
|
|
@@ -344,7 +344,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
344
344
|
},
|
|
345
345
|
{
|
|
346
346
|
name: 'huaweicloud_sandbox_sign_agreement',
|
|
347
|
-
description: 'Sign all unsigned or outdated agreements for the current user. Required before huaweicloud_sandbox_connect if check-user returns agreementSigned=false.',
|
|
347
|
+
description: 'Sign all unsigned or outdated agreements for the current user. Required before huaweicloud_sandbox_connect if check-user returns agreementSigned=false. CRITICAL: only call after the user explicitly consents to signing — never sign agreements on the user\'s behalf without their explicit request.',
|
|
348
348
|
inputSchema: {
|
|
349
349
|
type: 'object',
|
|
350
350
|
properties: {},
|
|
@@ -709,19 +709,29 @@ function serviceCatalog(intent = '') {
|
|
|
709
709
|
{ keywords: ['cts', 'audit', 'trace', 'tracker'], skills: ['huawei-cts'], services: ['CTS'] },
|
|
710
710
|
{ keywords: ['cbr', 'backup', 'restore', 'vault', 'snapshot'], skills: ['huawei-cbr'], services: ['CBR'] },
|
|
711
711
|
{ keywords: ['deployment', 'deploy', 'ci/cd', 'pipeline', 'release'], skills: ['huawei-deployment'], services: ['CloudDeploy'] },
|
|
712
|
-
{ keywords: ['sandbox', 'devstation', 'workspace', 'terminal', 'preview', 'hwlink'], skills: ['huawei-sandbox'], services: ['Sandbox', 'DevStation'] },
|
|
712
|
+
{ keywords: ['sandbox', 'devstation', 'workspace', 'terminal', 'preview', 'hwlink', 'website', 'web app', 'webapp', 'hosting', '网站', '网页', '静态'], skills: ['huawei-sandbox'], services: ['Sandbox', 'DevStation'] },
|
|
713
713
|
{ keywords: ['dds', 'dcs', 'mongodb', 'redis', 'memcached', 'cache', 'document db'], skills: ['huawei-dds-dcs'], services: ['DDS', 'DCS'] },
|
|
714
714
|
];
|
|
715
715
|
const matched = [];
|
|
716
716
|
const tokens = it.split(/[\s,./-]+/).filter((t) => t.length > 0);
|
|
717
|
+
const cjk = /[\u4e00-\u9fff]/;
|
|
717
718
|
for (const route of routeMap) {
|
|
718
|
-
if (route.keywords.some((kw) => tokens.includes(kw))) {
|
|
719
|
+
if (route.keywords.some((kw) => (kw.includes(' ') || cjk.test(kw)) ? it.includes(kw) : tokens.includes(kw))) {
|
|
719
720
|
matched.push(route);
|
|
720
721
|
}
|
|
721
722
|
}
|
|
722
723
|
const recommendedSkills = [...new Set(matched.flatMap((r) => r.skills))];
|
|
723
724
|
const recommendedServices = [...new Set(matched.flatMap((r) => r.services))].slice(0, 5);
|
|
724
725
|
|
|
726
|
+
// Deployment intent (deploy/host/publish a web app or static website) must never
|
|
727
|
+
// default to a storage/other service — recommend the sandbox first.
|
|
728
|
+
const deploymentIntent = /deploy|host|hosting|publish|website|web app|preview|部署|托管|发布|网站|网页/.test(it);
|
|
729
|
+
if (deploymentIntent && recommendedSkills.includes('huawei-sandbox')) {
|
|
730
|
+
const idx = recommendedSkills.indexOf('huawei-sandbox');
|
|
731
|
+
recommendedSkills.splice(idx, 1);
|
|
732
|
+
recommendedSkills.unshift('huawei-sandbox');
|
|
733
|
+
}
|
|
734
|
+
|
|
725
735
|
return {
|
|
726
736
|
intent,
|
|
727
737
|
recommendedSkills: recommendedSkills.length ? recommendedSkills : ['Use huaweicloud-core to route intent.'],
|