feat: OpenClaw Bot Dashboard - 查看机器人配置

This commit is contained in:
xmanrui
2026-02-18 01:25:57 +08:00
commit c0c85b378f
11 changed files with 2144 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules
.next
*.tsbuildinfo
next-env.d.ts
+95
View File
@@ -0,0 +1,95 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
// 配置文件路径
const CONFIG_PATH = path.join(process.env.HOME || "", ".openclaw", "openclaw.json");
export async function GET() {
try {
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
const config = JSON.parse(raw);
// 提取 agents 信息
const defaults = config.agents?.defaults || {};
const defaultModel = typeof defaults.model === "string"
? defaults.model
: defaults.model?.primary || "unknown";
const fallbacks = typeof defaults.model === "object"
? defaults.model?.fallbacks || []
: [];
const agentList = config.agents?.list || [];
const bindings = config.bindings || [];
const channels = config.channels || {};
const feishuAccounts = channels.feishu?.accounts || {};
// 构建 agent 详情
const agents = agentList.map((agent: any) => {
const id = agent.id;
const name = agent.name || id;
const emoji = agent.identity?.emoji || "🤖";
const model = agent.model || defaultModel;
// 查找绑定的平台
const platforms: { name: string; accountId?: string; appId?: string }[] = [];
// 检查飞书绑定
const feishuBinding = bindings.find(
(b: any) => b.agentId === id && b.match?.channel === "feishu"
);
if (feishuBinding) {
const accountId = feishuBinding.match?.accountId || id;
const appId = feishuAccounts[accountId]?.appId;
platforms.push({ name: "feishu", accountId, appId });
}
// main agent 特殊处理:默认绑定所有未显式绑定的 channel
if (id === "main") {
if (!feishuBinding && channels.feishu?.enabled) {
const appId = feishuAccounts["main"]?.appId || channels.feishu?.appId;
platforms.push({ name: "feishu", accountId: "main", appId });
}
if (channels.discord?.enabled) {
platforms.push({ name: "discord" });
}
}
return { id, name, emoji, model, platforms };
});
// 提取模型 providers
const providers = Object.entries(config.models?.providers || {}).map(
([providerId, provider]: [string, any]) => {
const models = (provider.models || []).map((m: any) => ({
id: m.id,
name: m.name,
contextWindow: m.contextWindow,
maxTokens: m.maxTokens,
reasoning: m.reasoning,
input: m.input,
}));
// 找出使用该 provider 的 agents
const usedBy = agents
.filter((a: any) => a.model.startsWith(providerId + "/"))
.map((a: any) => ({ id: a.id, emoji: a.emoji }));
return {
id: providerId,
api: provider.api,
models,
usedBy,
};
}
);
return NextResponse.json({
agents,
providers,
defaults: { model: defaultModel, fallbacks },
});
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
+19
View File
@@ -0,0 +1,19 @@
@import "tailwindcss";
:root {
--bg: #0f172a;
--card: #1e293b;
--border: #334155;
--text: #f1f5f9;
--text-muted: #94a3b8;
--accent: #38bdf8;
--accent2: #a78bfa;
--green: #4ade80;
--orange: #fb923c;
}
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
+15
View File
@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "OpenClaw Bot Dashboard",
description: "查看所有 OpenClaw 机器人配置",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>{children}</body>
</html>
);
}
+155
View File
@@ -0,0 +1,155 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
interface Model {
id: string;
name: string;
contextWindow: number;
maxTokens: number;
reasoning: boolean;
input: string[];
}
interface Provider {
id: string;
api: string;
models: Model[];
usedBy: { id: string; emoji: string }[];
}
interface ConfigData {
providers: Provider[];
defaults: { model: string; fallbacks: string[] };
}
// 格式化数字
function formatNum(n: number) {
if (n >= 1000) return `${(n / 1000).toFixed(0)}K`;
return String(n);
}
export default function ModelsPage() {
const [data, setData] = useState<ConfigData | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("/api/config")
.then((r) => r.json())
.then((d) => {
if (d.error) setError(d.error);
else setData(d);
})
.catch((e) => setError(e.message));
}, []);
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<p className="text-red-400">: {error}</p>
</div>
);
}
if (!data) {
return (
<div className="min-h-screen flex items-center justify-center">
<p className="text-[var(--text-muted)]">...</p>
</div>
);
}
return (
<main className="min-h-screen p-8 max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
🧠 Provider
</h1>
<p className="text-[var(--text-muted)] text-sm mt-1">
{data.providers.length} Provider
</p>
</div>
<Link
href="/"
className="px-4 py-2 rounded-lg bg-[var(--card)] border border-[var(--border)] text-sm font-medium hover:border-[var(--accent)] transition"
>
</Link>
</div>
<div className="space-y-6">
{data.providers.map((provider) => (
<div
key={provider.id}
className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-5"
>
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-lg font-semibold">{provider.id}</h2>
<span className="text-xs text-[var(--text-muted)]">
API: {provider.api}
</span>
</div>
{provider.usedBy.length > 0 && (
<div className="flex items-center gap-1">
<span className="text-xs text-[var(--text-muted)] mr-1">使:</span>
{provider.usedBy.map((a) => (
<span key={a.id} title={a.id} className="text-lg">
{a.emoji}
</span>
))}
</div>
)}
</div>
{provider.models.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-[var(--text-muted)] text-xs border-b border-[var(--border)]">
<th className="text-left py-2 pr-4"> ID</th>
<th className="text-left py-2 pr-4"></th>
<th className="text-left py-2 pr-4"></th>
<th className="text-left py-2 pr-4"></th>
<th className="text-left py-2 pr-4"></th>
<th className="text-left py-2"></th>
</tr>
</thead>
<tbody>
{provider.models.map((m) => (
<tr key={m.id} className="border-b border-[var(--border)]/50">
<td className="py-2 pr-4 font-mono text-[var(--accent)]">{m.id}</td>
<td className="py-2 pr-4">{m.name}</td>
<td className="py-2 pr-4">{formatNum(m.contextWindow)}</td>
<td className="py-2 pr-4">{formatNum(m.maxTokens)}</td>
<td className="py-2 pr-4">
<div className="flex gap-1">
{(m.input || []).map((t) => (
<span
key={t}
className="px-1.5 py-0.5 rounded bg-[var(--bg)] text-xs"
>
{t === "text" ? "📝" : "🖼️"} {t}
</span>
))}
</div>
</td>
<td className="py-2">{m.reasoning ? "✅" : "❌"}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-[var(--text-muted)] text-sm">
provider
</p>
)}
</div>
))}
</div>
</main>
);
}
+180
View File
@@ -0,0 +1,180 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
interface Platform {
name: string;
accountId?: string;
appId?: string;
}
interface Agent {
id: string;
name: string;
emoji: string;
model: string;
platforms: Platform[];
}
interface ConfigData {
agents: Agent[];
defaults: { model: string; fallbacks: string[] };
}
// 平台标签颜色
function PlatformBadge({ platform }: { platform: Platform }) {
const isFeishu = platform.name === "feishu";
return (
<span
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${
isFeishu
? "bg-blue-500/20 text-blue-300 border border-blue-500/30"
: "bg-purple-500/20 text-purple-300 border border-purple-500/30"
}`}
>
{isFeishu ? "📱 飞书" : "🎮 Discord"}
{platform.accountId && (
<span className="opacity-60">({platform.accountId})</span>
)}
</span>
);
}
// 模型标签
function ModelBadge({ model }: { model: string }) {
const [provider, modelName] = model.includes("/")
? model.split("/", 2)
: ["default", model];
const colors: Record<string, string> = {
"yunyi-claude": "bg-green-500/20 text-green-300 border-green-500/30",
minimax: "bg-orange-500/20 text-orange-300 border-orange-500/30",
volcengine: "bg-cyan-500/20 text-cyan-300 border-cyan-500/30",
bailian: "bg-yellow-500/20 text-yellow-300 border-yellow-500/30",
};
return (
<span
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${
colors[provider] || "bg-gray-500/20 text-gray-300 border-gray-500/30"
}`}
>
🧠 {modelName}
</span>
);
}
// Agent 卡片
function AgentCard({ agent }: { agent: Agent }) {
return (
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-5 hover:border-[var(--accent)] transition-colors">
<div className="flex items-center gap-3 mb-3">
<span className="text-3xl">{agent.emoji}</span>
<div>
<h3 className="text-lg font-semibold text-[var(--text)]">{agent.name}</h3>
<span className="text-xs text-[var(--text-muted)]">ID: {agent.id}</span>
</div>
</div>
<div className="space-y-2">
<div>
<span className="text-xs text-[var(--text-muted)] block mb-1"></span>
<ModelBadge model={agent.model} />
</div>
<div>
<span className="text-xs text-[var(--text-muted)] block mb-1"></span>
<div className="flex flex-wrap gap-1">
{agent.platforms.map((p, i) => (
<PlatformBadge key={i} platform={p} />
))}
</div>
</div>
{agent.platforms.some((p) => p.appId) && (
<div>
<span className="text-xs text-[var(--text-muted)] block mb-1"> App ID</span>
<code className="text-xs text-[var(--accent)] bg-[var(--bg)] px-2 py-0.5 rounded">
{agent.platforms.find((p) => p.appId)?.appId}
</code>
</div>
)}
</div>
</div>
);
}
export default function Home() {
const [data, setData] = useState<ConfigData | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("/api/config")
.then((r) => r.json())
.then((d) => {
if (d.error) setError(d.error);
else setData(d);
})
.catch((e) => setError(e.message));
}, []);
if (error) {
return (
<div className="min-h-screen flex items-center justify-center">
<p className="text-red-400">: {error}</p>
</div>
);
}
if (!data) {
return (
<div className="min-h-screen flex items-center justify-center">
<p className="text-[var(--text-muted)]">...</p>
</div>
);
}
return (
<main className="min-h-screen p-8 max-w-6xl mx-auto">
{/* 头部 */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
🐾 OpenClaw Bot Dashboard
</h1>
<p className="text-[var(--text-muted)] text-sm mt-1">
{data.agents.length} · : {data.defaults.model}
</p>
</div>
<Link
href="/models"
className="px-4 py-2 rounded-lg bg-[var(--accent)] text-[var(--bg)] text-sm font-medium hover:opacity-90 transition"
>
</Link>
</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} />
))}
</div>
{/* Fallback 信息 */}
{data.defaults.fallbacks.length > 0 && (
<div className="mt-8 p-4 rounded-xl border border-[var(--border)] bg-[var(--card)]">
<h2 className="text-sm font-semibold text-[var(--text-muted)] mb-2">
🔄 Fallback
</h2>
<div className="flex flex-wrap gap-2">
{data.defaults.fallbacks.map((f, i) => (
<ModelBadge key={i} model={f} />
))}
</div>
</div>
)}
</main>
);
}
+3
View File
@@ -0,0 +1,3 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
+1606
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "openclaw-bot-review",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"next": "^16.0.0",
"postcss": "^8.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0"
}
}
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};
+41
View File
@@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}