@zshuangmu/agenthub 0.4.14 → 0.4.16
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/LICENSE +21 -21
- package/README.md +268 -268
- package/package.json +41 -41
- package/src/api-server.js +518 -244
- package/src/cli.js +714 -671
- package/src/commands/api.js +9 -9
- package/src/commands/doctor.js +335 -335
- package/src/commands/info.js +15 -15
- package/src/commands/install.js +56 -56
- package/src/commands/list.js +78 -78
- package/src/commands/pack.js +249 -156
- package/src/commands/publish-remote.js +9 -9
- package/src/commands/publish.js +7 -7
- package/src/commands/rollback.js +59 -59
- package/src/commands/search.js +14 -14
- package/src/commands/serve.js +9 -9
- package/src/commands/stats.js +105 -105
- package/src/commands/uninstall.js +76 -76
- package/src/commands/update.js +54 -54
- package/src/commands/verify.js +133 -133
- package/src/commands/versions.js +75 -75
- package/src/commands/web.js +9 -9
- package/src/index.js +18 -18
- package/src/lib/auth.js +301 -0
- package/src/lib/bundle-transfer.js +58 -58
- package/src/lib/colors.js +60 -60
- package/src/lib/database.js +450 -244
- package/src/lib/debug.js +135 -135
- package/src/lib/fs-utils.js +107 -50
- package/src/lib/html.js +2163 -1824
- package/src/lib/http.js +168 -168
- package/src/lib/install.js +60 -60
- package/src/lib/manifest.js +124 -124
- package/src/lib/openclaw-config.js +40 -40
- package/src/lib/permissions.js +105 -0
- package/src/lib/privacy-engine.js +220 -0
- package/src/lib/registry.js +130 -130
- package/src/lib/remote.js +11 -11
- package/src/lib/security-scanner.js +233 -233
- package/src/lib/signing.js +158 -0
- package/src/lib/version-manager.js +77 -77
- package/src/server.js +176 -176
- package/src/web-server.js +135 -135
package/src/lib/http.js
CHANGED
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
import { request as httpsRequest } from "node:https";
|
|
2
|
-
import { request as httpRequest } from "node:http";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
|
|
6
|
-
const require = createRequire(import.meta.url);
|
|
7
|
-
const VERSION = require("../../package.json").version;
|
|
8
|
-
|
|
9
|
-
export function sendJson(response, statusCode, payload, extraHeaders = {}) {
|
|
10
|
-
response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8", ...extraHeaders });
|
|
11
|
-
response.end(`${JSON.stringify(payload, null, 2)}\n`);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function sendHtml(response, statusCode, html, extraHeaders = {}) {
|
|
15
|
-
response.writeHead(statusCode, { "content-type": "text/html; charset=utf-8", ...extraHeaders });
|
|
16
|
-
response.end(html);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function notFound(response, extraHeaders = {}) {
|
|
20
|
-
sendJson(response, 404, { error: "Not Found" }, extraHeaders);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function readJsonBody(request) {
|
|
24
|
-
const chunks = [];
|
|
25
|
-
for await (const chunk of request) {
|
|
26
|
-
chunks.push(Buffer.from(chunk));
|
|
27
|
-
}
|
|
28
|
-
if (chunks.length === 0) {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 共享的网络请求工具函数
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
function debugLog(message, details) {
|
|
39
|
-
if (!process.env.AGENTHUB_DEBUG_INSTALL) return;
|
|
40
|
-
const suffix = details ? ` | ${JSON.stringify(details)}` : "";
|
|
41
|
-
console.error(`[agenthub:http] ${message}${suffix}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 使用原生 http/https 模块请求
|
|
46
|
-
*/
|
|
47
|
-
export async function requestPayloadText(rawUrl) {
|
|
48
|
-
const parsed = new URL(rawUrl);
|
|
49
|
-
const transport = parsed.protocol === "http:" ? httpRequest : httpsRequest;
|
|
50
|
-
const requestOptions = {
|
|
51
|
-
method: "GET",
|
|
52
|
-
hostname: parsed.hostname,
|
|
53
|
-
port: parsed.port,
|
|
54
|
-
path: `${parsed.pathname}${parsed.search || ""}`,
|
|
55
|
-
protocol: parsed.protocol,
|
|
56
|
-
headers: {
|
|
57
|
-
"User-Agent": `agenthub-cli/${VERSION}`,
|
|
58
|
-
Accept: "application/json",
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
debugLog("http request begin", { url: rawUrl, protocol: parsed.protocol, hostname: parsed.hostname, path: requestOptions.path });
|
|
63
|
-
|
|
64
|
-
return await new Promise((resolve, reject) => {
|
|
65
|
-
const req = transport(requestOptions, (res) => {
|
|
66
|
-
let data = "";
|
|
67
|
-
res.on("data", (chunk) => {
|
|
68
|
-
data += chunk;
|
|
69
|
-
});
|
|
70
|
-
res.on("end", () => {
|
|
71
|
-
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
72
|
-
const reason = `${res.statusCode} ${res.statusMessage || ""}`;
|
|
73
|
-
debugLog("http request failed", { reason, rawUrl });
|
|
74
|
-
reject(new Error(`Remote request failed: ${reason}`));
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
debugLog("http request success", { rawUrl, bytes: data.length });
|
|
78
|
-
resolve(data);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
req.on("error", (error) => {
|
|
83
|
-
debugLog("http request error", { rawUrl, code: error.code, message: error.message, errno: error.errno });
|
|
84
|
-
const errorDetail = error.code || error.message || `errno ${error.errno}` || "unknown network error";
|
|
85
|
-
reject(new Error(`Network error: ${errorDetail} (${parsed.hostname})`));
|
|
86
|
-
});
|
|
87
|
-
req.end();
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* 使用 curl 作为 fallback(绕过 Cloudflare 等网络限制)
|
|
93
|
-
*/
|
|
94
|
-
export async function fetchWithCurl(rawUrl) {
|
|
95
|
-
debugLog("curl fallback begin", { url: rawUrl });
|
|
96
|
-
|
|
97
|
-
return await new Promise((resolve, reject) => {
|
|
98
|
-
const curl = spawn("curl", [
|
|
99
|
-
"-s", "-f",
|
|
100
|
-
"--connect-timeout", "30",
|
|
101
|
-
"-H", "Accept: application/json",
|
|
102
|
-
rawUrl
|
|
103
|
-
]);
|
|
104
|
-
|
|
105
|
-
let stdout = "";
|
|
106
|
-
let stderr = "";
|
|
107
|
-
|
|
108
|
-
curl.stdout.on("data", (data) => { stdout += data; });
|
|
109
|
-
curl.stderr.on("data", (data) => { stderr += data; });
|
|
110
|
-
|
|
111
|
-
curl.on("close", (code) => {
|
|
112
|
-
if (code !== 0) {
|
|
113
|
-
debugLog("curl fallback failed", { code, stderr: stderr.slice(0, 200) });
|
|
114
|
-
reject(new Error(`curl failed with code ${code}: ${stderr.slice(0, 100)}`));
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
debugLog("curl fallback success", { url: rawUrl, bytes: stdout.length });
|
|
118
|
-
resolve(stdout);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
curl.on("error", (err) => {
|
|
122
|
-
debugLog("curl spawn error", { message: err.message });
|
|
123
|
-
reject(new Error(`Failed to spawn curl: ${err.message}`));
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* 多层 fallback 的 JSON 请求
|
|
130
|
-
* 1. 原生 fetch
|
|
131
|
-
* 2. http/https 模块
|
|
132
|
-
* 3. curl 命令
|
|
133
|
-
*/
|
|
134
|
-
export async function fetchJsonWithFallback(url) {
|
|
135
|
-
const baseUrl = url.toString();
|
|
136
|
-
debugLog("requesting payload", { url: baseUrl });
|
|
137
|
-
|
|
138
|
-
// 1. 尝试原生 fetch
|
|
139
|
-
try {
|
|
140
|
-
const response = await fetch(baseUrl);
|
|
141
|
-
debugLog("primary fetch done", { url: baseUrl, ok: response.ok, status: response.status, statusText: response.statusText });
|
|
142
|
-
if (!response.ok) {
|
|
143
|
-
throw new Error(`${response.status} ${response.statusText}`);
|
|
144
|
-
}
|
|
145
|
-
return response.json();
|
|
146
|
-
} catch (err) {
|
|
147
|
-
debugLog("primary fetch failed", { url: baseUrl, error: err.message });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 2. 尝试 http/https 模块
|
|
151
|
-
try {
|
|
152
|
-
const payloadText = await requestPayloadText(baseUrl);
|
|
153
|
-
debugLog("http/https fallback success", { url: baseUrl });
|
|
154
|
-
return JSON.parse(payloadText);
|
|
155
|
-
} catch (err) {
|
|
156
|
-
debugLog("http/https fallback failed", { url: baseUrl, error: err.message });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// 3. 最终 fallback: 使用 curl
|
|
160
|
-
try {
|
|
161
|
-
const payloadText = await fetchWithCurl(baseUrl);
|
|
162
|
-
debugLog("curl fallback success", { url: baseUrl });
|
|
163
|
-
return JSON.parse(payloadText);
|
|
164
|
-
} catch (err) {
|
|
165
|
-
debugLog("all fetch methods failed", { url: baseUrl, finalError: err.message });
|
|
166
|
-
throw new Error(`All fetch methods failed. Last error: ${err.message}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
1
|
+
import { request as httpsRequest } from "node:https";
|
|
2
|
+
import { request as httpRequest } from "node:http";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const VERSION = require("../../package.json").version;
|
|
8
|
+
|
|
9
|
+
export function sendJson(response, statusCode, payload, extraHeaders = {}) {
|
|
10
|
+
response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8", ...extraHeaders });
|
|
11
|
+
response.end(`${JSON.stringify(payload, null, 2)}\n`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function sendHtml(response, statusCode, html, extraHeaders = {}) {
|
|
15
|
+
response.writeHead(statusCode, { "content-type": "text/html; charset=utf-8", ...extraHeaders });
|
|
16
|
+
response.end(html);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function notFound(response, extraHeaders = {}) {
|
|
20
|
+
sendJson(response, 404, { error: "Not Found" }, extraHeaders);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function readJsonBody(request) {
|
|
24
|
+
const chunks = [];
|
|
25
|
+
for await (const chunk of request) {
|
|
26
|
+
chunks.push(Buffer.from(chunk));
|
|
27
|
+
}
|
|
28
|
+
if (chunks.length === 0) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 共享的网络请求工具函数
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
function debugLog(message, details) {
|
|
39
|
+
if (!process.env.AGENTHUB_DEBUG_INSTALL) return;
|
|
40
|
+
const suffix = details ? ` | ${JSON.stringify(details)}` : "";
|
|
41
|
+
console.error(`[agenthub:http] ${message}${suffix}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 使用原生 http/https 模块请求
|
|
46
|
+
*/
|
|
47
|
+
export async function requestPayloadText(rawUrl) {
|
|
48
|
+
const parsed = new URL(rawUrl);
|
|
49
|
+
const transport = parsed.protocol === "http:" ? httpRequest : httpsRequest;
|
|
50
|
+
const requestOptions = {
|
|
51
|
+
method: "GET",
|
|
52
|
+
hostname: parsed.hostname,
|
|
53
|
+
port: parsed.port,
|
|
54
|
+
path: `${parsed.pathname}${parsed.search || ""}`,
|
|
55
|
+
protocol: parsed.protocol,
|
|
56
|
+
headers: {
|
|
57
|
+
"User-Agent": `agenthub-cli/${VERSION}`,
|
|
58
|
+
Accept: "application/json",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
debugLog("http request begin", { url: rawUrl, protocol: parsed.protocol, hostname: parsed.hostname, path: requestOptions.path });
|
|
63
|
+
|
|
64
|
+
return await new Promise((resolve, reject) => {
|
|
65
|
+
const req = transport(requestOptions, (res) => {
|
|
66
|
+
let data = "";
|
|
67
|
+
res.on("data", (chunk) => {
|
|
68
|
+
data += chunk;
|
|
69
|
+
});
|
|
70
|
+
res.on("end", () => {
|
|
71
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
72
|
+
const reason = `${res.statusCode} ${res.statusMessage || ""}`;
|
|
73
|
+
debugLog("http request failed", { reason, rawUrl });
|
|
74
|
+
reject(new Error(`Remote request failed: ${reason}`));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
debugLog("http request success", { rawUrl, bytes: data.length });
|
|
78
|
+
resolve(data);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
req.on("error", (error) => {
|
|
83
|
+
debugLog("http request error", { rawUrl, code: error.code, message: error.message, errno: error.errno });
|
|
84
|
+
const errorDetail = error.code || error.message || `errno ${error.errno}` || "unknown network error";
|
|
85
|
+
reject(new Error(`Network error: ${errorDetail} (${parsed.hostname})`));
|
|
86
|
+
});
|
|
87
|
+
req.end();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 使用 curl 作为 fallback(绕过 Cloudflare 等网络限制)
|
|
93
|
+
*/
|
|
94
|
+
export async function fetchWithCurl(rawUrl) {
|
|
95
|
+
debugLog("curl fallback begin", { url: rawUrl });
|
|
96
|
+
|
|
97
|
+
return await new Promise((resolve, reject) => {
|
|
98
|
+
const curl = spawn("curl", [
|
|
99
|
+
"-s", "-f",
|
|
100
|
+
"--connect-timeout", "30",
|
|
101
|
+
"-H", "Accept: application/json",
|
|
102
|
+
rawUrl
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
let stdout = "";
|
|
106
|
+
let stderr = "";
|
|
107
|
+
|
|
108
|
+
curl.stdout.on("data", (data) => { stdout += data; });
|
|
109
|
+
curl.stderr.on("data", (data) => { stderr += data; });
|
|
110
|
+
|
|
111
|
+
curl.on("close", (code) => {
|
|
112
|
+
if (code !== 0) {
|
|
113
|
+
debugLog("curl fallback failed", { code, stderr: stderr.slice(0, 200) });
|
|
114
|
+
reject(new Error(`curl failed with code ${code}: ${stderr.slice(0, 100)}`));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
debugLog("curl fallback success", { url: rawUrl, bytes: stdout.length });
|
|
118
|
+
resolve(stdout);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
curl.on("error", (err) => {
|
|
122
|
+
debugLog("curl spawn error", { message: err.message });
|
|
123
|
+
reject(new Error(`Failed to spawn curl: ${err.message}`));
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 多层 fallback 的 JSON 请求
|
|
130
|
+
* 1. 原生 fetch
|
|
131
|
+
* 2. http/https 模块
|
|
132
|
+
* 3. curl 命令
|
|
133
|
+
*/
|
|
134
|
+
export async function fetchJsonWithFallback(url) {
|
|
135
|
+
const baseUrl = url.toString();
|
|
136
|
+
debugLog("requesting payload", { url: baseUrl });
|
|
137
|
+
|
|
138
|
+
// 1. 尝试原生 fetch
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch(baseUrl);
|
|
141
|
+
debugLog("primary fetch done", { url: baseUrl, ok: response.ok, status: response.status, statusText: response.statusText });
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
144
|
+
}
|
|
145
|
+
return response.json();
|
|
146
|
+
} catch (err) {
|
|
147
|
+
debugLog("primary fetch failed", { url: baseUrl, error: err.message });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 2. 尝试 http/https 模块
|
|
151
|
+
try {
|
|
152
|
+
const payloadText = await requestPayloadText(baseUrl);
|
|
153
|
+
debugLog("http/https fallback success", { url: baseUrl });
|
|
154
|
+
return JSON.parse(payloadText);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
debugLog("http/https fallback failed", { url: baseUrl, error: err.message });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 3. 最终 fallback: 使用 curl
|
|
160
|
+
try {
|
|
161
|
+
const payloadText = await fetchWithCurl(baseUrl);
|
|
162
|
+
debugLog("curl fallback success", { url: baseUrl });
|
|
163
|
+
return JSON.parse(payloadText);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
debugLog("all fetch methods failed", { url: baseUrl, finalError: err.message });
|
|
166
|
+
throw new Error(`All fetch methods failed. Last error: ${err.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
package/src/lib/install.js
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { copyDir, ensureDir, readJson, writeJson } from "./fs-utils.js";
|
|
3
|
-
import { readAgentInfo, parseSpec } from "./registry.js";
|
|
4
|
-
import { materializeBundlePayload } from "./bundle-transfer.js";
|
|
5
|
-
import { fetchJsonWithFallback } from "./http.js";
|
|
6
|
-
|
|
7
|
-
function debugLog(message, details) {
|
|
8
|
-
if (!process.env.AGENTHUB_DEBUG_INSTALL) return;
|
|
9
|
-
const suffix = details ? ` | ${JSON.stringify(details)}` : "";
|
|
10
|
-
console.error(`[agenthub:install] ${message}${suffix}`);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function applyBundleDir({ bundleDir, targetWorkspace }) {
|
|
14
|
-
const manifest = await readJson(path.join(bundleDir, "MANIFEST.json"));
|
|
15
|
-
await ensureDir(targetWorkspace);
|
|
16
|
-
await copyDir(path.join(bundleDir, "WORKSPACE"), targetWorkspace);
|
|
17
|
-
const template = await readJson(path.join(bundleDir, "OPENCLAW.template.json"));
|
|
18
|
-
const appliedPath = path.join(targetWorkspace, ".agenthub", "OPENCLAW.applied.json");
|
|
19
|
-
await writeJson(appliedPath, template);
|
|
20
|
-
return { manifest, appliedPath };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function installFromRemote({ serverUrl, agentSpec, targetWorkspace }) {
|
|
24
|
-
const { slug, version } = parseSpec(agentSpec);
|
|
25
|
-
const url = new URL(`/api/agents/${slug}/download`, serverUrl);
|
|
26
|
-
if (version) {
|
|
27
|
-
url.searchParams.set("version", version);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
debugLog("installing from remote", { serverUrl, slug, version: version || "latest", targetWorkspace, url: url.toString() });
|
|
31
|
-
|
|
32
|
-
let payload;
|
|
33
|
-
try {
|
|
34
|
-
payload = await fetchJsonWithFallback(url);
|
|
35
|
-
} catch (error) {
|
|
36
|
-
debugLog("remote install failed", { slug, error: error.message, cause: error.cause });
|
|
37
|
-
// 提取更详细的错误信息
|
|
38
|
-
const causeMsg = error.cause?.errors?.[0]?.message || error.cause?.message || "";
|
|
39
|
-
const detailMsg = causeMsg ? `${error.message}: ${causeMsg}` : error.message;
|
|
40
|
-
throw new Error(`Remote install failed: ${detailMsg}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
debugLog("payload received", { slug, size: JSON.stringify(payload).length });
|
|
44
|
-
const bundleDir = await materializeBundlePayload(payload);
|
|
45
|
-
return applyBundleDir({ bundleDir, targetWorkspace });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export async function installBundle({ registryDir, serverUrl, agentSpec, targetWorkspace }) {
|
|
49
|
-
if (registryDir) {
|
|
50
|
-
const manifest = await readAgentInfo(registryDir, agentSpec);
|
|
51
|
-
const bundleDir = path.join(registryDir, "agents", manifest.slug, manifest.version);
|
|
52
|
-
return applyBundleDir({ bundleDir, targetWorkspace });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (serverUrl) {
|
|
56
|
-
return installFromRemote({ serverUrl, agentSpec, targetWorkspace });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
throw new Error("Either registryDir or serverUrl is required");
|
|
60
|
-
}
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { copyDir, ensureDir, readJson, writeJson } from "./fs-utils.js";
|
|
3
|
+
import { readAgentInfo, parseSpec } from "./registry.js";
|
|
4
|
+
import { materializeBundlePayload } from "./bundle-transfer.js";
|
|
5
|
+
import { fetchJsonWithFallback } from "./http.js";
|
|
6
|
+
|
|
7
|
+
function debugLog(message, details) {
|
|
8
|
+
if (!process.env.AGENTHUB_DEBUG_INSTALL) return;
|
|
9
|
+
const suffix = details ? ` | ${JSON.stringify(details)}` : "";
|
|
10
|
+
console.error(`[agenthub:install] ${message}${suffix}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function applyBundleDir({ bundleDir, targetWorkspace }) {
|
|
14
|
+
const manifest = await readJson(path.join(bundleDir, "MANIFEST.json"));
|
|
15
|
+
await ensureDir(targetWorkspace);
|
|
16
|
+
await copyDir(path.join(bundleDir, "WORKSPACE"), targetWorkspace);
|
|
17
|
+
const template = await readJson(path.join(bundleDir, "OPENCLAW.template.json"));
|
|
18
|
+
const appliedPath = path.join(targetWorkspace, ".agenthub", "OPENCLAW.applied.json");
|
|
19
|
+
await writeJson(appliedPath, template);
|
|
20
|
+
return { manifest, appliedPath };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function installFromRemote({ serverUrl, agentSpec, targetWorkspace }) {
|
|
24
|
+
const { slug, version } = parseSpec(agentSpec);
|
|
25
|
+
const url = new URL(`/api/agents/${slug}/download`, serverUrl);
|
|
26
|
+
if (version) {
|
|
27
|
+
url.searchParams.set("version", version);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
debugLog("installing from remote", { serverUrl, slug, version: version || "latest", targetWorkspace, url: url.toString() });
|
|
31
|
+
|
|
32
|
+
let payload;
|
|
33
|
+
try {
|
|
34
|
+
payload = await fetchJsonWithFallback(url);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
debugLog("remote install failed", { slug, error: error.message, cause: error.cause });
|
|
37
|
+
// 提取更详细的错误信息
|
|
38
|
+
const causeMsg = error.cause?.errors?.[0]?.message || error.cause?.message || "";
|
|
39
|
+
const detailMsg = causeMsg ? `${error.message}: ${causeMsg}` : error.message;
|
|
40
|
+
throw new Error(`Remote install failed: ${detailMsg}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
debugLog("payload received", { slug, size: JSON.stringify(payload).length });
|
|
44
|
+
const bundleDir = await materializeBundlePayload(payload);
|
|
45
|
+
return applyBundleDir({ bundleDir, targetWorkspace });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function installBundle({ registryDir, serverUrl, agentSpec, targetWorkspace }) {
|
|
49
|
+
if (registryDir) {
|
|
50
|
+
const manifest = await readAgentInfo(registryDir, agentSpec);
|
|
51
|
+
const bundleDir = path.join(registryDir, "agents", manifest.slug, manifest.version);
|
|
52
|
+
return applyBundleDir({ bundleDir, targetWorkspace });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (serverUrl) {
|
|
56
|
+
return installFromRemote({ serverUrl, agentSpec, targetWorkspace });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new Error("Either registryDir or serverUrl is required");
|
|
60
|
+
}
|