feat: deploy Codeg multi-agent workspace, Gitea hot/cold storage hub, and grill-me skill
This commit is contained in:
1 parent
c4f54203a4
commit
01a4681f98
18 files changed
+1237
-251
No files matched your search
@@ -8,6 +8,7 @@ import { AgyTerminalModal } from './components/AgyTerminalModal';
|
|||||||
import { AuthGateCard } from './components/AuthGateModal';
|
import { AuthGateCard } from './components/AuthGateModal';
|
||||||
import { WechatClawModal } from './components/WechatClawModal';
|
import { WechatClawModal } from './components/WechatClawModal';
|
||||||
import { StorybookWorkbench } from './components/StorybookWorkbench';
|
import { StorybookWorkbench } from './components/StorybookWorkbench';
|
||||||
|
import { AgentOrchestratorWorkbench } from './components/AgentOrchestratorWorkbench';
|
||||||
import { LAYOUT_PRESETS } from './design-system/tokens';
|
import { LAYOUT_PRESETS } from './design-system/tokens';
|
||||||
import type { ServiceGroup } from './types/telemetry';
|
import type { ServiceGroup } from './types/telemetry';
|
||||||
|
|
||||||
@@ -60,10 +61,42 @@ export const App: React.FC = () => {
|
|||||||
const t = telemetry;
|
const t = telemetry;
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: '核心工作台与私有云存储',
|
title: '核心工作台、多智能体编排与私有代码库',
|
||||||
subtitle: 'Core Workbench & Storage',
|
subtitle: 'AI Agent Orchestration, Code Hub & Storage',
|
||||||
showRouteHint: true,
|
showRouteHint: true,
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
id: 'codeg',
|
||||||
|
title: 'Codeg 多智能体协同工作台',
|
||||||
|
subtitle: 'codeg.961504.xyz · ACP 20+ CLI 底座',
|
||||||
|
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/github-copilot.png',
|
||||||
|
online: t.services.codeg ?? true,
|
||||||
|
desc: '原生聚合 Google Antigravity (agy)、Claude Code、Codex 等 20 种 CLI,支持 4 大分工角色单聊、@点名群聊委派、Git Worktree 待办与无限画布。',
|
||||||
|
edgeUrl: 'https://codeg.961504.xyz',
|
||||||
|
homeUrl: 'https://codeg.home.961504.xyz:10443',
|
||||||
|
lanUrl: 'http://192.168.31.164:3080',
|
||||||
|
stats: [
|
||||||
|
{ label: '预设分工角色', value: '4 角色就绪', highlight: true },
|
||||||
|
{ label: '底座引擎', value: 'Codeg v0.42.0' },
|
||||||
|
{ label: 'SSO 免密', value: 'Proxy Auth 已注入' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gitea',
|
||||||
|
title: 'Gitea 私有代码与冷备中枢',
|
||||||
|
subtitle: 'git.961504.xyz · Mac SSD 热层 + NAS 冷层',
|
||||||
|
iconUrl: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/gitea.png',
|
||||||
|
online: t.services.gitea ?? true,
|
||||||
|
desc: '自托管私有 Git 代码仓库,数据库与活跃索引跑在 M6 Mac mini 本地 SSD,Git LFS 大文件与 .bundle 全量冷备归档于极空间 20TB RAID。',
|
||||||
|
edgeUrl: 'https://git.961504.xyz',
|
||||||
|
homeUrl: 'https://git.home.961504.xyz:10443',
|
||||||
|
lanUrl: 'http://192.168.31.164:3005',
|
||||||
|
stats: [
|
||||||
|
{ label: '主仓库', value: 'admin/homelab', highlight: true },
|
||||||
|
{ label: '热层介质', value: 'M6 NVMe SSD' },
|
||||||
|
{ label: '冷存储池', value: '极空间 20TB NAS' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'agy',
|
id: 'agy',
|
||||||
title: 'AGY 云端工作台',
|
title: 'AGY 云端工作台',
|
||||||
@@ -194,9 +227,11 @@ export const App: React.FC = () => {
|
|||||||
{/* 1. Standard Two-Tier AppShell Header & Telemetry Subheader */}
|
{/* 1. Standard Two-Tier AppShell Header & Telemetry Subheader */}
|
||||||
<TopRibbon />
|
<TopRibbon />
|
||||||
|
|
||||||
{/* 2. View Switch: Dashboard vs Storybook Workbench */}
|
{/* 2. View Switch: Dashboard vs Agent Orchestrator vs Storybook Workbench */}
|
||||||
{viewMode === 'storybook' ? (
|
{viewMode === 'storybook' ? (
|
||||||
<StorybookWorkbench />
|
<StorybookWorkbench />
|
||||||
|
) : viewMode === 'agents' ? (
|
||||||
|
<AgentOrchestratorWorkbench />
|
||||||
) : (
|
) : (
|
||||||
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-7">
|
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-7">
|
||||||
{serviceGroups.map((group) => (
|
{serviceGroups.map((group) => (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface PortalState {
|
|||||||
resolvedTheme: 'light' | 'dark';
|
resolvedTheme: 'light' | 'dark';
|
||||||
routeMode: RouteModeId;
|
routeMode: RouteModeId;
|
||||||
layoutPreset: LayoutPresetId;
|
layoutPreset: LayoutPresetId;
|
||||||
viewMode: 'dashboard' | 'storybook';
|
viewMode: 'dashboard' | 'agents' | 'storybook';
|
||||||
showAgyModal: boolean;
|
showAgyModal: boolean;
|
||||||
showWechatModal: boolean;
|
showWechatModal: boolean;
|
||||||
showCmdPalette: boolean;
|
showCmdPalette: boolean;
|
||||||
@@ -18,7 +18,7 @@ interface PortalState {
|
|||||||
toggleTheme: () => void;
|
toggleTheme: () => void;
|
||||||
setRouteMode: (mode: RouteModeId) => void;
|
setRouteMode: (mode: RouteModeId) => void;
|
||||||
setLayoutPreset: (preset: LayoutPresetId) => void;
|
setLayoutPreset: (preset: LayoutPresetId) => void;
|
||||||
setViewMode: (view: 'dashboard' | 'storybook') => void;
|
setViewMode: (view: 'dashboard' | 'agents' | 'storybook') => void;
|
||||||
setShowAgyModal: (open: boolean) => void;
|
setShowAgyModal: (open: boolean) => void;
|
||||||
setShowWechatModal: (open: boolean) => void;
|
setShowWechatModal: (open: boolean) => void;
|
||||||
setShowCmdPalette: (open: boolean) => void;
|
setShowCmdPalette: (open: boolean) => void;
|
||||||
@@ -55,6 +55,8 @@ const initialLayout =
|
|||||||
const initialView =
|
const initialView =
|
||||||
typeof window !== 'undefined' && window.location.pathname.startsWith('/storybook')
|
typeof window !== 'undefined' && window.location.pathname.startsWith('/storybook')
|
||||||
? 'storybook'
|
? 'storybook'
|
||||||
|
: typeof window !== 'undefined' && window.location.pathname.startsWith('/agents')
|
||||||
|
? 'agents'
|
||||||
: 'dashboard';
|
: 'dashboard';
|
||||||
|
|
||||||
export const usePortalStore = create<PortalState>((set, get) => ({
|
export const usePortalStore = create<PortalState>((set, get) => ({
|
||||||
@@ -135,7 +137,8 @@ export const usePortalStore = create<PortalState>((set, get) => ({
|
|||||||
|
|
||||||
setViewMode: (viewMode) => {
|
setViewMode: (viewMode) => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const nextUrl = viewMode === 'storybook' ? '/storybook' : '/';
|
const nextUrl =
|
||||||
|
viewMode === 'storybook' ? '/storybook' : viewMode === 'agents' ? '/agents' : '/';
|
||||||
window.history.pushState({}, '', nextUrl);
|
window.history.pushState({}, '', nextUrl);
|
||||||
}
|
}
|
||||||
set({ viewMode });
|
set({ viewMode });
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export interface TelemetryData {
|
|||||||
};
|
};
|
||||||
services: {
|
services: {
|
||||||
agy: boolean;
|
agy: boolean;
|
||||||
|
codeg?: boolean;
|
||||||
|
gitea?: boolean;
|
||||||
vaultwarden: boolean;
|
vaultwarden: boolean;
|
||||||
nas: boolean;
|
nas: boolean;
|
||||||
clash: boolean;
|
clash: boolean;
|
||||||
|
|||||||
Reference in new issue
Block a user