feat: 增加父进程看护机制与操作来源标识

- 新增 startParentWatchdog 方法,监控宿主进程状态,确保后端在宿主崩溃时自动退出,避免端口占用。
- 在 API 请求中引入 X-MacTFS-Source 请求头,区分来自 MCP(AI)与手动操作的请求,增强操作日志的可追溯性。
- 更新操作日志结构,记录操作来源,支持在前端进行来源过滤展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
fenghp
2026-06-26 14:15:08 +08:00
co-authored by Cursor
parent 3cf42cdbaf
commit 24de23ee61
7 changed files with 210 additions and 15 deletions
+118 -11
View File
@@ -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>
)
}