initial: blog-app snapshot

This commit is contained in:
omo
2026-08-17 17:11:27 +08:00
commit 65cb9d5ead
125 changed files with 14720 additions and 0 deletions

119
templates/analyze.html Normal file
View File

@@ -0,0 +1,119 @@
{% extends "base.html" %}
{% block title %}AI 分析 - AI Agent 项目追踪{% endblock %}
{% block content %}
<h1 class="mb-4">AI 分析</h1>
<!-- Control panel -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<!-- Scope selector -->
<div class="col-md-3">
<label class="form-label">分析范围</label>
<select class="form-select" id="scopeSelect">
<option value="home">全站概览</option>
<option value="projects">所有项目</option>
<!-- 项目选项由 JS 从 /api/projects 动态填充, 与 DB 保持一致 -->
</select>
</div>
<!-- Preset buttons -->
<div class="col-md-6">
<label class="form-label">预设分析</label>
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-outline-primary preset-btn" data-preset="activity">
📊 活跃度评估
</button>
<button type="button" class="btn btn-outline-primary preset-btn" data-preset="evolution">
🚀 版本演进
</button>
<button type="button" class="btn btn-outline-primary preset-btn" data-preset="issues_hot">
🔥 Issue 热点
</button>
</div>
</div>
<!-- Start button -->
<div class="col-md-3 d-flex align-items-end">
<button type="button" class="btn btn-success w-100" id="startBtn">
开始分析
</button>
</div>
</div>
<!-- Custom question input -->
<div class="row g-3 mt-2">
<div class="col-12">
<label class="form-label">自定义问题 (可选)</label>
<input type="text" class="form-control" id="questionInput"
placeholder="例如: 这个项目的核心功能是什么? / 最近有什么重要更新?">
</div>
</div>
</div>
</div>
<!-- Output area with tabs -->
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" id="outputTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="observation-tab" data-bs-toggle="tab"
data-bs-target="#observation" type="button" role="tab">
数据观察
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="prompt-tab" data-bs-toggle="tab"
data-bs-target="#prompt" type="button" role="tab">
Prompt
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="reasoning-tab" data-bs-toggle="tab"
data-bs-target="#reasoning" type="button" role="tab">
LLM 推理
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="conclusion-tab" data-bs-toggle="tab"
data-bs-target="#conclusion" type="button" role="tab">
结论
</button>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content" id="outputTabContent">
<div class="tab-pane fade show active" id="observation" role="tabpanel">
<div class="output-content" id="observationContent">
<div class="text-muted">等待分析开始...</div>
</div>
</div>
<div class="tab-pane fade" id="prompt" role="tabpanel">
<div class="output-content" id="promptContent">
<div class="text-muted">等待分析开始...</div>
</div>
</div>
<div class="tab-pane fade" id="reasoning" role="tabpanel">
<div class="output-content" id="reasoningContent">
<div class="text-muted">等待分析开始...</div>
</div>
</div>
<div class="tab-pane fade" id="conclusion" role="tabpanel">
<div class="output-content" id="conclusionContent">
<div class="text-muted">等待分析开始...</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script type="module">
import { initAnalyzePage } from "{{ url_for('static', filename='js/pages/analyze.js') }}";
initAnalyzePage();
</script>
{% endblock %}

72
templates/base.html Normal file
View File

@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}AI Agent 项目追踪{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">AI Agent 追踪</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="/">首页</a></li>
<li class="nav-item"><a class="nav-link" href="/projects">项目列表</a></li>
<li class="nav-item"><a class="nav-link" href="/search">全局搜索</a></li>
<li class="nav-item"><a class="nav-link" href="/watchdog" title="每天 9:00 自动生成的 AI Agent 项目升级日报">日报</a></li>
</ul>
<form class="d-flex" action="/search" method="get" id="navSearchForm">
<input class="form-control me-2 nav-search-input" type="search"
name="q" placeholder="搜索项目 / release / issue" aria-label="Search">
<button class="btn btn-outline-light" type="submit">搜索</button>
</form>
</div>
</div>
</nav>
<main class="container py-4 flex-grow-1">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-messages">
{% for category, message in messages %}
<div class="alert alert-{{ category | default('info') }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer class="bg-dark text-light text-center py-3 mt-auto">
<div class="container">
AI Agent 项目追踪 - Powered by Flask
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"
integrity="sha384-948ahk4ZmxYVYOc+rxN1H2gM1EJ2Duhp7uHtZ4WSLkV4Vtx5MUqnV+l7u9B+jFv+"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"
integrity="sha384-ZuC+DIACqSIZTsp+7YF57cR5Y+6qXa7YFbEKdA/EHA/R0T+41dtorqucYl71Zp+t"
crossorigin="anonymous"></script>
{% block scripts %}{% endblock %}
<!-- 全站浮动 AI 分析面板 -->
<button id="ai-fab" type="button" title="AI 分析">🤖</button>
<script type="module" src="{{ url_for('static', filename='js/ai-panel.js') }}"></script>
</body>
</html>

57
templates/index.html Normal file
View File

@@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block title %}首页 - AI Agent 项目追踪{% endblock %}
{% block content %}
<h1 class="mb-4">项目总览</h1>
<!-- Stats cards -->
<div class="row g-3 mb-5" id="statsRow">
<div class="col-6 col-md-3">
<div class="card stat-card text-center">
<div class="card-body">
<div class="stat-number text-primary" id="statProjects">-</div>
<div class="stat-label">项目数</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card text-center">
<div class="card-body">
<div class="stat-number text-success" id="statReleases">-</div>
<div class="stat-label">Release 数</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card text-center">
<div class="card-body">
<div class="stat-number text-warning" id="statIssues">-</div>
<div class="stat-label">Issue 数</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card text-center">
<div class="card-body">
<div class="stat-number text-danger" id="statStars">-</div>
<div class="stat-label">总 Stars</div>
</div>
</div>
</div>
</div>
<h2 class="mb-3">项目 Dashboard</h2>
<div class="row g-3" id="projectCards">
<div class="empty-hint">加载中…</div>
</div>
{% endblock %}
{% block scripts %}
<script type="module">
import { renderProjectCard, bindProjectCardClicks } from "{{ url_for('static', filename='js/components/project-card.js') }}";
import { initIndexPage } from "{{ url_for('static', filename='js/pages/index.js') }}";
bindProjectCardClicks();
initIndexPage(renderProjectCard);
</script>
{% endblock %}

View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block title %}项目详情 - AI Agent 项目追踪{% endblock %}
{% block content %}
<div id="projectHeader">
<div class="empty-hint">加载项目信息…</div>
</div>
<div class="row g-4 mt-2">
<div class="col-lg-6">
<h2 class="h4 mb-3">Release 时间线</h2>
<div class="timeline">
<ul class="list-group" id="releaseTimeline">
<li class="list-group-item empty-hint">加载中…</li>
</ul>
</div>
</div>
<div class="col-lg-6">
<h2 class="h4 mb-3">最近 Issues</h2>
<div class="table-responsive">
<table class="table table-striped table-hover align-middle">
<thead class="table-dark">
<tr>
<th>标题</th>
<th>状态</th>
<th>创建时间</th>
<th>用户</th>
</tr>
</thead>
<tbody id="issueTableBody">
<tr><td colspan="4" class="empty-hint">加载中…</td></tr>
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script type="module">
import { initProjectDetailPage } from "{{ url_for('static', filename='js/pages/project-detail.js') }}";
initProjectDetailPage({{ project_id | tojson }});
</script>
{% endblock %}

33
templates/projects.html Normal file
View File

@@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block title %}项目列表 - AI Agent 项目追踪{% endblock %}
{% block content %}
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-2">
<h1 class="mb-0">项目列表</h1>
<div class="d-flex gap-2">
<div class="btn-group" role="group" aria-label="语言过滤">
<button type="button" class="btn btn-outline-primary active" data-lang="all">All</button>
<button type="button" class="btn btn-outline-primary" data-lang="Python">Python</button>
<button type="button" class="btn btn-outline-primary" data-lang="TypeScript">TypeScript</button>
</div>
<select class="form-select w-auto" id="sortSelect">
<option value="stars">按 Stars 排序</option>
<option value="updated_at">按更新时间排序</option>
</select>
</div>
</div>
<div class="row g-3" id="projectList">
<div class="empty-hint">加载中…</div>
</div>
{% endblock %}
{% block scripts %}
<script type="module">
import { renderProjectCard, bindProjectCardClicks } from "{{ url_for('static', filename='js/components/project-card.js') }}";
import { initProjectsPage } from "{{ url_for('static', filename='js/pages/projects.js') }}";
bindProjectCardClicks();
initProjectsPage(renderProjectCard);
</script>
{% endblock %}

36
templates/search.html Normal file
View File

@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}全局搜索 - AI Agent 项目追踪{% endblock %}
{% block content %}
<h1 class="mb-4">全局搜索</h1>
<div class="row justify-content-center mb-4">
<div class="col-md-8">
<input type="search" class="form-control form-control-lg search-input"
id="searchInput" placeholder="输入关键词搜索项目 / release / issue…">
</div>
</div>
<div id="searchHint" class="empty-hint">输入关键词开始搜索</div>
<div id="searchResults" class="d-none">
<h2 class="h4 mt-3">匹配的项目 <span class="badge bg-primary" id="projCount">0</span></h2>
<div class="row g-3 mb-4" id="resultProjects"></div>
<h2 class="h4">匹配的 Releases <span class="badge bg-success" id="relCount">0</span></h2>
<ul class="list-group mb-4" id="resultReleases"></ul>
<h2 class="h4">匹配的 Issues <span class="badge bg-warning text-dark" id="issueCount">0</span></h2>
<ul class="list-group mb-4" id="resultIssues"></ul>
</div>
{% endblock %}
{% block scripts %}
<script type="module">
import { renderProjectCard, bindProjectCardClicks } from "{{ url_for('static', filename='js/components/project-card.js') }}";
import { initSearchPage } from "{{ url_for('static', filename='js/pages/search.js') }}";
bindProjectCardClicks();
initSearchPage(renderProjectCard);
</script>
{% endblock %}

159
templates/watchdog.html Normal file
View File

@@ -0,0 +1,159 @@
{% extends "base.html" %}
{% block title %}日报 - AI Agent 项目追踪{% endblock %}
{% block content %}
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-2">
<div>
<h1 class="mb-1">升级迭代日报</h1>
<small class="text-muted">每天 09:00 自动更新 · 最新报告: {{ reports[0].date if reports else "无" }}</small>
</div>
<div class="d-flex align-items-center gap-2">
<select class="form-select form-select-sm w-auto" onchange="location.search='?days='+this.value" aria-label="历史天数">
{% for d in [7, 14, 30, 90] %}
<option value="{{ d }}" {{ 'selected' if days == d }}>最近 {{ d }} 天</option>
{% endfor %}
</select>
<form method="post" action="/watchdog/run" id="run-form" class="m-0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-primary" id="run-btn">立即运行</button>
</form>
</div>
</div>
<div id="wd-pending" class="alert alert-warning d-none mt-2 mb-3">
<span class="spinner-border spinner-border-sm me-2"></span>
报告生成中, 通常 1-2 分钟, 完成后页面自动刷新…
</div>
{% if not reports %}
<div class="alert alert-info">
暂无报告, 点击立即运行生成第一份日报
</div>
{% endif %}
{% if reports and reports[0].meta %}
{% set m = reports[0].meta %}
{% set projs = m.get("projects", {}) %}
<div class="row g-3 mb-4">
<div class="col-6 col-md-3"><div class="card stat-card text-center"><div class="card-body">
<div class="stat-number text-primary">{{ projs|length }}</div>
<div class="stat-label">监控项目</div></div></div></div>
<div class="col-6 col-md-3"><div class="card stat-card text-center"><div class="card-body">
<div class="stat-number text-success">{{ projs.values()|selectattr('status','equalto','ok')|list|length }}</div>
<div class="stat-label">✓ 正常</div></div></div></div>
<div class="col-6 col-md-3"><div class="card stat-card text-center"><div class="card-body">
<div class="stat-number text-warning">{{ projs.values()|selectattr('status','equalto','partial')|list|length }}</div>
<div class="stat-label">⚠ 部分</div></div></div></div>
<div class="col-6 col-md-3"><div class="card stat-card text-center"><div class="card-body">
<div class="stat-number text-danger">{{ m.get("sources_failed", [])|length }}</div>
<div class="stat-label">✗ 失败数据源</div></div></div></div>
</div>
{% endif %}
{% for r in reports %}
<div class="card mb-4">
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<strong>{{ r.date }}</strong>
<span class="text-muted small ms-2">生成于 {{ r.created_at[:19] }}</span>
</div>
{% set prev = reports[loop.index] if not loop.last else none %}
{% if prev and r.meta and prev.meta %}
{% set cur_failed = r.meta.get("sources_failed", []) %}
{% set prev_failed = prev.meta.get("sources_failed", []) %}
{% set fixed = prev_failed|reject('in', cur_failed)|list %}
{% set broke = cur_failed|reject('in', prev_failed)|list %}
<small class="text-muted">
{% if fixed %}<span class="badge bg-success-subtle text-success-emphasis">✅ 恢复: {{ fixed|join(", ") }}</span>{% endif %}
{% if broke %}<span class="badge bg-danger-subtle text-danger-emphasis">⚠ 新增失败: {{ broke|join(", ") }}</span>{% endif %}
{% if not fixed and not broke %}<span class="badge bg-secondary-subtle text-secondary-emphasis">与昨日相同</span>{% endif %}
</small>
{% endif %}
</div>
<div class="card-body">
{% if r.meta %}
<div class="d-flex flex-wrap gap-1 mb-2">
{% for name, info in r.meta.get("projects", {}).items() %}
{% set st = info.get('status', 'unknown') %}
{% set is_starred = name in starred %}
<span class="badge {{ 'badge-state-open' if st=='ok' else ('badge-state-partial' if st=='partial' else 'bg-danger') }}"
title="{{ name }}: {{ st }}{{ ' (已装)' if is_starred else '' }}">
{{ name }}{% if is_starred %} ★{% endif %}
{% if st=='ok' %} ✓{% elif st=='partial' %} ⚠{% else %} ✗{% endif %}
</span>
{% endfor %}
{% for s in r.meta.get("sources_failed", []) %}
<span class="badge bg-danger" title="数据源失败: {{ s }}">{{ s }} ✗</span>
{% endfor %}
</div>
{% endif %}
<details {% if loop.first %}open{% endif %}>
<summary class="text-primary">查看报告内容</summary>
<div class="watchdog-md mb-0 mt-2" data-md="{{ r.markdown | e }}"></div>
</details>
</div>
</div>
{% endfor %}
{% endblock %}
{% block scripts %}
<script>
document.querySelectorAll('.watchdog-md[data-md]').forEach(function(el){
var raw = el.getAttribute('data-md') || '';
el.innerHTML = DOMPurify.sanitize(marked.parse(raw));
el.querySelectorAll('a[href^="http"]').forEach(function(a){
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener noreferrer');
});
});
</script>
<script>
(function(){
var KEY = 'wd_pending_since';
var form = document.getElementById('run-form');
if (form) {
form.addEventListener('submit', function(){
var b = document.getElementById('run-btn');
b.disabled = true;
b.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>已触发…';
sessionStorage.setItem(KEY, String(Date.now()));
});
}
var since = parseInt(sessionStorage.getItem(KEY) || '0', 10);
if (!since) return;
var ageMin = (Date.now() - since) / 60000;
var pending = document.getElementById('wd-pending');
if (ageMin >= 5) {
sessionStorage.removeItem(KEY);
if (pending) {
pending.classList.remove('d-none', 'alert-warning');
pending.classList.add('alert-danger');
pending.textContent = '生成超时, 请检查 logs/daily-watchdog-run.log 后重试';
}
return;
}
if (pending) pending.classList.remove('d-none');
var knownTop = (document.querySelector('.card-header .text-muted.small') || {}).textContent || '';
var timer = setInterval(function(){
fetch(location.href, {credentials: 'same-origin'}).then(function(r){ return r.text(); }).then(function(html){
var m = html.match(/生成于\s*([0-9:\-\s]+)/);
var top = m ? m[1].trim() : '';
if (top && top !== knownTop.trim()) {
sessionStorage.removeItem(KEY);
clearInterval(timer);
location.reload();
return;
}
if ((Date.now() - since) / 60000 > 5) {
clearInterval(timer);
sessionStorage.removeItem(KEY);
if (pending) {
pending.classList.remove('alert-warning');
pending.classList.add('alert-danger');
pending.textContent = '生成超时, 请检查 logs/daily-watchdog-run.log 后重试';
}
}
}).catch(function(){ /* 网络抖动, 下一轮再试 */ });
}, 15000);
})();
</script>
{% endblock %}