mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-07-27 22:25:52 +00:00
Fix Windows OpenClaw path and session test compatibility
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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秒内存缓存
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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,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,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,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,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 || "";
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user