Fix Windows OpenClaw path and session test compatibility

This commit is contained in:
xmanrui
2026-03-09 03:11:21 +08:00
parent 22b1ddcb48
commit d7c15240ec
24 changed files with 352 additions and 156 deletions
+1 -2
View File
@@ -1,8 +1,7 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
import { OPENCLAW_HOME } from "@/lib/openclaw-paths";
// Server-side cache: 5 min TTL
let cache: { data: any; ts: number } | null = null;
+5 -5
View File
@@ -1,7 +1,8 @@
import { NextResponse } from 'next/server'
import { promises as fs, existsSync } from 'fs'
import path from 'path'
import os from 'os'
import { parseJsonText } from '@/lib/json'
import { OPENCLAW_AGENTS_DIR, OPENCLAW_CONFIG_PATH } from '@/lib/openclaw-paths'
export const dynamic = 'force-dynamic'
export const revalidate = 0
@@ -483,16 +484,15 @@ async function parseSubagents(agentSessionsDir: string, agentId: string): Promis
}
export async function GET() {
const openclawDir = path.join(os.homedir(), '.openclaw')
const configPath = path.join(openclawDir, 'openclaw.json')
const agentsDir = path.join(openclawDir, 'agents')
const configPath = OPENCLAW_CONFIG_PATH
const agentsDir = OPENCLAW_AGENTS_DIR
const agents: AgentActivity[] = []
try {
if (existsSync(configPath)) {
const configContent = await fs.readFile(configPath, 'utf8')
const config = JSON.parse(configContent)
const config = parseJsonText(configContent)
const agentList = Array.isArray(config.agents) ? config.agents : config.agents?.list
if (agentList && Array.isArray(agentList)) {
+1 -2
View File
@@ -1,8 +1,7 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
import { OPENCLAW_HOME } from "@/lib/openclaw-paths";
// 状态: working(2分钟内有assistant消息) / online(10分钟内) / idle(24小时内) / offline(超过24小时)
type AgentState = "working" | "online" | "idle" | "offline";
+3 -4
View File
@@ -1,8 +1,7 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
import { OPENCLAW_CONFIG_PATH, OPENCLAW_HOME } from "@/lib/openclaw-paths";
const ALERTS_CONFIG_PATH = path.join(OPENCLAW_HOME, "alerts.json");
interface AlertRule {
@@ -37,7 +36,7 @@ function getAlertConfig(): AlertConfig {
}
function getOpenclawConfig() {
const configPath = path.join(OPENCLAW_HOME, "openclaw.json");
const configPath = OPENCLAW_CONFIG_PATH;
try {
const raw = fs.readFileSync(configPath, "utf-8");
return JSON.parse(raw);
@@ -55,7 +54,7 @@ function saveAlertConfig(config: AlertConfig): void {
}
function getGatewayConfig() {
const configPath = path.join(OPENCLAW_HOME, "openclaw.json");
const configPath = OPENCLAW_CONFIG_PATH;
try {
const raw = fs.readFileSync(configPath, "utf-8");
const config = JSON.parse(raw);
+1 -2
View File
@@ -1,8 +1,7 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
import { OPENCLAW_HOME } from "@/lib/openclaw-paths";
const ALERTS_CONFIG_PATH = path.join(OPENCLAW_HOME, "alerts.json");
interface AlertRule {
+2 -2
View File
@@ -1,10 +1,10 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
import { OPENCLAW_CONFIG_PATH, OPENCLAW_HOME } from "@/lib/openclaw-paths";
// 配置文件路径:优先使用 OPENCLAW_HOME 环境变量,否则默认 ~/.openclaw
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json");
const CONFIG_PATH = OPENCLAW_CONFIG_PATH;
const OPENCLAW_DIR = OPENCLAW_HOME;
// 30秒内存缓存
+90 -27
View File
@@ -1,15 +1,39 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
import { execFile } from "child_process";
import { exec, execFile } from "child_process";
import { promisify } from "util";
import { readJsonFileSync } from "@/lib/json";
import { OPENCLAW_CONFIG_PATH } from "@/lib/openclaw-paths";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json");
const CONFIG_PATH = OPENCLAW_CONFIG_PATH;
const DEGRADED_LATENCY_MS = 1500;
const execFileAsync = promisify(execFile);
const execAsync = promisify(exec);
let cachedOpenclawVersion: { value: string | null; expiresAt: number } | null = null;
function quoteShellArg(arg: string): string {
if (/^[A-Za-z0-9_./:=@-]+$/.test(arg)) return arg;
return `"${arg.replace(/"/g, '""')}"`;
}
async function execOpenclaw(args: string[]): Promise<{ stdout: string; stderr: string }> {
const env = { ...process.env, FORCE_COLOR: "0" };
if (process.platform !== "win32") {
return execFileAsync("openclaw", args, {
maxBuffer: 10 * 1024 * 1024,
env,
});
}
const command = `openclaw ${args.map(quoteShellArg).join(" ")}`;
return execAsync(command, {
maxBuffer: 10 * 1024 * 1024,
env,
shell: "cmd.exe",
});
}
function parseJsonFromMixedOutput(output: string): any {
for (let i = 0; i < output.length; i++) {
if (output[i] !== "{") continue;
@@ -49,10 +73,7 @@ async function probeGatewayViaCli(token: string, timeoutMs = 5000): Promise<{ ok
try {
const args = ["gateway", "status", "--json", "--timeout", String(timeoutMs)];
if (token) args.push("--token", token);
const { stdout, stderr } = await execFileAsync("openclaw", args, {
maxBuffer: 10 * 1024 * 1024,
env: { ...process.env, FORCE_COLOR: "0" },
});
const { stdout, stderr } = await execOpenclaw(args);
const parsed = parseJsonFromMixedOutput(`${stdout}\n${stderr || ""}`);
const ok = parsed?.rpc?.ok === true;
const error =
@@ -66,16 +87,31 @@ async function probeGatewayViaCli(token: string, timeoutMs = 5000): Promise<{ ok
}
}
async function probeGatewayViaWeb(port: number, token: string, timeoutMs = 5000): Promise<{ ok: boolean; error?: string }> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const resp = await fetch(
`http://localhost:${port}/chat${token ? `?token=${encodeURIComponent(token)}` : ""}`,
{ signal: controller.signal, cache: "no-store", redirect: "manual" },
);
return resp.status >= 200 && resp.status < 400
? { ok: true }
: { ok: false, error: `HTTP ${resp.status}` };
} catch (err: any) {
return { ok: false, error: err?.message || "Failed to probe gateway web UI" };
} finally {
clearTimeout(timeout);
}
}
async function getOpenclawVersion(): Promise<string | undefined> {
const now = Date.now();
if (cachedOpenclawVersion && cachedOpenclawVersion.expiresAt > now) {
return cachedOpenclawVersion.value || undefined;
}
try {
const { stdout } = await execFileAsync("openclaw", ["--version"], {
maxBuffer: 1024 * 1024,
env: { ...process.env, FORCE_COLOR: "0" },
});
const { stdout } = await execOpenclaw(["--version"]);
const version = stdout.trim().split(/\s+/)[0] || null;
cachedOpenclawVersion = { value: version, expiresAt: now + 60 * 60 * 1000 };
return version || undefined;
@@ -89,10 +125,10 @@ export async function GET() {
const startedAt = Date.now();
try {
const openclawVersion = await getOpenclawVersion();
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
const config = JSON.parse(raw);
const config = readJsonFileSync<any>(CONFIG_PATH);
const port = config.gateway?.port || 18789;
const token = config.gateway?.auth?.token || "";
const webUrl = `http://localhost:${port}/chat${token ? '?token=' + encodeURIComponent(token) : ''}`;
const url = `http://localhost:${port}/api/health`;
const headers: Record<string, string> = {};
@@ -114,7 +150,22 @@ export async function GET() {
status: responseMs > DEGRADED_LATENCY_MS ? "degraded" : "healthy",
checkedAt,
responseMs,
webUrl: `http://localhost:${port}/chat${token ? '?token=' + encodeURIComponent(token) : ''}`,
webUrl,
});
}
const web = await probeGatewayViaWeb(port, token, 5000);
if (web.ok) {
const checkedAt = Date.now();
const responseMs = checkedAt - startedAt;
return NextResponse.json({
ok: true,
data: null,
openclawVersion,
status: resp.status === 404 ? "healthy" : "degraded",
checkedAt,
responseMs,
webUrl,
});
}
@@ -130,7 +181,7 @@ export async function GET() {
status: "healthy",
checkedAt,
responseMs,
webUrl: `http://localhost:${port}/chat${token ? '?token=' + encodeURIComponent(token) : ''}`,
webUrl,
});
}
@@ -152,26 +203,38 @@ export async function GET() {
: err.message;
const token = (() => {
try {
const rawCfg = fs.readFileSync(CONFIG_PATH, "utf-8");
const cfg = JSON.parse(rawCfg);
const cfg = readJsonFileSync<any>(CONFIG_PATH);
return cfg.gateway?.auth?.token || "";
} catch {
return "";
}
})();
const port = (() => {
try {
const cfg = readJsonFileSync<any>(CONFIG_PATH);
return cfg.gateway?.port || 18789;
} catch {
return 18789;
}
})();
const web = await probeGatewayViaWeb(port, token, 5000);
if (web.ok) {
const checkedAt = Date.now();
const responseMs = checkedAt - startedAt;
return NextResponse.json({
ok: true,
data: null,
openclawVersion,
status: "degraded",
checkedAt,
responseMs,
webUrl: `http://localhost:${port}/chat${token ? '?token=' + encodeURIComponent(token) : ''}`,
});
}
const cli = await probeGatewayViaCli(token, 5000);
const checkedAt = Date.now();
const responseMs = checkedAt - startedAt;
if (cli.ok) {
const port = (() => {
try {
const rawCfg = fs.readFileSync(CONFIG_PATH, "utf-8");
const cfg = JSON.parse(rawCfg);
return cfg.gateway?.port || 18789;
} catch {
return 18789;
}
})();
return NextResponse.json({
ok: true,
data: null,
+1 -2
View File
@@ -1,8 +1,7 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
import { OPENCLAW_HOME } from "@/lib/openclaw-paths";
const ACTIVE_GAP_MS = 2 * 60 * 1000; // 2 minutes — gaps longer than this count as idle
// Server-side cache: 5 min TTL
+2 -2
View File
@@ -1,9 +1,9 @@
import { NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
import os from 'os'
import { OPENCLAW_PIXEL_OFFICE_DIR } from '@/lib/openclaw-paths'
const LAYOUT_DIR = path.join(os.homedir(), '.openclaw', 'pixel-office')
const LAYOUT_DIR = OPENCLAW_PIXEL_OFFICE_DIR
const LAYOUT_FILE = path.join(LAYOUT_DIR, 'layout.json')
export async function GET() {
+1 -2
View File
@@ -1,8 +1,7 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
import { OPENCLAW_HOME } from "@/lib/openclaw-paths";
export async function GET(_req: Request, { params }: { params: Promise<{ agentId: string }> }) {
try {
+3 -8
View File
@@ -1,17 +1,12 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
import { getOpenclawPackageCandidates, OPENCLAW_CONFIG_PATH, OPENCLAW_HOME } from "@/lib/openclaw-paths";
// Find OpenClaw package directory
function findOpenClawPkg(): string {
// Check common locations
const candidates = [
path.join(process.env.HOME || "", ".nvm/versions/node", process.version, "lib/node_modules/openclaw"),
"/usr/local/lib/node_modules/openclaw",
"/usr/lib/node_modules/openclaw",
];
const candidates = getOpenclawPackageCandidates();
for (const c of candidates) {
if (fs.existsSync(path.join(c, "package.json"))) return c;
}
@@ -148,7 +143,7 @@ export async function GET() {
}
// 5. Get agent info for display
const configPath = path.join(OPENCLAW_HOME, "openclaw.json");
const configPath = OPENCLAW_CONFIG_PATH;
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
const agentList = config.agents?.list || [];
const agentMap: Record<string, { name: string; emoji: string }> = {};
+1 -2
View File
@@ -1,8 +1,7 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
import { OPENCLAW_HOME } from "@/lib/openclaw-paths";
// 30秒内存缓存
let statsCache: { data: any; ts: number } | null = null;
+1 -2
View File
@@ -1,8 +1,7 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
import { OPENCLAW_HOME } from "@/lib/openclaw-paths";
// 30秒内存缓存
let statsCache: { data: any; ts: number } | null = null;
+1 -2
View File
@@ -1,8 +1,7 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
import { OPENCLAW_HOME } from "@/lib/openclaw-paths";
interface DayStat {
date: string; // YYYY-MM-DD
+2 -3
View File
@@ -2,9 +2,9 @@ import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
import { DEFAULT_MODEL_PROBE_TIMEOUT_MS, parseModelRef, probeModel } from "@/lib/model-probe";
import { OPENCLAW_CONFIG_PATH, OPENCLAW_HOME } from "@/lib/openclaw-paths";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json");
const CONFIG_PATH = OPENCLAW_CONFIG_PATH;
const PROBE_TIMEOUT_MS = DEFAULT_MODEL_PROBE_TIMEOUT_MS;
type AgentConfig = {
@@ -98,4 +98,3 @@ export async function POST() {
export async function GET() {
return POST();
}
+12 -5
View File
@@ -1,9 +1,9 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json");
import { OPENCLAW_CONFIG_PATH, OPENCLAW_HOME } from "@/lib/openclaw-paths";
import { parseApiJsonSafely, shouldFallbackToCli, testSessionViaCli } from "@/lib/session-test-fallback";
const CONFIG_PATH = OPENCLAW_CONFIG_PATH;
interface DmSessionResult {
agentId: string;
@@ -65,11 +65,18 @@ async function testDmSession(
signal: AbortSignal.timeout(100000),
});
const data = await resp.json();
const rawText = await resp.text();
const data = parseApiJsonSafely(rawText);
const elapsed = Date.now() - startTime;
if (!resp.ok) {
return { agentId, platform, ok: false, error: data.error?.message || JSON.stringify(data), elapsed };
if (shouldFallbackToCli(resp, rawText)) {
const fallback = await testSessionViaCli(agentId);
return fallback.ok
? { agentId, platform, ok: true, detail: `${fallback.reply || "OK"} · DM fallback`, elapsed: fallback.elapsed }
: { agentId, platform, ok: false, error: fallback.error || "Gateway route not found", elapsed: fallback.elapsed };
}
return { agentId, platform, ok: false, error: data?.error?.message || rawText || JSON.stringify(data), elapsed };
}
const reply = data.choices?.[0]?.message?.content || "";
+8 -58
View File
@@ -1,9 +1,8 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json");
import { OPENCLAW_CONFIG_PATH, OPENCLAW_HOME } from "@/lib/openclaw-paths";
const CONFIG_PATH = OPENCLAW_CONFIG_PATH;
const QQBOT_TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
const QQBOT_API_BASE = "https://api.sgroup.qq.com";
@@ -641,53 +640,6 @@ async function testQqbot(
}
}
// Agent session test: use Gateway chatCompletions API to send health check
// When sessionKey is provided, message routes to that session (e.g. feishu DM session)
async function testAgentSession(agentId: string, sessionKey: string | undefined, gatewayPort: number, gatewayToken: string): Promise<{ agentId: string; ok: boolean; reply?: string; error?: string; elapsed: number }> {
const startTime = Date.now();
try {
const headers: Record<string, string> = {
"Content-Type": "application/json",
"Authorization": `Bearer ${gatewayToken}`,
"x-openclaw-agent-id": agentId,
};
if (sessionKey) {
headers["x-openclaw-session-key"] = sessionKey;
}
const resp = await fetch(`http://127.0.0.1:${gatewayPort}/v1/chat/completions`, {
method: "POST",
headers,
body: JSON.stringify({
model: `openclaw:${agentId}`,
messages: [{ role: "user", content: "Health check: reply with OK" }],
max_tokens: 64,
}),
signal: AbortSignal.timeout(100000),
});
const data = await resp.json();
const elapsed = Date.now() - startTime;
if (!resp.ok) {
return { agentId, ok: false, error: data.error?.message || JSON.stringify(data), elapsed };
}
const reply = data.choices?.[0]?.message?.content || "";
return {
agentId, ok: true,
reply: reply.slice(0, 200) || "(no reply)",
elapsed,
};
} catch (err: any) {
return {
agentId, ok: false,
error: err.message,
elapsed: Date.now() - startTime,
};
}
}
export async function POST() {
try {
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
@@ -725,12 +677,10 @@ export async function POST() {
// Phase 1: Platform API tests (parallel)
const platformTests: Promise<PlatformTestResult>[] = [];
const agentIds: string[] = [];
const testedFeishuAccounts = new Set<string>();
for (const agent of agentList) {
const id = agent.id;
agentIds.push(id);
// Feishu
const feishuBinding = bindings.find(
@@ -764,21 +714,21 @@ export async function POST() {
// WhatsApp: only test once, via gateway
if (id === "main" && whatsappConfig && whatsappConfig.enabled !== false) {
const sessionUser = getWhatsappDmUser(id);
const recentDmUser = getWhatsappDmUser(id);
const allowFromUser = getWhatsappAllowlistUser(whatsappConfig);
const whatsappTestUser = sessionUser || allowFromUser || null;
const whatsappTestUser = recentDmUser || allowFromUser || null;
const source: "session" | "allowFrom" | "none" =
sessionUser ? "session" : (allowFromUser ? "allowFrom" : "none");
recentDmUser ? "session" : (allowFromUser ? "allowFrom" : "none");
platformTests.push(testWhatsapp(id, gatewayPort, gatewayToken, whatsappTestUser, source));
}
// QQBot: only test once, via `openclaw message send`
if (id === "main" && qqbotConfig && qqbotConfig.enabled !== false) {
const sessionUser = normalizeQqbotTarget(getQqbotDmUser(id));
const recentDmUser = normalizeQqbotTarget(getQqbotDmUser(id));
const allowFromUser = normalizeQqbotTarget(getQqbotAllowlistUser(qqbotConfig));
const qqbotTestUser = sessionUser || allowFromUser || null;
const qqbotTestUser = recentDmUser || allowFromUser || null;
const source: "session" | "allowFrom" | "none" =
sessionUser ? "session" : (allowFromUser ? "allowFrom" : "none");
recentDmUser ? "session" : (allowFromUser ? "allowFrom" : "none");
platformTests.push(testQqbot(id, qqbotConfig, qqbotTestUser, source));
}
}
+16 -6
View File
@@ -1,9 +1,9 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = path.join(process.env.HOME || "/root", ".openclaw");
const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json");
import { OPENCLAW_CONFIG_PATH, OPENCLAW_HOME } from "@/lib/openclaw-paths";
import { parseApiJsonSafely, shouldFallbackToCli, testSessionViaCli } from "@/lib/session-test-fallback";
const CONFIG_PATH = OPENCLAW_CONFIG_PATH;
export async function POST(req: Request) {
try {
@@ -38,16 +38,26 @@ export async function POST(req: Request) {
}),
signal: AbortSignal.timeout(100000),
});
const data = await resp.json();
const rawText = await resp.text();
const data = parseApiJsonSafely(rawText);
const elapsed = Date.now() - startTime;
if (!resp.ok) {
if (shouldFallbackToCli(resp, rawText)) {
const fallback = await testSessionViaCli(agentId);
return NextResponse.json({
status: fallback.ok ? "ok" : "error",
sessionKey,
elapsed: fallback.elapsed,
reply: fallback.reply,
error: fallback.error,
});
}
return NextResponse.json({
status: "error",
sessionKey,
elapsed,
error: data.error?.message || JSON.stringify(data),
error: data?.error?.message || rawText || JSON.stringify(data),
});
}
+13 -5
View File
@@ -1,9 +1,9 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
const OPENCLAW_HOME = path.join(process.env.HOME || "/root", ".openclaw");
const CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json");
import { OPENCLAW_CONFIG_PATH, OPENCLAW_HOME } from "@/lib/openclaw-paths";
import { parseApiJsonSafely, shouldFallbackToCli, testSessionViaCli } from "@/lib/session-test-fallback";
const CONFIG_PATH = OPENCLAW_CONFIG_PATH;
function hasEmbeddedHttpError(reply: string): boolean {
// Some providers return error text in content while gateway still returns HTTP 200.
@@ -50,10 +50,18 @@ export async function POST() {
}),
signal: AbortSignal.timeout(100000),
});
const data = await resp.json();
const rawText = await resp.text();
const data = parseApiJsonSafely(rawText);
const elapsed = Date.now() - startTime;
if (!resp.ok) {
results.push({ agentId, ok: false, error: data.error?.message || "API error", elapsed });
if (shouldFallbackToCli(resp, rawText)) {
const fallback = await testSessionViaCli(agentId);
results.push(fallback.ok
? { agentId, ok: true, reply: fallback.reply, elapsed: fallback.elapsed }
: { agentId, ok: false, error: fallback.error || "Gateway route not found", elapsed: fallback.elapsed });
} else {
results.push({ agentId, ok: false, error: data?.error?.message || rawText || "API error", elapsed });
}
} else {
const reply = data.choices?.[0]?.message?.content || "";
const clippedReply = reply.slice(0, 200) || "(no reply)";
+17
View File
@@ -0,0 +1,17 @@
import fs, { promises as fsPromises } from "fs";
export function stripUtf8Bom(text: string): string {
return text.replace(/^\uFEFF/, "");
}
export function parseJsonText<T = any>(text: string): T {
return JSON.parse(stripUtf8Bom(text)) as T;
}
export function readJsonFileSync<T = any>(filePath: string): T {
return parseJsonText<T>(fs.readFileSync(filePath, "utf-8"));
}
export async function readJsonFile<T = any>(filePath: string): Promise<T> {
return parseJsonText<T>(await fsPromises.readFile(filePath, "utf-8"));
}
+30 -15
View File
@@ -1,9 +1,11 @@
import fs from "fs";
import path from "path";
import { execFile } from "child_process";
import { exec, execFile } from "child_process";
import { promisify } from "util";
import { readJsonFileSync } from "@/lib/json";
import { OPENCLAW_HOME } from "@/lib/openclaw-paths";
const execFileAsync = promisify(execFile);
const execAsync = promisify(exec);
export const DEFAULT_MODEL_PROBE_TIMEOUT_MS = 15000;
@@ -55,9 +57,31 @@ interface ProbeModelParams {
timeoutMs?: number;
}
const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "", ".openclaw");
const MODELS_PATH = path.join(OPENCLAW_HOME, "agents", "main", "agent", "models.json");
function quoteShellArg(arg: string): string {
if (/^[A-Za-z0-9_./:=@-]+$/.test(arg)) return arg;
return `"${arg.replace(/"/g, '""')}"`;
}
async function execOpenclaw(args: string[]): Promise<{ stdout: string; stderr: string }> {
const env = { ...process.env, FORCE_COLOR: "0" };
if (process.platform !== "win32") {
return execFileAsync("openclaw", args, {
maxBuffer: 10 * 1024 * 1024,
env,
});
}
const command = `openclaw ${args.map(quoteShellArg).join(" ")}`;
return execAsync(command, {
maxBuffer: 10 * 1024 * 1024,
env,
shell: "cmd.exe",
});
}
function parseJsonFromMixedOutput(output: string): any {
for (let i = 0; i < output.length; i++) {
if (output[i] !== "{") continue;
@@ -95,8 +119,7 @@ function parseJsonFromMixedOutput(output: string): any {
function loadProviderConfig(providerId: string): ProviderConfig | null {
try {
const raw = fs.readFileSync(MODELS_PATH, "utf-8");
const parsed = JSON.parse(raw);
const parsed = readJsonFileSync<any>(MODELS_PATH);
const providers = parsed?.providers;
if (!providers || typeof providers !== "object") return null;
const exact = providers[providerId];
@@ -281,9 +304,7 @@ async function probeModelDirect(params: ProbeModelParams): Promise<DirectProbeRe
async function probeProviderViaOpenclaw(params: ProbeModelParams): Promise<ModelProbeOutcome> {
const timeoutMs = params.timeoutMs ?? DEFAULT_MODEL_PROBE_TIMEOUT_MS;
const startedAt = Date.now();
const { stdout, stderr } = await execFileAsync(
"openclaw",
[
const { stdout, stderr } = await execOpenclaw([
"models",
"status",
"--probe",
@@ -292,12 +313,7 @@ async function probeProviderViaOpenclaw(params: ProbeModelParams): Promise<Model
String(timeoutMs),
"--probe-provider",
String(params.providerId),
],
{
maxBuffer: 10 * 1024 * 1024,
env: { ...process.env, FORCE_COLOR: "0" },
}
);
]);
const parsed = parseJsonFromMixedOutput(`${stdout}\n${stderr || ""}`);
const results: ProbeResult[] = parsed?.auth?.probes?.results || [];
const fullModel = `${params.providerId}/${params.modelId}`;
@@ -349,4 +365,3 @@ export async function probeModel(params: ProbeModelParams): Promise<ModelProbeOu
}
return probeProviderViaOpenclaw(params);
}
+63
View File
@@ -0,0 +1,63 @@
import { exec, execFile } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);
const execAsync = promisify(exec);
function quoteShellArg(arg: string): string {
if (/^[A-Za-z0-9_./:=@-]+$/.test(arg)) return arg;
return `"${arg.replace(/"/g, '""')}"`;
}
export async function execOpenclaw(args: string[]): Promise<{ stdout: string; stderr: string }> {
const env = { ...process.env, FORCE_COLOR: "0" };
if (process.platform !== "win32") {
return execFileAsync("openclaw", args, {
maxBuffer: 10 * 1024 * 1024,
env,
});
}
const command = `openclaw ${args.map(quoteShellArg).join(" ")}`;
return execAsync(command, {
maxBuffer: 10 * 1024 * 1024,
env,
shell: "cmd.exe",
});
}
export function parseJsonFromMixedOutput(output: string): any {
for (let i = 0; i < output.length; i++) {
if (output[i] !== "{") continue;
let depth = 0;
let inString = false;
let escaped = false;
for (let j = i; j < output.length; j++) {
const ch = output[j];
if (inString) {
if (escaped) escaped = false;
else if (ch === "\\") escaped = true;
else if (ch === "\"") inString = false;
continue;
}
if (ch === "\"") {
inString = true;
continue;
}
if (ch === "{") depth++;
else if (ch === "}") {
depth--;
if (depth === 0) {
const candidate = output.slice(i, j + 1).trim();
try {
return JSON.parse(candidate);
} catch {
break;
}
}
}
}
}
return null;
}
+17
View File
@@ -0,0 +1,17 @@
import os from "os";
import path from "path";
export const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(os.homedir(), ".openclaw");
export const OPENCLAW_CONFIG_PATH = path.join(OPENCLAW_HOME, "openclaw.json");
export const OPENCLAW_AGENTS_DIR = path.join(OPENCLAW_HOME, "agents");
export const OPENCLAW_PIXEL_OFFICE_DIR = path.join(OPENCLAW_HOME, "pixel-office");
export function getOpenclawPackageCandidates(version = process.version): string[] {
const home = os.homedir();
return [
path.join(home, ".nvm", "versions", "node", version, "lib", "node_modules", "openclaw"),
path.join(home, "AppData", "Roaming", "npm", "node_modules", "openclaw"),
"/usr/local/lib/node_modules/openclaw",
"/usr/lib/node_modules/openclaw",
];
}
+61
View File
@@ -0,0 +1,61 @@
import { execOpenclaw, parseJsonFromMixedOutput } from "@/lib/openclaw-cli";
function extractCliReply(parsed: any, stdout: string): string {
const candidates = [
parsed?.reply,
parsed?.text,
parsed?.outputText,
parsed?.result?.reply,
parsed?.result?.text,
parsed?.response?.text,
parsed?.response?.output_text,
parsed?.message,
];
const hit = candidates.find((value) => typeof value === "string" && value.trim());
if (hit) return hit.trim().slice(0, 200);
if (parsed?.status === "ok") {
const summary = typeof parsed?.summary === "string" && parsed.summary.trim() ? parsed.summary.trim() : "completed";
return `OK (${summary}, CLI fallback)`;
}
return (stdout || "(no reply)").trim().slice(0, 200);
}
export async function testSessionViaCli(agentId: string): Promise<{ ok: boolean; reply?: string; error?: string; elapsed: number }> {
const startTime = Date.now();
try {
const { stdout, stderr } = await execOpenclaw([
"agent",
"--agent",
agentId,
"--message",
"Health check: reply with OK",
"--json",
"--timeout",
"100",
]);
const elapsed = Date.now() - startTime;
const parsed = parseJsonFromMixedOutput(`${stdout}\n${stderr || ""}`);
const error = parsed?.error?.message || parsed?.error;
if (typeof error === "string" && error.trim()) {
return { ok: false, error: error.trim().slice(0, 300), elapsed };
}
return { ok: true, reply: extractCliReply(parsed, stdout), elapsed };
} catch (err: any) {
const elapsed = Date.now() - startTime;
return { ok: false, error: (err?.message || "CLI fallback failed").slice(0, 300), elapsed };
}
}
export function shouldFallbackToCli(resp: Response, rawText: string): boolean {
const text = rawText.trim();
return resp.status === 404 || text === "Not Found";
}
export function parseApiJsonSafely(rawText: string): any {
if (!rawText) return null;
try {
return JSON.parse(rawText);
} catch {
return null;
}
}