Stabilize gateway reload/restart behavior and remove doctor --json dependency (#504)
This commit is contained in:
committed by
GitHub
Unverified
parent
89bda3c7af
commit
7f3408559d
@@ -716,6 +716,65 @@ export interface ValidationResult {
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
const DOCTOR_PARSER_FALLBACK_HINT =
|
||||
'Doctor output could not be confidently interpreted; falling back to local channel config checks.';
|
||||
|
||||
type DoctorValidationParseResult = {
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
undetermined: boolean;
|
||||
};
|
||||
|
||||
export function parseDoctorValidationOutput(channelType: string, output: string): DoctorValidationParseResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const normalizedChannelType = channelType.toLowerCase();
|
||||
const normalizedOutput = output.trim();
|
||||
|
||||
if (!normalizedOutput) {
|
||||
return {
|
||||
errors,
|
||||
warnings: [DOCTOR_PARSER_FALLBACK_HINT],
|
||||
undetermined: true,
|
||||
};
|
||||
}
|
||||
|
||||
const lines = output
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
const channelLines = lines.filter((line) => line.toLowerCase().includes(normalizedChannelType));
|
||||
let classifiedCount = 0;
|
||||
|
||||
for (const line of channelLines) {
|
||||
const lowerLine = line.toLowerCase();
|
||||
if (lowerLine.includes('error') || lowerLine.includes('unrecognized key')) {
|
||||
errors.push(line);
|
||||
classifiedCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (lowerLine.includes('warning')) {
|
||||
warnings.push(line);
|
||||
classifiedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (channelLines.length === 0 || classifiedCount === 0) {
|
||||
warnings.push(DOCTOR_PARSER_FALLBACK_HINT);
|
||||
return {
|
||||
errors,
|
||||
warnings,
|
||||
undetermined: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
warnings,
|
||||
undetermined: false,
|
||||
};
|
||||
}
|
||||
|
||||
export interface CredentialValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
@@ -853,34 +912,41 @@ export async function validateChannelConfig(channelType: string): Promise<Valida
|
||||
|
||||
// Run openclaw doctor command to validate config (async to avoid
|
||||
// blocking the main thread).
|
||||
const output = await new Promise<string>((resolve, reject) => {
|
||||
exec(
|
||||
`node openclaw.mjs doctor --json 2>&1`,
|
||||
{
|
||||
cwd: openclawPath,
|
||||
encoding: 'utf-8',
|
||||
timeout: 30000,
|
||||
windowsHide: true,
|
||||
},
|
||||
(err, stdout) => {
|
||||
if (err) reject(err);
|
||||
else resolve(stdout);
|
||||
},
|
||||
);
|
||||
});
|
||||
const runDoctor = async (command: string): Promise<string> =>
|
||||
await new Promise<string>((resolve, reject) => {
|
||||
exec(
|
||||
command,
|
||||
{
|
||||
cwd: openclawPath,
|
||||
encoding: 'utf-8',
|
||||
timeout: 30000,
|
||||
windowsHide: true,
|
||||
},
|
||||
(err, stdout, stderr) => {
|
||||
const combined = `${stdout || ''}${stderr || ''}`;
|
||||
if (err) {
|
||||
const next = new Error(combined || err.message);
|
||||
reject(next);
|
||||
return;
|
||||
}
|
||||
resolve(combined);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
const lowerLine = line.toLowerCase();
|
||||
if (lowerLine.includes(channelType) && lowerLine.includes('error')) {
|
||||
result.errors.push(line.trim());
|
||||
result.valid = false;
|
||||
} else if (lowerLine.includes(channelType) && lowerLine.includes('warning')) {
|
||||
result.warnings.push(line.trim());
|
||||
} else if (lowerLine.includes('unrecognized key') && lowerLine.includes(channelType)) {
|
||||
result.errors.push(line.trim());
|
||||
result.valid = false;
|
||||
}
|
||||
const output = await runDoctor(`node openclaw.mjs doctor 2>&1`);
|
||||
|
||||
const parsedDoctor = parseDoctorValidationOutput(channelType, output);
|
||||
result.errors.push(...parsedDoctor.errors);
|
||||
result.warnings.push(...parsedDoctor.warnings);
|
||||
if (parsedDoctor.errors.length > 0) {
|
||||
result.valid = false;
|
||||
}
|
||||
if (parsedDoctor.undetermined) {
|
||||
logger.warn('Doctor output parsing fell back to local channel checks', {
|
||||
channelType,
|
||||
hint: DOCTOR_PARSER_FALLBACK_HINT,
|
||||
});
|
||||
}
|
||||
|
||||
const config = await readOpenClawConfig();
|
||||
|
||||
@@ -22,6 +22,7 @@ import { logger } from './logger';
|
||||
import { saveProvider, getProvider, ProviderConfig } from './secure-storage';
|
||||
import { getProviderDefaultModel } from './provider-registry';
|
||||
import { isOpenClawPresent } from './paths';
|
||||
import { proxyAwareFetch } from './proxy-fetch';
|
||||
import {
|
||||
loginMiniMaxPortalOAuth,
|
||||
type MiniMaxOAuthToken,
|
||||
@@ -47,6 +48,17 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
private active: boolean = false;
|
||||
private mainWindow: BrowserWindow | null = null;
|
||||
|
||||
private async runWithProxyAwareFetch<T>(task: () => Promise<T>): Promise<T> {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = ((input: string | URL, init?: RequestInit) =>
|
||||
proxyAwareFetch(input, init)) as typeof fetch;
|
||||
try {
|
||||
return await task();
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
}
|
||||
|
||||
setWindow(window: BrowserWindow) {
|
||||
this.mainWindow = window;
|
||||
}
|
||||
@@ -109,7 +121,7 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
}
|
||||
const provider = this.activeProvider!;
|
||||
|
||||
const token: MiniMaxOAuthToken = await loginMiniMaxPortalOAuth({
|
||||
const token: MiniMaxOAuthToken = await this.runWithProxyAwareFetch(() => loginMiniMaxPortalOAuth({
|
||||
region,
|
||||
openUrl: async (url) => {
|
||||
logger.info(`[DeviceOAuth] MiniMax opening browser: ${url}`);
|
||||
@@ -133,7 +145,7 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
update: (msg) => logger.info(`[DeviceOAuth] MiniMax progress: ${msg}`),
|
||||
stop: (msg) => logger.info(`[DeviceOAuth] MiniMax progress done: ${msg ?? ''}`),
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
if (!this.active) return;
|
||||
|
||||
@@ -159,7 +171,7 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
}
|
||||
const provider = this.activeProvider!;
|
||||
|
||||
const token: QwenOAuthToken = await loginQwenPortalOAuth({
|
||||
const token: QwenOAuthToken = await this.runWithProxyAwareFetch(() => loginQwenPortalOAuth({
|
||||
openUrl: async (url) => {
|
||||
logger.info(`[DeviceOAuth] Qwen opening browser: ${url}`);
|
||||
shell.openExternal(url).catch((err) =>
|
||||
@@ -179,7 +191,7 @@ class DeviceOAuthManager extends EventEmitter {
|
||||
update: (msg) => logger.info(`[DeviceOAuth] Qwen progress: ${msg}`),
|
||||
stop: (msg) => logger.info(`[DeviceOAuth] Qwen progress done: ${msg ?? ''}`),
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
if (!this.active) return;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, unlinkS
|
||||
import { createServer } from 'node:http';
|
||||
import { delimiter, dirname, join } from 'node:path';
|
||||
import { getClawXConfigDir } from './paths';
|
||||
import { proxyAwareFetch } from './proxy-fetch';
|
||||
|
||||
const CLIENT_ID_KEYS = ['OPENCLAW_GEMINI_OAUTH_CLIENT_ID', 'GEMINI_CLI_OAUTH_CLIENT_ID'];
|
||||
const CLIENT_SECRET_KEYS = [
|
||||
@@ -243,7 +244,7 @@ async function installViaNpm(onProgress?: (msg: string) => void): Promise<boolea
|
||||
async function installViaDirectDownload(onProgress?: (msg: string) => void): Promise<boolean> {
|
||||
try {
|
||||
onProgress?.('Downloading Gemini OAuth helper...');
|
||||
const metaRes = await fetch('https://registry.npmjs.org/@google/gemini-cli-core/latest');
|
||||
const metaRes = await proxyAwareFetch('https://registry.npmjs.org/@google/gemini-cli-core/latest');
|
||||
if (!metaRes.ok) {
|
||||
onProgress?.(`Failed to fetch Gemini package metadata: ${metaRes.status}`);
|
||||
return false;
|
||||
@@ -256,7 +257,7 @@ async function installViaDirectDownload(onProgress?: (msg: string) => void): Pro
|
||||
return false;
|
||||
}
|
||||
|
||||
const tarRes = await fetch(tarballUrl);
|
||||
const tarRes = await proxyAwareFetch(tarballUrl);
|
||||
if (!tarRes.ok) {
|
||||
onProgress?.(`Failed to download Gemini package: ${tarRes.status}`);
|
||||
return false;
|
||||
@@ -440,7 +441,7 @@ async function waitForLocalCallback(params: {
|
||||
|
||||
async function getUserEmail(accessToken: string): Promise<string | undefined> {
|
||||
try {
|
||||
const response = await fetch(USERINFO_URL, {
|
||||
const response = await proxyAwareFetch(USERINFO_URL, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
if (response.ok) {
|
||||
@@ -489,7 +490,7 @@ async function pollOperation(
|
||||
): Promise<{ done?: boolean; response?: { cloudaicompanionProject?: { id?: string } } }> {
|
||||
for (let attempt = 0; attempt < 24; attempt += 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
const response = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal/${operationName}`, { headers });
|
||||
const response = await proxyAwareFetch(`${CODE_ASSIST_ENDPOINT}/v1internal/${operationName}`, { headers });
|
||||
if (!response.ok) {
|
||||
continue;
|
||||
}
|
||||
@@ -530,7 +531,7 @@ async function discoverProject(accessToken: string): Promise<string> {
|
||||
allowedTiers?: Array<{ id?: string; isDefault?: boolean }>;
|
||||
} = {};
|
||||
|
||||
const response = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`, {
|
||||
const response = await proxyAwareFetch(`${CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(loadBody),
|
||||
@@ -583,7 +584,7 @@ async function discoverProject(accessToken: string): Promise<string> {
|
||||
(onboardBody.metadata as Record<string, unknown>).duetProject = envProject;
|
||||
}
|
||||
|
||||
const onboardResponse = await fetch(`${CODE_ASSIST_ENDPOINT}/v1internal:onboardUser`, {
|
||||
const onboardResponse = await proxyAwareFetch(`${CODE_ASSIST_ENDPOINT}/v1internal:onboardUser`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(onboardBody),
|
||||
@@ -638,7 +639,7 @@ async function exchangeCodeForTokens(
|
||||
body.set('client_secret', clientSecret);
|
||||
}
|
||||
|
||||
const response = await fetch(TOKEN_URL, {
|
||||
const response = await proxyAwareFetch(TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { createServer } from 'node:http';
|
||||
import { proxyAwareFetch } from './proxy-fetch';
|
||||
|
||||
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
||||
const AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
|
||||
@@ -206,7 +207,7 @@ async function exchangeAuthorizationCode(
|
||||
code: string,
|
||||
verifier: string,
|
||||
): Promise<{ access: string; refresh: string; expires: number }> {
|
||||
const response = await fetch(TOKEN_URL, {
|
||||
const response = await proxyAwareFetch(TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getUvMirrorEnv } from './uv-env';
|
||||
|
||||
const OPENCLAW_DOCTOR_TIMEOUT_MS = 60_000;
|
||||
const MAX_DOCTOR_OUTPUT_BYTES = 10 * 1024 * 1024;
|
||||
const OPENCLAW_DOCTOR_ARGS = ['doctor', '--json'];
|
||||
const OPENCLAW_DOCTOR_ARGS = ['doctor'];
|
||||
const OPENCLAW_DOCTOR_FIX_ARGS = ['doctor', '--fix', '--yes', '--non-interactive'];
|
||||
|
||||
export type OpenClawDoctorMode = 'diagnose' | 'fix';
|
||||
@@ -65,10 +65,12 @@ function getBundledBinPath(): string {
|
||||
: path.join(process.cwd(), 'resources', 'bin', target);
|
||||
}
|
||||
|
||||
async function runDoctorCommand(mode: OpenClawDoctorMode): Promise<OpenClawDoctorResult> {
|
||||
async function runDoctorCommandWithArgs(
|
||||
mode: OpenClawDoctorMode,
|
||||
args: string[],
|
||||
): Promise<OpenClawDoctorResult> {
|
||||
const openclawDir = getOpenClawDir();
|
||||
const entryScript = getOpenClawEntryPath();
|
||||
const args = mode === 'fix' ? OPENCLAW_DOCTOR_FIX_ARGS : OPENCLAW_DOCTOR_ARGS;
|
||||
const command = `openclaw ${args.join(' ')}`;
|
||||
const startedAt = Date.now();
|
||||
|
||||
@@ -194,9 +196,9 @@ async function runDoctorCommand(mode: OpenClawDoctorMode): Promise<OpenClawDocto
|
||||
}
|
||||
|
||||
export async function runOpenClawDoctor(): Promise<OpenClawDoctorResult> {
|
||||
return await runDoctorCommand('diagnose');
|
||||
return await runDoctorCommandWithArgs('diagnose', OPENCLAW_DOCTOR_ARGS);
|
||||
}
|
||||
|
||||
export async function runOpenClawDoctorFix(): Promise<OpenClawDoctorResult> {
|
||||
return await runDoctorCommand('fix');
|
||||
return await runDoctorCommandWithArgs('fix', OPENCLAW_DOCTOR_FIX_ARGS);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user