aixlab-skills 0.1.2 → 1.0.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 +16 -5
- package/bin/aixlab-skills.js +0 -0
- package/package.json +2 -2
- package/src/index.js +56 -207
package/README.md
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
# aixlab-skills
|
|
2
2
|
|
|
3
|
-
AixLab
|
|
3
|
+
先在 AixLab 领取或购买 Skill 并下载完整 ZIP,再从本地安装。安装器不登录平台、不打开设备授权页面、不保存账号令牌,也不代替用户领取或下载资产。
|
|
4
|
+
|
|
5
|
+
需要 Node.js 20+。将下载的 ZIP 放在当前目录或用户的 Downloads 目录,再执行:
|
|
4
6
|
|
|
5
7
|
```bash
|
|
6
|
-
npx aixlab-skills add
|
|
8
|
+
npx --yes aixlab-skills@1.0.0 add ppdesign-h3 -g -y
|
|
7
9
|
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
文件名可以是 `ppdesign-h3.zip`、`ppdesign-h3-1.0.0.zip` 或浏览器添加序号的同名文件。如果有多个版本,或 ZIP 放在其他目录,请明确指定:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx --yes aixlab-skills@1.0.0 add ppdesign-h3 -g -y --file "C:/Users/你的用户名/Downloads/ppdesign-h3-1.0.0.zip"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
可用 AIXLAB_SKILLS_DOWNLOAD_DIR 指定下载目录;安装器只检查当前目录和该下载目录,不递归扫描用户文件。
|
|
18
|
+
|
|
19
|
+
安装前会校验 ZIP 路径、文件类型、解压大小和唯一 SKILL.md 的名称,然后调用固定版本 skills@1.5.22 完成全局复制安装。Skill 的所有配套资源会随目录一起安装。首次获取通用安装器需要访问 npm,但不需要 AixLab 二次授权。
|
|
20
|
+
|
|
21
|
+
也可直接解压完整 Skill 目录并放入兼容 Agent 的 Skills 目录,无需运行本安装器。更新已有 Skill 前应保留用户自己的修改。
|
|
10
22
|
|
|
11
|
-
|
|
12
|
-
[`docs/agent/runbooks/AixLab-Skills-npm发布与交接手册.md`](../../../docs/agent/runbooks/AixLab-Skills-npm发布与交接手册.md)。
|
|
23
|
+
npm 只包含公开安装器代码;付费 Skill、素材、私有下载地址和用户凭据均不在 npm 包中。平台领取与下载继续按原有权益规则执行。
|
package/bin/aixlab-skills.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aixlab-skills",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Install downloaded AixLab Skill ZIPs locally without account authorization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"aixlab-skills": "bin/aixlab-skills.js"
|
package/src/index.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { createWriteStream } from "node:fs";
|
|
3
|
-
import {
|
|
2
|
+
import { copyFile, lstat, mkdir, mkdtemp, readFile, readdir, rm } from "node:fs/promises";
|
|
4
3
|
import { homedir, platform, tmpdir } from "node:os";
|
|
5
|
-
import { dirname, join, win32 as win32Path } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve, win32 as win32Path } from "node:path";
|
|
6
5
|
import { pipeline } from "node:stream/promises";
|
|
7
6
|
import { spawn } from "node:child_process";
|
|
8
7
|
import yauzl from "yauzl";
|
|
9
8
|
|
|
10
|
-
const DEFAULT_HOST = "https://www.aixc4d.com";
|
|
11
9
|
const GENERIC_INSTALLER_VERSION = "1.5.22";
|
|
12
10
|
const MAX_PACKAGE_BYTES = 100 * 1024 * 1024;
|
|
13
11
|
const MAX_ARCHIVE_ENTRIES = 2000;
|
|
@@ -16,7 +14,7 @@ export function parseArguments(args) {
|
|
|
16
14
|
const values = [...args];
|
|
17
15
|
|
|
18
16
|
if (values.shift() !== "add") {
|
|
19
|
-
throw new Error("用法:aixlab-skills add <skill-name> -g -y");
|
|
17
|
+
throw new Error("用法:aixlab-skills add <skill-name> -g -y [--file <已下载的 ZIP 路径>]");
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
const skillName = values.shift() ?? "";
|
|
@@ -26,31 +24,23 @@ export function parseArguments(args) {
|
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
const accepted = new Set(["-g", "--global", "-y", "--yes"]);
|
|
27
|
+
let packageFile;
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return { skillName };
|
|
35
|
-
}
|
|
29
|
+
while (values.length) {
|
|
30
|
+
const value = values.shift();
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const localHttp = url.protocol === "http:"
|
|
40
|
-
&& ["localhost", "127.0.0.1", "::1"].includes(url.hostname);
|
|
32
|
+
if (value === "--file" && packageFile === undefined) {
|
|
33
|
+
packageFile = values.shift();
|
|
41
34
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
throw new Error("AIXLAB_SKILLS_HOST 必须是 HTTPS 地址;仅本机调试允许 HTTP。");
|
|
35
|
+
if (typeof packageFile !== "string" || ! packageFile.trim() || packageFile.startsWith("-")) {
|
|
36
|
+
throw new Error("--file 后需要填写已下载的 ZIP 路径。");
|
|
37
|
+
}
|
|
38
|
+
} else if (! accepted.has(value)) {
|
|
39
|
+
throw new Error("包含不支持的安装参数。");
|
|
40
|
+
}
|
|
49
41
|
}
|
|
50
42
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return url;
|
|
43
|
+
return { skillName, ...(packageFile === undefined ? {} : { packageFile }) };
|
|
54
44
|
}
|
|
55
45
|
|
|
56
46
|
export function skillNameFromMarkdown(markdown) {
|
|
@@ -61,183 +51,43 @@ export function skillNameFromMarkdown(markdown) {
|
|
|
61
51
|
return match?.[2] ?? "";
|
|
62
52
|
}
|
|
63
53
|
|
|
64
|
-
function
|
|
65
|
-
|
|
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
|
-
export function openBrowser(url, {
|
|
126
|
-
platformName = platform(),
|
|
127
|
-
spawnProcess = spawn,
|
|
54
|
+
export async function resolvePackage(skillName, packageFile, {
|
|
55
|
+
cwd = process.cwd(),
|
|
56
|
+
downloadsDirectory = process.env.AIXLAB_SKILLS_DOWNLOAD_DIR || join(homedir(), "Downloads"),
|
|
128
57
|
} = {}) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
: ["xdg-open", [url]];
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const child = spawnProcess(command[0], command[1], { detached: true, stdio: "ignore" });
|
|
137
|
-
child.once("error", () => {
|
|
138
|
-
// Printing the URL is sufficient when the desktop opener is unavailable.
|
|
139
|
-
});
|
|
140
|
-
child.unref();
|
|
141
|
-
} catch {
|
|
142
|
-
// Printing the URL is sufficient when the desktop opener is unavailable.
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function authorizeDevice(host) {
|
|
147
|
-
const { payload } = await requestJson(host, "marketplace-cli/device-codes", { method: "POST" });
|
|
148
|
-
const expiresIn = Number(payload.expires_in);
|
|
149
|
-
const interval = Number(payload.interval);
|
|
58
|
+
if (packageFile !== undefined) {
|
|
59
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(packageFile) || ! /\.zip$/i.test(packageFile)) {
|
|
60
|
+
throw new Error("--file 只接受本地 ZIP 文件,不接受下载网址。");
|
|
61
|
+
}
|
|
150
62
|
|
|
151
|
-
|
|
152
|
-
|| typeof payload.user_code !== "string"
|
|
153
|
-
|| typeof payload.verification_uri_complete !== "string"
|
|
154
|
-
|| ! Number.isFinite(expiresIn)
|
|
155
|
-
|| ! Number.isFinite(interval)
|
|
156
|
-
) {
|
|
157
|
-
throw new Error("平台返回了无效的设备授权信息。");
|
|
63
|
+
return isAbsolute(packageFile) ? packageFile : resolve(cwd, packageFile);
|
|
158
64
|
}
|
|
159
65
|
|
|
160
|
-
|
|
161
|
-
openBrowser(payload.verification_uri_complete);
|
|
162
|
-
const deadline = Date.now() + (expiresIn * 1000);
|
|
163
|
-
|
|
164
|
-
while (Date.now() < deadline) {
|
|
165
|
-
await new Promise(resolve => setTimeout(resolve, Math.max(2, interval) * 1000));
|
|
166
|
-
const result = await requestJson(host, "marketplace-cli/device-token", {
|
|
167
|
-
method: "POST",
|
|
168
|
-
body: { device_code: payload.device_code },
|
|
169
|
-
statuses: [200, 202, 410],
|
|
170
|
-
});
|
|
66
|
+
const filename = new RegExp("^" + skillName + "(?:-[0-9][0-9A-Za-z.+_-]*)?(?: \\(\\d+\\))?\\.zip$", "i");
|
|
171
67
|
|
|
172
|
-
|
|
173
|
-
|
|
68
|
+
for (const directory of new Set([cwd, downloadsDirectory])) {
|
|
69
|
+
let entries;
|
|
174
70
|
|
|
175
|
-
|
|
176
|
-
|
|
71
|
+
try {
|
|
72
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.code === "ENOENT") continue;
|
|
75
|
+
throw error;
|
|
177
76
|
}
|
|
178
77
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return result.payload.access_token;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
throw new Error("安装授权已过期,请重新执行原命令。");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function validateManifest(payload, skillName, host) {
|
|
188
|
-
const packageData = payload?.package;
|
|
189
|
-
|
|
190
|
-
if (payload?.skill_name !== skillName
|
|
191
|
-
|| typeof payload.version !== "string"
|
|
192
|
-
|| ! packageData
|
|
193
|
-
|| typeof packageData.url !== "string"
|
|
194
|
-
|| typeof packageData.sha256 !== "string"
|
|
195
|
-
|| ! /^[0-9a-f]{64}$/.test(packageData.sha256)
|
|
196
|
-
|| ! Number.isSafeInteger(packageData.size_bytes)
|
|
197
|
-
|| packageData.size_bytes < 0
|
|
198
|
-
|| packageData.size_bytes > MAX_PACKAGE_BYTES
|
|
199
|
-
) {
|
|
200
|
-
throw new Error("平台返回了无效的 Skill 安装清单。");
|
|
201
|
-
}
|
|
78
|
+
const matches = entries.filter(entry => entry.isFile() && filename.test(entry.name));
|
|
202
79
|
|
|
203
|
-
|
|
80
|
+
if (matches.length > 1) {
|
|
81
|
+
throw new Error("找到多个同名 Skill 压缩包,请使用 --file 指定要安装的 ZIP 路径。");
|
|
82
|
+
}
|
|
204
83
|
|
|
205
|
-
|
|
206
|
-
throw new Error("平台返回的下载地址不属于当前 AixLab 站点。");
|
|
84
|
+
if (matches.length === 1) return join(directory, matches[0].name);
|
|
207
85
|
}
|
|
208
86
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
async function issuePackage(host, token, skillName) {
|
|
213
|
-
const { payload } = await requestJson(
|
|
214
|
-
host,
|
|
215
|
-
`marketplace-cli/skills/${encodeURIComponent(skillName)}/package-access`,
|
|
216
|
-
{ method: "POST", token },
|
|
87
|
+
throw new Error(
|
|
88
|
+
"未找到已下载的 Skill ZIP。请先在资产平台领取并下载,将 ZIP 放到当前目录或 Downloads,"
|
|
89
|
+
+ "也可在命令后添加 --file \"ZIP 文件路径\"。安装过程无需账号或设备授权。",
|
|
217
90
|
);
|
|
218
|
-
|
|
219
|
-
return validateManifest(payload, skillName, host);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async function downloadPackage(manifest, token, target) {
|
|
223
|
-
const response = await fetch(manifest.package.url, {
|
|
224
|
-
headers: { Authorization: `Bearer ${token}`, Accept: "application/zip" },
|
|
225
|
-
redirect: "manual",
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
if (response.status !== 200) {
|
|
229
|
-
throw new Error(`Skill 下载失败(HTTP ${response.status})。`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const bytes = Buffer.from(await response.arrayBuffer());
|
|
233
|
-
|
|
234
|
-
if (bytes.length !== manifest.package.size_bytes
|
|
235
|
-
|| createHash("sha256").update(bytes).digest("hex") !== manifest.package.sha256
|
|
236
|
-
) {
|
|
237
|
-
throw new Error("Skill 包完整性校验失败。");
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
await writeFile(target, bytes, { flag: "wx", mode: 0o600 });
|
|
241
91
|
}
|
|
242
92
|
|
|
243
93
|
function validatedEntryName(name) {
|
|
@@ -432,26 +282,25 @@ async function installSkill(skillRoot) {
|
|
|
432
282
|
}
|
|
433
283
|
}
|
|
434
284
|
|
|
435
|
-
async function execute(skillName) {
|
|
285
|
+
async function execute({ skillName, packageFile }, options) {
|
|
436
286
|
const major = Number(process.versions.node.split(".")[0]);
|
|
437
287
|
|
|
438
288
|
if (! Number.isInteger(major) || major < 20) {
|
|
439
289
|
throw new Error("aixlab-skills 需要 Node.js 20 或更高版本。");
|
|
440
290
|
}
|
|
441
291
|
|
|
442
|
-
const
|
|
443
|
-
let
|
|
444
|
-
|
|
445
|
-
if (! token) token = await authorizeDevice(host);
|
|
446
|
-
|
|
447
|
-
let manifest;
|
|
292
|
+
const source = await resolvePackage(skillName, packageFile, options);
|
|
293
|
+
let sourceInfo;
|
|
448
294
|
|
|
449
295
|
try {
|
|
450
|
-
|
|
296
|
+
sourceInfo = await lstat(source);
|
|
451
297
|
} catch (error) {
|
|
452
|
-
if (error.
|
|
453
|
-
|
|
454
|
-
|
|
298
|
+
if (error.code !== "ENOENT") throw error;
|
|
299
|
+
throw new Error("指定的 ZIP 文件不存在,请检查 --file 路径。");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (! sourceInfo.isFile() || sourceInfo.size === 0 || sourceInfo.size > MAX_PACKAGE_BYTES) {
|
|
303
|
+
throw new Error("请选择有效的本地 ZIP 文件,压缩包最大 100 MiB。");
|
|
455
304
|
}
|
|
456
305
|
|
|
457
306
|
const working = await mkdtemp(join(tmpdir(), "aixlab-skills-"));
|
|
@@ -459,30 +308,30 @@ async function execute(skillName) {
|
|
|
459
308
|
try {
|
|
460
309
|
const archive = join(working, "package.zip");
|
|
461
310
|
const extracted = join(working, "extracted");
|
|
462
|
-
await
|
|
311
|
+
await copyFile(source, archive);
|
|
463
312
|
await extractPackage(archive, extracted);
|
|
464
313
|
const skillRoot = await validateExtractedSkill(extracted, skillName);
|
|
465
|
-
await installSkill(skillRoot);
|
|
314
|
+
await (options.install ?? installSkill)(skillRoot);
|
|
466
315
|
|
|
467
316
|
return {
|
|
468
317
|
skill_name: skillName,
|
|
469
318
|
success: true,
|
|
470
|
-
version: manifest.version,
|
|
471
319
|
verified: true,
|
|
472
|
-
message: "
|
|
320
|
+
message: "已验证本地 ZIP 结构与 Skill 名称并完成全局安装,无需二次授权。",
|
|
473
321
|
};
|
|
474
322
|
} finally {
|
|
475
323
|
await rm(working, { recursive: true, force: true });
|
|
476
324
|
}
|
|
477
325
|
}
|
|
478
326
|
|
|
479
|
-
export async function run(args) {
|
|
327
|
+
export async function run(args, options = {}) {
|
|
480
328
|
let skillName = "";
|
|
481
329
|
|
|
482
330
|
try {
|
|
483
|
-
|
|
331
|
+
const parsed = parseArguments(args);
|
|
332
|
+
skillName = parsed.skillName;
|
|
484
333
|
|
|
485
|
-
return await execute(
|
|
334
|
+
return await execute(parsed, options);
|
|
486
335
|
} catch (error) {
|
|
487
336
|
return {
|
|
488
337
|
skill_name: skillName,
|