




















@@ -8,6 +8,7 @@ const REPO = "openclaw/openclaw";
88const PER_LINE = 10;
99const AVATAR_PROBE_SIZE = 40;
1010const AVATAR_PROBE_MAX_BYTES = 256 * 1024;
11+const AVATAR_PROBE_TIMEOUT_MS = 8000;
1112const AVATAR_SIZE = 48;
1213const CLAWTRIBUTORS_START = "<!-- clawtributors:start -->";
1314const CLAWTRIBUTORS_END = "<!-- clawtributors:end -->";
@@ -454,32 +455,130 @@ function isDefaultGitHubAvatar(login: string): Promise<boolean> {
454455455456async function probeDefaultGitHubAvatar(login: string): Promise<boolean> {
456457try {
457-const response = await fetch(`https://github.com/${login}.png?size=${AVATAR_PROBE_SIZE}`, {
458-headers: { "user-agent": "openclaw-clawtributors" },
459-signal: AbortSignal.timeout(8000),
458+return await withAvatarProbeTimeout(login, async ({ signal, timeoutPromise }) => {
459+const response = await fetch(`https://github.com/${login}.png?size=${AVATAR_PROBE_SIZE}`, {
460+headers: { "user-agent": "openclaw-clawtributors" },
461+ signal,
462+});
463+if (!response.ok) {
464+return false;
465+}
466+const buffer = await readAvatarProbeBuffer(response, timeoutPromise);
467+const dimensions = readImageDimensions(buffer);
468+return Boolean(
469+dimensions &&
470+(dimensions.width > AVATAR_PROBE_SIZE || dimensions.height > AVATAR_PROBE_SIZE),
471+);
460472});
461-if (!response.ok) {
462-return false;
463-}
464-const buffer = await readAvatarProbeBuffer(response);
465-const dimensions = readImageDimensions(buffer);
466-return Boolean(
467-dimensions && (dimensions.width > AVATAR_PROBE_SIZE || dimensions.height > AVATAR_PROBE_SIZE),
468-);
469473} catch {
470474return false;
471475}
472476}
473477474-async function readAvatarProbeBuffer(response: Response): Promise<Buffer> {
478+type AvatarProbeTimeout = {
479+signal: AbortSignal;
480+timeoutPromise: Promise<never>;
481+};
482+483+async function withAvatarProbeTimeout<T>(
484+login: string,
485+runProbe: (timeout: AvatarProbeTimeout) => Promise<T>,
486+): Promise<T> {
487+const controller = new AbortController();
488+let timeout: ReturnType<typeof setTimeout> | undefined;
489+const timeoutPromise = new Promise<never>((_resolve, reject) => {
490+timeout = setTimeout(() => {
491+const error = new Error(
492+`avatar probe for ${login} exceeded timeout of ${AVATAR_PROBE_TIMEOUT_MS}ms`,
493+);
494+reject(error);
495+controller.abort(error);
496+}, AVATAR_PROBE_TIMEOUT_MS);
497+});
498+499+try {
500+return await Promise.race([
501+runProbe({ signal: controller.signal, timeoutPromise }),
502+timeoutPromise,
503+]);
504+} finally {
505+if (timeout) {
506+clearTimeout(timeout);
507+}
508+}
509+}
510+511+function cancelAvatarProbeReaderSoon(reader: ReadableStreamDefaultReader<Uint8Array>): void {
512+void Promise.resolve()
513+.then(() => reader.cancel())
514+.catch(() => undefined);
515+}
516+517+function toAvatarProbeError(value: unknown, fallbackMessage: string): Error {
518+if (value instanceof Error) {
519+return value;
520+}
521+if (typeof value === "string") {
522+return new Error(value);
523+}
524+return new Error(fallbackMessage, { cause: value });
525+}
526+527+async function readAvatarProbeChunkWithTimeout(
528+reader: ReadableStreamDefaultReader<Uint8Array>,
529+timeoutPromise: Promise<never> | undefined,
530+markCanceled: () => void,
531+): Promise<ReadableStreamReadResult<Uint8Array>> {
532+const readPromise = reader.read();
533+if (!timeoutPromise) {
534+return await readPromise;
535+}
536+537+let waitingForRead = true;
538+const timeoutReadPromise = timeoutPromise.catch((error: unknown) => {
539+if (waitingForRead) {
540+markCanceled();
541+cancelAvatarProbeReaderSoon(reader);
542+}
543+throw toAvatarProbeError(error, "avatar probe response body read timed out");
544+});
545+546+try {
547+return await Promise.race([readPromise, timeoutReadPromise]);
548+} finally {
549+waitingForRead = false;
550+}
551+}
552+553+async function readAvatarProbeArrayBuffer(
554+response: Response,
555+timeoutPromise: Promise<never> | undefined,
556+): Promise<ArrayBuffer> {
557+if (!timeoutPromise) {
558+return await response.arrayBuffer();
559+}
560+return await Promise.race([
561+response.arrayBuffer(),
562+timeoutPromise.catch((error: unknown) => {
563+void response.body?.cancel().catch(() => undefined);
564+throw toAvatarProbeError(error, "avatar probe response body read timed out");
565+}),
566+]);
567+}
568+569+async function readAvatarProbeBuffer(
570+response: Response,
571+timeoutPromise?: Promise<never>,
572+): Promise<Buffer> {
475573const contentLength = Number(response.headers.get("content-length") ?? 0);
476574if (Number.isFinite(contentLength) && contentLength > AVATAR_PROBE_MAX_BYTES) {
575+await response.body?.cancel().catch(() => undefined);
477576throw new Error(`avatar probe exceeded ${AVATAR_PROBE_MAX_BYTES} bytes`);
478577}
479578480579const reader = response.body?.getReader?.();
481580if (!reader) {
482-const buffer = Buffer.from(await response.arrayBuffer());
581+const buffer = Buffer.from(await readAvatarProbeArrayBuffer(response, timeoutPromise));
483582if (buffer.byteLength > AVATAR_PROBE_MAX_BYTES) {
484583throw new Error(`avatar probe exceeded ${AVATAR_PROBE_MAX_BYTES} bytes`);
485584}
@@ -488,22 +587,32 @@ async function readAvatarProbeBuffer(response: Response): Promise<Buffer> {
488587489588const chunks: Buffer[] = [];
490589let total = 0;
491-for (;;) {
492-const { done, value } = await reader.read();
493-if (done) {
494-break;
495-}
496-if (!value?.byteLength) {
497-continue;
590+let canceled = false;
591+try {
592+for (;;) {
593+const { done, value } = await readAvatarProbeChunkWithTimeout(reader, timeoutPromise, () => {
594+canceled = true;
595+});
596+if (done) {
597+break;
598+}
599+if (!value?.byteLength) {
600+continue;
601+}
602+const chunk = Buffer.from(value);
603+const nextTotal = total + chunk.byteLength;
604+if (nextTotal > AVATAR_PROBE_MAX_BYTES) {
605+canceled = true;
606+await reader.cancel().catch(() => undefined);
607+throw new Error(`avatar probe exceeded ${AVATAR_PROBE_MAX_BYTES} bytes`);
608+}
609+chunks.push(chunk);
610+total = nextTotal;
498611}
499-const chunk = Buffer.from(value);
500-const nextTotal = total + chunk.byteLength;
501-if (nextTotal > AVATAR_PROBE_MAX_BYTES) {
502-await reader.cancel().catch(() => undefined);
503-throw new Error(`avatar probe exceeded ${AVATAR_PROBE_MAX_BYTES} bytes`);
612+} finally {
613+if (!canceled) {
614+reader.releaseLock();
504615}
505-chunks.push(chunk);
506-total = nextTotal;
507616}
508617return Buffer.concat(chunks, total);
509618}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。