feat: 增加父进程看护机制与操作来源标识
- 新增 startParentWatchdog 方法,监控宿主进程状态,确保后端在宿主崩溃时自动退出,避免端口占用。 - 在 API 请求中引入 X-MacTFS-Source 请求头,区分来自 MCP(AI)与手动操作的请求,增强操作日志的可追溯性。 - 更新操作日志结构,记录操作来源,支持在前端进行来源过滤展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,12 +1,23 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { CheckCircle2, Loader2, RefreshCw, XCircle } from "lucide-react"
|
||||
import { Bot, CheckCircle2, Loader2, RefreshCw, User, XCircle } from "lucide-react"
|
||||
|
||||
import { Badge } from "~/components/ui/badge"
|
||||
import { Button } from "~/components/ui/button"
|
||||
import { ScrollArea } from "~/components/ui/scroll-area"
|
||||
import { api } from "~/lib/api"
|
||||
import type { OperationLogEntry } from "~/lib/api"
|
||||
import type { OperationLogEntry, OperationSource } from "~/lib/api"
|
||||
import { cn, formatDateTime } from "~/lib/utils"
|
||||
|
||||
// 来源过滤选项:全部 / 仅手动 / 仅 AI。
|
||||
type SourceFilter = "all" | "ui" | "mcp"
|
||||
|
||||
/**
|
||||
* 归一化操作来源:旧记录可能缺省 source,统一按「手动(ui)」处理。
|
||||
*/
|
||||
function normalizeSource(entry: OperationLogEntry): OperationSource {
|
||||
return entry.source === "mcp" ? "mcp" : "ui"
|
||||
}
|
||||
|
||||
/**
|
||||
* 把毫秒耗时格式化为可读文本。
|
||||
*/
|
||||
@@ -33,6 +44,7 @@ export function ConsolePanel({
|
||||
const [logs, setLogs] = useState<OperationLogEntry[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [sourceFilter, setSourceFilter] = useState<SourceFilter>("all")
|
||||
|
||||
/**
|
||||
* 拉取最近操作日志。
|
||||
@@ -53,11 +65,25 @@ export function ConsolePanel({
|
||||
void loadLogs()
|
||||
}, [loadLogs, refreshToken])
|
||||
|
||||
// 最新的操作排在最上面。
|
||||
const ordered = useMemo(
|
||||
() => [...logs].sort((a, b) => b.startedAt - a.startedAt),
|
||||
[logs],
|
||||
)
|
||||
// 各来源计数,供过滤按钮展示数量。
|
||||
const counts = useMemo(() => {
|
||||
let mcp = 0
|
||||
for (const entry of logs) {
|
||||
if (normalizeSource(entry) === "mcp") {
|
||||
mcp += 1
|
||||
}
|
||||
}
|
||||
return { all: logs.length, mcp, ui: logs.length - mcp }
|
||||
}, [logs])
|
||||
|
||||
// 按来源过滤后,最新的操作排在最上面。
|
||||
const ordered = useMemo(() => {
|
||||
const filtered =
|
||||
sourceFilter === "all"
|
||||
? logs
|
||||
: logs.filter((entry) => normalizeSource(entry) === sourceFilter)
|
||||
return [...filtered].sort((a, b) => b.startedAt - a.startedAt)
|
||||
}, [logs, sourceFilter])
|
||||
|
||||
return (
|
||||
<footer className="flex h-full flex-col overflow-hidden rounded-xl border bg-card shadow-card">
|
||||
@@ -71,9 +97,29 @@ export function ConsolePanel({
|
||||
{busyText ?? "正在执行…"}
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-auto text-xs text-muted-foreground">
|
||||
{ordered.length > 0 && `${ordered.length} 条`}
|
||||
</span>
|
||||
|
||||
{/* 来源过滤:全部 / 手动 / AI */}
|
||||
<div className="ml-auto flex items-center gap-0.5 rounded-md border bg-background/60 p-0.5">
|
||||
<SourceFilterButton
|
||||
active={sourceFilter === "all"}
|
||||
label="全部"
|
||||
count={counts.all}
|
||||
onClick={() => setSourceFilter("all")}
|
||||
/>
|
||||
<SourceFilterButton
|
||||
active={sourceFilter === "ui"}
|
||||
label="手动"
|
||||
count={counts.ui}
|
||||
onClick={() => setSourceFilter("ui")}
|
||||
/>
|
||||
<SourceFilterButton
|
||||
active={sourceFilter === "mcp"}
|
||||
label="AI"
|
||||
count={counts.mcp}
|
||||
onClick={() => setSourceFilter("mcp")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
@@ -90,7 +136,13 @@ export function ConsolePanel({
|
||||
<p className="px-3 py-2 text-xs text-destructive">{error}</p>
|
||||
) : ordered.length === 0 ? (
|
||||
<p className="px-3 py-2 text-xs text-muted-foreground">
|
||||
{loading ? "正在加载操作日志…" : "暂无操作日志"}
|
||||
{loading
|
||||
? "正在加载操作日志…"
|
||||
: sourceFilter === "mcp"
|
||||
? "暂无 AI 操作记录"
|
||||
: sourceFilter === "ui"
|
||||
? "暂无手动操作记录"
|
||||
: "暂无操作日志"}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="px-2 py-1">
|
||||
@@ -106,6 +158,7 @@ export function ConsolePanel({
|
||||
) : (
|
||||
<XCircle className="size-3.5 shrink-0 text-destructive" />
|
||||
)}
|
||||
<SourceBadge source={normalizeSource(entry)} />
|
||||
<span className="w-36 shrink-0 truncate font-mono text-muted-foreground">
|
||||
{entry.operation}
|
||||
</span>
|
||||
@@ -129,3 +182,57 @@ export function ConsolePanel({
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 来源过滤按钮:高亮当前选中项,并显示该来源的记录条数。
|
||||
*/
|
||||
function SourceFilterButton({
|
||||
active,
|
||||
label,
|
||||
count,
|
||||
onClick,
|
||||
}: {
|
||||
active: boolean
|
||||
label: string
|
||||
count: number
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"rounded px-1.5 py-0.5 text-[11px] leading-none transition-colors",
|
||||
active
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
<span className={cn("ml-1 tabular-nums", active ? "opacity-80" : "opacity-60")}>{count}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 来源徽章:AI(MCP 调用)用主色高亮,手动(桌面端)用中性色,便于一眼区分。
|
||||
*/
|
||||
function SourceBadge({ source }: { source: OperationSource }) {
|
||||
if (source === "mcp") {
|
||||
return (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-4 shrink-0 gap-0.5 px-1 text-[10px] bg-violet-500/15 text-violet-600 dark:text-violet-300"
|
||||
>
|
||||
<Bot className="size-2.5" />
|
||||
AI
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Badge variant="outline" className="h-4 shrink-0 gap-0.5 px-1 text-[10px] text-muted-foreground">
|
||||
<User className="size-2.5" />
|
||||
手动
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user