mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(plugins): tolerate missing clawhub artifact resolver
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Plugins/externalization: keep ACPX, Google Chat, and LINE publishable plugin dist trees out of the core npm package file list.
|
||||
- Plugins/ClawHub: fall back to version metadata when the artifact resolver route is missing and keep the Docker ClawHub fixture aligned with npm-pack artifact resolution, avoiding false version-not-found failures during plugin install validation. Thanks @vincentkoc.
|
||||
- Status/channels: show configured channels in `openclaw status` and config-only `openclaw channels status` output even when the Gateway is unreachable, avoiding empty Channels tables on WSL and other no-Gateway paths. Thanks @vincentkoc.
|
||||
- Plugins/ClawHub: explain unavailable explicit ClawHub ClawPack artifact downloads with a temporary npm install hint while ClawHub artifact routing rolls out. Thanks @vincentkoc.
|
||||
- Media: accept home-relative `MEDIA:~/...` attachment paths while preserving existing file-read policy, traversal checks, and media type validation. Fixes #73796. Thanks @fabkury.
|
||||
|
||||
@@ -338,6 +338,23 @@ async function main() {
|
||||
response.writeHead(status, { "content-type": "application/json" });
|
||||
response.end(`${JSON.stringify(value)}\n`);
|
||||
};
|
||||
const artifactResolverDetail = {
|
||||
package: versionDetail.package ?? {
|
||||
name: packageName,
|
||||
displayName: packageDetail.package?.displayName ?? "OpenClaw Kitchen Sink",
|
||||
family: packageDetail.package?.family ?? "code-plugin",
|
||||
},
|
||||
version: versionDetail.version,
|
||||
artifact: {
|
||||
source: "clawhub",
|
||||
artifactKind: "npm-pack",
|
||||
packageName,
|
||||
version: fixture.version,
|
||||
artifactSha256: clawpack.clawpackSha256,
|
||||
npmIntegrity: clawpack.npmIntegrity,
|
||||
npmShasum: clawpack.npmShasum,
|
||||
},
|
||||
};
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
const url = new URL(request.url, "http://127.0.0.1");
|
||||
@@ -357,6 +374,13 @@ async function main() {
|
||||
json(response, versionDetail);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
url.pathname ===
|
||||
`/api/v1/packages/${encodeURIComponent(packageName)}/versions/${fixture.version}/artifact`
|
||||
) {
|
||||
json(response, artifactResolverDetail);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
betaStatus !== undefined &&
|
||||
url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/beta`
|
||||
|
||||
@@ -463,6 +463,77 @@ describe("installPluginFromClawHub", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to version metadata when the ClawHub artifact resolver route is missing", async () => {
|
||||
fetchClawHubPackageArtifactMock.mockRejectedValueOnce(
|
||||
new ClawHubRequestError({
|
||||
path: "/api/v1/packages/demo/versions/2026.3.22/artifact",
|
||||
status: 404,
|
||||
body: "Not Found",
|
||||
}),
|
||||
);
|
||||
fetchClawHubPackageVersionMock.mockResolvedValueOnce({
|
||||
package: {
|
||||
name: "demo",
|
||||
displayName: "Demo",
|
||||
family: "code-plugin",
|
||||
},
|
||||
version: {
|
||||
version: "2026.3.22",
|
||||
createdAt: 0,
|
||||
changelog: "",
|
||||
compatibility: {
|
||||
pluginApiRange: ">=2026.3.22",
|
||||
minGatewayVersion: "2026.3.0",
|
||||
},
|
||||
artifact: {
|
||||
kind: "npm-pack",
|
||||
format: "tgz",
|
||||
sha256: DEMO_CLAWPACK_SHA256,
|
||||
size: 4096,
|
||||
npmIntegrity: "sha512-clawpack",
|
||||
npmShasum: "1".repeat(40),
|
||||
},
|
||||
},
|
||||
});
|
||||
downloadClawHubPackageArchiveMock.mockResolvedValueOnce({
|
||||
archivePath: "/tmp/clawhub-demo/demo-2026.3.22.tgz",
|
||||
integrity: DEMO_CLAWPACK_INTEGRITY,
|
||||
sha256Hex: DEMO_CLAWPACK_SHA256,
|
||||
artifact: "clawpack",
|
||||
clawpackHeaderSha256: DEMO_CLAWPACK_SHA256,
|
||||
npmIntegrity: "sha512-clawpack",
|
||||
npmShasum: "1".repeat(40),
|
||||
cleanup: archiveCleanupMock,
|
||||
});
|
||||
|
||||
const result = await installPluginFromClawHub({
|
||||
spec: "clawhub:demo",
|
||||
baseUrl: "https://clawhub.ai",
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
clawhub: {
|
||||
artifactKind: "npm-pack",
|
||||
npmIntegrity: "sha512-clawpack",
|
||||
clawpackSha256: DEMO_CLAWPACK_SHA256,
|
||||
},
|
||||
});
|
||||
expect(fetchClawHubPackageVersionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "demo",
|
||||
version: "2026.3.22",
|
||||
}),
|
||||
);
|
||||
expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
artifact: "clawpack",
|
||||
name: "demo",
|
||||
version: "2026.3.22",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("installs ClawPack artifacts when version metadata has no legacy archive hash", async () => {
|
||||
fetchClawHubPackageVersionMock.mockResolvedValueOnce({
|
||||
version: {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
downloadClawHubPackageArchive,
|
||||
fetchClawHubPackageArtifact,
|
||||
fetchClawHubPackageDetail,
|
||||
fetchClawHubPackageVersion,
|
||||
normalizeClawHubSha256Integrity,
|
||||
normalizeClawHubSha256Hex,
|
||||
parseClawHubPluginSpec,
|
||||
@@ -330,6 +331,38 @@ function mapClawHubRequestError(
|
||||
return buildClawHubInstallFailure(formatErrorMessage(error));
|
||||
}
|
||||
|
||||
function isMissingArtifactResolverRoute(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof ClawHubRequestError &&
|
||||
error.status === 404 &&
|
||||
error.requestPath.endsWith("/artifact")
|
||||
);
|
||||
}
|
||||
|
||||
function buildArtifactResolverResponseFromVersion(params: {
|
||||
detail: ClawHubPackageDetail;
|
||||
versionDetail: ClawHubPackageVersion;
|
||||
}): ClawHubPackageArtifactResolverResponse {
|
||||
const packageDetail = params.detail.package;
|
||||
const versionPackage = params.versionDetail.package;
|
||||
return {
|
||||
package: versionPackage
|
||||
? {
|
||||
name: versionPackage.name,
|
||||
displayName: versionPackage.displayName,
|
||||
family: versionPackage.family,
|
||||
}
|
||||
: packageDetail
|
||||
? {
|
||||
name: packageDetail.name,
|
||||
displayName: packageDetail.displayName,
|
||||
family: packageDetail.family,
|
||||
}
|
||||
: null,
|
||||
version: params.versionDetail.version,
|
||||
};
|
||||
}
|
||||
|
||||
function formatClawHubClawPackDownloadError(params: {
|
||||
error: unknown;
|
||||
packageName: string;
|
||||
@@ -783,7 +816,7 @@ async function resolveCompatiblePackageVersion(params: {
|
||||
CLAWHUB_INSTALL_ERROR_CODE.NO_INSTALLABLE_VERSION,
|
||||
);
|
||||
}
|
||||
let artifactResponse;
|
||||
let artifactResponse: ClawHubPackageArtifactResolverResponse;
|
||||
try {
|
||||
artifactResponse = await fetchClawHubPackageArtifact({
|
||||
name: params.detail.package?.name ?? "",
|
||||
@@ -793,11 +826,33 @@ async function resolveCompatiblePackageVersion(params: {
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
} catch (error) {
|
||||
return mapClawHubRequestError(error, {
|
||||
stage: "version",
|
||||
name: params.detail.package?.name ?? "unknown",
|
||||
version: requestedVersion,
|
||||
});
|
||||
if (isMissingArtifactResolverRoute(error)) {
|
||||
try {
|
||||
const versionDetail = await fetchClawHubPackageVersion({
|
||||
name: params.detail.package?.name ?? "",
|
||||
version: requestedVersion,
|
||||
baseUrl: params.baseUrl,
|
||||
token: params.token,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
artifactResponse = buildArtifactResolverResponseFromVersion({
|
||||
detail: params.detail,
|
||||
versionDetail,
|
||||
});
|
||||
} catch (versionError) {
|
||||
return mapClawHubRequestError(versionError, {
|
||||
stage: "version",
|
||||
name: params.detail.package?.name ?? "unknown",
|
||||
version: requestedVersion,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return mapClawHubRequestError(error, {
|
||||
stage: "version",
|
||||
name: params.detail.package?.name ?? "unknown",
|
||||
version: requestedVersion,
|
||||
});
|
||||
}
|
||||
}
|
||||
const artifactVersion = readArtifactResolverVersion(artifactResponse, requestedVersion);
|
||||
const resolvedVersion = normalizeOptionalString(artifactVersion.version) ?? requestedVersion;
|
||||
|
||||
@@ -173,6 +173,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
|
||||
expect(assertionsScript).toContain('node_modules", "openclaw');
|
||||
expect(fixtureServer).toContain('"is-number": "7.0.0"');
|
||||
expect(fixtureServer).toContain('openclaw: ">=2026.4.11"');
|
||||
expect(fixtureServer).toContain("/versions/${fixture.version}/artifact");
|
||||
});
|
||||
|
||||
it("wires the full plugin prerelease plan into its release workflow", () => {
|
||||
|
||||
Reference in New Issue
Block a user