@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.
@@ -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
- export async function installBundle({ registryDir, agentSpec, targetWorkspace }) {
6
- const manifest = await readAgentInfo(registryDir, agentSpec);
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
+ }
@@ -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
- return index.agents.filter((entry) => entry.slug.toLowerCase().includes(normalized));
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
  }