feat(agent-infra): 卡D+E——栈确认UI + 看板腐化指示器
卡D:AgentInfraPanel——扫描→确认契约→生成三件套,嵌入 GovernancePanel 卡E:ProjectCard 加腐化评分小圆点(读缓存,不触发扫描) agent-infrastructure 模块全部 5 张卡完成,status→done
This commit is contained in:
parent
0c5730381d
commit
1651b21743
@ -381,8 +381,8 @@ modules:
|
||||
- id: agent-infrastructure
|
||||
name: Agent 友好基础设施生成
|
||||
area: backend
|
||||
status: planned
|
||||
progress: 0
|
||||
status: done
|
||||
progress: 100
|
||||
position: [3750, 0]
|
||||
|
||||
edges:
|
||||
|
||||
@ -105,15 +105,15 @@
|
||||
- files: src-tauri/src/commands/agent_infra.rs, src-tauri/src/db.rs, src/lib/commands.ts
|
||||
- acceptance: `scan_doc_freshness(project_path, project_id)` 用 `silent_cmd("git").current_dir(project_path)` 读取契约路径 vs `docs/ai-context/project-context.yaml` 的最后 commit 时间戳,project-context.yaml 不存在直接返回 RED;结果写入 `doc_freshness_cache` 表(migration 补齐);前端封装就绪
|
||||
|
||||
### 📋 D. 栈确认 UI + 生成按钮
|
||||
- status: todo
|
||||
### ✅ D. 栈确认 UI + 生成按钮
|
||||
- status: done
|
||||
- complexity: M
|
||||
- depends: A, B
|
||||
- files: src/components/dashboard/AgentInfraPanel.tsx, src/components/dashboard/BlueprintModal.tsx
|
||||
- acceptance: BlueprintModal 新增"Agent 基础设施"区块,展示 A 返回的检测结果(可编辑 contract 路径)、"生成三件套"按钮调用 B、展示写入结果(written/skipped/failed);UI 无类型错误
|
||||
|
||||
### 🔵 E. 看板腐化指示器
|
||||
- status: in_progress
|
||||
### ✅ E. 看板腐化指示器
|
||||
- status: done
|
||||
- complexity: S
|
||||
- depends: C
|
||||
- files: src/components/dashboard/ProjectCard.tsx, src/lib/commands.ts
|
||||
|
||||
221
src/components/dashboard/AgentInfraPanel.tsx
Normal file
221
src/components/dashboard/AgentInfraPanel.tsx
Normal file
@ -0,0 +1,221 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
scanAgentInfraStack,
|
||||
generateAgentInfraFiles,
|
||||
type AgentInfraStack,
|
||||
type ContractInput,
|
||||
type FileResult,
|
||||
} from "../../lib/commands";
|
||||
|
||||
const CONFIDENCE_BADGE: Record<string, string> = {
|
||||
high: "bg-green-50 text-green-700 border-green-200",
|
||||
medium: "bg-amber-50 text-amber-700 border-amber-200",
|
||||
low: "bg-gray-50 text-gray-500 border-gray-200",
|
||||
};
|
||||
const CONFIDENCE_LABEL: Record<string, string> = {
|
||||
high: "确认",
|
||||
medium: "推断",
|
||||
low: "弱信号",
|
||||
};
|
||||
|
||||
const ACTION_ICON: Record<string, string> = {
|
||||
written: "✓",
|
||||
updated: "↻",
|
||||
skipped: "○",
|
||||
failed: "✗",
|
||||
};
|
||||
const ACTION_COLOR: Record<string, string> = {
|
||||
written: "text-green-600",
|
||||
updated: "text-blue-600",
|
||||
skipped: "text-gray-400",
|
||||
failed: "text-red-600",
|
||||
};
|
||||
|
||||
export function AgentInfraPanel({
|
||||
projectPath,
|
||||
projectId,
|
||||
}: {
|
||||
projectPath: string;
|
||||
projectId?: string;
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [stack, setStack] = useState<AgentInfraStack | null>(null);
|
||||
const [contracts, setContracts] = useState<ContractInput[]>([]);
|
||||
const [report, setReport] = useState<FileResult[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function handleScan() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setReport(null);
|
||||
try {
|
||||
const result = await scanAgentInfraStack(projectPath);
|
||||
setStack(result);
|
||||
setContracts(
|
||||
result.detected_contracts.map((c) => ({
|
||||
label: c.label,
|
||||
paths: [...c.paths],
|
||||
doc: c.doc,
|
||||
}))
|
||||
);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function removeContract(idx: number) {
|
||||
setContracts((prev) => prev.filter((_, i) => i !== idx));
|
||||
}
|
||||
|
||||
async function handleGenerate() {
|
||||
if (!contracts.length) return;
|
||||
setGenerating(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await generateAgentInfraFiles(projectPath, contracts);
|
||||
setReport(result.files);
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
||||
{/* 标题行 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">
|
||||
Agent 基础设施
|
||||
</p>
|
||||
{stack && (
|
||||
<div className="flex gap-1">
|
||||
{stack.context_yaml_exists ? (
|
||||
<span className="text-[10px] text-green-600 bg-green-50 border border-green-200 px-1.5 py-0.5 rounded-full">
|
||||
yaml ✓
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[10px] text-amber-600 bg-amber-50 border border-amber-200 px-1.5 py-0.5 rounded-full">
|
||||
yaml 未生成
|
||||
</span>
|
||||
)}
|
||||
{stack.meta_rules_present && (
|
||||
<span className="text-[10px] text-blue-600 bg-blue-50 border border-blue-200 px-1.5 py-0.5 rounded-full">
|
||||
元规则 ✓
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-gray-500 leading-relaxed">
|
||||
扫描技术栈,生成三件套:
|
||||
<code className="text-[10px] bg-gray-100 px-1 rounded">project-context.yaml</code>
|
||||
、元规则注入、
|
||||
<code className="text-[10px] bg-gray-100 px-1 rounded">doc-freshness.md</code>
|
||||
</p>
|
||||
|
||||
{/* 扫描按钮 */}
|
||||
{!stack && (
|
||||
<button
|
||||
onClick={handleScan}
|
||||
disabled={loading}
|
||||
className="w-full py-1.5 rounded-lg bg-indigo-600 text-white text-xs hover:bg-indigo-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{loading ? "扫描中…" : "🔍 扫描技术栈"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 扫描结果 + 可编辑契约列表 */}
|
||||
{stack && (
|
||||
<div className="space-y-2">
|
||||
{contracts.length === 0 ? (
|
||||
<p className="text-[11px] text-gray-400 italic">
|
||||
未检测到框架特征,可直接生成空模板
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] text-gray-500 font-semibold">
|
||||
检测到 {contracts.length} 个外部契约(可移除不需要的)
|
||||
</p>
|
||||
{contracts.map((c, idx) => {
|
||||
const original = stack.detected_contracts[idx];
|
||||
const conf = original?.confidence ?? "medium";
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-start gap-1.5 bg-gray-50 rounded p-1.5"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<span className="text-[10px] font-medium text-gray-700">
|
||||
{c.label}
|
||||
</span>
|
||||
<span
|
||||
className={`text-[9px] border px-1 rounded-full ${CONFIDENCE_BADGE[conf] ?? CONFIDENCE_BADGE.medium}`}
|
||||
>
|
||||
{CONFIDENCE_LABEL[conf] ?? conf}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 font-mono mt-0.5 truncate">
|
||||
{c.paths.join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeContract(idx)}
|
||||
className="text-gray-300 hover:text-red-400 text-xs flex-shrink-0 mt-0.5"
|
||||
title="移除此契约"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 重新扫描 + 生成 */}
|
||||
<div className="flex gap-1.5 pt-0.5">
|
||||
<button
|
||||
onClick={() => { setStack(null); setContracts([]); setReport(null); }}
|
||||
className="flex-1 py-1.5 rounded-lg border border-gray-200 text-gray-500 text-xs hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
重新扫描
|
||||
</button>
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={generating || !projectId}
|
||||
title={!projectId ? "需要 projectId" : undefined}
|
||||
className="flex-1 py-1.5 rounded-lg bg-indigo-600 text-white text-xs hover:bg-indigo-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{generating ? "生成中…" : "⚡ 生成三件套"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 生成结果 */}
|
||||
{report && (
|
||||
<div className="space-y-0.5 pt-0.5 border-t border-gray-100">
|
||||
<p className="text-[10px] text-gray-500 font-semibold">生成结果</p>
|
||||
{report.map((f) => (
|
||||
<p key={f.path} className="text-[10px] font-mono flex items-center gap-1">
|
||||
<span className={ACTION_COLOR[f.action] ?? "text-gray-500"}>
|
||||
{ACTION_ICON[f.action] ?? "?"}
|
||||
</span>
|
||||
<span className="text-gray-600 truncate flex-1">{f.path}</span>
|
||||
<span className="text-gray-400 flex-shrink-0">{f.detail}</span>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-[11px] text-red-600 font-mono break-all">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
||||
import { AgentInfraPanel } from "./AgentInfraPanel";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import {
|
||||
@ -1545,6 +1546,9 @@ function GovernancePanel({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Agent 基础设施(接入包 v2)───────────────────────── */}
|
||||
<AgentInfraPanel projectPath={projectPath} projectId={projectId} />
|
||||
|
||||
{/* 项目信息 */}
|
||||
<div className="pt-2 border-t border-gray-100">
|
||||
<p className="text-[10px] text-gray-300 truncate">{projectName}</p>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { getActivity, getSettings, getProjectTools, openEditor, openTerminal, launchTool, gitStatus, gitFetch, gitPull, gitPush, gitUpstreamBehind, githubGetToken, githubGetAccount, gitCreateBranch, gitListBranches, gitCheckoutBranch, fetchActionsRuns, parseRepoPath, getBlueprintStatus, getBuffStatus, type Project, type Space, type ActionsRun, type BlueprintStatus, type BuffStatus } from "../../lib/commands";
|
||||
import { getActivity, getSettings, getProjectTools, openEditor, openTerminal, launchTool, gitStatus, gitFetch, gitPull, gitPush, gitUpstreamBehind, githubGetToken, githubGetAccount, gitCreateBranch, gitListBranches, gitCheckoutBranch, fetchActionsRuns, parseRepoPath, getBlueprintStatus, getBuffStatus, getDocFreshnessCache, type Project, type Space, type ActionsRun, type BlueprintStatus, type BuffStatus, type CachedFreshness } from "../../lib/commands";
|
||||
import { useUIStore } from "../../store/ui";
|
||||
import { PriorityBadge, StatusBadge } from "../ui/Badge";
|
||||
import { ProjectToolsModal } from "./ProjectToolsModal";
|
||||
@ -214,6 +214,13 @@ export function ProjectCard({ project: p, spacesJson }: Props) {
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const { data: freshnessCache } = useQuery<CachedFreshness | null>({
|
||||
queryKey: ["docFreshness", p.id],
|
||||
queryFn: () => getDocFreshnessCache(p.id),
|
||||
staleTime: 60000,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// PR 链接:从 repo_url 解析出 GitHub 路径
|
||||
const prUrl = (() => {
|
||||
if (!p.repo_url || !gitStat) return null;
|
||||
@ -401,6 +408,20 @@ export function ProjectCard({ project: p, spacesJson }: Props) {
|
||||
<span className="text-[10px] text-purple-600">Buff</span>
|
||||
</button>
|
||||
)}
|
||||
{/* 文档腐化评分指示器(仅有缓存时显示) */}
|
||||
{freshnessCache && (
|
||||
<button
|
||||
onClick={() => setShowBlueprint(true)}
|
||||
title={`文档新鲜度:${freshnessCache.score === "green" ? "正常" : freshnessCache.score === "yellow" ? "注意" : "高风险"}\n检查时间:${freshnessCache.checked_at}`}
|
||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded-full border border-gray-200 bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${
|
||||
freshnessCache.score === "green" ? "bg-green-400" :
|
||||
freshnessCache.score === "yellow" ? "bg-amber-400" : "bg-red-500"
|
||||
}`} />
|
||||
<span className="text-[10px] text-gray-500">文档</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user