AgentEvalTool/.scratch/architecture-review-round3.html
sinohqb 160332665e
Some checks failed
CI / test (push) Failing after 12s
refactor(exploration): absorb settlement.py into ExplorationSessionRepository
将 settlement.py 的 settle_campaign_sessions 函数吸收为
ExplorationSessionRepository.expire_running_sessions 方法。删除浅模块
settlement.py(30 行,接口宽如实现),会话生命周期操作集中在 repository。

- 新增 ExplorationSessionRepository.expire_running_sessions(campaign_id)
- 更新 campaigns.py 和 campaign_runner.py 两个调用点
- 删除 backend/agenteval/exploration/settlement.py
- 所有测试通过,行为不变
2026-08-04 13:28:23 +08:00

416 lines
27 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>架构深化审查 — AgentEvalTool</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.tailwindcss.com"></script>
<script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
mermaid.initialize({ startOnLoad: true, theme: "neutral", securityLevel: "loose" });
</script>
<style>
.seam { stroke-dasharray: 4 4; }
.leak { stroke: #dc2626; }
.deep { background: linear-gradient(135deg, #0f172a, #1e293b); }
body { font-feature-settings: "liga" 1; }
</style>
</head>
<body class="bg-stone-50 text-slate-900 font-sans">
<main class="max-w-5xl mx-auto px-6 py-12 space-y-12">
<!-- Header -->
<header class="border-b border-stone-200 pb-6">
<div class="flex items-baseline justify-between">
<h1 class="text-3xl font-serif font-bold tracking-tight">AgentEvalTool 架构深化审查</h1>
<span class="text-sm text-stone-500">2026-08-04 · 第三轮</span>
</div>
<div class="flex gap-6 mt-4 text-xs text-stone-500">
<span class="flex items-center gap-1.5"><span class="inline-block w-3 h-3 rounded border-2 border-slate-400"></span> module</span>
<span class="flex items-center gap-1.5"><span class="inline-block w-6 border-t-2 border-dashed border-slate-400"></span> seam</span>
<span class="flex items-center gap-1.5"><span class="inline-block w-6 border-t-2 border-red-500"></span> leakage</span>
<span class="flex items-center gap-1.5"><span class="inline-block w-3 h-3 rounded deep"></span> deep module</span>
</div>
<p class="mt-4 text-sm text-stone-600 max-w-2xl">
扫描范围:最近 30 次提交的热点区域(探索式评测、周期对比、活动报告、前端活动页)。
识别 6 个深化机会,按推荐强度排序。
</p>
</header>
<!-- Candidates -->
<section id="candidates" class="space-y-10">
<!-- Candidate A -->
<article class="bg-white rounded-xl border border-stone-200 shadow-sm overflow-hidden">
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<h2 class="text-xl font-bold">A. 用例判定证据构建收敛到 case_verdict</h2>
<span class="inline-block px-2.5 py-0.5 rounded-full text-xs font-bold bg-emerald-100 text-emerald-800">强推荐</span>
</div>
<div class="flex flex-wrap gap-1.5 mb-4">
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">evaluation/report.py</code>
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">web/routers/runs.py</code>
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">evaluation/case_verdict.py</code>
</div>
<!-- Before/After -->
<div class="grid md:grid-cols-2 gap-6 mb-6">
<div>
<p class="text-xs uppercase tracking-wider text-stone-500 font-bold mb-2">Before — 重复的证据构建</p>
<div class="rounded-lg border border-stone-200 bg-white p-4">
<pre class="mermaid text-xs">
flowchart LR
subgraph report_py["report.py"]
R1[generate_report] --> R2[构建 CaseEvidence]
R2 --> R3[resolve_case_verdicts]
end
subgraph runs_py["runs.py"]
U1[get_run_logs] --> U2[构建 CaseEvidence]
U2 --> U3[resolve_case_verdicts]
end
style R2 fill:#fecaca,stroke:#dc2626
style U2 fill:#fecaca,stroke:#dc2626
</pre>
</div>
<p class="text-xs text-stone-500 mt-2">两处各 ~25 行turns → evidence dict → CaseEvidence → verdicts</p>
</div>
<div>
<p class="text-xs uppercase tracking-wider text-stone-500 font-bold mb-2">After — 单一落点</p>
<div class="rounded-lg border border-stone-200 bg-white p-4">
<pre class="mermaid text-xs">
flowchart LR
subgraph case_verdict["case_verdict.py"]
CV[build_evidence_and_verdicts]
CV --> CV2[resolve_case_verdicts]
end
subgraph callers["调用方"]
R[report.py] --> CV
U[runs.py] --> CV
end
style CV fill:#0f172a,stroke:#0f172a,color:#fff
</pre>
</div>
<p class="text-xs text-stone-500 mt-2">一处构建两处调用locality 回归</p>
</div>
</div>
<p class="text-sm text-stone-700 mb-2"><strong>问题:</strong>CaseEvidence 构建 + resolve_case_verdicts 调用链在 report.py 和 runs.py 各写一遍(各 ~25 行turns/results → evidence dict 的映射逻辑漂移风险高。</p>
<p class="text-sm text-stone-700 mb-3"><strong>方案:</strong>在 case_verdict.py 新增 <code>build_evidence_and_verdicts(turns, results, summary)</code>report.py 和 runs.py 各一行调用。</p>
<ul class="text-sm text-stone-600 space-y-1">
<li><strong>locality</strong>证据构建逻辑改一处,全局生效</li>
<li><strong>depth</strong>case_verdict 从纯函数变深——接口不变,实现吸收编排</li>
<li><strong>测试面:</strong>一处可测,不用在两处重复覆盖</li>
</ul>
</div>
</article>
<!-- Candidate B -->
<article class="bg-white rounded-xl border border-stone-200 shadow-sm overflow-hidden">
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<h2 class="text-xl font-bold">B. 活动报告读模型补完load_campaign_view 统一出口</h2>
<span class="inline-block px-2.5 py-0.5 rounded-full text-xs font-bold bg-emerald-100 text-emerald-800">强推荐</span>
</div>
<div class="flex flex-wrap gap-1.5 mb-4">
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">evaluation/report.py</code>
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">evaluation/comparison.py</code>
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">web/routers/campaigns.py</code>
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">exploration/summary.py</code>
</div>
<div class="grid md:grid-cols-2 gap-6 mb-6">
<div>
<p class="text-xs uppercase tracking-wider text-stone-500 font-bold mb-2">Before — 9 次重复生成</p>
<div class="rounded-lg border border-stone-200 bg-white p-4">
<pre class="mermaid text-xs">
sequenceDiagram
participant C as comparison.py
participant R as load_campaign_report
participant DB as SQLite
C->>R: baseline report
R->>DB: SELECT runs WHERE campaign_id
DB-->>R: 50 runs
R-->>C: report dict
C->>R: current report
R->>DB: SELECT runs WHERE campaign_id
DB-->>R: 50 runs
R-->>C: report dict
Note over C,DB: 同一对比生成中重复 3-4 轮
</pre>
</div>
<p class="text-xs text-stone-500 mt-2">comparison.py 内 9 次调用 load_campaign_reportmarkdown handler 7 步手动拼装</p>
</div>
<div>
<p class="text-xs uppercase tracking-wider text-stone-500 font-bold mb-2">After — 单一 view 投影</p>
<div class="rounded-lg border border-stone-200 bg-white p-4">
<pre class="mermaid text-xs">
sequenceDiagram
participant Caller
participant V as load_campaign_view
participant DB as SQLite
Caller->>V: session, campaign
V->>DB: 一次查询
DB-->>V: runs + sessions + analysis
V-->>Caller: {report, exploration, analysis, comparison}
Note over Caller,DB: 所有出口走同一 view
</pre>
</div>
<p class="text-xs text-stone-500 mt-2">一个接口N 个投影JSON / markdown / comparison</p>
</div>
</div>
<p class="text-sm text-stone-700 mb-2"><strong>问题:</strong>load_campaign_report 在 comparison.py 被调 9 次(同一对比生成中 baseline/current 各重复 3-4 轮markdown handler 手动拼装 7 个数据源report + exploration 总是要一起取但没有统一入口。</p>
<p class="text-sm text-stone-700 mb-3"><strong>方案:</strong>在 report.py 新增 <code>load_campaign_view(session, campaign)</code>,返回 <code>{report, exploration, analysis, comparison}</code>,内部做缓存避免重复查询。</p>
<ul class="text-sm text-stone-600 space-y-1">
<li><strong>leverage</strong>一个接口5 个调用点统一</li>
<li><strong>locality</strong>新增导出格式只需投影 view不新增取数逻辑</li>
<li><strong>性能:</strong>消除 comparison 生成中的重复报告构建</li>
</ul>
</div>
</article>
<!-- Candidate C -->
<article class="bg-white rounded-xl border border-stone-200 shadow-sm overflow-hidden">
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<h2 class="text-xl font-bold">C. 仪表盘聚合逻辑下沉到 metrics 模块</h2>
<span class="inline-block px-2.5 py-0.5 rounded-full text-xs font-bold bg-amber-100 text-amber-800">值得探索</span>
</div>
<div class="flex flex-wrap gap-1.5 mb-4">
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">web/routers/stats.py</code>
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">evaluation/metrics.py</code>
</div>
<div class="grid md:grid-cols-2 gap-6 mb-6">
<div>
<p class="text-xs uppercase tracking-wider text-stone-500 font-bold mb-2">Before — 业务逻辑在 HTTP 层</p>
<div class="rounded-lg border border-stone-200 bg-stone-50 p-4 font-mono text-xs leading-relaxed">
<div class="text-stone-400"># stats.py router</div>
<div class="text-red-600">def _settled(runs): ...</div>
<div class="text-red-600">for r in runs:</div>
<div class="text-red-600">&nbsp;&nbsp;if started.date() == today: ...</div>
<div class="text-red-600">&nbsp;&nbsp;trigger_breakdown[...] += 1</div>
<div class="text-red-600">for sid, sruns in by_scenario.items():</div>
<div class="text-red-600">&nbsp;&nbsp;agg = aggregate_runs(sruns)</div>
<div class="mt-2 text-stone-500">~40 行聚合逻辑 + _settled 过滤器</div>
</div>
</div>
<div>
<p class="text-xs uppercase tracking-wider text-stone-500 font-bold mb-2">After — 接口薄、实现深</p>
<div class="rounded-lg border border-stone-200 bg-stone-50 p-4 font-mono text-xs leading-relaxed">
<div class="text-stone-400"># metrics.py</div>
<div class="text-emerald-700">def compute_dashboard(runs) -> DashboardStats:</div>
<div class="text-emerald-700">&nbsp;&nbsp;# settled, per-scenario, trigger, today</div>
<div class="mt-2 text-stone-400"># stats.py router</div>
<div class="text-slate-800">def dashboard(session):</div>
<div class="text-slate-800">&nbsp;&nbsp;runs = RunRepository(session).list_all()</div>
<div class="text-slate-800">&nbsp;&nbsp;return compute_dashboard(runs)</div>
<div class="mt-2 text-stone-500">handler 缩到 3 行</div>
</div>
</div>
</div>
<p class="text-sm text-stone-700 mb-2"><strong>问题:</strong><code>_settled</code> 过滤器和 per-scenario 聚合在 router 文件里定义,是领域概念却锁在 HTTP 层——无法复用、无法测试。</p>
<p class="text-sm text-stone-700 mb-3"><strong>方案:</strong><code>compute_dashboard(runs) -> DashboardStats</code> 到 metrics.pyrouter 只做 session 生命周期和序列化。</p>
<ul class="text-sm text-stone-600 space-y-1">
<li><strong>locality</strong>聚合规则改一处</li>
<li><strong>可测性:</strong>纯函数,输入 runs 输出 stats</li>
<li><strong>leverage</strong>未来趋势端点复用同一函数</li>
</ul>
</div>
</article>
<!-- Candidate D -->
<article class="bg-white rounded-xl border border-stone-200 shadow-sm overflow-hidden">
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<h2 class="text-xl font-bold">D. Campaigns.tsx 巨型组件拆解</h2>
<span class="inline-block px-2.5 py-0.5 rounded-full text-xs font-bold bg-amber-100 text-amber-800">值得探索</span>
</div>
<div class="flex flex-wrap gap-1.5 mb-4">
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">frontend/web/src/pages/Campaigns.tsx</code>
</div>
<div class="grid md:grid-cols-2 gap-6 mb-6">
<div>
<p class="text-xs uppercase tracking-wider text-stone-500 font-bold mb-2">Before — 1122 行单体</p>
<div class="rounded-lg border border-stone-200 bg-white p-4">
<div class="space-y-1.5">
<div class="h-6 bg-red-100 border border-red-300 rounded flex items-center px-2 text-xs font-mono">createOpen, submitting, form</div>
<div class="h-6 bg-red-100 border border-red-300 rounded flex items-center px-2 text-xs font-mono">reportOpen, reportLoading, report</div>
<div class="h-6 bg-red-100 border border-red-300 rounded flex items-center px-2 text-xs font-mono">reportRuns, reportTimeline</div>
<div class="h-6 bg-red-100 border border-red-300 rounded flex items-center px-2 text-xs font-mono">analysis, analysisBusy</div>
<div class="h-6 bg-red-100 border border-red-300 rounded flex items-center px-2 text-xs font-mono">comparison, expandedIds, timelines</div>
<div class="h-6 bg-red-100 border border-red-300 rounded flex items-center px-2 text-xs font-mono">4x usePolling</div>
<div class="h-6 bg-red-100 border border-red-300 rounded flex items-center px-2 text-xs font-mono">fetchReport: 5 并行 API 调用</div>
</div>
<p class="text-xs text-stone-500 mt-2">15+ useState接口宽如实现</p>
</div>
</div>
<div>
<p class="text-xs uppercase tracking-wider text-stone-500 font-bold mb-2">After — 深 hook + 薄页面</p>
<div class="rounded-lg border border-stone-200 bg-white p-4">
<div class="space-y-1.5">
<div class="h-8 deep rounded flex items-center px-2 text-xs font-mono text-white">useCampaignReport(id)</div>
<div class="h-5 bg-stone-100 border border-stone-300 rounded flex items-center px-2 text-xs font-mono">useCampaignList()</div>
<div class="h-5 bg-stone-100 border border-stone-300 rounded flex items-center px-2 text-xs font-mono">useCampaignCreate()</div>
<div class="mt-3 h-8 bg-emerald-100 border border-emerald-300 rounded flex items-center px-2 text-xs font-mono">Campaigns.tsx: ~200 行</div>
</div>
<p class="text-xs text-stone-500 mt-2">hook 深、页面薄;状态按职责分组</p>
</div>
</div>
</div>
<p class="text-sm text-stone-700 mb-2"><strong>问题:</strong>1122 行组件、15+ 状态变量、fetchReport 一次发 5 个并行请求。添加报告抽屉新功能(如探索详情)需向已超载的组件再加状态。</p>
<p class="text-sm text-stone-700 mb-3"><strong>方案:</strong><code>useCampaignReport(campaignId)</code> hook 封装报告 + 分析 + 对比 + 时间线的数据获取;页面组件只负责渲染和交互。</p>
<ul class="text-sm text-stone-600 space-y-1">
<li><strong>depth</strong>hook 接口小(一个 id 进,完整 view 出)</li>
<li><strong>locality</strong>报告数据逻辑集中,不再散落</li>
<li><strong>可测性:</strong>hook 可独立测试,不依赖页面渲染</li>
</ul>
</div>
</article>
<!-- Candidate E -->
<article class="bg-white rounded-xl border border-stone-200 shadow-sm overflow-hidden">
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<h2 class="text-xl font-bold">E. 分析 / 对比 Repository 双子合并</h2>
<span class="inline-block px-2.5 py-0.5 rounded-full text-xs font-bold bg-slate-100 text-slate-700">推测性</span>
</div>
<div class="flex flex-wrap gap-1.5 mb-4">
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">storage/repository.py</code>
</div>
<div class="grid md:grid-cols-2 gap-6 mb-6">
<div>
<p class="text-xs uppercase tracking-wider text-stone-500 font-bold mb-2">Before — 近乎相同的双子</p>
<div class="rounded-lg border border-stone-200 bg-white p-4">
<div class="grid grid-cols-2 gap-2 text-xs font-mono">
<div class="bg-blue-50 border border-blue-200 rounded p-2">
<div class="font-bold text-blue-800">AnalysisRepo</div>
<div class="text-blue-600 mt-1">get_by_campaign</div>
<div class="text-blue-600">upsert(...)</div>
<div class="text-blue-600">mark_orphans_failed</div>
</div>
<div class="bg-purple-50 border border-purple-200 rounded p-2">
<div class="font-bold text-purple-800">ComparisonRepo</div>
<div class="text-purple-600 mt-1">get_by_campaign</div>
<div class="text-purple-600">upsert(...)</div>
<div class="text-purple-600">mark_orphans_failed</div>
</div>
</div>
<p class="text-xs text-stone-500 mt-2">~60 行重复mark_orphans_failed 在 RunRepo 还有一份</p>
</div>
</div>
<div>
<p class="text-xs uppercase tracking-wider text-stone-500 font-bold mb-2">After — 泛型基类</p>
<div class="rounded-lg border border-stone-200 bg-white p-4">
<div class="bg-slate-800 rounded p-3 text-xs font-mono text-white">
<div class="text-slate-400"># 基类</div>
<div>class AsyncJobRepo(BaseRepo):</div>
<div>&nbsp;&nbsp;get_by_campaign()</div>
<div>&nbsp;&nbsp;upsert()</div>
<div>&nbsp;&nbsp;mark_orphans_failed()</div>
<div class="mt-2 text-slate-400"># 子类只提供表名和字段映射</div>
<div>class AnalysisRepo(AsyncJobRepo): ...</div>
<div>class ComparisonRepo(AsyncJobRepo): ...</div>
</div>
<p class="text-xs text-stone-500 mt-2">子类 ~5 行,差异在配置不在代码</p>
</div>
</div>
</div>
<p class="text-sm text-stone-700 mb-2"><strong>问题:</strong>CampaignAnalysisRepository 和 CampaignPeriodComparisonRepository 结构近乎相同get_by_campaign / upsert / mark_orphans_failed加上 RunRepository.mark_orphans_failed 共三份拷贝。</p>
<p class="text-sm text-stone-700 mb-3"><strong>方案:</strong>抽 AsyncJobRepository 泛型基类,子类只提供表类型和字段映射。</p>
<ul class="text-sm text-stone-600 space-y-1">
<li><strong>locality</strong>孤儿清理策略改一处</li>
<li><strong>leverage</strong>新增异步任务类型只需 5 行子类</li>
<li>风险:引入继承层次,需权衡是否值得</li>
</ul>
</div>
</article>
<!-- Candidate F -->
<article class="bg-white rounded-xl border border-stone-200 shadow-sm overflow-hidden">
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<h2 class="text-xl font-bold">F. settlement.py 吸收进 Repository</h2>
<span class="inline-block px-2.5 py-0.5 rounded-full text-xs font-bold bg-slate-100 text-slate-700">推测性</span>
</div>
<div class="flex flex-wrap gap-1.5 mb-4">
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">exploration/settlement.py</code>
<code class="text-xs bg-stone-100 px-2 py-0.5 rounded">storage/repository.py</code>
</div>
<div class="grid md:grid-cols-2 gap-6 mb-6">
<div>
<p class="text-xs uppercase tracking-wider text-stone-500 font-bold mb-2">Before — 浅模块独立成文件</p>
<div class="rounded-lg border border-stone-200 bg-stone-50 p-4 font-mono text-xs leading-relaxed">
<div class="text-stone-400"># settlement.py (30 行)</div>
<div>def settle_campaign_sessions(...):</div>
<div>&nbsp;&nbsp;for s in repo.list_by_campaign(...):</div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;if s.status == RUNNING:</div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;s.status = EXPIRED</div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;repo.update(s)</div>
<div class="mt-2 text-stone-500">接口宽 = 实现,无深度</div>
</div>
</div>
<div>
<p class="text-xs uppercase tracking-wider text-stone-500 font-bold mb-2">After — Repository 方法</p>
<div class="rounded-lg border border-stone-200 bg-stone-50 p-4 font-mono text-xs leading-relaxed">
<div class="text-stone-400"># ExplorationSessionRepository</div>
<div class="text-emerald-700">def expire_running(self, campaign_id) -> int:</div>
<div class="text-emerald-700">&nbsp;&nbsp;# 批量 UPDATE ... WHERE status=RUNNING</div>
<div class="mt-2 text-stone-400"># campaign_runner.py</div>
<div>repo.expire_running(campaign_id)</div>
<div class="mt-2 text-stone-500">删除 settlement.py调用方一行</div>
</div>
</div>
</div>
<p class="text-sm text-stone-700 mb-2"><strong>问题:</strong>settlement.py 是 30 行的浅模块——接口(一个函数)几乎和实现(一个循环)一样复杂。理解结算需要跨三个文件跳转。</p>
<p class="text-sm text-stone-700 mb-3"><strong>方案:</strong><code>settle_campaign_sessions</code> 吸收为 <code>ExplorationSessionRepository.expire_running(campaign_id)</code>,删除 settlement.py。</p>
<ul class="text-sm text-stone-600 space-y-1">
<li><strong>删除测试通过:</strong>删掉它,复杂度回到 repository本就在那里</li>
<li><strong>locality</strong>会话生命周期操作集中在 repository</li>
<li>减少一个文件跳转</li>
</ul>
</div>
</article>
</section>
<!-- Top Recommendation -->
<section id="top-recommendation" class="bg-gradient-to-br from-emerald-50 to-teal-50 border-2 border-emerald-200 rounded-xl p-8">
<h2 class="text-2xl font-serif font-bold text-emerald-900 mb-4">首选深化机会</h2>
<div class="grid md:grid-cols-3 gap-6">
<div class="md:col-span-2">
<h3 class="text-lg font-bold text-emerald-800 mb-2">A. 用例判定证据构建收敛到 case_verdict</h3>
<p class="text-sm text-stone-700 mb-3">
这是最干净的深化:两处 ~25 行的重复逻辑收敛到一个函数,接口不变、实现变深。
不涉及数据模型变更、不影响 API 契约、不触碰前端——纯粹的 locality 回归。
完成后runs.py 的 handler 从 60 行缩到 10 行report.py 的证据构建消失。
</p>
<p class="text-sm text-stone-600">
<strong>为什么先做它:</strong>成本最低(半天)、风险最小(纯重构)、收益最直接(删除 50 行重复)。
做完后 CaseEvidence 的构建逻辑有了唯一落点,后续任何判定相关的改动只改一处。
</p>
</div>
<div class="bg-white/60 rounded-lg p-4 border border-emerald-200">
<p class="text-xs uppercase tracking-wider text-emerald-700 font-bold mb-2">度量</p>
<ul class="text-sm text-stone-700 space-y-1">
<li>删除行数:~50</li>
<li>影响文件3</li>
<li>测试新增1 个</li>
<li>API 变更:无</li>
<li>前端变更:无</li>
</ul>
</div>
</div>
</section>
</main>
</body>
</html>