From f7d2074dddbb7d616e4d707eca80b2de05174650 Mon Sep 17 00:00:00 2001 From: techotaku39 Date: Sun, 24 May 2026 11:57:59 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(auth):=20=E6=96=B0=E5=A2=9E=E8=87=AA?= =?UTF-8?q?=E6=89=98=E7=AE=A1=E9=89=B4=E6=9D=83=E4=B8=8E=E7=AC=94=E8=AE=B0?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 9 + BillNote_extension/src/background/main.ts | 5 +- BillNote_extension/src/logic/api.ts | 66 +++++- BillNote_extension/src/logic/constants.ts | 1 + BillNote_extension/src/logic/types.ts | 14 ++ .../src/options/pages/General.vue | 62 +++++- BillNote_extension/src/popup/Popup.vue | 9 +- .../src/sidepanel/Sidepanel.vue | 9 +- BillNote_frontend/src/App.tsx | 57 ++--- BillNote_frontend/src/components/AuthGate.tsx | 92 +++++++++ BillNote_frontend/src/hooks/useTaskPolling.ts | 2 - BillNote_frontend/src/pages/HomePage/Home.tsx | 11 + .../HomePage/components/MarkdownViewer.tsx | 3 +- .../pages/HomePage/components/NoteHistory.tsx | 22 +- .../pages/HomePage/components/VideoBanner.tsx | 5 +- BillNote_frontend/src/services/auth.ts | 40 ++++ BillNote_frontend/src/services/note.ts | 20 +- .../src/store/taskStore/index.ts | 105 +++++++++- BillNote_frontend/src/utils/request.ts | 15 ++ Dockerfile.complete | 4 +- README.md | 20 +- backend/.env.example | 9 +- backend/app/__init__.py | 3 +- backend/app/db/video_task_dao.py | 19 +- backend/app/middlewares/__init__.py | 1 + backend/app/middlewares/auth.py | 39 ++++ backend/app/routers/auth.py | 55 +++++ backend/app/routers/note.py | 49 +++-- backend/app/services/auth.py | 116 +++++++++++ backend/app/services/note_storage.py | 195 ++++++++++++++++++ backend/main.py | 7 +- 31 files changed, 988 insertions(+), 76 deletions(-) create mode 100644 BillNote_frontend/src/components/AuthGate.tsx create mode 100644 BillNote_frontend/src/services/auth.ts create mode 100644 backend/app/middlewares/__init__.py create mode 100644 backend/app/middlewares/auth.py create mode 100644 backend/app/routers/auth.py create mode 100644 backend/app/services/auth.py create mode 100644 backend/app/services/note_storage.py diff --git a/.env.example b/.env.example index 3589634a..1bb489de 100644 --- a/.env.example +++ b/.env.example @@ -31,6 +31,15 @@ NOTE_OUTPUT_DIR=note_results IMAGE_BASE_URL=/static/screenshots DATA_DIR=data +# 自托管访问鉴权(默认关闭) +# 公网部署建议开启。开启后 Web 端会显示访问密码登录页,浏览器插件需要在设置页登录。 +BILINOTE_AUTH_ENABLED=false +# BILINOTE_AUTH_PASSWORD=please-change-me +# 可选:固定 token 签名密钥;不填时会从访问密码派生,改密码会让旧登录失效。 +# BILINOTE_AUTH_SECRET= +# 登录有效期(天) +BILINOTE_AUTH_TOKEN_EXPIRE_DAYS=30 + # FFMPEG 配置(Docker 镜像已内置 ffmpeg,留空即可;自建/桌面端可填绝对路径) FFMPEG_BIN_PATH= diff --git a/BillNote_extension/src/background/main.ts b/BillNote_extension/src/background/main.ts index 7883e316..d8925f3a 100644 --- a/BillNote_extension/src/background/main.ts +++ b/BillNote_extension/src/background/main.ts @@ -74,7 +74,10 @@ async function startTask(url: string, title?: string): Promise<{ ok: boolean, ta try { const res = await fetch(`${backend}/api/generate_note`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + ...(settings.authToken ? { Authorization: `Bearer ${settings.authToken}` } : {}), + }, body: JSON.stringify({ video_url: url, platform, diff --git a/BillNote_extension/src/logic/api.ts b/BillNote_extension/src/logic/api.ts index 9f935ea2..a61f020f 100644 --- a/BillNote_extension/src/logic/api.ts +++ b/BillNote_extension/src/logic/api.ts @@ -5,6 +5,7 @@ import type { Provider, ProviderCreatePayload, ProviderUpdatePayload, + ServerNote, TaskStatusResponse, TranscriberConfig, TranscriberModelsStatus, @@ -25,7 +26,7 @@ function backendUrl(): string { async function request(path: string, init?: RequestInit): Promise { const res = await fetch(`${backendUrl()}${path}`, { - headers: { 'Content-Type': 'application/json', ...(init?.headers || {}) }, + headers: { 'Content-Type': 'application/json', ...authHeaders(), ...(init?.headers || {}) }, ...init, }) if (!res.ok) @@ -41,6 +42,35 @@ async function request(path: string, init?: RequestInit): Promise { return body as T } +function authHeaders(): Record { + const token = settings.value?.authToken + return token ? { Authorization: `Bearer ${token}` } : {} +} + +function withAccessToken(url: string): string { + const token = settings.value?.authToken + if (!token) + return url + const sep = url.includes('?') ? '&' : '?' + return `${url}${sep}access_token=${encodeURIComponent(token)}` +} + +export async function getAuthStatus(): Promise<{ enabled: boolean, authenticated: boolean }> { + return request('/api/auth/status') +} + +export async function login(password: string): Promise { + const data = await request<{ token: string }>('/api/auth/login', { + method: 'POST', + body: JSON.stringify({ password }), + }) + settings.value.authToken = data.token || '' +} + +export async function listNotes(): Promise { + return request('/api/notes') +} + export async function getProviders(): Promise { return request('/api/get_all_providers') } @@ -189,7 +219,9 @@ export async function getTaskStatus(taskId: string): Promise // 成功:{code:0, data:{status, message, task_id, result?}} // 任务失败:{code:500, msg:'xxx', data:null} // 这里手动拆,把任务失败翻译成 status:'FAILED',避免 request() 抛错让 UI 收不到状态 - const res = await fetch(`${backendUrl()}/api/task_status/${taskId}`) + const res = await fetch(`${backendUrl()}/api/task_status/${taskId}`, { + headers: authHeaders(), + }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const body = (await res.json()) as { code: number, msg: string, data: TaskStatusResponse | null } @@ -211,7 +243,10 @@ export async function ping(): Promise { // markdown 里的 /static/screenshots/xxx 是相对路径,extension 渲染时需要拼绝对地址 export function absolutizeMarkdownImages(md: string): string { const base = backendUrl() - return md.replace(/!\[([^\]]*)\]\((\/static\/[^)]+)\)/g, (_, alt, path) => `![${alt}](${base}${path})`) + return md.replace( + /!\[([^\]]*)\]\((\/static\/[^)]+)\)/g, + (_, alt, path) => `![${alt}](${withAccessToken(`${base}${path}`)})`, + ) } // backend 用 note_helper 在笔记开头插一行 '> 来源链接:URL'。侧边栏顶部已经有原片链接卡片, @@ -227,9 +262,30 @@ export function resolveImageUrl(url: string | undefined | null): string { return '' const base = backendUrl() if (url.startsWith('/')) - return `${base}${url}` + return withAccessToken(`${base}${url}`) // B 站封面、抖音封面等会做 referer 校验;走后端代理 if (/(hdslb|byteimg|kpcdn|akamaized|ytimg)\.com/i.test(url)) - return `${base}/api/image_proxy?url=${encodeURIComponent(url)}` + return withAccessToken(`${base}/api/image_proxy?url=${encodeURIComponent(url)}`) return url } + +export function serverNoteToTask(note: ServerNote): import('./types').TaskRecord { + const formData = note.form_data || {} + return { + taskId: note.task_id, + videoUrl: formData.video_url || '', + platform: (formData.platform || note.audio_meta?.platform || 'bilibili') as import('./types').Platform, + status: note.status, + message: note.message || '', + createdAt: new Date(note.created_at || Date.now()).getTime(), + updatedAt: new Date(note.updated_at || note.created_at || Date.now()).getTime(), + result: note.markdown + ? { + markdown: note.markdown, + transcript: note.transcript, + audio_meta: note.audio_meta, + } + : undefined, + title: note.audio_meta?.title, + } +} diff --git a/BillNote_extension/src/logic/constants.ts b/BillNote_extension/src/logic/constants.ts index e2dac9df..4d81ec07 100644 --- a/BillNote_extension/src/logic/constants.ts +++ b/BillNote_extension/src/logic/constants.ts @@ -4,6 +4,7 @@ export const DEFAULT_BACKEND_URL = 'http://localhost:8483' export const DEFAULT_SETTINGS: Settings = { backendUrl: DEFAULT_BACKEND_URL, + authToken: '', providerId: '', modelName: '', quality: 'medium', diff --git a/BillNote_extension/src/logic/types.ts b/BillNote_extension/src/logic/types.ts index b729ae8a..60dd663a 100644 --- a/BillNote_extension/src/logic/types.ts +++ b/BillNote_extension/src/logic/types.ts @@ -83,6 +83,19 @@ export interface TaskRecord { title?: string } +export interface ServerNote { + id: string + task_id: string + status: TaskStatus + message?: string + created_at: string + updated_at: string + markdown: string + transcript?: unknown + audio_meta?: NoteResult['audio_meta'] + form_data?: Partial +} + // 与 backend/app/gpt/prompt_builder.py note_styles 一一对齐 export type NoteStyle = | 'minimal' | 'detailed' | 'academic' | 'tutorial' @@ -113,6 +126,7 @@ export const NOTE_FORMATS: Array<{ value: NoteFormat, label: string }> = [ export interface Settings { backendUrl: string + authToken: string providerId: string modelName: string quality: Quality diff --git a/BillNote_extension/src/options/pages/General.vue b/BillNote_extension/src/options/pages/General.vue index aa28c501..b1e603c5 100644 --- a/BillNote_extension/src/options/pages/General.vue +++ b/BillNote_extension/src/options/pages/General.vue @@ -1,6 +1,6 @@ @@ -347,9 +417,47 @@ onUnmounted(() => { {{ (t.result?.audio_meta as { title?: string } | undefined)?.title || t.title || t.videoUrl }} {{ t.status }} + + +
+
+
+ 确认删除这个任务? +
+
+ {{ pendingDeleteTitle }} +
+
+ + +
+
+
diff --git a/BillNote_extension/src/sidepanel/Sidepanel.vue b/BillNote_extension/src/sidepanel/Sidepanel.vue index 7d4dbb4d..e30b130f 100644 --- a/BillNote_extension/src/sidepanel/Sidepanel.vue +++ b/BillNote_extension/src/sidepanel/Sidepanel.vue @@ -1,7 +1,7 @@ @@ -155,6 +216,13 @@ onUnmounted(() => { {{ (t.result?.audio_meta as { title?: string } | undefined)?.title || t.title || t.videoUrl }} {{ STAGE_LABELS[t.status] || t.status }} + @@ -197,6 +265,13 @@ onUnmounted(() => { v-else class="text-xs px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 shrink-0 animate-pulse" >{{ STAGE_LABELS[activeTask.status] || activeTask.status }} + @@ -260,6 +335,37 @@ onUnmounted(() => { + +
+
+
+ 确认删除这个任务? +
+
+ {{ pendingDeleteTitle }} +
+
+ + +
+
+