mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-08-14 00:47:49 +00:00
feat: 新增平台连通性测试按钮,每个平台标签右侧显示测试结果
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { execSync } from "child_process";
|
||||
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");
|
||||
|
||||
interface TestRequest {
|
||||
agentId: string;
|
||||
platform: string;
|
||||
sessionKey: string;
|
||||
}
|
||||
|
||||
async function testPlatformSession(req: TestRequest): Promise<{
|
||||
agentId: string;
|
||||
platform: string;
|
||||
ok: boolean;
|
||||
reply?: string;
|
||||
error?: string;
|
||||
elapsed: number;
|
||||
}> {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const result = execSync(
|
||||
`openclaw agent --agent ${req.agentId} --session-id "${req.sessionKey}" --message "Platform health check: reply with OK" --timeout 30 --json`,
|
||||
{ timeout: 40000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
||||
);
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
const lines = result.split("\n");
|
||||
const jsonStartIdx = lines.findIndex(l => l.trimStart().startsWith("{"));
|
||||
if (jsonStartIdx === -1) {
|
||||
return { agentId: req.agentId, platform: req.platform, ok: false, error: "No JSON in CLI output", elapsed };
|
||||
}
|
||||
const jsonStr = lines.slice(jsonStartIdx).join("\n");
|
||||
const data = JSON.parse(jsonStr);
|
||||
const payloads = data?.result?.payloads || [];
|
||||
const reply = payloads[0]?.text || "";
|
||||
const durationMs = data?.result?.meta?.durationMs || elapsed;
|
||||
const ok = data.status === "ok";
|
||||
|
||||
return {
|
||||
agentId: req.agentId,
|
||||
platform: req.platform,
|
||||
ok,
|
||||
reply: reply ? reply.slice(0, 200) : (ok ? "(no reply)" : ""),
|
||||
error: ok ? undefined : "Agent returned error status",
|
||||
elapsed: durationMs,
|
||||
};
|
||||
} catch (execErr: any) {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const isTimeout = execErr.killed || execErr.signal === "SIGTERM";
|
||||
return {
|
||||
agentId: req.agentId,
|
||||
platform: req.platform,
|
||||
ok: false,
|
||||
error: isTimeout
|
||||
? "Timeout: platform not responding (30s)"
|
||||
: (execErr.stderr || execErr.message || "Unknown error").slice(0, 300),
|
||||
elapsed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
|
||||
const config = JSON.parse(raw);
|
||||
|
||||
const defaults = config.agents?.defaults || {};
|
||||
const defaultModel = typeof defaults.model === "string"
|
||||
? defaults.model
|
||||
: defaults.model?.primary || "unknown";
|
||||
const bindings = config.bindings || [];
|
||||
const channels = config.channels || {};
|
||||
const feishuAccounts = channels.feishu?.accounts || {};
|
||||
|
||||
let agentList = config.agents?.list || [];
|
||||
if (agentList.length === 0) {
|
||||
try {
|
||||
const agentsDir = path.join(OPENCLAW_HOME, "agents");
|
||||
const dirs = fs.readdirSync(agentsDir, { withFileTypes: true });
|
||||
agentList = dirs
|
||||
.filter((d: any) => d.isDirectory() && !d.name.startsWith("."))
|
||||
.map((d: any) => ({ id: d.name }));
|
||||
} catch {}
|
||||
if (agentList.length === 0) {
|
||||
agentList = [{ id: "main" }];
|
||||
}
|
||||
}
|
||||
|
||||
// Build test requests for each agent's platforms
|
||||
const testRequests: TestRequest[] = [];
|
||||
|
||||
for (const agent of agentList) {
|
||||
const id = agent.id;
|
||||
|
||||
// Find feishu sessions
|
||||
const feishuBinding = bindings.find(
|
||||
(b: any) => b.agentId === id && b.match?.channel === "feishu"
|
||||
);
|
||||
const hasFeishuBinding = !!feishuBinding;
|
||||
const hasFeishuAccount = !!feishuAccounts[id];
|
||||
|
||||
if (hasFeishuBinding || hasFeishuAccount) {
|
||||
// Find the most recent feishu DM session for this agent
|
||||
try {
|
||||
const sessionsPath = path.join(OPENCLAW_HOME, `agents/${id}/sessions/sessions.json`);
|
||||
const sessRaw = fs.readFileSync(sessionsPath, "utf-8");
|
||||
const sessions = JSON.parse(sessRaw);
|
||||
let bestKey: string | null = null;
|
||||
let bestTime = 0;
|
||||
for (const [key, val] of Object.entries(sessions)) {
|
||||
if (key.match(/^agent:[^:]+:feishu:direct:/)) {
|
||||
const updatedAt = (val as any).updatedAt || 0;
|
||||
if (updatedAt > bestTime) { bestTime = updatedAt; bestKey = key; }
|
||||
}
|
||||
}
|
||||
if (bestKey) {
|
||||
testRequests.push({ agentId: id, platform: "feishu", sessionKey: bestKey });
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// main agent special: also check discord
|
||||
if (id === "main" && channels.discord?.enabled) {
|
||||
try {
|
||||
const sessionsPath = path.join(OPENCLAW_HOME, `agents/${id}/sessions/sessions.json`);
|
||||
const sessRaw = fs.readFileSync(sessionsPath, "utf-8");
|
||||
const sessions = JSON.parse(sessRaw);
|
||||
let bestKey: string | null = null;
|
||||
let bestTime = 0;
|
||||
for (const [key, val] of Object.entries(sessions)) {
|
||||
if (key.match(/^agent:[^:]+:discord:direct:/)) {
|
||||
const updatedAt = (val as any).updatedAt || 0;
|
||||
if (updatedAt > bestTime) { bestTime = updatedAt; bestKey = key; }
|
||||
}
|
||||
}
|
||||
if (bestKey) {
|
||||
testRequests.push({ agentId: id, platform: "discord", sessionKey: bestKey });
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Non-main agents with discord bindings
|
||||
if (id !== "main") {
|
||||
const discordBinding = bindings.find(
|
||||
(b: any) => b.agentId === id && b.match?.channel === "discord"
|
||||
);
|
||||
if (discordBinding) {
|
||||
try {
|
||||
const sessionsPath = path.join(OPENCLAW_HOME, `agents/${id}/sessions/sessions.json`);
|
||||
const sessRaw = fs.readFileSync(sessionsPath, "utf-8");
|
||||
const sessions = JSON.parse(sessRaw);
|
||||
let bestKey: string | null = null;
|
||||
let bestTime = 0;
|
||||
for (const [key, val] of Object.entries(sessions)) {
|
||||
if (key.match(/^agent:[^:]+:discord:direct:/)) {
|
||||
const updatedAt = (val as any).updatedAt || 0;
|
||||
if (updatedAt > bestTime) { bestTime = updatedAt; bestKey = key; }
|
||||
}
|
||||
}
|
||||
if (bestKey) {
|
||||
testRequests.push({ agentId: id, platform: "discord", sessionKey: bestKey });
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run all tests in parallel
|
||||
const results = await Promise.all(testRequests.map(testPlatformSession));
|
||||
|
||||
return NextResponse.json({ results });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
+80
-23
@@ -159,8 +159,16 @@ function ResponseTrendChart({ data, height = 180, t }: { data: DayStat[]; height
|
||||
);
|
||||
}
|
||||
|
||||
// 平台测试结果类型
|
||||
interface PlatformTestResult {
|
||||
ok: boolean;
|
||||
reply?: string;
|
||||
error?: string;
|
||||
elapsed: number;
|
||||
}
|
||||
|
||||
// 平台标签颜色
|
||||
function PlatformBadge({ platform, agentId, gatewayPort, gatewayToken, t }: { platform: Platform; agentId: string; gatewayPort: number; gatewayToken?: string; t: TFunc }) {
|
||||
function PlatformBadge({ platform, agentId, gatewayPort, gatewayToken, t, testResult }: { platform: Platform; agentId: string; gatewayPort: number; gatewayToken?: string; t: TFunc; testResult?: PlatformTestResult | null }) {
|
||||
const isFeishu = platform.name === "feishu";
|
||||
|
||||
let sessionKey: string;
|
||||
@@ -175,24 +183,35 @@ function PlatformBadge({ platform, agentId, gatewayPort, gatewayToken, t }: { pl
|
||||
if (gatewayToken) sessionUrl += `&token=${encodeURIComponent(gatewayToken)}`;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={sessionUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title={t("agent.openChat")}
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium cursor-pointer transition-all hover:scale-105 hover:shadow-md ${
|
||||
isFeishu
|
||||
? "bg-blue-500/20 text-blue-300 border border-blue-500/30 hover:bg-blue-500/40 hover:border-blue-400"
|
||||
: "bg-purple-500/20 text-purple-300 border border-purple-500/30 hover:bg-purple-500/40 hover:border-purple-400"
|
||||
}`}
|
||||
>
|
||||
{isFeishu ? t("platform.feishu") : t("platform.discord")}
|
||||
{platform.accountId && (
|
||||
<span className="opacity-60">({platform.accountId})</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<a
|
||||
href={sessionUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title={t("agent.openChat")}
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium cursor-pointer transition-all hover:scale-105 hover:shadow-md ${
|
||||
isFeishu
|
||||
? "bg-blue-500/20 text-blue-300 border border-blue-500/30 hover:bg-blue-500/40 hover:border-blue-400"
|
||||
: "bg-purple-500/20 text-purple-300 border border-purple-500/30 hover:bg-purple-500/40 hover:border-purple-400"
|
||||
}`}
|
||||
>
|
||||
{isFeishu ? t("platform.feishu") : t("platform.discord")}
|
||||
{platform.accountId && (
|
||||
<span className="opacity-60">({platform.accountId})</span>
|
||||
)}
|
||||
<span className="opacity-50 text-[10px]">↗</span>
|
||||
</a>
|
||||
{testResult === undefined ? (
|
||||
<span className="text-xs text-[var(--text-muted)]">--</span>
|
||||
) : testResult === null ? (
|
||||
<span className="text-xs text-[var(--text-muted)] animate-pulse">⏳</span>
|
||||
) : testResult.ok ? (
|
||||
<span className="text-green-400 text-sm cursor-help" title={`${testResult.elapsed}ms${testResult.reply ? ' · ' + testResult.reply : ''}`}>✅</span>
|
||||
) : (
|
||||
<span className="text-red-400 text-sm cursor-help" title={testResult.error || ''}>❌</span>
|
||||
)}
|
||||
<span className="opacity-50 text-[10px]">↗</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,7 +240,7 @@ function ModelBadge({ model }: { model: string }) {
|
||||
}
|
||||
|
||||
// Agent 卡片
|
||||
function AgentCard({ agent, gatewayPort, gatewayToken, t, testResult }: { agent: Agent; gatewayPort: number; gatewayToken?: string; t: TFunc; testResult?: { ok: boolean; text?: string; error?: string; elapsed: number } | null }) {
|
||||
function AgentCard({ agent, gatewayPort, gatewayToken, t, testResult, platformTestResults }: { agent: Agent; gatewayPort: number; gatewayToken?: string; t: TFunc; testResult?: { ok: boolean; text?: string; error?: string; elapsed: number } | null; platformTestResults?: Record<string, PlatformTestResult | null> }) {
|
||||
const sessionKey = `agent:${agent.id}:main`;
|
||||
let sessionUrl = `http://localhost:${gatewayPort}/chat?session=${encodeURIComponent(sessionKey)}`;
|
||||
if (gatewayToken) sessionUrl += `&token=${encodeURIComponent(gatewayToken)}`;
|
||||
@@ -272,9 +291,13 @@ function AgentCard({ agent, gatewayPort, gatewayToken, t, testResult }: { agent:
|
||||
<div>
|
||||
<span className="text-xs text-[var(--text-muted)] block mb-1">{t("agent.platform")}</span>
|
||||
<div className="flex flex-col gap-1">
|
||||
{agent.platforms.map((p, i) => (
|
||||
<PlatformBadge key={i} platform={p} agentId={agent.id} gatewayPort={gatewayPort} gatewayToken={gatewayToken} t={t} />
|
||||
))}
|
||||
{agent.platforms.map((p, i) => {
|
||||
const pKey = `${agent.id}:${p.name}`;
|
||||
const pResult = platformTestResults ? platformTestResults[pKey] : undefined;
|
||||
return (
|
||||
<PlatformBadge key={i} platform={p} agentId={agent.id} gatewayPort={gatewayPort} gatewayToken={gatewayToken} t={t} testResult={pResult} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -339,6 +362,8 @@ export default function Home() {
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [testResults, setTestResults] = useState<Record<string, { ok: boolean; text?: string; error?: string; elapsed: number }> | null>(null);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [platformTestResults, setPlatformTestResults] = useState<Record<string, PlatformTestResult | null> | null>(null);
|
||||
const [testingPlatforms, setTestingPlatforms] = useState(false);
|
||||
|
||||
const RANGE_LABELS: Record<TimeRange, string> = { daily: t("range.daily"), weekly: t("range.weekly"), monthly: t("range.monthly") };
|
||||
|
||||
@@ -389,6 +414,31 @@ export default function Home() {
|
||||
.finally(() => setTesting(false));
|
||||
}, [data]);
|
||||
|
||||
const testAllPlatforms = useCallback(() => {
|
||||
setTestingPlatforms(true);
|
||||
// Set all agent:platform combos to null (⏳)
|
||||
const pending: Record<string, any> = {};
|
||||
if (data) {
|
||||
for (const a of data.agents) {
|
||||
for (const p of a.platforms) {
|
||||
pending[`${a.id}:${p.name}`] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
setPlatformTestResults(pending);
|
||||
fetch("/api/test-platforms", { method: "POST" })
|
||||
.then((r) => r.json())
|
||||
.then((resp) => {
|
||||
if (resp.results) {
|
||||
const map: Record<string, PlatformTestResult> = {};
|
||||
for (const r of resp.results) map[`${r.agentId}:${r.platform}`] = r;
|
||||
setPlatformTestResults(map);
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setTestingPlatforms(false));
|
||||
}, [data]);
|
||||
|
||||
// 定时刷新
|
||||
useEffect(() => {
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
@@ -462,13 +512,20 @@ export default function Home() {
|
||||
>
|
||||
{testing ? t("home.testingAll") : t("home.testAll")}
|
||||
</button>
|
||||
<button
|
||||
onClick={testAllPlatforms}
|
||||
disabled={testingPlatforms}
|
||||
className="px-4 py-2 rounded-lg bg-[var(--card)] border border-[var(--border)] text-[var(--text)] text-sm font-medium hover:border-[var(--accent)] transition disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
{testingPlatforms ? t("home.testingPlatforms") : t("home.testPlatforms")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 卡片墙 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{data.agents.map((agent) => (
|
||||
<AgentCard key={agent.id} agent={agent} gatewayPort={data.gateway?.port || 18789} gatewayToken={data.gateway?.token} t={t} testResult={testResults?.[agent.id]} />
|
||||
<AgentCard key={agent.id} agent={agent} gatewayPort={data.gateway?.port || 18789} gatewayToken={data.gateway?.token} t={t} testResult={testResults?.[agent.id]} platformTestResults={platformTestResults || undefined} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
"home.discordChannel": "Discord 频道",
|
||||
"home.bots": "个机器人",
|
||||
"home.noResponseData": "暂无响应时间数据",
|
||||
"home.testPlatforms": "🧪 测试平台连通",
|
||||
"home.testingPlatforms": "⏳ 测试平台中...",
|
||||
|
||||
// agent card
|
||||
"agent.model": "模型",
|
||||
@@ -221,6 +223,8 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
"home.discordChannel": "Discord Channel",
|
||||
"home.bots": "bots",
|
||||
"home.noResponseData": "No response time data",
|
||||
"home.testPlatforms": "🧪 Test Platforms",
|
||||
"home.testingPlatforms": "⏳ Testing Platforms...",
|
||||
|
||||
// agent card
|
||||
"agent.model": "Model",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "failed",
|
||||
"failedTests": []
|
||||
}
|
||||
Reference in New Issue
Block a user