@zshuangmu/agenthub 0.1.0 → 0.1.2
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/package.json +1 -1
- package/src/api-server.js +22 -1
- package/src/cli.js +6 -3
- package/src/commands/install.js +20 -2
- package/src/lib/html.js +420 -347
- package/src/lib/install.js +52 -3
- package/src/lib/registry.js +38 -3
- package/src/server.js +36 -1
package/src/lib/install.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { copyDir, ensureDir, readJson, writeJson } from "./fs-utils.js";
|
|
3
3
|
import { readAgentInfo } from "./registry.js";
|
|
4
|
+
import { materializeBundlePayload } from "./bundle-transfer.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
const manifest = await
|
|
7
|
-
const bundleDir = path.join(registryDir, "agents", manifest.slug, manifest.version);
|
|
6
|
+
async function applyBundleDir({ bundleDir, targetWorkspace }) {
|
|
7
|
+
const manifest = await readJson(path.join(bundleDir, "MANIFEST.json"));
|
|
8
8
|
await ensureDir(targetWorkspace);
|
|
9
9
|
await copyDir(path.join(bundleDir, "WORKSPACE"), targetWorkspace);
|
|
10
10
|
const template = await readJson(path.join(bundleDir, "OPENCLAW.template.json"));
|
|
@@ -12,3 +12,52 @@ export async function installBundle({ registryDir, agentSpec, targetWorkspace })
|
|
|
12
12
|
await writeJson(appliedPath, template);
|
|
13
13
|
return { manifest, appliedPath };
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 解析 agentSpec,支持两种格式:
|
|
18
|
+
* - 短名格式:slug 或 slug:version
|
|
19
|
+
* - URI 格式:agenthub://owner/slug@version
|
|
20
|
+
*/
|
|
21
|
+
function parseAgentSpec(agentSpec) {
|
|
22
|
+
// URI 格式:agenthub://owner/slug@version
|
|
23
|
+
if (agentSpec.startsWith("agenthub://")) {
|
|
24
|
+
const uri = agentSpec.slice("agenthub://".length);
|
|
25
|
+
// owner/slug@version -> slug@version
|
|
26
|
+
const parts = uri.split("/");
|
|
27
|
+
const lastPart = parts[parts.length - 1] || parts[parts.length - 2];
|
|
28
|
+
const [slug, version] = lastPart.split("@");
|
|
29
|
+
return { slug, version: version || undefined };
|
|
30
|
+
}
|
|
31
|
+
// 短名格式:slug 或 slug:version
|
|
32
|
+
const [slug, version] = agentSpec.split(":");
|
|
33
|
+
return { slug, version: version || undefined };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function installFromRemote({ serverUrl, agentSpec, targetWorkspace }) {
|
|
37
|
+
const { slug, version } = parseAgentSpec(agentSpec);
|
|
38
|
+
const url = new URL(`/api/agents/${slug}/download`, serverUrl);
|
|
39
|
+
if (version) {
|
|
40
|
+
url.searchParams.set("version", version);
|
|
41
|
+
}
|
|
42
|
+
const response = await fetch(url);
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error(`Remote install failed: ${response.status} ${await response.text()}`);
|
|
45
|
+
}
|
|
46
|
+
const payload = await response.json();
|
|
47
|
+
const bundleDir = await materializeBundlePayload(payload);
|
|
48
|
+
return applyBundleDir({ bundleDir, targetWorkspace });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function installBundle({ registryDir, serverUrl, agentSpec, targetWorkspace }) {
|
|
52
|
+
if (registryDir) {
|
|
53
|
+
const manifest = await readAgentInfo(registryDir, agentSpec);
|
|
54
|
+
const bundleDir = path.join(registryDir, "agents", manifest.slug, manifest.version);
|
|
55
|
+
return applyBundleDir({ bundleDir, targetWorkspace });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (serverUrl) {
|
|
59
|
+
return installFromRemote({ serverUrl, agentSpec, targetWorkspace });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw new Error("Either registryDir or serverUrl is required");
|
|
63
|
+
}
|
package/src/lib/registry.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { copyDir, ensureDir, pathExists, readJson, writeJson } from "./fs-utils.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* 解析 agentSpec,支持两种格式:
|
|
6
|
+
* - 短名格式:slug 或 slug:version
|
|
7
|
+
* - URI 格式:agenthub://owner/slug@version
|
|
8
|
+
*/
|
|
4
9
|
function parseSpec(agentSpec) {
|
|
10
|
+
// URI 格式:agenthub://owner/slug@version
|
|
11
|
+
if (agentSpec.startsWith("agenthub://")) {
|
|
12
|
+
const uri = agentSpec.slice("agenthub://".length);
|
|
13
|
+
const parts = uri.split("/");
|
|
14
|
+
const lastPart = parts[parts.length - 1] || parts[parts.length - 2];
|
|
15
|
+
const [slug, version] = lastPart.split("@");
|
|
16
|
+
return { slug, version: version || undefined };
|
|
17
|
+
}
|
|
18
|
+
// 短名格式:slug 或 slug:version
|
|
5
19
|
const [slug, version] = agentSpec.split(":");
|
|
6
|
-
return { slug, version };
|
|
20
|
+
return { slug, version: version || undefined };
|
|
7
21
|
}
|
|
8
22
|
|
|
9
23
|
export async function publishBundle(bundleDir, registryDir) {
|
|
@@ -30,6 +44,9 @@ export async function publishBundle(bundleDir, registryDir) {
|
|
|
30
44
|
name: manifest.name,
|
|
31
45
|
description: manifest.description,
|
|
32
46
|
runtime: manifest.runtime,
|
|
47
|
+
updatedAt: new Date().toISOString(),
|
|
48
|
+
tags: manifest.metadata?.tags || [],
|
|
49
|
+
category: manifest.metadata?.category || 'General',
|
|
33
50
|
});
|
|
34
51
|
index.agents.sort((left, right) => left.slug.localeCompare(right.slug));
|
|
35
52
|
await writeJson(indexPath, index);
|
|
@@ -43,8 +60,26 @@ export async function searchRegistry(registryDir, query) {
|
|
|
43
60
|
return [];
|
|
44
61
|
}
|
|
45
62
|
const index = await readJson(indexPath);
|
|
46
|
-
const normalized = query.toLowerCase();
|
|
47
|
-
|
|
63
|
+
const normalized = query.toLowerCase().trim();
|
|
64
|
+
|
|
65
|
+
// 空查询返回所有
|
|
66
|
+
if (!normalized) {
|
|
67
|
+
return index.agents;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return index.agents.filter((entry) => {
|
|
71
|
+
// 搜索 slug
|
|
72
|
+
if (entry.slug?.toLowerCase().includes(normalized)) return true;
|
|
73
|
+
// 搜索 name
|
|
74
|
+
if (entry.name?.toLowerCase().includes(normalized)) return true;
|
|
75
|
+
// 搜索 description
|
|
76
|
+
if (entry.description?.toLowerCase().includes(normalized)) return true;
|
|
77
|
+
// 搜索 tags
|
|
78
|
+
if (entry.tags?.some(tag => tag.toLowerCase().includes(normalized))) return true;
|
|
79
|
+
// 搜索 category
|
|
80
|
+
if (entry.category?.toLowerCase().includes(normalized)) return true;
|
|
81
|
+
return false;
|
|
82
|
+
});
|
|
48
83
|
}
|
|
49
84
|
|
|
50
85
|
export async function readAgentInfo(registryDir, agentSpec) {
|
package/src/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import http from "node:http";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { infoCommand, installCommand, publishCommand, searchCommand } from "./index.js";
|
|
5
|
-
import { publishUploadedBundle } from "./lib/bundle-transfer.js";
|
|
5
|
+
import { publishUploadedBundle, serializeBundleDir } from "./lib/bundle-transfer.js";
|
|
6
6
|
import { notFound, readJsonBody, sendHtml, sendJson } from "./lib/http.js";
|
|
7
7
|
import { renderAgentDetailPage, renderAgentListPage, renderStatsPage } from "./lib/html.js";
|
|
8
8
|
import {
|
|
@@ -47,6 +47,16 @@ export async function createServer({ registryDir, port = 3000, host = "0.0.0.0"
|
|
|
47
47
|
const slugs = agents.map(a => a.slug);
|
|
48
48
|
const downloads = await getAgentsDownloads(registryDir, slugs);
|
|
49
49
|
const agentsWithDownloads = agents.map(a => ({ ...a, downloads: downloads[a.slug] || 0 }));
|
|
50
|
+
// 按下载量降序排序,下载量一致时按更新时间降序排序
|
|
51
|
+
agentsWithDownloads.sort((a, b) => {
|
|
52
|
+
if (b.downloads !== a.downloads) {
|
|
53
|
+
return b.downloads - a.downloads;
|
|
54
|
+
}
|
|
55
|
+
// 下载量一致时,按更新时间降序(最近更新的排前面)
|
|
56
|
+
const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
57
|
+
const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
58
|
+
return timeB - timeA;
|
|
59
|
+
});
|
|
50
60
|
sendJson(response, 200, { agents: agentsWithDownloads });
|
|
51
61
|
return;
|
|
52
62
|
}
|
|
@@ -99,6 +109,21 @@ export async function createServer({ registryDir, port = 3000, host = "0.0.0.0"
|
|
|
99
109
|
return;
|
|
100
110
|
}
|
|
101
111
|
|
|
112
|
+
if (url.pathname.startsWith("/api/agents/") && url.pathname.endsWith("/download")) {
|
|
113
|
+
const slug = url.pathname.slice("/api/agents/".length, -"/download".length);
|
|
114
|
+
const version = url.searchParams.get("version") || undefined;
|
|
115
|
+
const manifest = await infoCommand(version ? `${slug}:${version}` : slug, { registry: registryDir });
|
|
116
|
+
const bundleDir = path.join(registryDir, "agents", manifest.slug, manifest.version);
|
|
117
|
+
const payload = await serializeBundleDir(bundleDir);
|
|
118
|
+
// 记录下载(包含元数据)
|
|
119
|
+
await incrementDownloads(registryDir, manifest.slug, {
|
|
120
|
+
ip: request.socket.remoteAddress,
|
|
121
|
+
userAgent: request.headers['user-agent']
|
|
122
|
+
});
|
|
123
|
+
sendJson(response, 200, payload);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
102
127
|
if (url.pathname.startsWith("/api/agents/")) {
|
|
103
128
|
const slug = url.pathname.slice("/api/agents/".length);
|
|
104
129
|
const manifest = await infoCommand(slug, { registry: registryDir });
|
|
@@ -116,6 +141,16 @@ export async function createServer({ registryDir, port = 3000, host = "0.0.0.0"
|
|
|
116
141
|
const downloads = await getAgentsDownloads(registryDir, slugs);
|
|
117
142
|
const totalDownloads = await getTotalDownloads(registryDir);
|
|
118
143
|
const agentsWithDownloads = agents.map(a => ({ ...a, downloads: downloads[a.slug] || 0 }));
|
|
144
|
+
// 按下载量降序排序,下载量一致时按更新时间降序排序
|
|
145
|
+
agentsWithDownloads.sort((a, b) => {
|
|
146
|
+
if (b.downloads !== a.downloads) {
|
|
147
|
+
return b.downloads - a.downloads;
|
|
148
|
+
}
|
|
149
|
+
// 下载量一致时,按更新时间降序(最近更新的排前面)
|
|
150
|
+
const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
151
|
+
const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
152
|
+
return timeB - timeA;
|
|
153
|
+
});
|
|
119
154
|
sendHtml(response, 200, renderAgentListPage({ query, agents: agentsWithDownloads, totalDownloads }));
|
|
120
155
|
return;
|
|
121
156
|
}
|