huaweicloud-devkit 0.1.17
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/.agents/plugins/marketplace.json +20 -0
- package/LICENSE +201 -0
- package/README.md +148 -0
- package/bin/setup.cjs +8 -0
- package/integrations/opencode/commands/huaweicloud-doctor.md +14 -0
- package/integrations/opencode/opencode.json +14 -0
- package/integrations/opencode/skills/huaweicloud-core/SKILL.md +30 -0
- package/package.json +43 -0
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +44 -0
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +43 -0
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +44 -0
- package/plugins/huaweicloud-core/.mcp.json +13 -0
- package/plugins/huaweicloud-core/hooks/hooks.json +26 -0
- package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +88 -0
- package/plugins/huaweicloud-core/safety/policy.json +86 -0
- package/plugins/huaweicloud-core/skills/huawei-apig/SKILL.md +144 -0
- package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +48 -0
- package/plugins/huaweicloud-core/skills/huawei-cbr/SKILL.md +50 -0
- package/plugins/huaweicloud-core/skills/huawei-cce/SKILL.md +101 -0
- package/plugins/huaweicloud-core/skills/huawei-cloud-eye/SKILL.md +73 -0
- package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/SKILL.md +190 -0
- package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/scripts/search-skills.mjs +124 -0
- package/plugins/huaweicloud-core/skills/huawei-cts/SKILL.md +55 -0
- package/plugins/huaweicloud-core/skills/huawei-dds-dcs/SKILL.md +70 -0
- package/plugins/huaweicloud-core/skills/huawei-deployment/SKILL.md +56 -0
- package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +97 -0
- package/plugins/huaweicloud-core/skills/huawei-dew/references/csms-usage.md +29 -0
- package/plugins/huaweicloud-core/skills/huawei-dew/references/kms-usage.md +23 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +131 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +92 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/evs.md +31 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +28 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +52 -0
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +132 -0
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/apig-event-format.md +58 -0
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/create-function.md +71 -0
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/deploy-workflow.md +119 -0
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/triggers.md +142 -0
- package/plugins/huaweicloud-core/skills/huawei-gaussdb/SKILL.md +56 -0
- package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +65 -0
- package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +111 -0
- package/plugins/huaweicloud-core/skills/huawei-iam/references/policy-examples.md +122 -0
- package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +56 -0
- package/plugins/huaweicloud-core/skills/huawei-modelarts/SKILL.md +65 -0
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +118 -0
- package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-create.md +49 -0
- package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-lifecycle.md +26 -0
- package/plugins/huaweicloud-core/skills/huawei-obs/references/common-pitfalls.md +100 -0
- package/plugins/huaweicloud-core/skills/huawei-obs/references/static-website.md +56 -0
- package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +181 -0
- package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +69 -0
- package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +77 -0
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +52 -0
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/security-group.md +59 -0
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/subnet.md +50 -0
- package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +63 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/SKILL.md +56 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/agents/openai.yaml +4 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +47 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/agents/openai.yaml +4 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +125 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/agents/openai.yaml +4 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +94 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-core/agents/openai.yaml +4 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-core/references/report-issue.md +20 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-core/references/select.md +28 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +46 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-safety/agents/openai.yaml +4 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +83 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/agents/openai.yaml +4 -0
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +265 -0
- package/plugins/huaweicloud-core/src/mcp-server.mjs +93 -0
- package/plugins/huaweicloud-core/src/safety-policy.mjs +289 -0
- package/plugins/huaweicloud-core/src/search-market.mjs +169 -0
- package/plugins/huaweicloud-core/src/setup-cli.mjs +628 -0
- package/plugins/huaweicloud-core/src/tools.mjs +672 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const policyPath = join(__dirname, '..', 'safety', 'policy.json');
|
|
7
|
+
|
|
8
|
+
export function loadPolicy() {
|
|
9
|
+
return JSON.parse(readFileSync(policyPath, 'utf8'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_POLICY = loadPolicy();
|
|
13
|
+
|
|
14
|
+
function regexFrom(pattern) {
|
|
15
|
+
return new RegExp(pattern, 'i');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isSecretKeyName(key, policy = DEFAULT_POLICY) {
|
|
19
|
+
const normalized = String(key).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
20
|
+
if (
|
|
21
|
+
/access.*key|secret.*key|security.*token|xauth.*token|authorization|password|passwd|adminpass|credential|private.*key/.test(normalized)
|
|
22
|
+
) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return policy.secretKeyNamePatterns.some((pattern) => regexFrom(`^(${pattern})$`).test(key));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function redactString(text) {
|
|
29
|
+
return String(text)
|
|
30
|
+
.replace(/((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|authorization|password|passwd|adminPass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi, '$1<redacted>')
|
|
31
|
+
.replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function redactSecrets(value, policy = DEFAULT_POLICY) {
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
return value.map((item) => redactSecrets(item, policy));
|
|
37
|
+
}
|
|
38
|
+
if (value && typeof value === 'object') {
|
|
39
|
+
return Object.fromEntries(
|
|
40
|
+
Object.entries(value).map(([key, val]) => [
|
|
41
|
+
key,
|
|
42
|
+
isSecretKeyName(key, policy) ? '<redacted>' : redactSecrets(val, policy),
|
|
43
|
+
]),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
return redactString(value);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function stripExecutable(args) {
|
|
53
|
+
if (!args.length) return [];
|
|
54
|
+
const first = String(args[0]).toLowerCase();
|
|
55
|
+
if (first === 'hcloud' || first.endsWith('/hcloud') || first.endsWith('\\hcloud') || first === 'hcloud.exe') {
|
|
56
|
+
return args.slice(1);
|
|
57
|
+
}
|
|
58
|
+
return args;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function commandOperation(args) {
|
|
62
|
+
const stripped = stripExecutable(args).map(String).filter(Boolean);
|
|
63
|
+
if (stripped[0]?.toLowerCase() === 'configure') {
|
|
64
|
+
return { service: 'configure', operation: stripped[1] || '', args: stripped };
|
|
65
|
+
}
|
|
66
|
+
const nonFlags = stripped.filter((arg) => !arg.startsWith('-'));
|
|
67
|
+
return {
|
|
68
|
+
service: nonFlags[0] || '',
|
|
69
|
+
operation: nonFlags[1] || nonFlags[0] || '',
|
|
70
|
+
args: stripped,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function matchesAny(value, patterns) {
|
|
75
|
+
return patterns.some((pattern) => regexFrom(pattern).test(value));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hasWritePrefix(operation, policy) {
|
|
79
|
+
const normalized = String(operation);
|
|
80
|
+
return policy.writeOperationPrefixes.some((prefix) =>
|
|
81
|
+
new RegExp(`(^|[A-Za-z0-9])${prefix}`, 'i').test(normalized),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hasReadPrefix(operation, policy) {
|
|
86
|
+
return policy.readOperationPrefixes.some((prefix) =>
|
|
87
|
+
new RegExp(`^${prefix}`, 'i').test(operation),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isLocalMetadataCommand(args) {
|
|
92
|
+
return args.some((arg) => /^(--help|-h|help|version|--version)$/i.test(String(arg)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function classifyHcloudArgs(args, options = {}) {
|
|
96
|
+
const policy = options.policy || DEFAULT_POLICY;
|
|
97
|
+
const { service, operation, args: normalizedArgs } = commandOperation(args);
|
|
98
|
+
const joined = normalizedArgs.join(' ');
|
|
99
|
+
|
|
100
|
+
if (!normalizedArgs.length) {
|
|
101
|
+
return {
|
|
102
|
+
decision: 'deny',
|
|
103
|
+
risk: 'invalid',
|
|
104
|
+
reason: 'Empty hcloud command arguments are not executable.',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (isLocalMetadataCommand(normalizedArgs)) {
|
|
109
|
+
return {
|
|
110
|
+
decision: 'allow',
|
|
111
|
+
risk: 'local_metadata',
|
|
112
|
+
reason: 'KooCLI local help and version commands are read-only and do not call Huawei Cloud resource APIs.',
|
|
113
|
+
service,
|
|
114
|
+
operation,
|
|
115
|
+
args: normalizedArgs,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (service.toLowerCase() === 'configure') {
|
|
120
|
+
const subcommand = operation.toLowerCase();
|
|
121
|
+
if (
|
|
122
|
+
policy.blockedConfigureSubcommands.map((cmd) => cmd.toLowerCase()).includes(subcommand)
|
|
123
|
+
&& options.allowCredentialRead !== true
|
|
124
|
+
) {
|
|
125
|
+
return {
|
|
126
|
+
decision: 'deny',
|
|
127
|
+
risk: 'credential',
|
|
128
|
+
reason: 'Direct hcloud configure inspection may expose profile credentials. Use the redacted toolkit tools instead.',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (policy.blockedSecretOperations.some((op) => op.toLowerCase() === operation.toLowerCase())) {
|
|
134
|
+
return {
|
|
135
|
+
decision: 'deny',
|
|
136
|
+
risk: 'secret',
|
|
137
|
+
reason: 'Direct secret value reads are blocked so plaintext secrets do not enter the agent context.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (/secret[_-]?string|secret[_-]?binary|showsecretversion|getsecretvalue/i.test(joined)) {
|
|
142
|
+
return {
|
|
143
|
+
decision: 'deny',
|
|
144
|
+
risk: 'secret',
|
|
145
|
+
reason: 'The command appears to retrieve a secret value. Use a runtime secret reference pattern instead.',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const readOnly = hasReadPrefix(operation, policy);
|
|
150
|
+
const executionOps = /(^|\.)(Invoke|SyncInvoke|AsyncInvoke|Send|Trigger|Execute|Start|Reboot|Restart|Stop|Publish|Deploy)/i;
|
|
151
|
+
const isExecution = executionOps.test(operation) && !readOnly;
|
|
152
|
+
const isWrite = !readOnly && hasWritePrefix(operation, policy);
|
|
153
|
+
|
|
154
|
+
if (isExecution && !options.allowWrites) {
|
|
155
|
+
return {
|
|
156
|
+
decision: 'deny',
|
|
157
|
+
risk: 'execution',
|
|
158
|
+
reason: 'Huawei Cloud execution/trigger operation blocked until approved.',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (isWrite && !options.allowWrites) {
|
|
163
|
+
return {
|
|
164
|
+
decision: 'deny',
|
|
165
|
+
risk: 'write',
|
|
166
|
+
reason: 'Huawei Cloud write operation blocked until the agent presents a plan and receives explicit user approval.',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const obsutilWrites = ['mb', 'cp', 'mv', 'rm', 'delete', 'mkdir', 'sync', 'restore', 'chattri', 'bucketpolicy', 'lifecycle', 'cors', 'website', 'sign', 'share-add', 'share-update', 'share-rm'];
|
|
171
|
+
const obsutilReads = ['ls', 'stat', 'cat', 'help', 'version'];
|
|
172
|
+
const isObs = service.toLowerCase() === 'obs' || service.toLowerCase() === 'hcloud obs';
|
|
173
|
+
const isObsWrite = isObs && obsutilWrites.includes(operation);
|
|
174
|
+
const isObsRead = isObs && obsutilReads.includes(operation);
|
|
175
|
+
if (isObsWrite && !options.allowWrites) {
|
|
176
|
+
return {
|
|
177
|
+
decision: 'deny',
|
|
178
|
+
risk: 'write',
|
|
179
|
+
reason: 'OBS write operation blocked until the agent presents a plan and receives explicit user approval.',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (isObsRead) {
|
|
183
|
+
return {
|
|
184
|
+
decision: 'allow',
|
|
185
|
+
risk: 'read_only',
|
|
186
|
+
reason: 'OBS read-only operation.',
|
|
187
|
+
service,
|
|
188
|
+
operation,
|
|
189
|
+
args: normalizedArgs,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (isObsWrite && options.allowWrites) {
|
|
193
|
+
return {
|
|
194
|
+
decision: 'allow',
|
|
195
|
+
risk: 'write',
|
|
196
|
+
reason: 'OBS write operation approved by user.',
|
|
197
|
+
service,
|
|
198
|
+
operation,
|
|
199
|
+
args: normalizedArgs,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (isExecution && options.allowWrites) {
|
|
204
|
+
return {
|
|
205
|
+
decision: 'allow',
|
|
206
|
+
risk: 'execution',
|
|
207
|
+
reason: 'Huawei Cloud execution/trigger operation approved by user.',
|
|
208
|
+
service,
|
|
209
|
+
operation,
|
|
210
|
+
args: normalizedArgs,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (isWrite && options.allowWrites) {
|
|
215
|
+
return {
|
|
216
|
+
decision: 'allow',
|
|
217
|
+
risk: 'write',
|
|
218
|
+
reason: 'Huawei Cloud write operation approved by user.',
|
|
219
|
+
service,
|
|
220
|
+
operation,
|
|
221
|
+
args: normalizedArgs,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
decision: 'allow',
|
|
227
|
+
risk: readOnly ? 'read_only' : 'unknown_read',
|
|
228
|
+
reason: readOnly
|
|
229
|
+
? 'Command appears to be a read-only Huawei Cloud operation.'
|
|
230
|
+
: 'Command does not match a known write or secret operation; treat output as untrusted and redact it.',
|
|
231
|
+
service,
|
|
232
|
+
operation,
|
|
233
|
+
args: normalizedArgs,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function splitSimpleCommand(command) {
|
|
238
|
+
return String(command)
|
|
239
|
+
.match(/"[^"]*"|'[^']*'|\S+/g)
|
|
240
|
+
?.map((part) => part.replace(/^['"]|['"]$/g, '')) || [];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function classifyTextCommand(command, options = {}) {
|
|
244
|
+
const policy = options.policy || DEFAULT_POLICY;
|
|
245
|
+
const text = String(command || '');
|
|
246
|
+
|
|
247
|
+
if (matchesAny(text, policy.credentialFilePatterns)) {
|
|
248
|
+
return {
|
|
249
|
+
decision: 'deny',
|
|
250
|
+
risk: 'credential',
|
|
251
|
+
reason: 'Reading Huawei Cloud credential or profile files is blocked. Use redacted profile inspection tools instead.',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (/(^|\s)(env|printenv|Get-ChildItem\s+Env:|gci\s+Env:|dir\s+Env:)/i.test(text) && /HUAWEICLOUD|HWC_|HCLOUD|OS_/i.test(text)) {
|
|
256
|
+
return {
|
|
257
|
+
decision: 'deny',
|
|
258
|
+
risk: 'credential',
|
|
259
|
+
reason: 'Dumping cloud credential environment variables is blocked.',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (/(^|\s)hcloud(\.exe)?\s+/i.test(text)) {
|
|
264
|
+
return classifyHcloudArgs(splitSimpleCommand(text), options);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (/ShowSecretVersion|GetSecretValue|secret_string|secret_binary/i.test(text)) {
|
|
268
|
+
return {
|
|
269
|
+
decision: 'deny',
|
|
270
|
+
risk: 'secret',
|
|
271
|
+
reason: 'Direct secret value retrieval patterns are blocked.',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
decision: 'allow',
|
|
277
|
+
risk: 'not_huaweicloud',
|
|
278
|
+
reason: 'No Huawei Cloud safety rule matched.',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function assertAllowed(result) {
|
|
283
|
+
if (result.decision === 'deny') {
|
|
284
|
+
const error = new Error(result.reason);
|
|
285
|
+
error.policy = result;
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const INDEX_URL = 'https://gitcode.com/api/v5/repos/2501_91318609/skills-for-index/contents/skills-index/index.json?ref=main';
|
|
2
|
+
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
|
+
const HTTP_TIMEOUT_MS = 10000;
|
|
4
|
+
|
|
5
|
+
const GENERIC_KEYWORDS = new Set([
|
|
6
|
+
'华为云', 'huawei', 'huawei cloud', '云', 'cloud',
|
|
7
|
+
'技能', 'skill', 'skills', '所有', 'all', '全部',
|
|
8
|
+
'有什么', '有哪些', '相关', '列表', 'list',
|
|
9
|
+
'查找', '搜索', '发现', '浏览', 'find', 'search',
|
|
10
|
+
'discover', 'browse', 'show', 'explore',
|
|
11
|
+
'agent', '市场', 'market', '类目', 'category',
|
|
12
|
+
'安装', 'install',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
let cachedIndex = null;
|
|
16
|
+
let cachedCnEnMap = null;
|
|
17
|
+
|
|
18
|
+
async function fetchJson(url, label = '') {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
|
21
|
+
try {
|
|
22
|
+
const resp = await fetch(url, {
|
|
23
|
+
headers: { 'User-Agent': 'huaweicloud-devkit/1.0' },
|
|
24
|
+
signal: controller.signal,
|
|
25
|
+
});
|
|
26
|
+
const data = await resp.json();
|
|
27
|
+
if (data && data.encoding === 'base64' && data.content) {
|
|
28
|
+
const decoded = Buffer.from(data.content, 'base64').toString('utf8');
|
|
29
|
+
return JSON.parse(decoded);
|
|
30
|
+
}
|
|
31
|
+
return data;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
throw new Error(`Failed to fetch ${label}: ${e.message}`);
|
|
34
|
+
} finally {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function loadIndex() {
|
|
40
|
+
if (!cachedIndex) cachedIndex = await fetchJson(INDEX_URL, 'index.json');
|
|
41
|
+
return cachedIndex;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function loadCnEnMap() {
|
|
45
|
+
if (!cachedCnEnMap) cachedCnEnMap = await fetchJson(CN_EN_MAP_URL, 'cn-en-map.json');
|
|
46
|
+
return cachedCnEnMap;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function clearMarketCache() {
|
|
50
|
+
cachedIndex = null;
|
|
51
|
+
cachedCnEnMap = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isGeneric(kw) {
|
|
55
|
+
const k = kw.toLowerCase().trim();
|
|
56
|
+
return GENERIC_KEYWORDS.has(k) || /华为云|huawei/i.test(k);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function expandKeywords(rawKeyword, cnEnMap) {
|
|
60
|
+
if (!rawKeyword) return [[], []];
|
|
61
|
+
const parts = rawKeyword.replace(/[,;]/g, ' ').split(/\s+/).filter(Boolean);
|
|
62
|
+
const expanded = [...parts];
|
|
63
|
+
for (const p of parts) {
|
|
64
|
+
const lower = p.toLowerCase();
|
|
65
|
+
if (cnEnMap[p]) expanded.push(cnEnMap[p]);
|
|
66
|
+
for (const [cn, en] of Object.entries(cnEnMap)) {
|
|
67
|
+
if (en === lower) expanded.push(cn);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const unique = [...new Set(expanded)].sort();
|
|
71
|
+
const specific = unique.filter((kw) => !isGeneric(kw));
|
|
72
|
+
const generic = unique.filter((kw) => isGeneric(kw));
|
|
73
|
+
return [specific, generic];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function scoreSkill(skill, specificKws, genericKws) {
|
|
77
|
+
if (!specificKws.length && !genericKws.length) return [1, []];
|
|
78
|
+
let total = 0;
|
|
79
|
+
const matched = [];
|
|
80
|
+
const nameLower = (skill.name || '').toLowerCase();
|
|
81
|
+
const descLower = (skill.description || '').toLowerCase();
|
|
82
|
+
const serviceLower = (skill.service || '').toLowerCase();
|
|
83
|
+
const triggers = (skill.triggers || []).map((t) => (t || '').toLowerCase());
|
|
84
|
+
|
|
85
|
+
for (const kw of specificKws) {
|
|
86
|
+
const k = kw.toLowerCase();
|
|
87
|
+
let s = 0;
|
|
88
|
+
if (nameLower.includes(k)) s += 10;
|
|
89
|
+
else if (triggers.some((t) => t.includes(k))) s += 8;
|
|
90
|
+
else if (descLower.includes(k)) s += 5;
|
|
91
|
+
else if (serviceLower.includes(k)) s += 3;
|
|
92
|
+
if (s > 0) { total += s; matched.push(kw); }
|
|
93
|
+
}
|
|
94
|
+
for (const kw of genericKws) {
|
|
95
|
+
const k = kw.toLowerCase();
|
|
96
|
+
let s = 0;
|
|
97
|
+
if (nameLower.includes(k)) s += 10;
|
|
98
|
+
else if (triggers.some((t) => t.includes(k))) s += 4;
|
|
99
|
+
else if (descLower.includes(k)) s += 2;
|
|
100
|
+
else if (serviceLower.includes(k)) s += 1;
|
|
101
|
+
if (s > 0) { total += s; matched.push(kw); }
|
|
102
|
+
}
|
|
103
|
+
if (!specificKws.length && total === 0) {
|
|
104
|
+
total = 1;
|
|
105
|
+
if (descLower.length > 20) total += 1;
|
|
106
|
+
if (triggers.length) total += 1;
|
|
107
|
+
}
|
|
108
|
+
return [total, matched];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function truncate(desc, limit = 150) {
|
|
112
|
+
if (!desc) return '';
|
|
113
|
+
return desc.length > limit ? desc.slice(0, limit) + '...' : desc;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function searchMarketplace(query = '', category = '') {
|
|
117
|
+
const idx = await loadIndex();
|
|
118
|
+
const cnEnMap = await loadCnEnMap();
|
|
119
|
+
const [specificKws, genericKws] = expandKeywords(query, cnEnMap);
|
|
120
|
+
const hasSpecific = specificKws.length > 0;
|
|
121
|
+
|
|
122
|
+
const results = [];
|
|
123
|
+
for (const skill of idx.skills || []) {
|
|
124
|
+
if (category && skill.category !== category) continue;
|
|
125
|
+
const [score, matched] = scoreSkill(skill, specificKws, genericKws);
|
|
126
|
+
if (hasSpecific && score === 0) continue;
|
|
127
|
+
results.push({
|
|
128
|
+
score,
|
|
129
|
+
name: skill.name || '',
|
|
130
|
+
category: skill.category || '',
|
|
131
|
+
service: skill.service || '',
|
|
132
|
+
description: truncate(skill.description),
|
|
133
|
+
triggers: (skill.triggers || []).slice(0, 5),
|
|
134
|
+
matched,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
results.sort((a, b) => b.score - a.score);
|
|
138
|
+
|
|
139
|
+
if (!results.length) {
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
query,
|
|
143
|
+
category,
|
|
144
|
+
count: 0,
|
|
145
|
+
results: [],
|
|
146
|
+
fallback: [
|
|
147
|
+
'Try broader or alternative keywords',
|
|
148
|
+
'Remove category filter',
|
|
149
|
+
'Switch CN<->EN (e.g., obs <-> object storage)',
|
|
150
|
+
'List all: search with empty keyword and a category',
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const allKws = [...new Set([...specificKws, ...genericKws])];
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
query,
|
|
159
|
+
category,
|
|
160
|
+
count: results.length,
|
|
161
|
+
expandedKeywords: allKws.length > 1 ? allKws : undefined,
|
|
162
|
+
results,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function getMarketplaceCategories() {
|
|
167
|
+
const idx = await loadIndex();
|
|
168
|
+
return { ok: true, categories: idx.categories || [] };
|
|
169
|
+
}
|