fix(clawhub): accept live artifact resolver field aliases

Accept live ClawHub artifact resolver kind/sha256 aliases for npm-pack and legacy ZIP installs.\n\nVerified locally after rebase:\n- pnpm exec oxfmt --check --threads=1 src/plugins/clawhub.ts src/plugins/clawhub.test.ts CHANGELOG.md\n- pnpm test:serial src/plugins/clawhub.test.ts
This commit is contained in:
Dallin Romney
2026-05-04 01:31:27 +08:00
committed by GitHub
parent 68918ab36a
commit ec73e9985c
3 changed files with 124 additions and 7 deletions

View File

@@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai
- Memory/status: keep plain `openclaw memory status` and `openclaw memory status --json` on the cheap read-only path by reserving vector and embedding provider probes for `--deep` or `--index`. Fixes #76769. Thanks @daruire.
- Telegram: suppress stale same-session replies when a newer accepted message arrives before an older in-flight Telegram dispatch finalizes. Fixes #76642. Thanks @chinar-amrutkar.
- Gateway/diagnostics: abort-drain embedded runs after an extended no-progress stall so a single dead session no longer leaves queued Discord/channel turns blocked behind repeated `recovery=none` liveness warnings.
- Plugins/ClawHub: accept the live artifact resolver `kind`/`sha256` field names alongside the typed `artifactKind`/`artifactSha256` form so `clawhub:` installs of npm-pack and legacy ZIP packages no longer miss downloadable artifacts. Thanks @romneyda.
- Control UI/Sessions: avoid full `sessions.list` reloads for chat-turn `sessions.changed` payloads, so large session stores no longer add multi-second delays while chat responses are being delivered. (#76676) Thanks @VACInc.
- Gateway/watch: run `doctor --fix --non-interactive` once and retry when the dev Gateway child exits during startup, so stale local plugin install/config state does not leave the tmux watch session disappearing without a repair attempt.
- Doctor/Telegram: warn when selected Telegram quote replies can suppress `streaming.preview.toolProgress`, and document the `replyToMode` trade-off without changing runtime delivery. Fixes #73487. Thanks @GodsBoy.

View File

@@ -51,10 +51,12 @@ vi.mock("../infra/archive.js", async () => {
});
const { ClawHubRequestError } = await import("../infra/clawhub.js");
type ClawHubResolvedArtifact = import("../infra/clawhub.js").ClawHubResolvedArtifact;
const { CLAWHUB_INSTALL_ERROR_CODE, formatClawHubSpecifier, installPluginFromClawHub } =
await import("./clawhub.js");
const DEMO_ARCHIVE_INTEGRITY = "sha256-qerEjGEpvES2+Tyan0j2xwDRkbcnmh4ZFfKN9vWbsa8=";
const DEMO_ARCHIVE_SHA256 = "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af";
const DEMO_CLAWPACK_SHA256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const DEMO_CLAWPACK_INTEGRITY = `sha256-${Buffer.from(DEMO_CLAWPACK_SHA256, "hex").toString(
"base64",
@@ -463,6 +465,102 @@ describe("installPluginFromClawHub", () => {
);
});
it("accepts the live ClawHub artifact resolver shape with kind/sha256 field names", async () => {
fetchClawHubPackageVersionMock.mockClear();
fetchClawHubPackageArtifactMock.mockResolvedValueOnce({
package: {
name: "demo",
displayName: "Demo",
family: "code-plugin",
},
version: "2026.3.22",
artifact: {
kind: "npm-pack",
sha256: DEMO_CLAWPACK_SHA256,
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
} as unknown as ClawHubResolvedArtifact,
});
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",
artifactFormat: "tgz",
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
clawpackSha256: DEMO_CLAWPACK_SHA256,
},
});
expect(fetchClawHubPackageVersionMock).not.toHaveBeenCalled();
expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith(
expect.objectContaining({
artifact: "clawpack",
name: "demo",
version: "2026.3.22",
}),
);
});
it("accepts the live ClawHub legacy zip resolver shape with kind/sha256 field names", async () => {
fetchClawHubPackageVersionMock.mockClear();
fetchClawHubPackageArtifactMock.mockResolvedValueOnce({
package: {
name: "demo",
displayName: "Demo",
family: "code-plugin",
},
version: "2026.3.22",
artifact: {
kind: "legacy-zip",
sha256: DEMO_ARCHIVE_SHA256,
} as unknown as ClawHubResolvedArtifact,
});
downloadClawHubPackageArchiveMock.mockResolvedValueOnce({
archivePath: "/tmp/clawhub-demo/archive.zip",
integrity: DEMO_ARCHIVE_INTEGRITY,
cleanup: archiveCleanupMock,
});
const result = await installPluginFromClawHub({
spec: "clawhub:demo",
baseUrl: "https://clawhub.ai",
});
expect(result).toMatchObject({
ok: true,
pluginId: "demo",
clawhub: {
artifactKind: "legacy-zip",
artifactFormat: "zip",
integrity: DEMO_ARCHIVE_INTEGRITY,
},
});
expect(fetchClawHubPackageVersionMock).not.toHaveBeenCalled();
expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith(
expect.objectContaining({
artifact: "archive",
name: "demo",
version: "2026.3.22",
}),
);
});
it("falls back to version metadata when the ClawHub artifact resolver route is missing", async () => {
fetchClawHubPackageArtifactMock.mockRejectedValueOnce(
new ClawHubRequestError({

View File

@@ -265,29 +265,47 @@ function normalizeArtifactResolverFiles(
return files as NonNullable<ClawHubPackageVersion["version"]>["files"];
}
type ClawHubResolvedArtifactWire = {
artifactKind?: string | null;
kind?: string | null;
artifactSha256?: string | null;
sha256?: string | null;
npmIntegrity?: string | null;
npmShasum?: string | null;
downloadUrl?: string | null;
};
function resolveTopLevelNpmPackArtifact(
artifact: ClawHubResolvedArtifact | null | undefined,
): ClawHubPackageArtifactSummary | null {
if (artifact?.artifactKind !== "npm-pack") {
const wire = artifact as ClawHubResolvedArtifactWire | null | undefined;
const artifactKind = wire?.artifactKind ?? wire?.kind;
if (artifactKind !== "npm-pack") {
return null;
}
if (typeof wire?.npmIntegrity !== "string") {
return null;
}
return {
kind: "npm-pack",
format: "tgz",
sha256: artifact.artifactSha256 ?? null,
npmIntegrity: artifact.npmIntegrity,
npmShasum: artifact.npmShasum ?? null,
downloadUrl: artifact.downloadUrl ?? null,
sha256: wire.artifactSha256 ?? wire.sha256 ?? null,
npmIntegrity: wire.npmIntegrity,
npmShasum: wire.npmShasum ?? null,
downloadUrl: wire.downloadUrl ?? null,
};
}
function resolveTopLevelLegacyArchiveVerification(
artifact: ClawHubResolvedArtifact | null | undefined,
): ClawHubArchiveVerification | null {
if (artifact?.artifactKind !== "legacy-zip" || typeof artifact.artifactSha256 !== "string") {
const wire = artifact as ClawHubResolvedArtifactWire | null | undefined;
const artifactKind = wire?.artifactKind ?? wire?.kind;
const artifactSha256 = wire?.artifactSha256 ?? wire?.sha256;
if (artifactKind !== "legacy-zip" || typeof artifactSha256 !== "string") {
return null;
}
const integrity = normalizeClawHubSha256Integrity(artifact.artifactSha256);
const integrity = normalizeClawHubSha256Integrity(artifactSha256);
return integrity ? { kind: "archive-integrity", integrity } : null;
}