fix(production-readiness): 3-batch production readiness cleanup — 12 tasks
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Batch 1 — User-facing fixes: - B1-1: Pipeline verified end-to-end (14 Rust commands, 8 frontend invoke, fully connected) - B1-2: MessageSearch restored to ChatArea with search button in DeerFlow header - B1-3: Viking cleanup — removed 5 orphan invokes (no Rust impl), added addWithMetadata + storeWithSummaries methods + summary generation UI - B1-4: api-fallbacks transparency — added _isFallback markers + console.warn to all 6 fallback functions Batch 2 — System health: - B2-1: Document drift calibration — TRUTH.md/README.md numbers verified and updated - B2-2: @reserved annotations on 15 SaaS handler functions with no frontend callers - B2-3: Scheduled Task Admin V2 — new service + page + route + sidebar navigation - B2-4: TRUTH.md Pipeline/Viking/ScheduledTask records corrected Batch 3 — Long-term quality: - B3-1: hand_run_status/hand_run_list verified as fully implemented (not stubs) - B3-2: Identity snapshot rollback UI added to RightPanel - B3-3: P2 code quality — 4 fixes (TODO comments, fire-and-forget notes, design notes, table name validation), 2 verified N/A, 1 upstream - B3-4: Config PATCH→PUT alignment (admin-v2 config.ts matched to SaaS backend)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { getStoredGatewayUrl } from '../lib/gateway-client';
|
||||
import { useConnectionStore } from '../store/connectionStore';
|
||||
@@ -6,10 +6,12 @@ import { useAgentStore, type PluginStatus } from '../store/agentStore';
|
||||
import { useConfigStore } from '../store/configStore';
|
||||
import { toChatAgent, useChatStore, type CodeBlock } from '../store/chatStore';
|
||||
import { useConversationStore } from '../store/chat/conversationStore';
|
||||
import { intelligenceClient, type IdentitySnapshot } from '../lib/intelligence-client';
|
||||
import {
|
||||
Wifi, WifiOff, Bot, BarChart3, Plug, RefreshCw,
|
||||
MessageSquare, Cpu, FileText, User, Activity, Brain,
|
||||
Shield, Sparkles, List, Network, Dna
|
||||
Shield, Sparkles, List, Network, Dna, History,
|
||||
ChevronDown, ChevronUp, RotateCcw, AlertCircle, Loader2,
|
||||
} from 'lucide-react';
|
||||
|
||||
// === Helper to extract code blocks from markdown content ===
|
||||
@@ -109,6 +111,14 @@ export function RightPanel() {
|
||||
const [isEditingAgent, setIsEditingAgent] = useState(false);
|
||||
const [agentDraft, setAgentDraft] = useState<AgentDraft | null>(null);
|
||||
|
||||
// Identity snapshot state
|
||||
const [snapshots, setSnapshots] = useState<IdentitySnapshot[]>([]);
|
||||
const [snapshotsExpanded, setSnapshotsExpanded] = useState(false);
|
||||
const [snapshotsLoading, setSnapshotsLoading] = useState(false);
|
||||
const [snapshotsError, setSnapshotsError] = useState<string | null>(null);
|
||||
const [restoringSnapshotId, setRestoringSnapshotId] = useState<string | null>(null);
|
||||
const [confirmRestoreId, setConfirmRestoreId] = useState<string | null>(null);
|
||||
|
||||
const connected = connectionState === 'connected';
|
||||
const selectedClone = useMemo(
|
||||
() => clones.find((clone) => clone.id === currentAgent?.id),
|
||||
@@ -170,6 +180,46 @@ export function RightPanel() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadSnapshots = useCallback(async () => {
|
||||
const agentId = currentAgent?.id;
|
||||
if (!agentId) return;
|
||||
setSnapshotsLoading(true);
|
||||
setSnapshotsError(null);
|
||||
try {
|
||||
const result = await intelligenceClient.identity.getSnapshots(agentId, 20);
|
||||
setSnapshots(result);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setSnapshotsError(`加载快照失败: ${msg}`);
|
||||
} finally {
|
||||
setSnapshotsLoading(false);
|
||||
}
|
||||
}, [currentAgent?.id]);
|
||||
|
||||
const handleRestoreSnapshot = useCallback(async (snapshotId: string) => {
|
||||
const agentId = currentAgent?.id;
|
||||
if (!agentId) return;
|
||||
setRestoringSnapshotId(snapshotId);
|
||||
setSnapshotsError(null);
|
||||
setConfirmRestoreId(null);
|
||||
try {
|
||||
await intelligenceClient.identity.restoreSnapshot(agentId, snapshotId);
|
||||
await loadSnapshots();
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setSnapshotsError(`回滚失败: ${msg}`);
|
||||
} finally {
|
||||
setRestoringSnapshotId(null);
|
||||
}
|
||||
}, [currentAgent?.id, loadSnapshots]);
|
||||
|
||||
// Load snapshots when agent tab is active and agent changes
|
||||
useEffect(() => {
|
||||
if (activeTab === 'agent' && currentAgent?.id) {
|
||||
loadSnapshots();
|
||||
}
|
||||
}, [activeTab, currentAgent?.id, loadSnapshots]);
|
||||
|
||||
const userMsgCount = messages.filter(m => m.role === 'user').length;
|
||||
const assistantMsgCount = messages.filter(m => m.role === 'assistant').length;
|
||||
const toolCallCount = messages.filter(m => m.role === 'tool').length;
|
||||
@@ -479,6 +529,118 @@ export function RightPanel() {
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 历史快照 */}
|
||||
<motion.div
|
||||
whileHover={cardHover}
|
||||
transition={defaultTransition}
|
||||
className="rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-sm"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center justify-between mb-0"
|
||||
onClick={() => setSnapshotsExpanded(!snapshotsExpanded)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<History className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">历史快照</span>
|
||||
{snapshots.length > 0 && (
|
||||
<Badge variant="default" className="text-xs">{snapshots.length}</Badge>
|
||||
)}
|
||||
</div>
|
||||
{snapshotsExpanded ? (
|
||||
<ChevronUp className="w-4 h-4 text-gray-400" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4 text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{snapshotsExpanded && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{snapshotsError && (
|
||||
<div className="flex items-center gap-2 p-2 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-xs">
|
||||
<AlertCircle className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span>{snapshotsError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{snapshotsLoading ? (
|
||||
<div className="flex items-center justify-center py-4 text-gray-500 dark:text-gray-400 text-xs">
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
加载中...
|
||||
</div>
|
||||
) : snapshots.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-500 dark:text-gray-400 text-xs bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-100 dark:border-gray-700">
|
||||
暂无快照记录
|
||||
</div>
|
||||
) : (
|
||||
snapshots.map((snap) => {
|
||||
const isRestoring = restoringSnapshotId === snap.id;
|
||||
const isConfirming = confirmRestoreId === snap.id;
|
||||
const timeLabel = formatSnapshotTime(snap.timestamp);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={snap.id}
|
||||
className="flex items-start gap-3 p-3 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700"
|
||||
>
|
||||
<div className="w-7 h-7 rounded-md bg-gray-200 dark:bg-gray-700 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<History className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">{timeLabel}</span>
|
||||
{isConfirming ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setConfirmRestoreId(null)}
|
||||
disabled={isRestoring}
|
||||
className="text-xs px-2 py-0.5 h-auto"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => handleRestoreSnapshot(snap.id)}
|
||||
disabled={isRestoring}
|
||||
className="text-xs px-2 py-0.5 h-auto bg-orange-500 hover:bg-orange-600"
|
||||
>
|
||||
{isRestoring ? (
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
确认回滚
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setConfirmRestoreId(snap.id)}
|
||||
disabled={restoringSnapshotId !== null}
|
||||
className="text-xs text-gray-500 hover:text-orange-600 px-2 py-0.5 h-auto"
|
||||
title="回滚到此版本"
|
||||
>
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
回滚
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mt-1 truncate" title={snap.reason}>
|
||||
{snap.reason || '自动快照'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
) : activeTab === 'files' ? (
|
||||
<div className="p-4">
|
||||
@@ -791,3 +953,15 @@ function AgentToggle({
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function formatSnapshotTime(timestamp: string): string {
|
||||
const now = Date.now();
|
||||
const then = new Date(timestamp).getTime();
|
||||
const diff = now - then;
|
||||
|
||||
if (diff < 60000) return '刚刚';
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`;
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`;
|
||||
if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前`;
|
||||
return new Date(timestamp).toLocaleDateString('zh-CN');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user