Skip to content

Commit 30f6376

Browse files
committed
refactor: split src/ into api/ data/ ai/ runtime/ pipeline/ subpackages
Reorganize 18 modules into 5 themed subpackages so the layering is visible at the directory level rather than buried in import statements. Mapping: api/ webvpn, icourse, emailer data/ database, schema, sharder, crypto_box ai/ transcriber, ocr, summarizer, bucketer, ppt_dedup runtime/ scheduler, reporter, config pipeline/ lecture_runner, ppt_pipeline All imports rewritten to absolute form (from src.SUBPKG.X import Y) so the dependency graph reads consistently across src/ internals, tests/, scripts/, and main.py. 72 tests pass post-move. Also incidentally bundles work already in flight on this branch: - merge_db all_courses section guarded for old local DBs lacking the table - explicit DETACH on Windows so tests' os.unlink doesn't hit a file lock - PPT pipeline (dHash dedup + 30-pattern invalid-page filter) - 10-min bucketed prompts + summary_format_version v2 - v2 password (sha256+PBKDF2) with legacy fallback - Sharded encrypted DB with deterministic shard hashes - Frontend: subscriptions editor, prev/next, star pin, summary/PPT cycle - MODEL_PROVIDERS list with DASHSCOPE backward compat - Reporter image/CPU/concurrency snapshots
1 parent e55ceb0 commit 30f6376

36 files changed

Lines changed: 3190 additions & 1354 deletions

.github/workflows/check.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ jobs:
159159
StuId: ${{ secrets.STUID }}
160160
UISPsw: ${{ secrets.UISPSW }}
161161
COURSE_IDS: ${{ secrets.COURSE_IDS }}
162+
CRAWL_TERM: ${{ secrets.CRAWL_TERM }}
162163
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
164+
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
163165
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
164166
SMTP_EMAIL: ${{ secrets.SMTP_EMAIL }}
165167
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}

frontend/index.html

Lines changed: 148 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
<!-- Left: back arrow or logo -->
2828
<div class="flex items-center gap-2 min-w-0">
29-
<button x-show="['lectures','detail','settings'].includes(view)"
29+
<button x-show="['lectures','detail','settings','subscriptions'].includes(view)"
3030
@click="goBack()" class="p-1 -ml-1 text-gray-600 hover:text-gray-900 flex-shrink-0">
3131
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
3232
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/>
@@ -36,6 +36,7 @@
3636
view === 'lectures' ? currentCourse?.title :
3737
view === 'detail' ? currentLecture?.sub_title :
3838
view === 'settings' ? 'Settings' :
39+
view === 'subscriptions' ? '编辑订阅' :
3940
'iCourse'
4041
"></h1>
4142
</div>
@@ -63,6 +64,17 @@
6364
</svg>
6465
Search
6566
</button>
67+
<button @click="openSubscriptions()"
68+
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
69+
:class="view === 'subscriptions'
70+
? 'bg-blue-50 text-blue-600'
71+
: 'text-gray-500 hover:text-gray-800 hover:bg-gray-100'">
72+
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
73+
<path stroke-linecap="round" stroke-linejoin="round"
74+
d="M19 11H5m14-4H5m14 8H5m14 4H5"/>
75+
</svg>
76+
订阅
77+
</button>
6678
<button @click="openSettings()"
6779
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
6880
:class="view === 'settings'
@@ -102,7 +114,7 @@
102114
</header>
103115

104116
<!-- Mobile bottom nav (hidden on desktop) -->
105-
<nav x-show="['courses','lectures','detail','search','settings'].includes(view)"
117+
<nav x-show="['courses','lectures','detail','search','settings','subscriptions'].includes(view)"
106118
class="md:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 safe-bottom z-30">
107119
<div class="flex justify-around py-2">
108120
<button @click="_go('courses')" class="flex flex-col items-center gap-0.5 px-4 py-1"
@@ -120,6 +132,14 @@
120132
</svg>
121133
<span class="text-[10px]">Search</span>
122134
</button>
135+
<button @click="openSubscriptions()" class="flex flex-col items-center gap-0.5 px-4 py-1"
136+
:class="view === 'subscriptions' ? 'text-blue-600' : 'text-gray-400'">
137+
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
138+
<path stroke-linecap="round" stroke-linejoin="round"
139+
d="M19 11H5m14-4H5m14 8H5m14 4H5"/>
140+
</svg>
141+
<span class="text-[10px]">订阅</span>
142+
</button>
123143
<button @click="openSettings()" class="flex flex-col items-center gap-0.5 px-4 py-1"
124144
:class="view === 'settings' ? 'text-blue-600' : 'text-gray-400'">
125145
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
@@ -255,9 +275,17 @@ <h2 class="text-xl font-bold text-gray-800 mb-1">iCourse Subscriber</h2>
255275
<button @click="openCourse(c.course_id)"
256276
class="w-full text-left bg-white rounded-xl shadow-sm border border-gray-100 p-4 hover:shadow-md transition-shadow active:bg-gray-50">
257277
<div class="flex items-start justify-between gap-3">
258-
<div class="min-w-0">
259-
<h3 class="font-semibold text-gray-800 truncate" x-text="c.title"></h3>
260-
<p class="text-sm text-gray-500 mt-0.5" x-text="c.teacher"></p>
278+
<div class="min-w-0 flex items-start gap-2">
279+
<button @click.stop="toggleStar(c.course_id)"
280+
class="flex-shrink-0 mt-0.5 text-lg leading-none"
281+
:class="isStarred(c.course_id) ? 'text-amber-400' : 'text-gray-300 hover:text-amber-400'"
282+
:title="isStarred(c.course_id) ? '取消置顶' : '置顶'">
283+
<span x-text="isStarred(c.course_id) ? '★' : '☆'"></span>
284+
</button>
285+
<div class="min-w-0">
286+
<h3 class="font-semibold text-gray-800 truncate" x-text="c.title"></h3>
287+
<p class="text-sm text-gray-500 mt-0.5" x-text="c.teacher"></p>
288+
</div>
261289
</div>
262290
<div class="flex flex-col items-end flex-shrink-0">
263291
<span class="text-xs font-medium bg-blue-50 text-blue-700 px-2 py-0.5 rounded-full"
@@ -348,6 +376,27 @@ <h4 class="font-medium text-gray-800 text-sm truncate" x-text="lec.sub_title ||
348376
</template>
349377
</div>
350378

379+
<!-- Prev / next lecture nav (foot of detail view). Wraps within the
380+
current course's lecture list, in the same order the lectures
381+
page renders. Hidden on the only/last/first lecture as
382+
appropriate; respects sub_id ordering. -->
383+
<nav class="mt-8 pt-4 border-t border-gray-100 flex items-stretch gap-2 text-sm">
384+
<button @click="gotoPrevLecture()"
385+
:disabled="!prevLecture()"
386+
class="flex-1 text-left px-3 py-2 rounded-lg border border-gray-200 hover:bg-gray-50 disabled:opacity-40 disabled:hover:bg-transparent">
387+
<div class="text-xs text-gray-400">← 上一节</div>
388+
<div class="truncate text-gray-700"
389+
x-text="prevLecture()?.sub_title || '(无)'"></div>
390+
</button>
391+
<button @click="gotoNextLecture()"
392+
:disabled="!nextLecture()"
393+
class="flex-1 text-right px-3 py-2 rounded-lg border border-gray-200 hover:bg-gray-50 disabled:opacity-40 disabled:hover:bg-transparent">
394+
<div class="text-xs text-gray-400">下一节 →</div>
395+
<div class="truncate text-gray-700"
396+
x-text="nextLecture()?.sub_title || '(无)'"></div>
397+
</button>
398+
</nav>
399+
351400
<!-- Mobile cycle button (FAB-style) -->
352401
<button x-show="currentLecture"
353402
@click="cycleDetailView()"
@@ -377,14 +426,104 @@ <h4 class="font-medium text-gray-800 text-sm truncate" x-text="lec.sub_title ||
377426
<template x-for="r in searchResults" :key="r.sub_id">
378427
<button @click="openLecture(r.sub_id)"
379428
class="w-full text-left bg-white rounded-lg border border-gray-100 p-3 hover:shadow-md transition-shadow active:bg-gray-50">
380-
<p class="text-xs text-blue-600 font-medium" x-text="r.course_title"></p>
429+
<div class="flex items-center justify-between gap-2">
430+
<p class="text-xs text-blue-600 font-medium truncate" x-text="r.course_title"></p>
431+
<span class="text-[10px] uppercase tracking-wide text-gray-400 flex-shrink-0"
432+
x-text="r.hit_field === 'transcript' ? '转录命中'
433+
: r.hit_field === 'sub_title' ? '标题命中'
434+
: '摘要命中'"></span>
435+
</div>
381436
<h4 class="font-medium text-gray-800 text-sm mt-0.5" x-text="r.sub_title || 'Untitled'"></h4>
382-
<p class="text-xs text-gray-400 mt-1 line-clamp-2" x-html="highlight(r.summary, searchQuery)"></p>
437+
<p class="text-xs text-gray-400 mt-1 line-clamp-2"
438+
x-html="highlight(r.hit_field === 'transcript' ? r.transcript : r.summary, searchQuery)"></p>
383439
</button>
384440
</template>
385441
</div>
386442
</div>
387443

444+
<!-- ─── SUBSCRIPTIONS (course list + checkbox + save to GitHub Secret) ─── -->
445+
<div x-show="view === 'subscriptions'" class="view-enter max-w-2xl mx-auto p-4">
446+
447+
<!-- Empty catalog message -->
448+
<div x-show="allCourses.length === 0"
449+
class="bg-yellow-50 border border-yellow-200 text-yellow-800 rounded-lg p-4 text-sm leading-relaxed">
450+
课程目录为空。请在仓库的 Actions secrets 里设置 <code class="bg-yellow-100 px-1 rounded">CRAWL_TERM</code>
451+
(例如 <code class="bg-yellow-100 px-1 rounded">25</code>)并跑一次 workflow 来抓取学期课程目录。
452+
</div>
453+
454+
<template x-if="allCourses.length > 0">
455+
<div>
456+
<!-- Term selector + search -->
457+
<div class="flex flex-col sm:flex-row gap-2 mb-3">
458+
<select x-model="subsTerm" @change="rebuildSubsFiltered()"
459+
class="border rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-300 focus:outline-none">
460+
<template x-for="t in allCoursesTerms" :key="t">
461+
<option :value="t" x-text="'Term ' + t"></option>
462+
</template>
463+
</select>
464+
<input x-model="subsSearch" @input="rebuildSubsFiltered()" type="text"
465+
placeholder="搜索课程名 / 老师 / 院系 / ID…"
466+
class="flex-1 border rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-300 focus:outline-none">
467+
</div>
468+
469+
<p class="text-xs text-gray-500 mb-3">
470+
已选 <span class="font-semibold text-blue-600" x-text="subsSelectedIds.length"></span>
471+
/ 目录共 <span x-text="allCoursesForTerm.length"></span> 门课。
472+
<span class="text-gray-400">(点击复选框切换订阅;保存后会写入 GitHub Secret <code class="bg-gray-100 px-1 rounded">COURSE_IDS</code>。)</span>
473+
</p>
474+
475+
<!-- Course list -->
476+
<div class="bg-white border border-gray-100 rounded-lg overflow-hidden divide-y divide-gray-100 max-h-[60vh] overflow-y-auto">
477+
<template x-for="c in subsFiltered" :key="c.course_id + '@' + c.term">
478+
<label class="flex items-start gap-3 px-3 py-2 hover:bg-gray-50 cursor-pointer">
479+
<input type="checkbox" class="mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-400"
480+
:checked="subsSelectedIds.includes(c.course_id)"
481+
@change="toggleSubscription(c.course_id, $event.target.checked)">
482+
<div class="min-w-0 flex-1">
483+
<p class="text-sm font-medium text-gray-800 truncate" x-text="c.title || '(no title)'"></p>
484+
<p class="text-xs text-gray-500 mt-0.5">
485+
<span x-text="c.teacher || '—'"></span>
486+
<span x-show="c.dept" class="text-gray-400"> · <span x-text="c.dept"></span></span>
487+
<span class="text-gray-400"> · ID <span x-text="c.course_id"></span></span>
488+
</p>
489+
</div>
490+
</label>
491+
</template>
492+
<p x-show="subsFiltered.length === 0" class="text-center text-gray-400 text-sm py-6">
493+
没有匹配的课程。
494+
</p>
495+
</div>
496+
497+
<!-- Save error -->
498+
<p x-show="subsError" x-text="subsError"
499+
class="text-sm text-red-600 bg-red-50 rounded-lg p-3 mt-3"></p>
500+
501+
<!-- Action buttons -->
502+
<div class="mt-4 flex flex-col sm:flex-row sm:items-center gap-2">
503+
<button @click="saveSubscriptions()" :disabled="subsSaving"
504+
class="flex-1 bg-blue-500 text-white py-2.5 rounded-lg font-medium hover:bg-blue-600 disabled:opacity-50 flex items-center justify-center gap-2">
505+
<div x-show="subsSaving" class="spinner !w-4 !h-4 !border-2 !border-white/30 !border-t-white"></div>
506+
<span x-text="subsSaving ? '加密上传中…' : '保存到 GitHub Secret'"></span>
507+
</button>
508+
<button @click="resetSubscriptionsToCurrent()" :disabled="subsSaving"
509+
class="text-sm text-gray-500 underline disabled:opacity-50">
510+
重置为当前已订阅
511+
</button>
512+
</div>
513+
<button @click="triggerCheckRun()" :disabled="triggeringCheck"
514+
class="w-full mt-2 bg-gray-100 text-gray-700 py-2 rounded-lg text-sm font-medium hover:bg-gray-200 disabled:opacity-50 flex items-center justify-center gap-2">
515+
<div x-show="triggeringCheck" class="spinner !w-4 !h-4 !border-2 !border-gray-400/30 !border-t-gray-700"></div>
516+
<span x-text="triggeringCheck ? '触发中…' : '立即跑一次 workflow(使用当前 secret)'"></span>
517+
</button>
518+
<p class="text-xs text-gray-400 mt-2 leading-relaxed">
519+
保存需要你的 GitHub PAT 具备 <strong>Secrets: Read and write</strong> 权限;
520+
立即运行需要 <strong>Actions: Read and write</strong>
521+
COURSE_IDS 会被 libsodium sealed box 加密后才离开浏览器。
522+
</p>
523+
</div>
524+
</template>
525+
</div>
526+
388527
<!-- ─── SETTINGS ─── -->
389528
<div x-show="view === 'settings'" class="view-enter max-w-md mx-auto p-6">
390529
<h2 class="text-lg font-bold text-gray-800 mb-4">Credentials</h2>
@@ -525,6 +664,8 @@ <h3 class="font-semibold text-gray-800">导出课程摘要</h3>
525664
<!-- KaTeX + auto-render -->
526665
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js"></script>
527666
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/contrib/auto-render.min.js"></script>
667+
<!-- libsodium-wrappers (sealed-box encryption for GitHub Secrets PUT) -->
668+
<script src="https://cdn.jsdelivr.net/npm/libsodium-wrappers@0.7.13/dist/browsers/sodium.js"></script>
528669
<!-- App scripts (defer, in order, before Alpine) -->
529670
<script defer src="js/crypto.js"></script>
530671
<script defer src="js/github.js"></script>

0 commit comments

Comments
 (0)