aixlab-skills 0.1.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 +9 -0
- package/bin/aixlab-skills.js +11 -0
- package/package.json +32 -0
- package/src/index.js +463 -0
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# aixlab-skills
|
|
2
|
+
|
|
3
|
+
AixLab 自有数字资产平台的 Agent Skill 安装器。它通过浏览器设备授权取得最小权限令牌,下载用户已拥有的私有 ZIP,校验 SHA-256 与 `SKILL.md` 名称后,再调用固定版本的通用 `skills` CLI 完成全局复制安装。
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx aixlab-skills add xhs-creator -g -y
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
本地联调可设置 `AIXLAB_SKILLS_HOST=http://localhost:8180`。生产环境默认连接 `https://www.aixc4d.com`。本目录只包含待发布的 npm 包源码;发布属于独立操作。
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aixlab-skills",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Secure installer for owned AixLab agent Skills",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aixlab-skills": "bin/aixlab-skills.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"agent-skills",
|
|
22
|
+
"codex",
|
|
23
|
+
"aixlab"
|
|
24
|
+
],
|
|
25
|
+
"license": "UNLICENSED",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"yauzl": "3.4.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"yazl": "3.3.1"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir, platform, tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import yauzl from "yauzl";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_HOST = "https://www.aixc4d.com";
|
|
11
|
+
const GENERIC_INSTALLER_VERSION = "1.5.22";
|
|
12
|
+
const MAX_PACKAGE_BYTES = 100 * 1024 * 1024;
|
|
13
|
+
const MAX_ARCHIVE_ENTRIES = 2000;
|
|
14
|
+
|
|
15
|
+
export function parseArguments(args) {
|
|
16
|
+
const values = [...args];
|
|
17
|
+
|
|
18
|
+
if (values.shift() !== "add") {
|
|
19
|
+
throw new Error("用法:aixlab-skills add <skill-name> -g -y");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const skillName = values.shift() ?? "";
|
|
23
|
+
|
|
24
|
+
if (! /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(skillName)) {
|
|
25
|
+
throw new Error("Skill 名称格式无效。");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const accepted = new Set(["-g", "--global", "-y", "--yes"]);
|
|
29
|
+
|
|
30
|
+
if (values.some(value => ! accepted.has(value))) {
|
|
31
|
+
throw new Error("包含不支持的安装参数。");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { skillName };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalizeHost(rawHost) {
|
|
38
|
+
const url = new URL(rawHost || DEFAULT_HOST);
|
|
39
|
+
const localHttp = url.protocol === "http:"
|
|
40
|
+
&& ["localhost", "127.0.0.1", "::1"].includes(url.hostname);
|
|
41
|
+
|
|
42
|
+
if ((url.protocol !== "https:" && ! localHttp)
|
|
43
|
+
|| url.username
|
|
44
|
+
|| url.password
|
|
45
|
+
|| url.search
|
|
46
|
+
|| url.hash
|
|
47
|
+
) {
|
|
48
|
+
throw new Error("AIXLAB_SKILLS_HOST 必须是 HTTPS 地址;仅本机调试允许 HTTP。");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
url.pathname = url.pathname.replace(/\/?$/, "/");
|
|
52
|
+
|
|
53
|
+
return url;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function skillNameFromMarkdown(markdown) {
|
|
57
|
+
const text = String(markdown).replace(/^\uFEFF/, "");
|
|
58
|
+
const frontmatter = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] ?? "";
|
|
59
|
+
const match = frontmatter.match(/^name:\s*(["']?)([a-z0-9]+(?:-[a-z0-9]+)*)\1\s*$/m);
|
|
60
|
+
|
|
61
|
+
return match?.[2] ?? "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function configPath() {
|
|
65
|
+
return join(homedir(), ".config", "aixlab-skills", "config.json");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function loadToken(host) {
|
|
69
|
+
const environmentToken = process.env.AIXLAB_SKILLS_TOKEN?.trim();
|
|
70
|
+
|
|
71
|
+
if (environmentToken) return environmentToken;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const stored = JSON.parse(await readFile(configPath(), "utf8"));
|
|
75
|
+
|
|
76
|
+
return stored.host === host.origin && typeof stored.access_token === "string"
|
|
77
|
+
? stored.access_token
|
|
78
|
+
: null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function saveToken(host, token) {
|
|
85
|
+
const target = configPath();
|
|
86
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
87
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
88
|
+
await writeFile(temporary, `${JSON.stringify({ host: host.origin, access_token: token })}\n`, {
|
|
89
|
+
encoding: "utf8",
|
|
90
|
+
mode: 0o600,
|
|
91
|
+
});
|
|
92
|
+
await chmod(temporary, 0o600);
|
|
93
|
+
await rename(temporary, target);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function requestJson(host, path, { method = "GET", token, body, statuses = [200] } = {}) {
|
|
97
|
+
const url = new URL(path.replace(/^\//, ""), host);
|
|
98
|
+
const response = await fetch(url, {
|
|
99
|
+
method,
|
|
100
|
+
redirect: "manual",
|
|
101
|
+
headers: {
|
|
102
|
+
Accept: "application/json",
|
|
103
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
104
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
105
|
+
},
|
|
106
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
107
|
+
});
|
|
108
|
+
let payload = {};
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
payload = await response.json();
|
|
112
|
+
} catch {
|
|
113
|
+
// The status remains the source of truth for malformed error responses.
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (! statuses.includes(response.status)) {
|
|
117
|
+
const error = new Error(payload.message || payload.error || `平台请求失败(HTTP ${response.status})。`);
|
|
118
|
+
error.status = response.status;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { status: response.status, payload };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function openBrowser(url) {
|
|
126
|
+
const command = platform() === "win32"
|
|
127
|
+
? ["explorer.exe", [url]]
|
|
128
|
+
: platform() === "darwin"
|
|
129
|
+
? ["open", [url]]
|
|
130
|
+
: ["xdg-open", [url]];
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const child = spawn(command[0], command[1], { detached: true, stdio: "ignore" });
|
|
134
|
+
child.unref();
|
|
135
|
+
} catch {
|
|
136
|
+
// Printing the URL is sufficient when the desktop opener is unavailable.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function authorizeDevice(host) {
|
|
141
|
+
const { payload } = await requestJson(host, "marketplace-cli/device-codes", { method: "POST" });
|
|
142
|
+
const expiresIn = Number(payload.expires_in);
|
|
143
|
+
const interval = Number(payload.interval);
|
|
144
|
+
|
|
145
|
+
if (typeof payload.device_code !== "string"
|
|
146
|
+
|| typeof payload.user_code !== "string"
|
|
147
|
+
|| typeof payload.verification_uri_complete !== "string"
|
|
148
|
+
|| ! Number.isFinite(expiresIn)
|
|
149
|
+
|| ! Number.isFinite(interval)
|
|
150
|
+
) {
|
|
151
|
+
throw new Error("平台返回了无效的设备授权信息。");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
process.stderr.write(`请在浏览器中确认 AixLab 安装授权:\n${payload.verification_uri_complete}\n授权码:${payload.user_code}\n`);
|
|
155
|
+
openBrowser(payload.verification_uri_complete);
|
|
156
|
+
const deadline = Date.now() + (expiresIn * 1000);
|
|
157
|
+
|
|
158
|
+
while (Date.now() < deadline) {
|
|
159
|
+
await new Promise(resolve => setTimeout(resolve, Math.max(2, interval) * 1000));
|
|
160
|
+
const result = await requestJson(host, "marketplace-cli/device-token", {
|
|
161
|
+
method: "POST",
|
|
162
|
+
body: { device_code: payload.device_code },
|
|
163
|
+
statuses: [200, 202, 410],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (result.status === 202) continue;
|
|
167
|
+
if (result.status === 410) break;
|
|
168
|
+
|
|
169
|
+
if (typeof result.payload.access_token !== "string" || result.payload.token_type !== "Bearer") {
|
|
170
|
+
throw new Error("平台返回了无效的访问令牌。");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await saveToken(host, result.payload.access_token);
|
|
174
|
+
|
|
175
|
+
return result.payload.access_token;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
throw new Error("安装授权已过期,请重新执行原命令。");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function validateManifest(payload, skillName, host) {
|
|
182
|
+
const packageData = payload?.package;
|
|
183
|
+
|
|
184
|
+
if (payload?.skill_name !== skillName
|
|
185
|
+
|| typeof payload.version !== "string"
|
|
186
|
+
|| ! packageData
|
|
187
|
+
|| typeof packageData.url !== "string"
|
|
188
|
+
|| typeof packageData.sha256 !== "string"
|
|
189
|
+
|| ! /^[0-9a-f]{64}$/.test(packageData.sha256)
|
|
190
|
+
|| ! Number.isSafeInteger(packageData.size_bytes)
|
|
191
|
+
|| packageData.size_bytes < 0
|
|
192
|
+
|| packageData.size_bytes > MAX_PACKAGE_BYTES
|
|
193
|
+
) {
|
|
194
|
+
throw new Error("平台返回了无效的 Skill 安装清单。");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const packageUrl = new URL(packageData.url);
|
|
198
|
+
|
|
199
|
+
if (packageUrl.origin !== host.origin || packageUrl.protocol !== host.protocol) {
|
|
200
|
+
throw new Error("平台返回的下载地址不属于当前 AixLab 站点。");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { ...payload, package: { ...packageData, url: packageUrl } };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function issuePackage(host, token, skillName) {
|
|
207
|
+
const { payload } = await requestJson(
|
|
208
|
+
host,
|
|
209
|
+
`marketplace-cli/skills/${encodeURIComponent(skillName)}/package-access`,
|
|
210
|
+
{ method: "POST", token },
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return validateManifest(payload, skillName, host);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function downloadPackage(manifest, token, target) {
|
|
217
|
+
const response = await fetch(manifest.package.url, {
|
|
218
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/zip" },
|
|
219
|
+
redirect: "manual",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (response.status !== 200) {
|
|
223
|
+
throw new Error(`Skill 下载失败(HTTP ${response.status})。`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
227
|
+
|
|
228
|
+
if (bytes.length !== manifest.package.size_bytes
|
|
229
|
+
|| createHash("sha256").update(bytes).digest("hex") !== manifest.package.sha256
|
|
230
|
+
) {
|
|
231
|
+
throw new Error("Skill 包完整性校验失败。");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await writeFile(target, bytes, { flag: "wx", mode: 0o600 });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function validatedEntryName(name) {
|
|
238
|
+
if (! name
|
|
239
|
+
|| name.includes("\\")
|
|
240
|
+
|| name.includes("\0")
|
|
241
|
+
|| name.startsWith("/")
|
|
242
|
+
|| /^[A-Za-z]:/.test(name)
|
|
243
|
+
) {
|
|
244
|
+
throw new Error("Skill 包包含不安全的文件路径。");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const trimmed = name.endsWith("/") ? name.slice(0, -1) : name;
|
|
248
|
+
const segments = trimmed.split("/");
|
|
249
|
+
|
|
250
|
+
if (! trimmed || segments.some(segment => ! segment || segment === "." || segment === "..")) {
|
|
251
|
+
throw new Error("Skill 包包含不安全的文件路径。");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return segments;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function extractPackage(archive, destination) {
|
|
258
|
+
await mkdir(destination, { recursive: true, mode: 0o700 });
|
|
259
|
+
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
yauzl.open(archive, { lazyEntries: true, strictFileNames: true, validateEntrySizes: true }, (openError, zipfile) => {
|
|
262
|
+
if (openError || ! zipfile) {
|
|
263
|
+
reject(new Error("Skill 包不是有效的 ZIP 文件。", { cause: openError }));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let settled = false;
|
|
268
|
+
let entryCount = 0;
|
|
269
|
+
let totalBytes = 0;
|
|
270
|
+
const paths = new Set();
|
|
271
|
+
const fail = error => {
|
|
272
|
+
if (settled) return;
|
|
273
|
+
settled = true;
|
|
274
|
+
zipfile.close();
|
|
275
|
+
reject(error);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
zipfile.on("error", fail);
|
|
279
|
+
zipfile.on("end", () => {
|
|
280
|
+
if (settled) return;
|
|
281
|
+
settled = true;
|
|
282
|
+
resolve();
|
|
283
|
+
});
|
|
284
|
+
zipfile.on("entry", async entry => {
|
|
285
|
+
try {
|
|
286
|
+
entryCount += 1;
|
|
287
|
+
totalBytes += entry.uncompressedSize;
|
|
288
|
+
|
|
289
|
+
if (entryCount > MAX_ARCHIVE_ENTRIES || totalBytes > MAX_PACKAGE_BYTES) {
|
|
290
|
+
throw new Error("Skill 包超过安全解压限制。");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const segments = validatedEntryName(entry.fileName);
|
|
294
|
+
const relative = segments.join("/");
|
|
295
|
+
|
|
296
|
+
if (paths.has(relative)) {
|
|
297
|
+
throw new Error("Skill 包包含重复文件路径。");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
paths.add(relative);
|
|
301
|
+
const mode = (entry.externalFileAttributes >>> 16) & 0o170000;
|
|
302
|
+
const isDirectory = entry.fileName.endsWith("/");
|
|
303
|
+
|
|
304
|
+
if (mode === 0o120000 || (mode !== 0 && mode !== 0o100000 && mode !== 0o040000)) {
|
|
305
|
+
throw new Error("Skill 包包含不支持的文件类型。");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const target = join(destination, ...segments);
|
|
309
|
+
|
|
310
|
+
if (isDirectory) {
|
|
311
|
+
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
312
|
+
zipfile.readEntry();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
317
|
+
zipfile.openReadStream(entry, async (streamError, stream) => {
|
|
318
|
+
if (streamError || ! stream) {
|
|
319
|
+
fail(new Error("无法读取 Skill 包内容。", { cause: streamError }));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
await pipeline(stream, createWriteStream(target, { flags: "wx", mode: 0o600 }));
|
|
325
|
+
zipfile.readEntry();
|
|
326
|
+
} catch (error) {
|
|
327
|
+
fail(error);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
} catch (error) {
|
|
331
|
+
fail(error);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
zipfile.readEntry();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function findSkillRoots(directory, depth = 0) {
|
|
340
|
+
if (depth > 4) return [];
|
|
341
|
+
|
|
342
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
343
|
+
const roots = [];
|
|
344
|
+
|
|
345
|
+
if (entries.some(entry => entry.isFile() && entry.name === "SKILL.md")) {
|
|
346
|
+
roots.push(directory);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
if (entry.isDirectory() && entry.name !== "__MACOSX") {
|
|
351
|
+
roots.push(...await findSkillRoots(join(directory, entry.name), depth + 1));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return roots;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function validateExtractedSkill(directory, expectedName) {
|
|
359
|
+
const roots = await findSkillRoots(directory);
|
|
360
|
+
|
|
361
|
+
if (roots.length !== 1) {
|
|
362
|
+
throw new Error("Skill 包必须且只能包含一个 SKILL.md。");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const markdown = await readFile(join(roots[0], "SKILL.md"), "utf8");
|
|
366
|
+
|
|
367
|
+
if (skillNameFromMarkdown(markdown) !== expectedName) {
|
|
368
|
+
throw new Error("SKILL.md 的 name 与安装名称不一致。");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return roots[0];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function installerArguments(skillRoot) {
|
|
375
|
+
return [
|
|
376
|
+
"--yes",
|
|
377
|
+
`skills@${GENERIC_INSTALLER_VERSION}`,
|
|
378
|
+
"add",
|
|
379
|
+
skillRoot,
|
|
380
|
+
"--global",
|
|
381
|
+
"--yes",
|
|
382
|
+
"--copy",
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function installSkill(skillRoot) {
|
|
387
|
+
const executable = platform() === "win32" ? "npx.cmd" : "npx";
|
|
388
|
+
const child = spawn(executable, installerArguments(skillRoot), {
|
|
389
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
390
|
+
windowsHide: true,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
child.stdout.pipe(process.stderr);
|
|
394
|
+
child.stderr.pipe(process.stderr);
|
|
395
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
396
|
+
child.once("error", reject);
|
|
397
|
+
child.once("close", resolve);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (exitCode !== 0) {
|
|
401
|
+
throw new Error(`通用 Skills 安装器执行失败(退出码 ${exitCode})。`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function execute(skillName) {
|
|
406
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
407
|
+
|
|
408
|
+
if (! Number.isInteger(major) || major < 20) {
|
|
409
|
+
throw new Error("aixlab-skills 需要 Node.js 20 或更高版本。");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const host = normalizeHost(process.env.AIXLAB_SKILLS_HOST || DEFAULT_HOST);
|
|
413
|
+
let token = await loadToken(host);
|
|
414
|
+
|
|
415
|
+
if (! token) token = await authorizeDevice(host);
|
|
416
|
+
|
|
417
|
+
let manifest;
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
manifest = await issuePackage(host, token, skillName);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (error.status !== 401) throw error;
|
|
423
|
+
token = await authorizeDevice(host);
|
|
424
|
+
manifest = await issuePackage(host, token, skillName);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const working = await mkdtemp(join(tmpdir(), "aixlab-skills-"));
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
const archive = join(working, "package.zip");
|
|
431
|
+
const extracted = join(working, "extracted");
|
|
432
|
+
await downloadPackage(manifest, token, archive);
|
|
433
|
+
await extractPackage(archive, extracted);
|
|
434
|
+
const skillRoot = await validateExtractedSkill(extracted, skillName);
|
|
435
|
+
await installSkill(skillRoot);
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
skill_name: skillName,
|
|
439
|
+
success: true,
|
|
440
|
+
version: manifest.version,
|
|
441
|
+
verified: true,
|
|
442
|
+
message: "Skill 已通过 AixLab 权益校验、完整性校验并完成全局安装。",
|
|
443
|
+
};
|
|
444
|
+
} finally {
|
|
445
|
+
await rm(working, { recursive: true, force: true });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export async function run(args) {
|
|
450
|
+
let skillName = "";
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
({ skillName } = parseArguments(args));
|
|
454
|
+
|
|
455
|
+
return await execute(skillName);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
return {
|
|
458
|
+
skill_name: skillName,
|
|
459
|
+
success: false,
|
|
460
|
+
message: error instanceof Error ? error.message : "未知安装错误。",
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|