













@@ -5,14 +5,26 @@ import os from "node:os";
55import path from "node:path";
66import { pathToFileURL } from "node:url";
77import { normalizeOptionalString } from "../src/shared/string-coerce.ts";
8-import { maskIdentifier, previewForDevToolLog, redactHomePath } from "./lib/dev-tooling-safety.ts";
8+import {
9+maskIdentifier,
10+parseStrictIntegerOption,
11+previewForDevToolLog,
12+redactHomePath,
13+} from "./lib/dev-tooling-safety.ts";
9141015type Args = {
1116agentId: string;
1217reveal: boolean;
1318sessionKey?: string;
1419};
152021+type FetchOptions = {
22+fetchImpl?: typeof fetch;
23+timeoutMs?: number;
24+};
25+26+const DEFAULT_FETCH_TIMEOUT_MS = 30_000;
27+1628const mask = (value: string) => {
1729return maskIdentifier(
1830value,
@@ -80,17 +92,68 @@ const pickAnthropicTokens = (store: {
8092return found;
8193};
829483-const fetchAnthropicOAuthUsage = async (token: string) => {
84-const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
85-headers: {
86-Authorization: `Bearer ${token}`,
87-Accept: "application/json",
88-"anthropic-version": "2023-06-01",
89-"anthropic-beta": "oauth-2025-04-20",
90-"User-Agent": "openclaw-debug",
91-},
95+const resolveFetchTimeoutMs = (raw = process.env.OPENCLAW_DEBUG_CLAUDE_USAGE_FETCH_TIMEOUT_MS) => {
96+return parseStrictIntegerOption({
97+fallback: DEFAULT_FETCH_TIMEOUT_MS,
98+label: "OPENCLAW_DEBUG_CLAUDE_USAGE_FETCH_TIMEOUT_MS",
99+min: 1,
100+ raw,
101+});
102+};
103+104+const withFetchTimeout = async <T>(
105+label: string,
106+timeoutMs: number,
107+run: (signal: AbortSignal) => Promise<T>,
108+): Promise<T> => {
109+const controller = new AbortController();
110+let timeout: ReturnType<typeof setTimeout> | undefined;
111+const timeoutPromise = new Promise<T>((_resolve, reject) => {
112+timeout = setTimeout(() => {
113+const error = new Error(`${label} exceeded timeout of ${timeoutMs}ms`);
114+reject(error);
115+controller.abort(error);
116+}, timeoutMs);
92117});
93-const text = await res.text();
118+try {
119+return await Promise.race([run(controller.signal), timeoutPromise]);
120+} finally {
121+if (timeout) {
122+clearTimeout(timeout);
123+}
124+}
125+};
126+127+const fetchText = async (
128+label: string,
129+url: string,
130+init: RequestInit,
131+options: FetchOptions = {},
132+) => {
133+const fetchImpl = options.fetchImpl ?? fetch;
134+const timeoutMs = options.timeoutMs ?? resolveFetchTimeoutMs();
135+return await withFetchTimeout(label, timeoutMs, async (signal) => {
136+const res = await fetchImpl(url, { ...init, signal });
137+const text = await res.text();
138+return { res, text };
139+});
140+};
141+142+const fetchAnthropicOAuthUsage = async (token: string, options: FetchOptions = {}) => {
143+const { res, text } = await fetchText(
144+"Anthropic OAuth usage request",
145+"https://api.anthropic.com/api/oauth/usage",
146+{
147+headers: {
148+Authorization: `Bearer ${token}`,
149+Accept: "application/json",
150+"anthropic-version": "2023-06-01",
151+"anthropic-beta": "oauth-2025-04-20",
152+"User-Agent": "openclaw-debug",
153+},
154+},
155+options,
156+);
94157return { status: res.status, contentType: res.headers.get("content-type"), text };
95158};
96159@@ -303,15 +366,19 @@ const findClaudeSessionKey = (): { sessionKey: string; source: string } | null =
303366return null;
304367};
305368306-const fetchClaudeWebUsage = async (sessionKey: string) => {
369+const fetchClaudeWebUsage = async (sessionKey: string, options: FetchOptions = {}) => {
307370const headers = {
308371Cookie: `sessionKey=${sessionKey}`,
309372Accept: "application/json",
310373"User-Agent":
311374"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
312375};
313-const orgRes = await fetch("https://claude.ai/api/organizations", { headers });
314-const orgText = await orgRes.text();
376+const { res: orgRes, text: orgText } = await fetchText(
377+"Claude organizations request",
378+"https://claude.ai/api/organizations",
379+{ headers },
380+options,
381+);
315382if (!orgRes.ok) {
316383return { ok: false as const, step: "organizations", status: orgRes.status, body: orgText };
317384}
@@ -321,8 +388,12 @@ const fetchClaudeWebUsage = async (sessionKey: string) => {
321388return { ok: false as const, step: "organizations", status: 200, body: orgText };
322389}
323390324-const usageRes = await fetch(`https://claude.ai/api/organizations/${orgId}/usage`, { headers });
325-const usageText = await usageRes.text();
391+const { res: usageRes, text: usageText } = await fetchText(
392+"Claude usage request",
393+`https://claude.ai/api/organizations/${orgId}/usage`,
394+{ headers },
395+options,
396+);
326397return usageRes.ok
327398 ? { ok: true as const, orgId, body: usageText }
328399 : { ok: false as const, step: "usage", status: usageRes.status, body: usageText };
@@ -397,7 +468,9 @@ export const testing = {
397468CLAUDE_COOKIE_HOST_SQL,
398469CLAUDE_FIREFOX_COOKIE_HOST_SQL,
399470 browserRootLabel,
471+ fetchAnthropicOAuthUsage,
400472 mask,
473+ resolveFetchTimeoutMs,
401474};
402475403476if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。