























@@ -2,6 +2,7 @@
22import { execFileSync } from "node:child_process";
33import { resolve } from "node:path";
44import { validateExternalCodePluginPackageJson } from "../../packages/plugin-package-contract/src/index.ts";
5+import { readBoundedResponseText } from "./bounded-response.ts";
56import {
67collectExtensionPackageJsonCandidates,
78collectChangedPathsFromGitRange,
@@ -86,6 +87,8 @@ type ClawHubPublishablePluginPackageFilters = {
8687};
87888889const CLAWHUB_DEFAULT_REGISTRY = "https://clawhub.ai";
90+const CLAWHUB_REQUEST_TIMEOUT_MS = 30_000;
91+const CLAWHUB_RESPONSE_BODY_MAX_BYTES = 64 * 1024;
8992const OPENCLAW_PLUGIN_CLAWHUB_REPOSITORY = "openclaw/openclaw";
9093const OPENCLAW_PLUGIN_CLAWHUB_WORKFLOW_FILENAME = "plugin-clawhub-release.yml";
9194const SAFE_EXTENSION_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
@@ -114,6 +117,58 @@ function getRegistryBaseUrl(explicit?: string) {
114117);
115118}
116119120+type ClawHubRequestOptions = {
121+fetchImpl?: typeof fetch;
122+requestTimeoutMs?: number;
123+};
124+125+async function fetchClawHubRequest(
126+url: URL,
127+options: ClawHubRequestOptions = {},
128+): Promise<{
129+clearTimeout: () => void;
130+response: Response;
131+signal: AbortSignal;
132+timeoutPromise: Promise<never>;
133+}> {
134+const timeoutMs = options.requestTimeoutMs ?? CLAWHUB_REQUEST_TIMEOUT_MS;
135+const controller = new AbortController();
136+const timeoutError = Object.assign(
137+new Error(`ClawHub request timed out after ${timeoutMs}ms: ${url.href}`),
138+{ code: "ETIMEDOUT" },
139+);
140+let timeout: ReturnType<typeof setTimeout> | undefined;
141+const timeoutPromise = new Promise<never>((_resolve, reject) => {
142+timeout = setTimeout(() => {
143+controller.abort(timeoutError);
144+reject(timeoutError);
145+}, timeoutMs);
146+timeout.unref?.();
147+});
148+149+try {
150+const response = await Promise.race([
151+(options.fetchImpl ?? fetch)(url, {
152+method: "GET",
153+headers: {
154+Accept: "application/json",
155+},
156+signal: controller.signal,
157+}),
158+timeoutPromise,
159+]);
160+return {
161+clearTimeout: () => clearTimeout(timeout),
162+ response,
163+signal: controller.signal,
164+ timeoutPromise,
165+};
166+} catch (error) {
167+clearTimeout(timeout);
168+throw error;
169+}
170+}
171+117172function formatClawHubPackageArtifactName(
118173plugin: Pick<PublishablePluginPackage, "packageName" | "version">,
119174) {
@@ -346,98 +401,116 @@ async function isPluginVersionPublishedOnClawHub(
346401options: {
347402fetchImpl?: typeof fetch;
348403registryBaseUrl?: string;
404+requestTimeoutMs?: number;
349405} = {},
350406): Promise<boolean> {
351-const fetchImpl = options.fetchImpl ?? fetch;
352407const url = new URL(
353408`/api/v1/packages/${encodeURIComponent(packageName)}/versions/${encodeURIComponent(version)}`,
354409getRegistryBaseUrl(options.registryBaseUrl),
355410);
356-const response = await fetchImpl(url, {
357-method: "GET",
358-headers: {
359-Accept: "application/json",
360-},
411+const request = await fetchClawHubRequest(url, {
412+fetchImpl: options.fetchImpl,
413+requestTimeoutMs: options.requestTimeoutMs,
361414});
415+const { response } = request;
362416363-if (response.status === 404) {
364-return false;
365-}
366-if (response.ok) {
367-return true;
368-}
417+try {
418+if (response.status === 404) {
419+return false;
420+}
421+if (response.ok) {
422+return true;
423+}
369424370-throw new Error(
371-`Failed to query ClawHub for ${packageName}@${version}: ${response.status} ${response.statusText}`,
372-);
425+throw new Error(
426+`Failed to query ClawHub for ${packageName}@${version}: ${response.status} ${response.statusText}`,
427+);
428+} finally {
429+request.clearTimeout();
430+}
373431}
374432375433async function doesClawHubPackageExist(
376434packageName: string,
377435options: {
378436fetchImpl?: typeof fetch;
379437registryBaseUrl?: string;
438+requestTimeoutMs?: number;
380439} = {},
381440): Promise<boolean> {
382-const fetchImpl = options.fetchImpl ?? fetch;
383441const url = new URL(
384442`/api/v1/packages/${encodeURIComponent(packageName)}`,
385443getRegistryBaseUrl(options.registryBaseUrl),
386444);
387-const response = await fetchImpl(url, {
388-method: "GET",
389-headers: {
390-Accept: "application/json",
391-},
445+const request = await fetchClawHubRequest(url, {
446+fetchImpl: options.fetchImpl,
447+requestTimeoutMs: options.requestTimeoutMs,
392448});
449+const { response } = request;
393450394-if (response.status === 404) {
395-return false;
396-}
397-if (!response.ok) {
398-throw new Error(
399-`Failed to query ClawHub package ${packageName}: ${response.status} ${response.statusText}`,
400-);
401-}
451+try {
452+if (response.status === 404) {
453+return false;
454+}
455+if (!response.ok) {
456+throw new Error(
457+`Failed to query ClawHub package ${packageName}: ${response.status} ${response.statusText}`,
458+);
459+}
402460403-return true;
461+return true;
462+} finally {
463+request.clearTimeout();
464+}
404465}
405466406467async function hasClawHubTrustedPublisher(
407468packageName: string,
408469options: {
409470fetchImpl?: typeof fetch;
410471registryBaseUrl?: string;
472+requestTimeoutMs?: number;
411473} = {},
412474): Promise<boolean> {
413-const fetchImpl = options.fetchImpl ?? fetch;
414475const url = new URL(
415476`/api/v1/packages/${encodeURIComponent(packageName)}/trusted-publisher`,
416477getRegistryBaseUrl(options.registryBaseUrl),
417478);
418-const response = await fetchImpl(url, {
419-method: "GET",
420-headers: {
421-Accept: "application/json",
422-},
479+const request = await fetchClawHubRequest(url, {
480+fetchImpl: options.fetchImpl,
481+requestTimeoutMs: options.requestTimeoutMs,
423482});
483+const { response } = request;
424484425-if (!response.ok) {
426-throw new Error(
427-`Failed to query ClawHub trusted publisher for ${packageName}: ${response.status} ${response.statusText}`,
485+try {
486+if (!response.ok) {
487+throw new Error(
488+`Failed to query ClawHub trusted publisher for ${packageName}: ${response.status} ${response.statusText}`,
489+);
490+}
491+492+let trustedPublisherDetail: ClawHubTrustedPublisherDetail;
493+const text = await readBoundedResponseText(
494+response,
495+`ClawHub trusted publisher ${packageName}`,
496+CLAWHUB_RESPONSE_BODY_MAX_BYTES,
497+{
498+signal: request.signal,
499+timeoutPromise: request.timeoutPromise,
500+},
428501);
429-}
502+try {
503+trustedPublisherDetail = JSON.parse(text) as ClawHubTrustedPublisherDetail;
504+} catch (error) {
505+throw new Error(`Failed to parse ClawHub trusted publisher ${packageName} response.`, {
506+cause: error,
507+});
508+}
430509431-let trustedPublisherDetail: ClawHubTrustedPublisherDetail;
432-try {
433-trustedPublisherDetail = (await response.json()) as ClawHubTrustedPublisherDetail;
434-} catch (error) {
435-throw new Error(`Failed to parse ClawHub trusted publisher ${packageName} response.`, {
436-cause: error,
437-});
510+return isOpenClawPluginTrustedPublisher(trustedPublisherDetail.trustedPublisher);
511+} finally {
512+request.clearTimeout();
438513}
439-440-return isOpenClawPluginTrustedPublisher(trustedPublisherDetail.trustedPublisher);
441514}
442515443516function isOpenClawPluginTrustedPublisher(value: unknown): boolean {
@@ -470,6 +543,7 @@ export async function collectPluginClawHubReleasePlan(params?: {
470543gitRange?: GitRangeSelection;
471544registryBaseUrl?: string;
472545fetchImpl?: typeof fetch;
546+requestTimeoutMs?: number;
473547}): Promise<PluginReleasePlan> {
474548const rootDir = params?.rootDir;
475549const selection = params?.selection ?? [];
@@ -506,17 +580,20 @@ export async function collectPluginClawHubReleasePlan(params?: {
506580const packageExists = await doesClawHubPackageExist(plugin.packageName, {
507581registryBaseUrl: params?.registryBaseUrl,
508582fetchImpl: params?.fetchImpl,
583+requestTimeoutMs: params?.requestTimeoutMs,
509584});
510585const hasTrustedPublisher = packageExists
511586 ? await hasClawHubTrustedPublisher(plugin.packageName, {
512587registryBaseUrl: params?.registryBaseUrl,
513588fetchImpl: params?.fetchImpl,
589+requestTimeoutMs: params?.requestTimeoutMs,
514590})
515591 : false;
516592const alreadyPublished = packageExists
517593 ? await isPluginVersionPublishedOnClawHub(plugin.packageName, plugin.version, {
518594registryBaseUrl: params?.registryBaseUrl,
519595fetchImpl: params?.fetchImpl,
596+requestTimeoutMs: params?.requestTimeoutMs,
520597})
521598 : false;
522599此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。