'use client'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { AlertCircle, BarChart3, CheckCircle2, ChevronDown, ClipboardList, Download, FileCode2, FileText, GitCompare, Info, ListChecks, Loader2, Puzzle, RefreshCw, Rocket, Send, ShieldCheck, ShieldAlert, Trash2, Upload, X, XCircle, } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { deleteSkill, disablePublishedSkill, downloadSkill, getSkillDetail, getSkillFile, getSkillVersion, listSkillCandidates, listSkillDrafts, listSkills, publishSkillDraft, recheckSkillDraftSafety, regenerateSkillDraft, rejectSkillDraft, rollbackPublishedSkill, submitSkillDraft, synthesizeSkillDraft, uploadSkill, } from '@/lib/api'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { SkillDetailView } from '@/components/skills/SkillDetailView'; import type { Skill, SkillDetailResponse, SkillDraft, SkillDraftEvalReport, SkillDraftSafetyReport, SkillFileContent, SkillLearningCandidate, } from '@/types'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; import { containedJsonTextClass, containedLongTextClass } from '@/lib/text-wrapping'; const TERMINAL_DRAFT_STATUSES = new Set(['rejected', 'published', 'disabled', 'archived']); const REJECTABLE_DRAFT_STATUSES = new Set(['draft', 'in_review', 'approved']); type SkillsTab = 'published' | 'candidates' | 'drafts'; function normalizeSkillsTab(value: string | null | undefined): SkillsTab { if (value === 'candidates' || value === 'drafts') { return value; } return 'published'; } export default function SkillsPage() { const { locale } = useAppI18n(); const pathname = usePathname(); const router = useRouter(); const searchParams = useSearchParams(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); const [skills, setSkills] = useState([]); const [candidates, setCandidates] = useState([]); const [drafts, setDrafts] = useState([]); const [activeTab, setActiveTab] = useState(() => normalizeSkillsTab(searchParams?.get('tab'))); const [loading, setLoading] = useState(true); const [actionId, setActionId] = useState(null); const [error, setError] = useState(null); const [showUpload, setShowUpload] = useState(false); const [ignoredCandidates, setIgnoredCandidates] = useState>(new Set()); const [selectedSkillName, setSelectedSkillName] = useState(null); const [skillDetail, setSkillDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [versionLoading, setVersionLoading] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [fileLoading, setFileLoading] = useState(false); const load = useCallback(async () => { setLoading(true); setError(null); try { const [skillData, candidateData, draftData] = await Promise.all([ listSkills(), listSkillCandidates().catch(() => []), listSkillDrafts().catch(() => []), ]); setSkills(Array.isArray(skillData) ? skillData : []); setCandidates(Array.isArray(candidateData) ? candidateData : []); setDrafts(Array.isArray(draftData) ? draftData : []); } catch (err: any) { setError(err.message || pickAppText(locale, '加载技能失败', 'Failed to load skills')); } finally { setLoading(false); } }, [locale]); useEffect(() => { void load(); }, [load]); useEffect(() => { setActiveTab(normalizeSkillsTab(searchParams?.get('tab'))); }, [searchParams]); const changeTab = (value: string) => { const nextTab = normalizeSkillsTab(value); setActiveTab(nextTab); const nextParams = new URLSearchParams(searchParams?.toString()); if (nextTab === 'published') { nextParams.delete('tab'); } else { nextParams.set('tab', nextTab); } const query = nextParams.toString(); router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false }); }; const runAction = async (id: string, action: () => Promise) => { setActionId(id); setError(null); try { await action(); await load(); } catch (err: any) { setError(err.message || t('操作失败', 'Action failed')); } finally { setActionId(null); } }; const openSkillDetail = async (name: string) => { setSelectedSkillName(name); setSkillDetail(null); setSelectedFile(null); setDetailLoading(true); setError(null); try { setSkillDetail(await getSkillDetail(name)); } catch (err: any) { setError(err.message || t('加载技能详情失败', 'Failed to load skill details')); setSelectedSkillName(null); } finally { setDetailLoading(false); } }; const openSkillVersion = async (version: string) => { if (!selectedSkillName || skillDetail?.currentVersion === version) return; setVersionLoading(true); setSelectedFile(null); setError(null); try { setSkillDetail(await getSkillVersion(selectedSkillName, version)); } catch (err: any) { setError(err.message || t('加载技能版本失败', 'Failed to load skill version')); } finally { setVersionLoading(false); } }; const openSkillFile = async (filePath: string) => { if (!selectedSkillName || !skillDetail) return; setFileLoading(true); setError(null); try { setSelectedFile(await getSkillFile(selectedSkillName, skillDetail.currentVersion, filePath)); } catch (err: any) { setError(err.message || t('加载文件失败', 'Failed to load file')); } finally { setFileLoading(false); } }; const hiddenCandidateStatuses = new Set(['draft_ready', 'in_review', 'approved', 'rejected', 'superseded', 'published']); const visibleCandidates = candidates.filter( (candidate) => !ignoredCandidates.has(candidate.candidate_id) && !hiddenCandidateStatuses.has(candidate.status) && !candidate.draft_id ); const visibleDrafts = drafts.filter((draft) => !TERMINAL_DRAFT_STATUSES.has(draft.status)); if (loading) { return (
); } return (

{t('技能', 'Skills')}

{error && (
{error}
)} {showUpload && ( { setShowUpload(false); void load(); }} onCancel={() => setShowUpload(false)} onError={(msg) => setError(msg)} /> )} {selectedSkillName && (
{detailLoading || !skillDetail ? ( ) : ( void openSkillVersion(version)} onOpenFile={(filePath) => void openSkillFile(filePath)} badges={ <> {skillDetail.skill.source === 'builtin' ? t('内置', 'Built in') : t('工作区', 'Workspace')} {skillDetail.skill.available ? t('可用', 'Available') : t('不可用', 'Unavailable')} } actions={
{skillDetail.skill.source === 'workspace' && ( <> )}
} labels={{ overview: t('说明', 'Overview'), files: t('文件', 'Files'), versions: t('版本', 'Versions'), noReadme: t('暂无说明', 'No overview available'), noFiles: t('暂无文件', 'No files'), selectFile: t('选择一个文件查看详情', 'Select a file to view details'), binaryFile: t('二进制文件暂不预览', 'Binary file preview is not available'), current: t('当前', 'Current'), size: t('大小', 'Size'), }} /> )}
)} {!selectedSkillName && ( {t('已发布', 'Published')} {t('候选', 'Candidates')} {t('草稿评审', 'Draft review')} void openSkillDetail(name)} onDownload={(name) => downloadSkill(name).catch((err) => setError(err.message))} onDelete={(name) => void runAction(`delete:${name}`, () => deleteSkill(name))} onDisable={(name) => runAction(`disable:${name}`, () => disablePublishedSkill(name, t('人工禁用', 'Manual disable'))) } onRollback={onRollbackSkill} /> {t('学习候选', 'Learning candidates')} {visibleCandidates.length === 0 ? ( } text={t('暂无学习候选', 'No learning candidates yet')} /> ) : (
{visibleCandidates.map((candidate) => ( setIgnoredCandidates((prev) => new Set(prev).add(candidate.candidate_id))} onSynthesize={() => runAction(`draft:${candidate.candidate_id}`, () => synthesizeSkillDraft(candidate.candidate_id) ) } onRegenerate={() => runAction(`regen:${candidate.candidate_id}`, () => regenerateSkillDraft(candidate.candidate_id) ) } /> ))}
)}
{t('草稿、评审与发布', 'Drafts, review, and publish')} {visibleDrafts.length === 0 ? ( } text={t('暂无草稿', 'No drafts yet')} /> ) : (
{visibleDrafts.map((draft) => ( runAction(`submit:${draft.draft_id}`, () => submitSkillDraft(draft.skill_name, draft.draft_id) ) } onReject={() => runAction(`reject:${draft.draft_id}`, () => rejectSkillDraft(draft.skill_name, draft.draft_id) ) } onRecheckSafety={() => runAction(`safety:${draft.draft_id}`, () => recheckSkillDraftSafety(draft.skill_name, draft.draft_id) ) } onPublish={(confirmHighRisk) => runAction(`publish:${draft.draft_id}`, () => publishSkillDraft(draft.skill_name, draft.draft_id, '', confirmHighRisk) ) } /> ))}
)}
)}
); function onRollbackSkill(name: string) { const target = window.prompt(t('回滚到版本,例如 v0001', 'Rollback target version, for example v0001')); if (target) { void runAction(`rollback:${name}`, () => rollbackPublishedSkill(name, target, t('人工回滚', 'Manual rollback')) ).then(() => { if (selectedSkillName === name) { void openSkillDetail(name); } }); } } } function PublishedSkillsTable({ skills, onOpen, onDownload, onDelete, onDisable, onRollback, }: { skills: Skill[]; onOpen: (name: string) => void; onDownload: (name: string) => void; onDelete: (name: string) => void; onDisable: (name: string) => void; onRollback: (name: string) => void; }) { const { locale } = useAppI18n(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); return ( {skills.length === 0 ? ( } text={t('暂无技能', 'No skills yet')} /> ) : ( <>
{skills.map((skill) => (
{skill.source === 'builtin' ? t('内置', 'Built in') : t('工作区', 'Workspace')} {skill.available ? t('可用', 'Available') : t('不可用', 'Unavailable')}
{skill.source === 'workspace' && ( <> )}
))}
{t('名称', 'Name')} {t('描述', 'Description')} {t('来源', 'Source')} {t('状态', 'Status')} {t('操作', 'Actions')} {skills.map((skill) => ( onOpen(skill.name)} > {skill.name} {skill.description} {skill.source === 'builtin' ? t('内置', 'Built in') : t('工作区', 'Workspace')} {skill.available ? t('可用', 'Available') : t('不可用', 'Unavailable')}
{skill.source === 'workspace' && ( <> )}
))}
)}
); } function CandidateCard({ candidate, actionId, onIgnore, onSynthesize, onRegenerate, }: { candidate: SkillLearningCandidate; actionId: string | null; onIgnore: () => void; onSynthesize: () => Promise; onRegenerate: () => Promise; }) { const { locale } = useAppI18n(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); const evidence = candidate.evidence || {}; const title = candidateTitle(candidate, t); const affectedSkills = candidate.draft_skill_name ? [candidate.draft_skill_name] : candidate.related_skill_names.length > 0 ? candidate.related_skill_names : []; const sourceRuns = candidate.source_run_ids || []; const sourceSessions = candidate.source_session_ids || []; const risk = candidate.risk_level || 'medium'; const confidence = typeof candidate.confidence === 'number' && candidate.confidence > 0 ? `${Math.round(candidate.confidence * 100)}%` : null; return (
{candidateKindLabel(candidate.kind, t)} {candidateStatusLabel(candidate.status, t)} {t('风险', 'Risk')}: {riskLabel(risk, t)} {confidence && {t('置信度', 'Confidence')}: {confidence}} {typeof candidate.priority === 'number' && candidate.priority > 0 && ( {t('优先级', 'Priority')}: {candidate.priority} )}

{title}

{candidate.reason || t('没有提供候选理由。', 'No candidate reason was provided.')}

} label={t('候选任务', 'Candidate task')} value={candidateTaskSummary(candidate, t)} /> } label={t('影响范围', 'Impact')} value={affectedSkills.length > 0 ? affectedSkills.join(', ') : t('新增技能', 'New skill')} /> } label={t('证据数量', 'Evidence')} value={t( `${sourceRuns.length} 个运行,${sourceSessions.length} 个会话`, `${sourceRuns.length} run(s), ${sourceSessions.length} session(s)` )} />
{(candidate.evidence_summary || candidate.trigger_reason || candidate.last_error) && (
{candidate.evidence_summary && (

{t('证据摘要', 'Evidence summary')}:{' '} {candidate.evidence_summary}

)} {candidate.trigger_reason && (

{t('触发原因', 'Trigger')}:{' '} {triggerReasonLabel(candidate.trigger_reason, t)}

)} {candidate.last_error && (

{t('最近错误', 'Last error')}: {candidate.last_error}

)}
)}
{candidate.candidate_id} {String(evidence.task_id || '') && {t('任务', 'Task')}: {String(evidence.task_id)}} {String(evidence.skill_version || '') && {t('基线版本', 'Base version')}: {String(evidence.skill_version)}} {candidate.created_at && {t('创建于', 'Created')}: {formatDateTime(candidate.created_at)}}
{candidate.draft_id && ( )}
); } function DraftCard({ draft, actionId, onSubmit, onReject, onRecheckSafety, onPublish, }: { draft: SkillDraft; actionId: string | null; onSubmit: () => Promise; onReject: () => Promise; onRecheckSafety: () => Promise; onPublish: (confirmHighRisk: boolean) => Promise; }) { const { locale } = useAppI18n(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); const busy = Boolean(actionId); const safety = draft.safety_report; const evalReport = draft.eval_report; const frontmatter = draft.proposed_frontmatter || {}; const description = String(frontmatter.description || '').trim(); const toolHints = normalizeStringList(frontmatter.tools); const submittedForReview = draft.status === 'in_review' || draft.status === 'approved'; const isRevision = draft.proposal_kind === 'revise_skill' && Boolean(draft.base_skill); const publishBlocked = !submittedForReview || !safety || safety.risk_level === 'critical' || (evalReport?.status !== 'skipped_provider_unavailable' && evalReport?.passed === false); const isHighRisk = safety?.risk_level === 'high'; const highRiskReason = [ ...(safety?.blocked_reasons ?? []), ...(safety?.issues ?? []), safety?.suggested_fix, ].filter(Boolean).join('\n'); const safetyBlocksReview = Boolean(safety && (!safety.passed || safety.risk_level === 'critical')); const submitBlocked = draft.status !== 'draft' || safetyBlocksReview; const rejectBlocked = !REJECTABLE_DRAFT_STATUSES.has(draft.status); const canPublishLabel = publishBlocked ? publishBlockReason(draft, t) : isHighRisk ? t('高风险草稿,发布前需要再次确认。', 'High-risk draft; publishing requires confirmation.') : t('已满足发布门禁。', 'Publish gates are satisfied.'); const handlePublish = () => { if (isHighRisk) { const confirmed = window.confirm( t('该草稿被标记为高风险。确认发布?', 'This draft is marked high risk. Publish anyway?') ); if (!confirmed) return; } void onPublish(isHighRisk); }; return (
{candidateKindLabel(draft.proposal_kind, t)} {draftStatusLabel(draft.status, t)} {safety && ( {t('安全', 'Safety')}: {safety.passed ? t('通过', 'Pass') : t('阻断', 'Blocked')} · {riskLabel(safety.risk_level, t)} )} {evalReport && ( {evalReport.status === 'skipped_provider_unavailable' ? t('评估跳过', 'Eval skipped') : evalReport.passed ? t('评估通过', 'Eval passed') : t('评估失败', 'Eval failed')} )}
{t('技能名', 'Skill name')}

{draft.skill_name}

{draft.reason || description || t('没有提供草稿说明。', 'No draft notes were provided.')}

{draft.proposal_kind === 'revise_skill' && draft.base_version && (
{draft.skill_name}: {draft.base_version} → {draft.target_version || t('下一版本', 'Next version')}
)}
} label={t('草稿内容', 'Draft content')} value={description || t('未填写技能描述', 'No skill description')} /> } label={t('基线版本', 'Base version')} value={draft.base_version || t('新增技能,无基线', 'New skill, no base')} /> } label={t('目标版本', 'Target version')} value={draft.target_version || '-'} /> } label={t('来源', 'Source')} value={draft.trigger_run_id || draft.trigger_session_id || draft.created_by || '-'} />
{isHighRisk && (
{t('高风险理由来自 safety report', 'High-risk reason from safety report')}
{highRiskReason || t('未提供具体理由', 'No concrete reason provided')}
)}
{draft.skill_name}/{draft.draft_id} {t('创建者', 'Author')}: {draft.created_by} {t('创建于', 'Created')}: {formatDateTime(draft.created_at)}
{isRevision ? t('修改对比', 'Revision comparison') : t('拟发布的技能正文', 'Proposed skill body')}
{toolHints.length > 0 && (
{toolHints.map((tool) => ( {tool} ))}
)}
{isRevision && draft.base_skill ? ( ) : draft.proposed_content.trim() ? ( ) : (

{t('草稿没有正文内容。', 'This draft has no body content.')}

)}
); } function SafetyReportPanel({ report }: { report?: SkillDraftSafetyReport | null }) { const { locale } = useAppI18n(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); if (!report) { return ( } title={t('安全报告', 'Safety report')} empty={t('还没有安全报告,不能送审或发布。', 'No safety report yet; review and publishing are blocked.')} /> ); } const problems = [...(report.blocked_reasons || []), ...(report.issues || [])]; return (
{report.passed ? : } {t('安全报告', 'Safety report')}
{report.passed ? t('通过', 'Pass') : t('阻断', 'Blocked')} · {riskLabel(report.risk_level, t)}
{problems.length > 0 ? (

{t('发现的问题', 'Findings')}

    {problems.map((item, index) => (
  • {item}
  • ))}
) : (

{t('没有发现阻断项或需要人工注意的问题。', 'No blockers or manual-review issues were found.')}

)} {report.suggested_fix && (

{t('建议处理', 'Suggested fix')}: {report.suggested_fix}

)}
{formatDateTime(report.created_at)}
); } function RevisionComparison({ baseVersion, targetVersion, baseContent, proposedContent, }: { baseVersion: string; targetVersion: string; baseContent: string; proposedContent: string; }) { const { locale } = useAppI18n(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); const diff = lineDiffSummary(baseContent, proposedContent); return (
{baseVersion} {targetVersion} {t('新增', 'Added')}: {diff.added} {t('删除', 'Removed')}: {diff.removed} {t('修改', 'Changed')}: {diff.changed}
); } function DiffPane({ title, content }: { title: string; content: string }) { return (
{title}
        {content.trim() || '-'}
      
); } function lineDiffSummary(baseContent: string, proposedContent: string): { added: number; removed: number; changed: number } { const baseLines = baseContent.split(/\r?\n/); const proposedLines = proposedContent.split(/\r?\n/); const maxLength = Math.max(baseLines.length, proposedLines.length); let added = 0; let removed = 0; let changed = 0; for (let index = 0; index < maxLength; index += 1) { const baseLine = baseLines[index]; const proposedLine = proposedLines[index]; if (baseLine === proposedLine) continue; if (baseLine === undefined) { added += 1; } else if (proposedLine === undefined) { removed += 1; } else { changed += 1; } } return { added, removed, changed }; } function EvalReportPanel({ report }: { report?: SkillDraftEvalReport | null }) { const { locale } = useAppI18n(); const t = (zh: string, en: string) => pickAppText(locale, zh, en); if (!report) { return ( } title={t('评估报告', 'Eval report')} empty={t('还没有评估报告。若 provider 不可用,系统会标记为跳过。', 'No eval report yet. If the provider is unavailable, it will be marked as skipped.')} /> ); } if (report.status === 'skipped_provider_unavailable') { return (
{t('评估报告', 'Eval report')}

{t('评估被跳过:当前没有可用 provider。这个状态不会阻断发布,但代表没有回放验证。', 'Eval was skipped because no provider was available. This does not block publishing, but no replay validation was run.')}

); } const abilitySummary = report.ability_score_summary || {}; const toolExecutionSummary = report.tool_execution_summary || report.tool_mode_summary || {}; const caseSelectionSummary = report.case_selection_summary || {}; const realScore = report.real_score_avg ?? abilitySummary.real_score_avg; const syntheticScore = report.synthetic_score_avg ?? abilitySummary.synthetic_score_avg; const overallScore = report.overall_score_avg ?? abilitySummary.overall_score_avg ?? report.candidate_score_avg; const realCaseCount = toNumber(abilitySummary.real_case_count); const syntheticCaseCount = toNumber(abilitySummary.synthetic_case_count); const excludedSynthetic = toNumber(caseSelectionSummary.excluded_synthetic_without_validator); return (
{t('评估报告', 'Eval report')}
{report.passed ? t('通过', 'Pass') : t('失败', 'Failed')}
= 0 ? '+' : ''}${formatScore(report.score_delta)}`} tone={report.score_delta < 0 ? 'bad' : report.score_delta > 0 ? 'good' : 'neutral'} />
} label={t('改进', 'Improved')} value={String(report.improved_count)} /> } label={t('回退', 'Regressed')} value={String(report.regression_count)} /> } label={t('不变', 'Unchanged')} value={String(report.unchanged_count)} />
} label={t('真实案例', 'Real cases')} value={String(realCaseCount)} /> } label={t('模拟案例', 'Synthetic cases')} value={String(syntheticCaseCount)} /> } label={t('无验证器已排除', 'No-validator excluded')} value={String(excludedSynthetic)} />
{report.cases.length > 0 && (
{t('回放案例', 'Replay cases')}
{report.cases.map((item, index) => (
{String(item.run_id || '-')}
{String(item.synthetic) === 'true' ? t('模拟案例', 'Synthetic case') : t('真实案例', 'Real case')} {item.tier ? ` · ${String(item.tier)}` : ''}
))}
{report.cases.map((item, index) => ( ))}
{t('运行', 'Run')} {t('来源', 'Source')} {t('基线', 'Baseline')} {t('候选', 'Candidate')} {t('变化', 'Delta')}
{String(item.run_id || '-')} {String(item.synthetic) === 'true' ? t('模拟', 'Synthetic') : t('真实', 'Real')} {item.tier ? ` · ${String(item.tier)}` : ''} {formatScore(toNumber(item.baseline_score))} {formatScore(toNumber(item.candidate_score))} {formatSignedScore(toNumber(item.delta))}
)} {Array.isArray(report.case_reports) && report.case_reports.length > 0 ? ( ) : null} {Object.keys(abilitySummary).length > 0 ? ( ) : null} {Object.keys(toolExecutionSummary).length > 0 ? ( ) : null} {report.preservation_report ? ( ) : null}
{formatDateTime(report.created_at)}
); } function GateSummary({ title, summary, items, }: { title: string; summary: string; items: Array<{ label: string; ok: boolean }>; }) { return (
{title}

{summary}

{items.map((item) => (
{item.ok ? : } {item.label}
))}
); } function ReadablePanel({ icon, title, empty, }: { icon: React.ReactNode; title: string; empty: string; }) { return (
{icon} {title}

{empty}

); } function ReadableFact({ icon, label, value, }: { icon: React.ReactNode; label: string; value: string; }) { return (
{icon} {label}
{value || '-'}
); } function MetricTile({ label, value, tone = 'neutral', }: { label: string; value: string; tone?: 'neutral' | 'good' | 'bad'; }) { const toneClass = tone === 'bad' ? 'text-destructive' : 'text-foreground'; return (
{label}
{value}
); } function RawDetails({ title, payload }: { title: string; payload: unknown }) { return (
{title}
        {JSON.stringify(payload, null, 2)}
      
); } function MarkdownPreview({ content }: { content: string }) { return (
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_*]:min-w-0 ${containedLongTextClass}`}> {content}
); } function candidateTitle(candidate: SkillLearningCandidate, t: (zh: string, en: string) => string): string { const evidence = candidate.evidence || {}; const theme = String(evidence.theme || '').trim(); const taskId = String(evidence.task_id || '').trim(); const related = candidate.related_skill_names.filter(Boolean).join(', '); if (candidate.draft_skill_name) { return t(`为 ${candidate.draft_skill_name} 生成草稿`, `Draft for ${candidate.draft_skill_name}`); } if (candidate.kind === 'new_skill') { return theme ? t(`把“${theme}”沉淀成新技能`, `Extract "${theme}" into a new skill`) : taskId ? t(`从任务 ${taskId} 提炼新技能`, `Extract a new skill from task ${taskId}`) : t('提炼一个新技能', 'Extract a new skill'); } if (candidate.kind === 'revise_skill') { return related ? t(`修订技能 ${related}`, `Revise skill ${related}`) : t('修订已有技能', 'Revise an existing skill'); } if (candidate.kind === 'merge_skills') { return related ? t(`合并技能 ${related}`, `Merge skills ${related}`) : t('合并相似技能', 'Merge similar skills'); } if (candidate.kind === 'retire_skill') { return related ? t(`考虑下线技能 ${related}`, `Consider retiring ${related}`) : t('考虑下线技能', 'Consider retiring a skill'); } return candidate.reason || candidate.candidate_id; } function candidateTaskSummary(candidate: SkillLearningCandidate, t: (zh: string, en: string) => string): string { const evidence = candidate.evidence || {}; const taskId = String(evidence.task_id || '').trim(); const theme = String(evidence.theme || '').trim(); const triggerRun = String(evidence.trigger_run_id || '').trim(); if (taskId) return t(`任务 ${taskId}`, `Task ${taskId}`); if (theme) return t(`主题:${theme}`, `Theme: ${theme}`); if (triggerRun) return t(`由运行 ${triggerRun} 触发`, `Triggered by run ${triggerRun}`); const firstRun = candidate.source_run_ids?.[0]; if (firstRun) return t(`来自 ${candidate.source_run_ids.length} 个运行`, `From ${candidate.source_run_ids.length} run(s)`); return t('系统学习候选', 'System learning candidate'); } function candidateKindLabel(kind: string, t: (zh: string, en: string) => string): string { const labels: Record = { new_skill: t('新增技能', 'New skill'), revise_skill: t('修订技能', 'Revise skill'), merge_skills: t('合并技能', 'Merge skills'), retire_skill: t('下线技能', 'Retire skill'), }; return labels[kind] || kind; } function candidateStatusLabel(status: string, t: (zh: string, en: string) => string): string { const labels: Record = { open: t('待处理', 'Open'), queued: t('排队中', 'Queued'), synthesizing: t('生成中', 'Synthesizing'), draft_ready: t('草稿已就绪', 'Draft ready'), safety_failed: t('安全未通过', 'Safety failed'), eval_failed: t('评估未通过', 'Eval failed'), review_pending: t('等待评审', 'Review pending'), approved: t('已批准', 'Approved'), rejected: t('已拒绝', 'Rejected'), published: t('已发布', 'Published'), failed: t('失败', 'Failed'), superseded: t('已被替代', 'Superseded'), }; return labels[status] || status; } function draftStatusLabel(status: string, t: (zh: string, en: string) => string): string { const labels: Record = { draft: t('草稿', 'Draft'), in_review: t('评审中', 'In review'), approved: t('已批准', 'Approved'), rejected: t('已拒绝', 'Rejected'), published: t('已发布', 'Published'), disabled: t('已禁用', 'Disabled'), archived: t('已归档', 'Archived'), }; return labels[status] || candidateStatusLabel(status, t); } function riskLabel(risk: string, t: (zh: string, en: string) => string): string { const labels: Record = { low: t('低', 'Low'), medium: t('中', 'Medium'), high: t('高', 'High'), critical: t('严重', 'Critical'), }; return labels[risk] || risk; } function triggerReasonLabel(reason: string, t: (zh: string, en: string) => string): string { const labels: Record = { task_accepted: t('任务已接受', 'Task accepted'), }; return labels[reason] || reason; } function publishBlockReason(draft: SkillDraft, t: (zh: string, en: string) => string): string { if (draft.status !== 'in_review' && draft.status !== 'approved') { return t('草稿还没有送审,不能发布。', 'The draft has not been submitted yet.'); } if (!draft.safety_report) return t('缺少安全报告,不能发布。', 'A safety report is required before publishing.'); if (draft.safety_report.risk_level === 'critical' || !draft.safety_report.passed) { return t('安全报告存在阻断项,不能发布。', 'The safety report has blockers.'); } if (draft.eval_report?.status !== 'skipped_provider_unavailable' && draft.eval_report?.passed === false) { return t('评估报告显示回退或未达标,不能发布。', 'The eval report shows a regression or failed score.'); } return t('当前状态不能发布。', 'The current state cannot be published.'); } function normalizeStringList(value: unknown): string[] { if (Array.isArray(value)) { return value.map((item) => String(item).trim()).filter(Boolean); } if (typeof value === 'string') { return value.split(',').map((item) => item.trim()).filter(Boolean); } return []; } function formatDateTime(value?: string | null): string { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString(); } function formatScore(value: number): string { if (!Number.isFinite(value)) return '-'; return value.toFixed(2); } function formatOptionalScore(value: unknown): string { const parsed = toOptionalNumber(value); return typeof parsed === 'number' ? formatScore(parsed) : '-'; } function formatPercent(value?: number | null): string { if (typeof value !== 'number' || Number.isNaN(value)) return '0%'; return `${Math.round(value * 100)}%`; } function formatSignedScore(value: number): string { if (!Number.isFinite(value)) return '-'; return `${value >= 0 ? '+' : ''}${value.toFixed(2)}`; } function toNumber(value: unknown): number { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : 0; } function toOptionalNumber(value: unknown): number | null { if (value === null || value === undefined || value === '') return null; const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } function EmptyState({ icon, text }: { icon: React.ReactNode; text: string }) { return (
{icon}

{text}

); } function UploadSkillForm({ onDone, onCancel, onError, }: { onDone: () => void; onCancel: () => void; onError: (msg: string) => void; }) { const { locale } = useAppI18n(); const [uploading, setUploading] = useState(false); const fileRef = useRef(null); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); const file = fileRef.current?.files?.[0]; if (!file) return; setUploading(true); try { await uploadSkill(file); onDone(); } catch (err: any) { onError(err.message || pickAppText(locale, '上传失败', 'Upload failed')); } finally { setUploading(false); } }; return (
{pickAppText(locale, '上传技能', 'Upload skill')}

{pickAppText(locale, '上传后生成草稿;送审后再运行 safety 和 eval。', 'After upload, a draft is created; safety and eval run after submission.')}

); }