mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-07-27 04:50:24 +00:00
feat: OpenClaw Bot Dashboard - 查看机器人配置
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
.next
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
export default nextConfig;
|
||||
Generated
+1606
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user