feat: deploy Codeg multi-agent workspace, Gitea hot/cold storage hub, and grill-me skill

Browse Source
This commit is contained in:
Yin Kou committed 2026-11-07 01:41:54 +08:00
1 parent c4f54203a4
commit 01a4681f98
18 files changed
+1238 -252

No files matched your search

@@ -0,0 +1,531 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
Archive,
Bot,
Copy,
ExternalLink,
FolderGit2,
GitBranch,
HardDrive,
Layers,
MessageSquare,
RefreshCw,
Sparkles,
Terminal,
Users,
} from 'lucide-react';
import { usePortalStore } from '../store/usePortalStore';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Tabs, TabsList, TabsTrigger } from './ui/tabs';
interface AssistantItem {
id: number;
name: string;
description: string;
agent_type: string;
instructions: string;
avatar?: { kind: string; color?: string };
}
interface RepoItem {
name: string;
description: string;
default_branch: string;
updated_at: string;
size_kb: number;
html_url: string;
clone_url: string;
ssh_url: string;
}
interface ColdBundleItem {
name: string;
size_mb: number;
mtime: string;
}
interface OverviewData {
codeg: {
online: boolean;
latency_ms: number;
version: string;
assistants: AssistantItem[];
folders: Array<{ id: number; name: string; path: string }>;
conversations: Array<{ id: number; title: string; agent_type: string; status: string }>;
};
gitea: {
online: boolean;
latency_ms: number;
version: string;
hot_path: string;
cold_path: string;
repos: RepoItem[];
cold_bundles: ColdBundleItem[];
};
tmux_windows: Array<{ index: string; name: string; active: boolean; command: string }>;
}
const COLOR_MAP: Record<string, string> = {
blue: '#3b82f6',
violet: '#8b5cf6',
teal: '#14b8a6',
amber: '#f59e0b',
};
export const AgentOrchestratorWorkbench: React.FC = () => {
const { getEffectiveMode, routeMode, copyCmd, showToast, setShowAgyModal, setShowWechatModal } =
usePortalStore();
const [subTab, setSubTab] = useState<'native' | 'embedded'>('native');
const [data, setData] = useState<OverviewData | null>(null);
const [loading, setLoading] = useState(false);
const [syncingCold, setSyncingCold] = useState(false);
const effectiveMode = useMemo(() => getEffectiveMode(), [getEffectiveMode, routeMode]);
const codegUrl = useMemo(() => {
if (effectiveMode === 'lan') return 'http://192.168.31.164:3080';
if (effectiveMode === 'home10443') return 'https://codeg.home.961504.xyz:10443';
return 'https://codeg.961504.xyz';
}, [effectiveMode]);
const giteaUrl = useMemo(() => {
if (effectiveMode === 'lan') return 'http://192.168.31.164:3005';
if (effectiveMode === 'home10443') return 'https://git.home.961504.xyz:10443';
return 'https://git.961504.xyz';
}, [effectiveMode]);
const fetchOverview = useCallback(async () => {
setLoading(true);
try {
const res = await fetch('/api/agents/overview');
if (res.ok) {
const json = await res.json();
setData(json);
}
} catch {
// ignore transient network errors
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchOverview();
const timer = setInterval(fetchOverview, 6000);
return () => clearInterval(timer);
}, [fetchOverview]);
const handleSyncCold = async () => {
setSyncingCold(true);
showToast('正在将 Gitea 热层仓库与 LFS 大文件归档至极空间 20TB NAS...');
try {
const res = await fetch('/api/action/sync_git_nas', { method: 'POST' });
const d = await res.json();
showToast(d.message || '极空间 Git 冷备同步完成');
await fetchOverview();
} catch {
showToast('冷备同步请求失败');
} finally {
setSyncingCold(false);
}
};
const assistants = data?.codeg.assistants || [];
const repos = data?.gitea.repos || [];
const bundles = data?.gitea.cold_bundles || [];
const tmuxWindows = data?.tmux_windows || [];
return (
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6">
{/* Top Banner: Architecture Philosophy & Quick Switcher */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-2 border-b hl-border">
<div className="space-y-1">
<div className="flex items-center gap-2.5">
<h1 className="text-lg font-semibold tracking-tight flex items-center gap-2">
<Bot className="w-5 h-5" style={{ color: 'var(--accent)' }} />
<span>Multi-Agent 协同编排与私有 Git 中枢</span>
</h1>
<Badge variant="success">Codeg v0.42 + Gitea v28.3</Badge>
</div>
<p className="text-xs" style={{ color: 'var(--text-muted)' }}>
以开源 Codeg (ACP 协议) 为底座、Gitea (Mac SSD 热层 + 极空间 20TB 冷层) 为代码仓库,支持单聊分工角色与{' '}
<code className="font-mono-num px-1 py-0.5 rounded hl-muted">@点名</code> 跨智能体群聊协作。
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<Tabs value={subTab} onValueChange={(v) => setSubTab(v as 'native' | 'embedded')}>
<TabsList>
<TabsTrigger value="native">
<Layers className="w-3.5 h-3.5" />
<span>自研编排 & Git 大盘</span>
</TabsTrigger>
<TabsTrigger value="embedded">
<Sparkles className="w-3.5 h-3.5" />
<span>内嵌 Codeg 完整画布</span>
</TabsTrigger>
</TabsList>
</Tabs>
<Button
variant="outline"
size="sm"
onClick={() => window.open(codegUrl, '_blank', 'noopener,noreferrer')}
>
<ExternalLink className="w-3.5 h-3.5" />
<span>全屏打开 Codeg</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => window.open(giteaUrl, '_blank', 'noopener,noreferrer')}
>
<FolderGit2 className="w-3.5 h-3.5" />
<span>打开 Gitea</span>
</Button>
</div>
</div>
{subTab === 'embedded' ? (
<Card className="overflow-hidden">
<div
className="px-4 py-2.5 border-b hl-border flex items-center justify-between text-xs"
style={{ backgroundColor: 'var(--bg-muted)' }}
>
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: 'var(--success)' }} />
<span className="font-medium">Codeg 原生多智能体工作台 (已注入 SSO 免密凭证)</span>
<span className="font-mono-num" style={{ color: 'var(--text-muted)' }}>
{codegUrl}
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => window.open(codegUrl, '_blank', 'noopener,noreferrer')}
>
<ExternalLink className="w-3.5 h-3.5" />
<span>新标签页全屏</span>
</Button>
</div>
</div>
<iframe
src={codegUrl}
title="Codeg Multi-Agent Workspace"
className="w-full border-0"
style={{ height: 'calc(100vh - 220px)', minHeight: '640px' }}
/>
</Card>
) : (
<div className="space-y-6">
{/* Section 1: 4 Specialist Role Assistants (Single Chat + @Mention Squad) */}
<section className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Users className="w-4 h-4" style={{ color: 'var(--accent)' }} />
<h2 className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--text-muted)' }}>
分工角色会话与 @群聊战队 · Specialist Role Squad (`Google Antigravity` 底座)
</h2>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={fetchOverview} disabled={loading}>
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
<span>刷新状态</span>
</Button>
<Button variant="outline" size="sm" onClick={() => setShowWechatModal(true)}>
<MessageSquare className="w-3.5 h-3.5" />
<span>微信入口绑定</span>
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{assistants.map((a) => {
const badgeColor = COLOR_MAP[a.avatar?.color || 'blue'] || '#3b82f6';
return (
<Card key={a.id} className="p-4 flex flex-col justify-between gap-3">
<div className="space-y-2">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2.5">
<div
className="w-8 h-8 rounded-lg flex items-center justify-center text-white font-semibold text-sm shrink-0"
style={{ backgroundColor: badgeColor }}
>
{a.name.slice(0, 1)}
</div>
<div>
<div className="font-semibold text-sm flex items-center gap-1.5">
<span>@{a.name}</span>
</div>
<div className="text-[11px] font-mono-num" style={{ color: 'var(--text-muted)' }}>
CLI: {a.agent_type === 'antigravity' ? 'Google Antigravity (agy)' : a.agent_type}
</div>
</div>
</div>
<Badge variant="success">就绪</Badge>
</div>
<p className="text-xs leading-relaxed" style={{ color: 'var(--text-muted)' }}>
{a.description}
</p>
<div
className="p-2 rounded-md text-[11px] font-mono-num line-clamp-2 hl-muted"
style={{ color: 'var(--text-subtle)' }}
title={a.instructions}
>
Prompt: {a.instructions}
</div>
</div>
<div className="pt-2 border-t hl-border flex items-center gap-2">
<Button
variant="default"
size="sm"
className="flex-1"
onClick={() => setSubTab('embedded')}
>
<span>发起单聊</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => copyCmd(`@${a.name} `, `群聊点名 @${a.name}`)}
title="复制 @点名 指令,可在主会话或群聊中直接委派该角色"
>
<Copy className="w-3.5 h-3.5" />
<span>@点名</span>
</Button>
</div>
</Card>
);
})}
</div>
</section>
{/* Section 2: Self-Hosted Gitea (Mac SSD Hot Tier + ZSpace 20TB Cold Tier) & Active AGY Sessions */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
{/* Left 2 Columns: Gitea Hot/Cold Tier Architecture */}
<Card className="lg:col-span-2 p-5 space-y-4">
<CardHeader className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 pb-3 border-b hl-border">
<div>
<CardTitle className="flex items-center gap-2">
<FolderGit2 className="w-4 h-4" style={{ color: 'var(--accent)' }} />
<span>私有 Git 代码中枢 (Gitea 热冷分层存储架构)</span>
<Badge variant={data?.gitea.online ? 'success' : 'warning'}>
{data?.gitea.online ? `在线 · ${data.gitea.latency_ms}ms` : '检测中'}
</Badge>
</CardTitle>
<CardDescription className="mt-1">
服务端与活跃索引常驻 M6 Mac mini NVMe SSD,LFS 大文件与全量 <code className="font-mono-num">.bundle</code> 镜像实时归档至极空间 Z4 20TB RAID。
</CardDescription>
</div>
<div className="flex items-center gap-2 shrink-0">
<Button
variant="outline"
size="sm"
onClick={handleSyncCold}
disabled={syncingCold}
>
<Archive className={`w-3.5 h-3.5 ${syncingCold ? 'animate-spin' : ''}`} />
<span>{syncingCold ? '归档中...' : '立即同步至极空间冷存'}</span>
</Button>
<Button
variant="default"
size="sm"
onClick={() => window.open(giteaUrl, '_blank', 'noopener,noreferrer')}
>
<ExternalLink className="w-3.5 h-3.5" />
<span>Gitea 控制台</span>
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Hot vs Cold Tier Storage Boxes */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="p-3 rounded-lg border hl-border hl-muted space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="font-semibold flex items-center gap-1.5">
🔥 Mac mini 热存储层 (NVMe SSD)
</span>
<Badge variant="default">毫秒级读写</Badge>
</div>
<div className="text-[11px] font-mono-num truncate" style={{ color: 'var(--text-muted)' }}>
{data?.gitea.hot_path || '/Users/ixvii0iv/homelab/stacks/git/data'}
</div>
<div className="text-[11px]" style={{ color: 'var(--text-subtle)' }}>
承载 SQLite 元数据、活跃 Git 裸仓库与 Agent Worktree 并发合并
</div>
</div>
<div className="p-3 rounded-lg border hl-border hl-muted space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="font-semibold flex items-center gap-1.5">
<HardDrive className="w-3.5 h-3.5" style={{ color: 'var(--success)' }} />
🧊 极空间 Z4 冷存储层 (20TB RAID)
</span>
<Badge variant="success">{bundles.length} 个全量冷备包</Badge>
</div>
<div className="text-[11px] font-mono-num truncate" style={{ color: 'var(--text-muted)' }}>
{data?.gitea.cold_path || '/Users/ixvii0iv/nas/个人空间/Git_Cold_Storage'}
</div>
<div className="text-[11px]" style={{ color: 'var(--text-subtle)' }}>
承载 Git LFS 大文件、Release 制品包与自包含 .bundle 离线灾备包
</div>
</div>
</div>
{/* Hosted Repositories List */}
<div className="space-y-2">
<div className="text-xs font-medium" style={{ color: 'var(--text-muted)' }}>
当前托管仓库 ({repos.length})
</div>
{repos.map((r) => (
<div
key={r.name}
className="p-3 rounded-lg border hl-border flex flex-col sm:flex-row sm:items-center justify-between gap-3"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<GitBranch className="w-3.5 h-3.5" style={{ color: 'var(--accent)' }} />
<span className="font-semibold text-sm font-mono-num">{r.name}</span>
<Badge variant="default">{r.default_branch}</Badge>
{bundles.some((b) => b.name.includes(r.name.replace('/', '_'))) && (
<Badge variant="success">已冷备至 NAS</Badge>
)}
</div>
<p className="text-xs" style={{ color: 'var(--text-muted)' }}>
{r.description || 'HomeLab 核心基础设施与前端工程仓库'}
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<Button
variant="outline"
size="sm"
onClick={() =>
copyCmd(`git clone ${giteaUrl}/${r.name}.git`, `克隆 ${r.name}`)
}
>
<Copy className="w-3.5 h-3.5" />
<span>复制 Clone</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() =>
window.open(`${giteaUrl}/${r.name}`, '_blank', 'noopener,noreferrer')
}
>
<ExternalLink className="w-3.5 h-3.5" />
<span>查看代码</span>
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Right Column: Active AGY Tmux & Workspace Sessions */}
<Card className="p-5 flex flex-col justify-between space-y-4">
<div className="space-y-4">
<CardHeader className="pb-3 border-b hl-border">
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Terminal className="w-4 h-4" style={{ color: 'var(--accent)' }} />
<span>AGY 原生常驻会话流</span>
</span>
<Badge variant="success">{tmuxWindows.length || 1} 窗口活跃</Badge>
</CardTitle>
<CardDescription className="mt-1">
M6 Mac mini 本机 <code className="font-mono-num">tmux (agy)</code> 与 Codeg 工作区实时状态。
</CardDescription>
</CardHeader>
<div className="space-y-2.5">
<div className="text-xs font-medium" style={{ color: 'var(--text-muted)' }}>
已挂载 Codeg 工作区目录
</div>
{(data?.codeg.folders || []).map((f) => (
<div
key={f.id}
className="p-2.5 rounded-lg border hl-border hl-muted flex items-center justify-between text-xs font-mono-num"
>
<span className="truncate">{f.path}</span>
<Badge variant="success">已绑定</Badge>
</div>
))}
<div className="text-xs font-medium pt-2" style={{ color: 'var(--text-muted)' }}>
Mac mini Tmux 会话窗格
</div>
{tmuxWindows.length > 0 ? (
tmuxWindows.map((w) => (
<div
key={w.index}
className="p-2.5 rounded-lg border hl-border flex items-center justify-between text-xs"
>
<div className="flex items-center gap-2 font-mono-num">
<span
className="w-2 h-2 rounded-full"
style={{
backgroundColor: w.active ? 'var(--success)' : 'var(--text-muted)',
}}
/>
<span className="font-medium">
#{w.index} {w.name}
</span>
</div>
<Badge variant="default">{w.command}</Badge>
</div>
))
) : (
<div className="p-2.5 rounded-lg border hl-border text-xs font-mono-num">
#0 agy (Google Antigravity 常驻守护)
</div>
)}
{bundles.length > 0 && (
<div className="pt-2 space-y-1.5">
<div className="text-xs font-medium" style={{ color: 'var(--text-muted)' }}>
最近极空间 NAS 冷备快照
</div>
{bundles.map((b) => (
<div
key={b.name}
className="p-2 rounded-md hl-muted flex items-center justify-between text-[11px] font-mono-num"
>
<span className="truncate">{b.name}</span>
<span style={{ color: 'var(--success)' }}>
{b.size_mb} MB · {b.mtime.slice(5, 16)}
</span>
</div>
))}
</div>
)}
</div>
</div>
<div className="pt-3 border-t hl-border flex items-center gap-2">
<Button className="flex-1" onClick={() => setShowAgyModal(true)}>
<Terminal className="w-3.5 h-3.5" />
<span>唤起 AGY 终端</span>
</Button>
<Button variant="outline" onClick={() => setSubTab('embedded')}>
<Sparkles className="w-3.5 h-3.5" />
<span>进入画布</span>
</Button>
</div>
</Card>
</div>
</div>
)}
</main>
);
};
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { BookOpen, LayoutGrid, MessageSquare, Moon, Search, Sun, Terminal } from 'lucide-react'; import { BookOpen, Bot, LayoutGrid, MessageSquare, Moon, Search, Sun, Terminal } from 'lucide-react';
import { usePortalStore } from '../store/usePortalStore'; import { usePortalStore } from '../store/usePortalStore';
import type { RouteModeId } from '../types/telemetry'; import type { RouteModeId } from '../types/telemetry';
import { Button } from './ui/button'; import { Button } from './ui/button';
@@ -64,13 +64,17 @@ export const TopRibbon: React.FC = () => {
{/* View Switcher Tabs via @radix-ui/react-tabs */} {/* View Switcher Tabs via @radix-ui/react-tabs */}
<Tabs <Tabs
value={viewMode} value={viewMode}
onValueChange={(v) => setViewMode(v as 'dashboard' | 'storybook')} onValueChange={(v) => setViewMode(v as 'dashboard' | 'agents' | 'storybook')}
> >
<TabsList> <TabsList>
<TabsTrigger value="dashboard"> <TabsTrigger value="dashboard">
<LayoutGrid className="w-3.5 h-3.5" /> <LayoutGrid className="w-3.5 h-3.5" />
<span>中控台</span> <span>中控台</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="agents">
<Bot className="w-3.5 h-3.5" />
<span>Agent 编排</span>
</TabsTrigger>
<TabsTrigger value="storybook"> <TabsTrigger value="storybook">
<BookOpen className="w-3.5 h-3.5" /> <BookOpen className="w-3.5 h-3.5" />
<span>Storybook 设计规范</span> <span>Storybook 设计规范</span>