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
|
- id: agent-infrastructure
|
||||||
name: Agent 友好基础设施生成
|
name: Agent 友好基础设施生成
|
||||||
area: backend
|
area: backend
|
||||||
status: planned
|
status: done
|
||||||
progress: 0
|
progress: 100
|
||||||
position: [3750, 0]
|
position: [3750, 0]
|
||||||
|
|
||||||
edges:
|
edges:
|
||||||
|
|||||||
@ -105,15 +105,15 @@
|
|||||||
- files: src-tauri/src/commands/agent_infra.rs, src-tauri/src/db.rs, src/lib/commands.ts
|
- 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 补齐);前端封装就绪
|
- 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 + 生成按钮
|
### ✅ D. 栈确认 UI + 生成按钮
|
||||||
- status: todo
|
- status: done
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- depends: A, B
|
- depends: A, B
|
||||||
- files: src/components/dashboard/AgentInfraPanel.tsx, src/components/dashboard/BlueprintModal.tsx
|
- files: src/components/dashboard/AgentInfraPanel.tsx, src/components/dashboard/BlueprintModal.tsx
|
||||||
- acceptance: BlueprintModal 新增"Agent 基础设施"区块,展示 A 返回的检测结果(可编辑 contract 路径)、"生成三件套"按钮调用 B、展示写入结果(written/skipped/failed);UI 无类型错误
|
- acceptance: BlueprintModal 新增"Agent 基础设施"区块,展示 A 返回的检测结果(可编辑 contract 路径)、"生成三件套"按钮调用 B、展示写入结果(written/skipped/failed);UI 无类型错误
|
||||||
|
|
||||||
### 🔵 E. 看板腐化指示器
|
### ✅ E. 看板腐化指示器
|
||||||
- status: in_progress
|
- status: done
|
||||||
- complexity: S
|
- complexity: S
|
||||||
- depends: C
|
- depends: C
|
||||||
- files: src/components/dashboard/ProjectCard.tsx, src/lib/commands.ts
|
- 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 { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
||||||
|
import { AgentInfraPanel } from "./AgentInfraPanel";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import {
|
import {
|
||||||
@ -1545,6 +1546,9 @@ function GovernancePanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Agent 基础设施(接入包 v2)───────────────────────── */}
|
||||||
|
<AgentInfraPanel projectPath={projectPath} projectId={projectId} />
|
||||||
|
|
||||||
{/* 项目信息 */}
|
{/* 项目信息 */}
|
||||||
<div className="pt-2 border-t border-gray-100">
|
<div className="pt-2 border-t border-gray-100">
|
||||||
<p className="text-[10px] text-gray-300 truncate">{projectName}</p>
|
<p className="text-[10px] text-gray-300 truncate">{projectName}</p>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
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 { useUIStore } from "../../store/ui";
|
||||||
import { PriorityBadge, StatusBadge } from "../ui/Badge";
|
import { PriorityBadge, StatusBadge } from "../ui/Badge";
|
||||||
import { ProjectToolsModal } from "./ProjectToolsModal";
|
import { ProjectToolsModal } from "./ProjectToolsModal";
|
||||||
@ -214,6 +214,13 @@ export function ProjectCard({ project: p, spacesJson }: Props) {
|
|||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: freshnessCache } = useQuery<CachedFreshness | null>({
|
||||||
|
queryKey: ["docFreshness", p.id],
|
||||||
|
queryFn: () => getDocFreshnessCache(p.id),
|
||||||
|
staleTime: 60000,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
// PR 链接:从 repo_url 解析出 GitHub 路径
|
// PR 链接:从 repo_url 解析出 GitHub 路径
|
||||||
const prUrl = (() => {
|
const prUrl = (() => {
|
||||||
if (!p.repo_url || !gitStat) return null;
|
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>
|
<span className="text-[10px] text-purple-600">Buff</span>
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user