



























@@ -5,13 +5,23 @@ const email = process.env.OPENWEBUI_ADMIN_EMAIL ?? "";
55const password = process.env.OPENWEBUI_ADMIN_PASSWORD ?? "";
66const expectedNonce = process.env.OPENWEBUI_EXPECTED_NONCE ?? "";
77const prompt = process.env.OPENWEBUI_PROMPT ?? "";
8-const modelAttempts = Number.parseInt(process.env.OPENWEBUI_MODEL_ATTEMPTS ?? "72", 10);
9-const modelRetryMs = Number.parseInt(process.env.OPENWEBUI_MODEL_RETRY_MS ?? "5000", 10);
10-const fetchTimeoutMs = Number.parseInt(process.env.OPENWEBUI_FETCH_TIMEOUT_MS ?? "720000", 10);
8+const modelAttempts = readPositiveInt(process.env.OPENWEBUI_MODEL_ATTEMPTS, 72);
9+const modelRetryMs = readNonNegativeInt(process.env.OPENWEBUI_MODEL_RETRY_MS, 5000);
10+const fetchTimeoutMs = readPositiveInt(process.env.OPENWEBUI_FETCH_TIMEOUT_MS, 720000);
11+const controlTimeoutMs = readPositiveInt(
12+process.env.OPENWEBUI_CONTROL_TIMEOUT_MS,
13+Math.min(fetchTimeoutMs, 30000),
14+);
15+const chatTimeoutMs = readPositiveInt(process.env.OPENWEBUI_CHAT_TIMEOUT_MS, fetchTimeoutMs);
1116const smokeMode =
1217process.env.OPENWEBUI_SMOKE_MODE ?? process.env.OPENCLAW_OPENWEBUI_SMOKE_MODE ?? "chat";
131814-setGlobalDispatcher(new Agent({ bodyTimeout: fetchTimeoutMs, headersTimeout: fetchTimeoutMs }));
19+setGlobalDispatcher(
20+new Agent({
21+bodyTimeout: Math.max(controlTimeoutMs, chatTimeoutMs),
22+headersTimeout: Math.max(controlTimeoutMs, chatTimeoutMs),
23+}),
24+);
15251626if (!baseUrl || !email || !password || !expectedNonce || !prompt) {
1727throw new Error("Missing required OPENWEBUI_* environment variables");
@@ -20,6 +30,41 @@ if (smokeMode !== "models" && smokeMode !== "chat") {
2030throw new Error(`Unsupported OPENWEBUI_SMOKE_MODE: ${smokeMode}`);
2131}
223233+function readPositiveInt(raw, fallback) {
34+const parsed = Number.parseInt(String(raw || ""), 10);
35+return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
36+}
37+38+function readNonNegativeInt(raw, fallback) {
39+const parsed = Number.parseInt(String(raw || ""), 10);
40+return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
41+}
42+43+function createTimeoutError(label, timeoutMs) {
44+const error = new Error(`${label} timed out after ${timeoutMs}ms`);
45+error.code = "ETIMEDOUT";
46+return error;
47+}
48+49+async function withRequestTimeout(label, timeoutMs, run) {
50+const controller = new AbortController();
51+const timeoutError = createTimeoutError(label, timeoutMs);
52+const timer = setTimeout(() => {
53+controller.abort(timeoutError);
54+}, timeoutMs);
55+timer.unref?.();
56+try {
57+return await run(controller.signal);
58+} catch (error) {
59+if (controller.signal.aborted) {
60+throw timeoutError;
61+}
62+throw error;
63+} finally {
64+clearTimeout(timer);
65+}
66+}
67+2368function getCookieHeader(res) {
2469const raw = res.headers.get("set-cookie");
2570if (!raw) {
@@ -49,6 +94,69 @@ function sleep(ms) {
4994});
5095}
519697+async function fetchSignin() {
98+return await withRequestTimeout("Open WebUI signin", controlTimeoutMs, async (signal) => {
99+const response = await fetch(`${baseUrl}/api/v1/auths/signin`, {
100+method: "POST",
101+headers: { "content-type": "application/json" },
102+body: JSON.stringify({ email, password }),
103+ signal,
104+});
105+if (!response.ok) {
106+const body = await response.text();
107+throw new Error(`signin failed: HTTP ${response.status} ${body}`);
108+}
109+return {
110+cookie: getCookieHeader(response),
111+json: await response.json(),
112+};
113+});
114+}
115+116+async function fetchModels(authHeaders, attempt) {
117+return await withRequestTimeout(
118+`Open WebUI models attempt ${attempt}`,
119+controlTimeoutMs,
120+async (signal) => {
121+const response = await fetch(`${baseUrl}/api/models`, { headers: authHeaders, signal });
122+if (!response.ok) {
123+return {
124+ok: false,
125+status: response.status,
126+text: await response.text(),
127+};
128+}
129+return {
130+json: await response.json(),
131+ok: true,
132+};
133+},
134+);
135+}
136+137+async function fetchChatCompletion(authHeaders, targetModel) {
138+return await withRequestTimeout("Open WebUI chat completion", chatTimeoutMs, async (signal) => {
139+const response = await fetch(`${baseUrl}/api/chat/completions`, {
140+method: "POST",
141+headers: {
142+ ...authHeaders,
143+"content-type": "application/json",
144+},
145+body: JSON.stringify({
146+model: targetModel,
147+messages: [{ role: "user", content: prompt }],
148+}),
149+ signal,
150+});
151+if (!response.ok) {
152+throw new Error(
153+`/api/chat/completions failed: HTTP ${response.status} ${await response.text()}`,
154+);
155+}
156+return await response.json();
157+});
158+}
159+52160function extractModelIds(modelsJson) {
53161const models = Array.isArray(modelsJson)
54162 ? modelsJson
@@ -62,48 +170,37 @@ function extractModelIds(modelsJson) {
62170.filter((value) => typeof value === "string");
63171}
6417265-const signinRes = await fetch(`${baseUrl}/api/v1/auths/signin`, {
66-method: "POST",
67-headers: { "content-type": "application/json" },
68-body: JSON.stringify({ email, password }),
69-});
70-if (!signinRes.ok) {
71-const body = await signinRes.text();
72-throw new Error(`signin failed: HTTP ${signinRes.status} ${body}`);
73-}
74-75-const signinJson = await signinRes.json();
173+const signin = await fetchSignin();
174+const signinJson = signin.json;
76175const token =
77176signinJson?.token ?? signinJson?.access_token ?? signinJson?.jwt ?? signinJson?.data?.token ?? "";
78-const cookie = getCookieHeader(signinRes);
79177const authHeaders = {
80- ...buildAuthHeaders(token, cookie),
178+ ...buildAuthHeaders(token, signin.cookie),
81179accept: "application/json",
82180};
8318184182let modelIds = [];
85183let targetModel = "";
86184let lastModelsError = "";
87185for (let attempt = 1; attempt <= modelAttempts; attempt += 1) {
88-const modelsRes = await fetch(`${baseUrl}/api/models`, { headers: authHeaders }).catch(
89-(error) => {
90-lastModelsError = error instanceof Error ? error.message : String(error);
91-return undefined;
92-},
93-);
94-if (modelsRes?.ok) {
95-const modelsJson = await modelsRes.json();
96-modelIds = extractModelIds(modelsJson);
186+const modelsResult = await fetchModels(authHeaders, attempt).catch((error) => {
187+lastModelsError = error instanceof Error ? error.message : String(error);
188+return undefined;
189+});
190+if (modelsResult?.ok) {
191+modelIds = extractModelIds(modelsResult.json);
97192targetModel =
98193modelIds.find((id) => id === "openclaw/default") ?? modelIds.find((id) => id === "openclaw");
99194if (targetModel) {
100195break;
101196}
102197lastModelsError = `missing openclaw model: ${JSON.stringify(modelIds)}`;
103-} else if (modelsRes) {
104-lastModelsError = `HTTP ${modelsRes.status} ${await modelsRes.text()}`;
198+} else if (modelsResult) {
199+lastModelsError = `HTTP ${modelsResult.status} ${modelsResult.text}`;
200+}
201+if (attempt < modelAttempts) {
202+await sleep(modelRetryMs);
105203}
106-await sleep(modelRetryMs);
107204}
108205if (!targetModel) {
109206throw new Error(
@@ -115,21 +212,7 @@ if (smokeMode === "models") {
115212process.exit(0);
116213}
117214118-const chatRes = await fetch(`${baseUrl}/api/chat/completions`, {
119-method: "POST",
120-headers: {
121- ...authHeaders,
122-"content-type": "application/json",
123-},
124-body: JSON.stringify({
125-model: targetModel,
126-messages: [{ role: "user", content: prompt }],
127-}),
128-});
129-if (!chatRes.ok) {
130-throw new Error(`/api/chat/completions failed: HTTP ${chatRes.status} ${await chatRes.text()}`);
131-}
132-const chatJson = await chatRes.json();
215+const chatJson = await fetchChatCompletion(authHeaders, targetModel);
133216const reply =
134217chatJson?.choices?.[0]?.message?.content ?? chatJson?.message?.content ?? chatJson?.content ?? "";
135218if (typeof reply !== "string" || !reply.includes(expectedNonce)) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。