mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-08-14 00:47:49 +00:00
feat: add Agent workspace and Daily board pages
- Add /workspace page: Agent grid with real-time status - Add /daily page: Daily report board with TTS playback - Add new API routes: /api/agents, /api/daily - Add new components: agent-card, agent-grid - Add sidebar navigation for new pages - Add i18n labels (zh-CN + en)
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
// app/api/agents/route.ts
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getAgents } from '@/lib/agents';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const agents = getAgents();
|
||||
return NextResponse.json({ agents });
|
||||
} catch (error) {
|
||||
console.error('Failed to get agents:', error);
|
||||
return NextResponse.json({ agents: [], error: 'Failed to fetch agents' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// app/api/daily/route.ts
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getTodayItems } from '@/lib/daily';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const items = getTodayItems();
|
||||
return NextResponse.json({
|
||||
items,
|
||||
date: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get daily items:', error);
|
||||
return NextResponse.json({ items: [], date: new Date().toISOString().split('T')[0] }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// app/daily/page.tsx
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface DailyItem {
|
||||
time: string;
|
||||
content: string;
|
||||
status: 'completed' | 'in-progress' | 'pending';
|
||||
}
|
||||
|
||||
function DailyBoard() {
|
||||
const [items, setItems] = useState<DailyItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [speaking, setSpeaking] = useState(false);
|
||||
const [utterance, setUtterance] = useState<SpeechSynthesisUtterance | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchDaily() {
|
||||
try {
|
||||
const res = await fetch('/api/daily');
|
||||
const data = await res.json();
|
||||
setItems(data.items || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch daily:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchDaily();
|
||||
}, []);
|
||||
|
||||
const completedItems = items.filter(i => i.status === 'completed');
|
||||
const inProgressItems = items.filter(i => i.status === 'in-progress');
|
||||
|
||||
const reportText = completedItems.length > 0
|
||||
? `今日共完成 ${completedItems.length} 项任务。${completedItems.map(i => i.content).join('。')}`
|
||||
: '今日暂无完成记录';
|
||||
|
||||
const speak = () => {
|
||||
if (!('speechSynthesis' in window)) {
|
||||
alert('您的浏览器不支持语音合成');
|
||||
return;
|
||||
}
|
||||
|
||||
if (speaking && utterance) {
|
||||
window.speechSynthesis.cancel();
|
||||
setSpeaking(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const u = new SpeechSynthesisUtterance(reportText);
|
||||
u.lang = 'zh-CN';
|
||||
u.rate = 1.0;
|
||||
|
||||
u.onstart = () => setSpeaking(true);
|
||||
u.onend = () => setSpeaking(false);
|
||||
u.onerror = () => setSpeaking(false);
|
||||
|
||||
setUtterance(u);
|
||||
window.speechSynthesis.speak(u);
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
if ('speechSynthesis' in window) {
|
||||
window.speechSynthesis.cancel();
|
||||
}
|
||||
setSpeaking(false);
|
||||
};
|
||||
|
||||
const today = new Date().toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long',
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="min-h-screen p-4 md:p-8 max-w-3xl mx-auto">
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">📅 今日播报板</h1>
|
||||
<p className="text-sm text-[var(--text-muted)] mt-1">{today}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={speak}
|
||||
disabled={loading || items.length === 0}
|
||||
className={`px-4 py-2 rounded-lg font-medium text-sm transition-all flex items-center gap-2 ${
|
||||
speaking
|
||||
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||
: 'bg-blue-500 hover:bg-blue-600 text-white'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{speaking ? (
|
||||
<>
|
||||
<span>⏹️</span> 停止
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>🎤</span> 播报
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计 */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 text-center">
|
||||
<div className="text-2xl font-bold text-green-400">{completedItems.length}</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">已完成</div>
|
||||
</div>
|
||||
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 text-center">
|
||||
<div className="text-2xl font-bold text-yellow-400">{inProgressItems.length}</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">进行中</div>
|
||||
</div>
|
||||
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 text-center">
|
||||
<div className="text-2xl font-bold text-gray-400">
|
||||
{items.length - completedItems.length - inProgressItems.length}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">待开始</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 播报列表 */}
|
||||
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="p-8 text-center text-[var(--text-muted)]">加载中...</div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="p-8 text-center text-[var(--text-muted)]">
|
||||
今日暂无记录
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[var(--border)]">
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-4 flex items-start gap-3 ${
|
||||
speaking && item.status === 'completed' ? 'opacity-60' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg mt-0.5">
|
||||
{item.status === 'completed' ? '✅' :
|
||||
item.status === 'in-progress' ? '🔄' : '⏳'}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className={`text-sm ${
|
||||
item.status === 'completed'
|
||||
? 'text-[var(--text-muted)] line-through'
|
||||
: ''
|
||||
}`}>
|
||||
{item.content}
|
||||
</p>
|
||||
{item.time && (
|
||||
<p className="text-xs text-[var(--text-muted)] mt-1">{item.time}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 提示 */}
|
||||
{speaking && (
|
||||
<div className="mt-4 p-3 rounded-lg bg-blue-500/10 border border-blue-500/30 text-xs text-blue-400">
|
||||
🎤 正在语音播报今日完成事项...
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DailyPage() {
|
||||
return <DailyBoard />;
|
||||
}
|
||||
+35
-1
@@ -10,7 +10,7 @@ const BUGS_ENABLED_KEY = "pixel-office-bugs-enabled";
|
||||
const BUGS_COUNT_KEY = "pixel-office-bugs-count";
|
||||
const BUGS_MAX = 400;
|
||||
|
||||
type NavIconName = "agents" | "pixelOffice" | "models" | "sessions" | "stats" | "alerts" | "skills";
|
||||
type NavIconName = "agents" | "pixelOffice" | "models" | "sessions" | "stats" | "alerts" | "skills" | "workspace" | "daily";
|
||||
type PixelTone = "base" | "shade" | "light";
|
||||
type PixelRect = { x: number; y: number; w?: number; h?: number; tone?: PixelTone; opacity?: number };
|
||||
type PixelPalette = { base: string; shade: string; light: string };
|
||||
@@ -176,6 +176,38 @@ function NavPixelIcon({ name, active }: { name: NavIconName; active: boolean })
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "workspace":
|
||||
return (
|
||||
<PixelSvg
|
||||
className={baseClass}
|
||||
palette={palette}
|
||||
pixels={[
|
||||
{ x: 2, y: 2, w: 12, h: 1, tone: "light" },
|
||||
{ x: 1, y: 3, w: 14, h: 10, tone: "base" },
|
||||
{ x: 2, y: 13, w: 12, h: 1, tone: "shade" },
|
||||
{ x: 3, y: 5, w: 3, h: 3, tone: "light", opacity: 0.6 },
|
||||
{ x: 8, y: 5, w: 3, h: 3, tone: "light", opacity: 0.6 },
|
||||
{ x: 3, y: 9, w: 3, h: 3, tone: "shade" },
|
||||
{ x: 8, y: 9, w: 3, h: 3, tone: "shade" },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case "daily":
|
||||
return (
|
||||
<PixelSvg
|
||||
className={baseClass}
|
||||
palette={palette}
|
||||
pixels={[
|
||||
{ x: 4, y: 1, w: 8, h: 1, tone: "light" },
|
||||
{ x: 3, y: 2, w: 10, h: 12, tone: "base" },
|
||||
{ x: 4, y: 14, w: 8, h: 1, tone: "shade" },
|
||||
{ x: 4, y: 4, w: 8, h: 1, tone: "light" },
|
||||
{ x: 4, y: 7, w: 8, h: 1, tone: "base" },
|
||||
{ x: 4, y: 10, w: 8, h: 1, tone: "base" },
|
||||
{ x: 6, y: 6, w: 4, h: 1, tone: "light", opacity: 0.7 },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +237,8 @@ const NAV_ITEMS: { group: string; items: { href: string; icon: NavIconName; labe
|
||||
items: [
|
||||
{ href: "/", icon: "agents", labelKey: "nav.agents" },
|
||||
{ href: "/pixel-office", icon: "pixelOffice", labelKey: "nav.pixelOffice" },
|
||||
{ href: "/workspace", icon: "workspace", labelKey: "nav.workspace" },
|
||||
{ href: "/daily", icon: "daily", labelKey: "nav.daily" },
|
||||
{ href: "/models", icon: "models", labelKey: "nav.models" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
// app/workspace/page.tsx
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface AgentState {
|
||||
agentId: string;
|
||||
state: 'working' | 'online' | 'idle' | 'offline';
|
||||
lastActive: number | null;
|
||||
}
|
||||
|
||||
interface Skill {
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const STATE_CONFIG = {
|
||||
working: { label: '工作中', emoji: '🔥', color: 'bg-green-500' },
|
||||
online: { label: '在线', emoji: '🟢', color: 'bg-blue-500' },
|
||||
idle: { label: '空闲', emoji: '😴', color: 'bg-yellow-500' },
|
||||
offline: { label: '离线', emoji: '⚫', color: 'bg-gray-500' },
|
||||
};
|
||||
|
||||
function AgentCard({ agent }: { agent: AgentState }) {
|
||||
const config = STATE_CONFIG[agent.state] || STATE_CONFIG.offline;
|
||||
const lastActiveText = agent.lastActive
|
||||
? new Date(agent.lastActive).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
: '无记录';
|
||||
|
||||
return (
|
||||
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-4 hover:border-[var(--accent)]/50 transition-all">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className={`w-3 h-3 rounded-full ${config.color}`} />
|
||||
<span className="text-2xl">{config.emoji}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-sm truncate">{agent.agentId}</h3>
|
||||
<p className="text-xs text-[var(--text-muted)]">{config.label}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">
|
||||
最后活跃: {lastActiveText}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillCard({ skill }: { skill: Skill }) {
|
||||
return (
|
||||
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] p-3 hover:border-[var(--accent)]/50 transition-all">
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<h4 className="font-medium text-sm truncate">{skill.name}</h4>
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-medium ${
|
||||
skill.status === 'running'
|
||||
? 'bg-green-500/20 text-green-400'
|
||||
: 'bg-gray-500/20 text-gray-400'
|
||||
}`}>
|
||||
{skill.status === 'running' ? '运行中' : '已禁用'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-muted)] line-clamp-2">
|
||||
{skill.description || '无描述'}
|
||||
</p>
|
||||
<p className="text-[10px] text-[var(--accent)] mt-2">
|
||||
v{skill.version}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WorkspacePage() {
|
||||
const [agents, setAgents] = useState<AgentState[]>([]);
|
||||
const [skills, setSkills] = useState<Skill[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastRefresh, setLastRefresh] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [agentRes, skillsRes] = await Promise.all([
|
||||
fetch('/api/agent-status'),
|
||||
fetch('/api/skills'),
|
||||
]);
|
||||
const agentData = await agentRes.json();
|
||||
const skillsData = await skillsRes.json();
|
||||
|
||||
// Handle both old and new skill format
|
||||
if (Array.isArray(skillsData.skills)) {
|
||||
setSkills(skillsData.skills.map((s: any) => ({
|
||||
name: s.name || s.id || '未知',
|
||||
description: s.description || '',
|
||||
version: s.version || 'unknown',
|
||||
status: 'running',
|
||||
})));
|
||||
}
|
||||
|
||||
setAgents(agentData.statuses || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLastRefresh(new Date());
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 15000); // 15秒刷新
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const workingCount = agents.filter(a => a.state === 'working').length;
|
||||
const onlineCount = agents.filter(a => a.state === 'online' || a.state === 'working').length;
|
||||
|
||||
return (
|
||||
<main className="min-h-screen p-4 md:p-8 max-w-7xl mx-auto">
|
||||
{/* 头部 */}
|
||||
<div className="flex flex-col gap-4 mb-8 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">🤖 Agent 工作台</h1>
|
||||
<p className="text-sm text-[var(--text-muted)] mt-1">
|
||||
实时监控所有 Agent 状态 · {lastRefresh.toLocaleTimeString('zh-CN')} 更新
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="px-3 py-1.5 rounded-lg bg-[var(--card)] border border-[var(--border)] text-xs">
|
||||
<span className="text-[var(--text-muted)]">在线:</span>
|
||||
<span className="font-semibold ml-1">{onlineCount}/{agents.length}</span>
|
||||
</div>
|
||||
<div className="px-3 py-1.5 rounded-lg bg-[var(--card)] border border-[var(--border)] text-xs">
|
||||
<span className="text-[var(--text-muted)]">工作中:</span>
|
||||
<span className="font-semibold ml-1 text-green-400">{workingCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent 网格 */}
|
||||
<section className="mb-8">
|
||||
<h2 className="text-lg font-semibold mb-4">📊 Agent 状态</h2>
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-[var(--text-muted)]">加载中...</div>
|
||||
) : agents.length === 0 ? (
|
||||
<div className="text-center py-8 text-[var(--text-muted)] rounded-xl border border-[var(--border)] bg-[var(--card)]">
|
||||
暂无 Agent 数据
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{agents.map((agent) => (
|
||||
<AgentCard key={agent.agentId} agent={agent} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 技能列表 */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold">📦 已安装技能</h2>
|
||||
<span className="text-xs text-[var(--text-muted)]">
|
||||
共 {skills.length} 个技能
|
||||
</span>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-[var(--text-muted)]">加载中...</div>
|
||||
) : skills.length === 0 ? (
|
||||
<div className="text-center py-8 text-[var(--text-muted)] rounded-xl border border-[var(--border)] bg-[var(--card)]">
|
||||
暂无技能
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{skills.map((skill) => (
|
||||
<SkillCard key={skill.name} skill={skill} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// components/agent-card.tsx
|
||||
'use client';
|
||||
|
||||
import { AgentStatus } from '@/lib/agents';
|
||||
|
||||
interface AgentCardProps {
|
||||
agent: AgentStatus;
|
||||
}
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
working: '工作中',
|
||||
idle: '空闲',
|
||||
thinking: '思考中',
|
||||
waiting: '等待指令',
|
||||
};
|
||||
|
||||
const MOOD_EMOJIS: Record<string, string> = {
|
||||
happy: '😊',
|
||||
neutral: '😐',
|
||||
busy: '🔥',
|
||||
tired: '😴',
|
||||
thinking: '🤯',
|
||||
};
|
||||
|
||||
export function AgentCard({ agent }: AgentCardProps) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="text-3xl">{agent.emoji}</span>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">{agent.name}</h3>
|
||||
<span className="text-sm text-gray-500">{agent.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">状态:</span>
|
||||
<span className="text-sm font-medium">
|
||||
{STATUS_LABELS[agent.status] || agent.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">心情:</span>
|
||||
<span className="text-lg">{MOOD_EMOJIS[agent.mood] || '😐'}</span>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-gray-100 dark:border-gray-700">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
当前:{agent.currentTask}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// components/agent-grid.tsx
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AgentCard } from './agent-card';
|
||||
import { AgentStatus } from '@/lib/agents';
|
||||
|
||||
export function AgentGrid() {
|
||||
const [agents, setAgents] = useState<AgentStatus[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchAgents() {
|
||||
try {
|
||||
const res = await fetch('/api/agents');
|
||||
const data = await res.json();
|
||||
setAgents(data.agents || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch agents:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchAgents();
|
||||
// 每 10 秒刷新一次
|
||||
const interval = setInterval(fetchAgents, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center py-8 text-gray-500">加载中...</div>;
|
||||
}
|
||||
|
||||
if (agents.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
暂无 Agent 数据
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{agents.map((agent) => (
|
||||
<AgentCard key={agent.id} agent={agent} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// lib/agents.ts
|
||||
import { readFileSync, existsSync, readdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface AgentStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
emoji: string;
|
||||
status: 'working' | 'idle' | 'thinking' | 'waiting';
|
||||
mood: 'happy' | 'neutral' | 'busy' | 'tired' | 'thinking';
|
||||
currentTask: string;
|
||||
uptime: number;
|
||||
lastActive: string;
|
||||
}
|
||||
|
||||
export function getAgents(): AgentStatus[] {
|
||||
const configPath = join(process.env.HOME || '', '.openclaw/openclaw.json');
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let config: any = {};
|
||||
try {
|
||||
config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 从配置中提取 Agent 信息
|
||||
const agents = Object.entries(config.agents || {}).map(([id, agent]: [string, any]) => ({
|
||||
id,
|
||||
name: agent.name || id,
|
||||
emoji: agent.emoji || '🤖',
|
||||
status: 'idle' as const,
|
||||
mood: 'neutral' as const,
|
||||
currentTask: '等待指令',
|
||||
uptime: 0,
|
||||
lastActive: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
return agents;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// lib/daily.ts
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface DailyItem {
|
||||
time: string;
|
||||
content: string;
|
||||
status: 'completed' | 'in-progress' | 'pending';
|
||||
agent?: string;
|
||||
}
|
||||
|
||||
export function getTodayItems(): DailyItem[] {
|
||||
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
const memoryDir = join(process.env.HOME || '', '.openclaw/workspace/memory');
|
||||
const dailyPath = join(memoryDir, `${today}.md`);
|
||||
|
||||
if (!existsSync(dailyPath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let content = '';
|
||||
try {
|
||||
content = readFileSync(dailyPath, 'utf-8');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: DailyItem[] = [];
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
// 匹配 - [x] 或 - [ ] 格式
|
||||
const match = line.match(/^-\s+\[([ x])\]\s+(.+)/);
|
||||
if (!match) continue;
|
||||
|
||||
const status = match[1] === 'x' ? 'completed' : 'pending';
|
||||
const text = match[2];
|
||||
|
||||
// 提取时间(如果存在)
|
||||
const timeMatch = text.match(/\[([\d: -]+)\]/);
|
||||
const time = timeMatch?.[1] || '';
|
||||
const contentText = text.replace(/\[[\d: -]+\]\s*/, '').trim();
|
||||
|
||||
items.push({
|
||||
time,
|
||||
content: contentText,
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -301,6 +301,8 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
// nav sidebar
|
||||
"nav.overview": "总览",
|
||||
"nav.agents": "机器人",
|
||||
"nav.workspace": "Agent 工作台",
|
||||
"nav.daily": "今日播报",
|
||||
"nav.models": "模型列表",
|
||||
"nav.monitor": "监控",
|
||||
"nav.sessions": "会话列表",
|
||||
@@ -589,6 +591,8 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
// nav sidebar
|
||||
"nav.overview": "Overview",
|
||||
"nav.agents": "Bots",
|
||||
"nav.workspace": "Agent Workspace",
|
||||
"nav.daily": "Daily Report",
|
||||
"nav.models": "Models",
|
||||
"nav.monitor": "Monitor",
|
||||
"nav.sessions": "Sessions",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// lib/skills.ts
|
||||
import { readdirSync, existsSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
export interface SkillInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
version: string;
|
||||
description: string;
|
||||
status: 'running' | 'disabled' | 'error';
|
||||
lastRun: string | null;
|
||||
}
|
||||
|
||||
export function scanSkills(skillsDir: string): SkillInfo[] {
|
||||
if (!existsSync(skillsDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let entries: { name: string; isDirectory: () => boolean }[] = [];
|
||||
try {
|
||||
entries = readdirSync(skillsDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const skills: SkillInfo[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
|
||||
const skillPath = join(skillsDir, entry.name);
|
||||
const skillMdPath = join(skillPath, 'SKILL.md');
|
||||
|
||||
let description = '无描述';
|
||||
let version = 'unknown';
|
||||
|
||||
if (existsSync(skillMdPath)) {
|
||||
try {
|
||||
const content = readFileSync(skillMdPath, 'utf-8');
|
||||
const titleMatch = content.match(/^#\s+(.+)/m);
|
||||
const versionMatch = content.match(/version:\s*(.+)/i);
|
||||
description = titleMatch?.[1] || entry.name;
|
||||
version = versionMatch?.[1] || 'unknown';
|
||||
} catch {
|
||||
description = entry.name;
|
||||
}
|
||||
}
|
||||
|
||||
skills.push({
|
||||
name: entry.name,
|
||||
path: skillPath,
|
||||
version,
|
||||
description,
|
||||
status: 'running',
|
||||
lastRun: null,
|
||||
});
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
Reference in New Issue
Block a user