23 Commits

Author SHA1 Message Date
huangjianwu
2e3fda7df4 feat: 新增 BiliNote AI笔记系统一对一搭建服务二维码(README + 关于页),bump 版本至 2.3.4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 02:51:15 +08:00
Jianwu Huang
2ea95b2fad Merge pull request #382 from techotaku39/fix/frontend-about-version
fix(frontend): 同步关于页版本号
2026-05-27 02:37:03 +08:00
techotaku39
9d1a7cd699 fix(frontend): 同步关于页版本号 2026-05-26 15:54:05 +08:00
Jianwu Huang
f2d8ece0c1 Merge pull request #374 from techotaku39/fix/backend-api-key-mask-and-ffmpeg-path
fix(backend): 防御 API Key 掩码污染并修复 EXE 版 .env 加载路径
2026-05-25 13:21:38 +08:00
Jianwu Huang
7bc4b0114e Merge pull request #375 from techotaku39/feature/extension-show-title-instead-of-url
feat(extension): 侧边栏与 popup 用视频标题替代链接显示
2026-05-25 13:21:11 +08:00
Jianwu Huang
f7ea6f72d9 Merge pull request #377 from techotaku39/fix/markdown-anchor-navigation
fix(frontend): 修复 Markdown 目录锚点跳转与 Tauri 路由
2026-05-25 13:20:19 +08:00
techotaku39
905dbcce47 fix(frontend): 增强锚点链接模糊匹配,兼容 LLM 生成的不一致目录格式 2026-05-24 03:21:20 +08:00
techotaku39
ebdb254fc6 fix(frontend): 修复 Markdown 目录锚点跳转与 Tauri 路由
- 安装 rehype-slug 插件,自动为 heading 生成 id,解决目录链接无锚点目标的问题
- 自定义 <a> 组件处理内部锚点链接,阻止默认刷新行为并使用 scrollIntoView 平滑滚动
- Tauri 桌面端改用 HashRouter,避免刷新时 404 及错误打开外部浏览器

fixes #xxx

Co-Authored-By: HAPI <noreply@hapi.run>
2026-05-24 03:21:20 +08:00
techotaku39
1eb213e215 feat(extension): 侧边栏与 popup 用视频标题替代链接显示
在任务未完成的早期阶段(PENDING/DOWNLOADING 等),侧边栏和 popup
只能回退到 videoUrl,用户看到的是一长串链接,难以辨认。

改动:
- TaskRecord 新增 title 字段,用于存储浏览器标签页标题
- popup 创建任务时保存 tab.title
- background startTask 接收可选 title,右键菜单和悬浮按钮均传入
- 显示优先级:result.audio_meta.title > title > videoUrl
- 所有平台(Bilibili / YouTube / Douyin / Kuaishou)均受益

测试:
- pnpm typecheck 通过
- pnpm build 通过
- 在 B 站、YouTube 视频页提交任务,侧边栏和 popup 均显示标题而非链接
2026-05-24 00:06:21 +08:00
techotaku39
4425239717 fix(backend): 防御 API Key 掩码污染并修复 EXE 版 .env 加载路径
- provider.py: 更新供应商时,若 api_key 包含 '*'(掩码字符),
  跳过该字段,防止前端展示用的 mask_key() 值被误写入数据库。

- ffmpeg_helper.py: load_dotenv() 默认只从 CWD 查找 .env,
  PyInstaller 打包后 CWD 为 EXE 目录,导致 _internal/.env 被忽略。
  改为遍历多个候选路径(CWD、脚本目录、项目根目录、_internal/),
  确保源码和打包两种场景都能正确加载环境变量。
2026-05-23 22:49:56 +08:00
huangjianwu
717df2af7b Merge branch 'release/2.3.3' into develop
回合并 v2.3.3 版本号。
2026-05-22 14:07:00 +08:00
huangjianwu
db556b8991 Merge branch 'release/2.3.3'
v2.3.3:预构建镜像持久化数据库/笔记,修升级丢配置与历史。
2026-05-22 14:07:00 +08:00
huangjianwu
b431db545a docs: v2.3.3 CHANGELOG + README + tauri 版本
Docker 镜像数据持久化修复版本。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:07:00 +08:00
huangjianwu
25face4b67 Merge branch 'fix/docker-data-persistence' into develop
v2.3.3:预构建镜像持久化数据库/笔记,修升级丢配置与历史。
2026-05-22 14:06:24 +08:00
huangjianwu
edfd6e4765 fix(docker): 预构建镜像持久化数据库/笔记,修升级丢配置与历史
README 的 docker run 只挂了 -v …:/app/backend/data(仅媒体缓存),但
SQLite 库在 /app/backend/bili_note.db、笔记在 /app/backend/note_results
都不在该卷下 → 用户删容器重建(如 docker pull 升级镜像)会丢失 LLM
供应商配置和全部笔记历史。

- Dockerfile.complete:DATABASE_URL 重定向到 /app/backend/data/bili_note.db、
  NOTE_OUTPUT_DIR 改到 data/note_results,并预建 /app/backend/data 目录;
  两条 supervisord environment 行同步(兜底默认值 + %(ENV_*)s 透传)。
- README:docker run 改挂 data(库+笔记)/ config(供应商·Cookie·转写配置)/
  static(截图)/ models(模型缓存,可选)四个数据卷,并警告不要挂整个
  /app/backend(命名卷会固化镜像内代码,导致 docker pull 升级不生效)。
- docker-build.yml:同步发布说明里 echo 的 run 命令。

截图仍留在 /app/backend/static(static_dir 硬编码服务,不能重定向),单独挂卷。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:30:42 +08:00
huangjianwu
b53cafda5a Merge branch 'release/2.3.2' into develop
回合并 v2.3.2 版本号。
2026-05-22 11:41:17 +08:00
huangjianwu
adda5fd240 Merge branch 'release/2.3.2'
v2.3.2:修后端启动崩溃(ctranslate2 4.6.0) + whisper 模型路径 + 桌面端版本号。
2026-05-22 11:41:04 +08:00
huangjianwu
3e28f1fe38 docs: v2.3.2 CHANGELOG + README + tauri 版本
docker 启动崩溃 / whisper 模型路径 / 桌面端版本号 修复版本。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:40:55 +08:00
huangjianwu
bffa285cd0 Merge branch 'fix/docker-startup-and-qr' into develop
v2.3.2 修复集:
- ctranslate2 4.5.0→4.6.0 修后端启动崩溃(可执行栈 / glibc 2.41)
- whisper 模型下载/加载统一 HF cache 布局
- 桌面端构建版本号从 tag 注入(修产物恒为 2.0.0)
2026-05-22 11:40:21 +08:00
huangjianwu
b740e70068 fix(desktop): 构建时从 tag 注入版本号,修产物版本恒为 2.0.0
桌面端构建产物(.dmg/.msi 文件名 + app 内部版本)一直是 2.0.0:
Tauri 取 tauri.conf.json 的静态 version 字段作为产物版本,而 Release
工作流只把 tag 名用作 Release 标题,没同步到 conf → 产物版本与 Release
版本错位。

在 pnpm tauri build 前新增 Sync version 步骤,从 github.ref_name(形如
v2.3.2,去掉前缀 v)注入版本到 tauri.conf.json。以后每次 tag 发版自动
对齐;workflow_dispatch 手动构建无 tag 时跳过,保留静态值不破坏。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:40:04 +08:00
huangjianwu
261c95cf12 fix(transcriber): whisper 模型下载/加载统一走 HF cache 布局
此前用 modelscope 下到自定义目录 whisper-{size}/ 再把该路径传给
WhisperModel。但 faster-whisper 1.1.1 只要 path 含 '/' 就当成 HF
repo_id 处理,没有「本地目录直接返回」分支 → 在线请求失败后 fallback
local_files_only,又因 modelscope 布局命不中 HF cache → LocalEntryNotFound,
误导用户以为是「离线模式」。

改为下载与加载路径对齐:
- 下载:huggingface_hub.snapshot_download(cache_dir=model_dir),落到 HF
  cache 布局 models--Systran--faster-whisper-{size}/snapshots/<hash>/
- 加载:WhisperModel(model_size_or_path=size, download_root=model_dir),
  让 faster-whisper 自己映射到 Systran/faster-whisper-* 并命中同一 cache
- 完整性检测 / 损坏自愈(_purge_cache) 同步按 HF cache 布局,并兼容老
  modelscope 目录(向后兼容已下载的老用户)

HF_ENDPOINT 已在 Dockerfile 指向 hf-mirror.com,国内可用。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:27:03 +08:00
huangjianwu
1cc7f38e14 fix(backend): 升级 ctranslate2 4.5.0→4.6.0 修复后端启动崩溃
Docker 容器反复重启,启动即报:
  ImportError: libctranslate2-*.so.4.5.0: cannot enable executable
  stack as shared object requires: Invalid argument

根因:ctranslate2 4.5.0 预编译 wheel 把共享库标记为「需要可执行栈」,
新内核 / glibc 2.41+ 加载时拒绝并返回 EINVAL。faster-whisper 在
whisper.py 顶层 import,import 失败直接拖垮整个后端启动 → 重启死循环。

ctranslate2 4.6.0 加入 noexecstack 链接标志(OpenNMT/CTranslate2 #1852、
#1861)从 wheel 层根治。faster-whisper 1.1.1 依赖 ctranslate2<5,>=4.0,
4.6.0 兼容;同时覆盖 web / GPU / 桌面 三条构建链。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:26:49 +08:00
huangjianwu
7fffd6873b Merge branch 'release/2.3.1' into develop
回合并 v2.3.1 版本号(CHANGELOG + README)。
2026-05-22 10:51:49 +08:00
25 changed files with 413 additions and 163 deletions

View File

@@ -42,6 +42,16 @@ jobs:
type=sha,prefix=
type=raw,value=latest,enable={{is_default_branch}}
- name: Resolve app version
id: app-version
shell: bash
run: |
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
else
echo "version=" >> "$GITHUB_OUTPUT"
fi
- name: Build and Push Docker Image
uses: docker/build-push-action@v6
with:
@@ -50,6 +60,8 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VITE_APP_VERSION=${{ steps.app-version.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
@@ -66,6 +78,9 @@ jobs:
echo "Run the container:"
echo " docker run -d -p 80:80 \\"
echo " -v bilinote-data:/app/backend/data \\"
echo " -v bilinote-config:/app/backend/config \\"
echo " -v bilinote-static:/app/backend/static \\"
echo " -v bilinote-models:/app/backend/models \\"
echo " --name bilinote \\"
echo " ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
echo ""

View File

@@ -79,6 +79,19 @@ jobs:
key: ${{ runner.os }}-cargo-${{ hashFiles('BillNote_frontend/src-tauri/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
# 从 tag 注入版本号到 tauri.conf.jsonTauri 取该文件的静态 version 作为
# 产物版本,不同步的话构建产物会恒为 conf 里写死的值(此前的 2.0.0)。
# github.ref_name 形如 v2.3.2,去掉前缀 v。workflow_dispatch无 tag时跳过保留静态值。
- name: Sync version from tag
if: startsWith(github.ref, 'refs/tags/v')
working-directory: BillNote_frontend
shell: bash
run: |
VERSION="${GITHUB_REF_NAME#v}"
echo "Injecting version $VERSION into tauri.conf.json"
node -e "const f='src-tauri/tauri.conf.json'; const fs=require('fs'); const j=JSON.parse(fs.readFileSync(f,'utf8')); j.version=process.argv[1]; fs.writeFileSync(f, JSON.stringify(j,null,2)+'\n');" "$VERSION"
node -e "console.log('tauri.conf.json version =', require('./src-tauri/tauri.conf.json').version)"
# 打包 Tauri 应用
- name: Build Tauri App
working-directory: BillNote_frontend

View File

@@ -56,7 +56,7 @@ async function upsertTask(record: TaskRecord) {
// ---------- 启动任务 ----------
async function startTask(url: string): Promise<{ ok: boolean, taskId?: string, error?: string }> {
async function startTask(url: string, title?: string): Promise<{ ok: boolean, taskId?: string, error?: string }> {
const platform = detectPlatform(url)
if (!platform)
return { ok: false, error: '当前链接不是支持的视频平台' }
@@ -107,6 +107,7 @@ async function startTask(url: string): Promise<{ ok: boolean, taskId?: string, e
message: '已提交',
createdAt: Date.now(),
updatedAt: Date.now(),
title,
})
return { ok: true, taskId: body.data.task_id }
}
@@ -129,8 +130,8 @@ async function openSidePanelInTab(tabId?: number) {
// ---------- 消息桥 ----------
onMessage<{ url: string }, 'bilinote-start'>('bilinote-start', async ({ data, sender }) => {
const result = await startTask(data.url)
onMessage<{ url: string; title?: string }, 'bilinote-start'>('bilinote-start', async ({ data, sender }) => {
const result = await startTask(data.url, data.title)
// 成功就把侧边栏拉起来给用户看进度
if (result.ok)
await openSidePanelInTab(sender?.tabId)
@@ -168,7 +169,7 @@ browser.contextMenus?.onClicked.addListener(async (info, tab) => {
const url = info.linkUrl || tab?.url
if (!url)
return
const result = await startTask(url)
const result = await startTask(url, tab?.title)
if (result.ok)
await openSidePanelInTab(tab?.id)
else

View File

@@ -19,6 +19,7 @@ async function trigger() {
const res = await sendMessage('bilinote-start', {
url: window.location.href,
platform,
title: document.title,
}, 'background')
const ok = res && (res as any).ok
toast.value = ok

View File

@@ -79,6 +79,8 @@ export interface TaskRecord {
createdAt: number
updatedAt: number
result?: NoteResult
// 从浏览器 tab.title 抓取,任务完成前用来替代 videoUrl 显示
title?: string
}
// 与 backend/app/gpt/prompt_builder.py note_styles 一一对齐

View File

@@ -43,6 +43,7 @@ async function poll(taskId: string) {
createdAt: activeTask.value?.createdAt ?? Date.now(),
updatedAt: Date.now(),
result: res.result ?? activeTask.value?.result,
title: activeTask.value?.title,
})
if (res.status !== 'SUCCESS' && res.status !== 'FAILED')
pollTimer = setTimeout(() => poll(taskId), 3000)
@@ -94,6 +95,7 @@ async function start() {
message: '已提交',
createdAt: Date.now(),
updatedAt: Date.now(),
title: tabTitle.value || undefined,
})
poll(task_id)
// 提交后顺手把侧边栏拉起来,免得用户来回切窗口
@@ -142,7 +144,10 @@ function selectTask(id: string) {
}
const activeCover = computed(() => activeTask.value?.result?.audio_meta?.cover_url as string | undefined)
const activeTitle = computed(() => (activeTask.value?.result?.audio_meta?.title as string | undefined) || tabTitle.value)
const activeTitle = computed(() =>
(activeTask.value?.result?.audio_meta?.title as string | undefined)
|| activeTask.value?.title
|| tabTitle.value)
function fmtTime(ts?: number) {
if (!ts)
@@ -331,8 +336,8 @@ onUnmounted(() => {
:class="{ 'bg-blue-50': t.taskId === activeTaskId }"
@click="selectTask(t.taskId)"
>
<span class="truncate flex-1" :title="t.videoUrl">
{{ (t.result?.audio_meta as { title?: string } | undefined)?.title || t.videoUrl }}
<span class="truncate flex-1" :title="t.title || t.videoUrl">
{{ (t.result?.audio_meta as { title?: string } | undefined)?.title || t.title || t.videoUrl }}
</span>
<span class="text-gray-500 shrink-0">{{ t.status }}</span>
</li>

View File

@@ -41,6 +41,7 @@ async function poll(taskId: string) {
message: res.message,
result: res.result ?? cur.result,
updatedAt: Date.now(),
title: cur.title,
})
}
if (res.status !== 'SUCCESS' && res.status !== 'FAILED')
@@ -89,7 +90,10 @@ function downloadMarkdown() {
}
const activeTitle = computed(() =>
(activeTask.value?.result?.audio_meta as { title?: string } | undefined)?.title || activeTask.value?.videoUrl || '')
(activeTask.value?.result?.audio_meta as { title?: string } | undefined)?.title
|| activeTask.value?.title
|| activeTask.value?.videoUrl
|| '')
const activeCover = computed(() =>
(activeTask.value?.result?.audio_meta as { cover_url?: string } | undefined)?.cover_url)
@@ -140,8 +144,8 @@ onUnmounted(() => {
:class="{ 'bg-white border': t.taskId === activeTaskId }"
@click="selectTask(t.taskId)"
>
<span class="truncate flex-1" :title="t.videoUrl">
{{ (t.result?.audio_meta as { title?: string } | undefined)?.title || t.videoUrl }}
<span class="truncate flex-1" :title="t.title || t.videoUrl">
{{ (t.result?.audio_meta as { title?: string } | undefined)?.title || t.title || t.videoUrl }}
</span>
<span class="text-gray-400 shrink-0">{{ STAGE_LABELS[t.status] || t.status }}</span>
</li>
@@ -170,7 +174,7 @@ onUnmounted(() => {
class="text-sm font-medium leading-tight line-clamp-1 break-all flex-1 min-w-0 hover:text-blue-600"
:href="activeTask.videoUrl"
target="_blank"
:title="activeTask.videoUrl"
:title="activeTitle || activeTask.videoUrl"
>{{ activeTitle }}</a>
<span
v-if="isDone"

View File

@@ -5,6 +5,10 @@
ARG BASE_REGISTRY=docker.io
FROM ${BASE_REGISTRY}/library/node:20-alpine AS builder
# 可由发布 workflow 从 git tag 注入,用于前端 About 页展示版本;未传时由 Vite 回退读取 tauri.conf.json。
ARG VITE_APP_VERSION=
ENV VITE_APP_VERSION=${VITE_APP_VERSION}
# pnpm pin 到 9.xlockfile 是 v9 生成pnpm 11 要求 Node 22+ 与 node:20 不兼容
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate

View File

@@ -57,6 +57,7 @@
"react-router-dom": "^7.5.1",
"react-syntax-highlighter": "^15.6.1",
"rehype-katex": "^6.0.2",
"rehype-slug": "5.1.0",
"remark-gfm": "3.0.1",
"remark-math": "^5.1.1",
"sonner": "^2.0.3",

View File

@@ -13,7 +13,7 @@ importers:
version: 2.4.0(antd@5.29.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@hookform/resolvers':
specifier: ^5.0.1
version: 5.2.2(react-hook-form@7.72.0(react@19.2.4))
version: 5.4.0(react-hook-form@7.72.0(react@19.2.4))
'@lobehub/icons':
specifier: ^1.97.1
version: 1.98.0(@babel/core@7.29.0)(@types/mdast@4.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(antd@5.29.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -52,7 +52,7 @@ importers:
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@tailwindcss/vite':
specifier: ^4.1.3
version: 4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
version: 4.2.2(vite@6.4.2(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
'@tauri-apps/api':
specifier: ^2.11.0
version: 2.11.0
@@ -61,7 +61,7 @@ importers:
version: 2.3.5
'@uiw/react-markdown-preview':
specifier: ^5.1.3
version: 5.1.5(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
version: 5.2.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
antd:
specifier: ^5.24.8
version: 5.29.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -76,13 +76,13 @@ importers:
version: 2.1.1
fuse.js:
specifier: ^7.1.0
version: 7.1.0
version: 7.3.0
github-markdown-css:
specifier: ^5.8.1
version: 5.9.0
idb-keyval:
specifier: ^6.2.2
version: 6.2.2
version: 6.2.4
jszip:
specifier: ^3.10.1
version: 3.10.1
@@ -149,6 +149,9 @@ importers:
rehype-katex:
specifier: ^6.0.2
version: 6.0.3
rehype-slug:
specifier: 5.1.0
version: 5.1.0
remark-gfm:
specifier: 3.0.1
version: 3.0.1
@@ -169,7 +172,7 @@ importers:
version: 1.4.0
uuid:
specifier: ^11.1.0
version: 11.1.0
version: 11.1.1
zod:
specifier: ^3.24.2
version: 3.25.76
@@ -197,7 +200,7 @@ importers:
version: 19.2.3(@types/react@19.2.14)
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
version: 4.7.0(vite@6.4.2(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
autoprefixer:
specifier: ^10.4.21
version: 10.4.27(postcss@8.5.8)
@@ -227,7 +230,7 @@ importers:
version: 8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.7.3)
vite:
specifier: ^6.2.0
version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
version: 6.4.2(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
packages:
@@ -732,8 +735,8 @@ packages:
react: ^16 || ^17 || ^18 || ^19
react-dom: ^16 || ^17 || ^18 || ^19
'@hookform/resolvers@5.2.2':
resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==}
'@hookform/resolvers@5.4.0':
resolution: {integrity: sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==}
peerDependencies:
react-hook-form: ^7.55.0
@@ -1786,6 +1789,9 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
'@types/geojson@7946.0.16':
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
@@ -1910,8 +1916,8 @@ packages:
'@uiw/copy-to-clipboard@1.0.20':
resolution: {integrity: sha512-IFQhS62CLNon1YgYJTEzXR2N3WVXg7V1FaBRDLMlzU6JY5X6Hr3OPAcw4WNoKcz2XcFD6XCgwEjlsmj+JA0mWA==}
'@uiw/react-markdown-preview@5.1.5':
resolution: {integrity: sha512-DNOqx1a6gJR7Btt57zpGEKTfHRlb7rWbtctMRO2f82wWcuoJsxPBrM+JWebDdOD0LfD8oe2CQvW2ICQJKHQhZg==}
'@uiw/react-markdown-preview@5.2.1':
resolution: {integrity: sha512-JjvcHveT6glhlJYJx1XGBZij6wkw+VwREV6Z6m/GpsjPPdLjF1x8nlPBSB/ATyUF4lD7C8ttMkCqVH9N9XMgEA==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
@@ -1919,6 +1925,9 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@ungap/structured-clone@1.3.1':
resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==}
'@upsetjs/venn.js@2.0.0':
resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
@@ -2689,8 +2698,8 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
fuse.js@7.1.0:
resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==}
fuse.js@7.3.0:
resolution: {integrity: sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==}
engines: {node: '>=10'}
gensync@1.0.0-beta.2:
@@ -2794,9 +2803,15 @@ packages:
hast-util-from-parse5@8.0.3:
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
hast-util-has-property@2.0.1:
resolution: {integrity: sha512-X2+RwZIMTMKpXUzlotatPzWj8bspCymtXH3cfG3iQKV+wPF53Vgaqxi/eLqGck0wKq1kS9nvoB1wchbCPEL8sg==}
hast-util-has-property@3.0.0:
resolution: {integrity: sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==}
hast-util-heading-rank@2.1.1:
resolution: {integrity: sha512-iAuRp+ESgJoRFJbSyaqsfvJDY6zzmFoEnL1gtz1+U8gKtGGj1p0CVlysuUAUjq95qlZESHINLThwJzNGmgGZxA==}
hast-util-heading-rank@3.0.0:
resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==}
@@ -2833,6 +2848,9 @@ packages:
hast-util-to-parse5@8.0.1:
resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==}
hast-util-to-string@2.0.0:
resolution: {integrity: sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==}
hast-util-to-string@3.0.1:
resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==}
@@ -2883,8 +2901,8 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
idb-keyval@6.2.2:
resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==}
idb-keyval@6.2.4:
resolution: {integrity: sha512-D/NzHWUmYJGXi++z67aMSrnisb9A3621CyRK5G89JyTlN13C8xf0g04DLxUKMufPem3e3L2JAXR6Z00OWy183Q==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
@@ -4228,12 +4246,6 @@ packages:
'@types/react': '>=16'
react: '>=16'
react-markdown@9.0.3:
resolution: {integrity: sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==}
peerDependencies:
'@types/react': '>=18'
react: '>=18'
react-medium-image-zoom@5.4.1:
resolution: {integrity: sha512-DD2iZYaCfAwiQGR8AN62r/cDJYoXhezlYJc5HY4TzBUGuGge43CptG0f7m0PEIM72aN6GfpjohvY1yYdtCJB7g==}
peerDependencies:
@@ -4372,8 +4384,8 @@ packages:
regex@6.1.0:
resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
rehype-attr@3.0.3:
resolution: {integrity: sha512-Up50Xfra8tyxnkJdCzLBIBtxOcB2M1xdeKe1324U06RAvSjYm7ULSeoM+b/nYPQPVd7jsXJ9+39IG1WAJPXONw==}
rehype-attr@4.0.0:
resolution: {integrity: sha512-tANn9EmhG4mEZlNdDDRKuS0OXPDvc6P6OjJ1yApzOjIdCvKNLiuU2HdMSLTpiVi3D/FyLK6B+ZZ8PYtRxiGg7Q==}
engines: {node: '>=16'}
rehype-autolink-headings@7.1.0:
@@ -4408,6 +4420,9 @@ packages:
resolution: {integrity: sha512-L/FO96EOzSA6bzOam4DVu61/PB3AGKcSPXpa53yMIozoxH4qg1+bVZDF8zh1EsuxtSauAhzt5cCnvoplAaSLrw==}
engines: {node: '>=16.0.0'}
rehype-slug@5.1.0:
resolution: {integrity: sha512-Gf91dJoXneiorNEnn+Phx97CO7oRMrpi+6r155tTxzGuLtm+QrI4cTwCa9e1rtePdL4i9tSO58PeSS6HWfgsiw==}
rehype-slug@6.0.0:
resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
@@ -4822,12 +4837,12 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
uuid@11.1.1:
resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==}
hasBin: true
uuid@13.0.0:
resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
uuid@13.0.2:
resolution: {integrity: sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==}
hasBin: true
uvu@0.5.6:
@@ -4856,8 +4871,8 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
vite@6.4.1:
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
vite@6.4.2:
resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
@@ -5534,7 +5549,7 @@ snapshots:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@hookform/resolvers@5.2.2(react-hook-form@7.72.0(react@19.2.4))':
'@hookform/resolvers@5.4.0(react-hook-form@7.72.0(react@19.2.4))':
dependencies:
'@standard-schema/utils': 0.3.0
react-hook-form: 7.72.0(react@19.2.4)
@@ -5725,7 +5740,7 @@ snapshots:
unified: 11.0.5
url-join: 5.0.0
use-merge-value: 1.2.0(react@19.2.4)
uuid: 13.0.0
uuid: 13.0.2
transitivePeerDependencies:
- '@babel/core'
- '@types/mdast'
@@ -6456,12 +6471,12 @@ snapshots:
postcss: 8.5.8
tailwindcss: 4.2.2
'@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))':
'@tailwindcss/vite@4.2.2(vite@6.4.2(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))':
dependencies:
'@tailwindcss/node': 4.2.2
'@tailwindcss/oxide': 4.2.2
tailwindcss: 4.2.2
vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
vite: 6.4.2(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
'@tauri-apps/api@2.11.0': {}
@@ -6664,6 +6679,8 @@ snapshots:
'@types/estree@1.0.8': {}
'@types/estree@1.0.9': {}
'@types/geojson@7946.0.16': {}
'@types/hast@2.3.10':
@@ -6811,14 +6828,14 @@ snapshots:
'@uiw/copy-to-clipboard@1.0.20': {}
'@uiw/react-markdown-preview@5.1.5(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
'@uiw/react-markdown-preview@5.2.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.29.2
'@uiw/copy-to-clipboard': 1.0.20
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
react-markdown: 9.0.3(@types/react@19.2.14)(react@19.2.4)
rehype-attr: 3.0.3
react-markdown: 10.1.0(@types/react@19.2.14)(react@19.2.4)
rehype-attr: 4.0.0
rehype-autolink-headings: 7.1.0
rehype-ignore: 2.0.3
rehype-prism-plus: 2.0.0
@@ -6834,6 +6851,8 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@ungap/structured-clone@1.3.1': {}
'@upsetjs/venn.js@2.0.0':
optionalDependencies:
d3-selection: 3.0.0
@@ -6846,7 +6865,7 @@ snapshots:
'@use-gesture/core': 10.3.1
react: 19.2.4
'@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))':
'@vitejs/plugin-react@4.7.0(vite@6.4.2(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))':
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
@@ -6854,7 +6873,7 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.27
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
vite: 6.4.2(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3)
transitivePeerDependencies:
- supports-color
@@ -7630,7 +7649,7 @@ snapshots:
estree-util-attach-comments@3.0.0:
dependencies:
'@types/estree': 1.0.8
'@types/estree': 1.0.9
estree-util-build-jsx@3.0.1:
dependencies:
@@ -7742,7 +7761,7 @@ snapshots:
function-bind@1.1.2: {}
fuse.js@7.1.0: {}
fuse.js@7.3.0: {}
gensync@1.0.0-beta.2: {}
@@ -7871,10 +7890,16 @@ snapshots:
vfile-location: 5.0.3
web-namespaces: 2.0.1
hast-util-has-property@2.0.1: {}
hast-util-has-property@3.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-heading-rank@2.1.1:
dependencies:
'@types/hast': 2.3.10
hast-util-heading-rank@3.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -7934,7 +7959,7 @@ snapshots:
hast-util-to-estree@3.1.3:
dependencies:
'@types/estree': 1.0.8
'@types/estree': 1.0.9
'@types/estree-jsx': 1.0.5
'@types/hast': 3.0.4
comma-separated-tokens: 2.0.3
@@ -7969,7 +7994,7 @@ snapshots:
hast-util-to-jsx-runtime@2.3.6:
dependencies:
'@types/estree': 1.0.8
'@types/estree': 1.0.9
'@types/hast': 3.0.4
'@types/unist': 3.0.3
comma-separated-tokens: 2.0.3
@@ -7997,6 +8022,10 @@ snapshots:
web-namespaces: 2.0.1
zwitch: 2.0.4
hast-util-to-string@2.0.0:
dependencies:
'@types/hast': 2.3.10
hast-util-to-string@3.0.1:
dependencies:
'@types/hast': 3.0.4
@@ -8070,7 +8099,7 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
idb-keyval@6.2.2: {}
idb-keyval@6.2.4: {}
ignore@5.3.2: {}
@@ -8690,7 +8719,7 @@ snapshots:
dependencies:
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@ungap/structured-clone': 1.3.0
'@ungap/structured-clone': 1.3.1
devlop: 1.1.0
micromark-util-sanitize-uri: 2.0.1
trim-lines: 3.0.1
@@ -8760,7 +8789,7 @@ snapshots:
roughjs: 4.6.6
stylis: 4.3.6
ts-dedent: 2.2.0
uuid: 11.1.0
uuid: 11.1.1
micromark-core-commonmark@1.1.0:
dependencies:
@@ -8957,7 +8986,7 @@ snapshots:
micromark-extension-mdx-expression@3.0.1:
dependencies:
'@types/estree': 1.0.8
'@types/estree': 1.0.9
devlop: 1.1.0
micromark-factory-mdx-expression: 2.0.3
micromark-factory-space: 2.0.1
@@ -8968,7 +8997,7 @@ snapshots:
micromark-extension-mdx-jsx@3.0.2:
dependencies:
'@types/estree': 1.0.8
'@types/estree': 1.0.9
devlop: 1.1.0
estree-util-is-identifier-name: 3.0.0
micromark-factory-mdx-expression: 2.0.3
@@ -8985,7 +9014,7 @@ snapshots:
micromark-extension-mdxjs-esm@3.0.0:
dependencies:
'@types/estree': 1.0.8
'@types/estree': 1.0.9
devlop: 1.1.0
micromark-core-commonmark: 2.0.3
micromark-util-character: 2.1.1
@@ -9034,7 +9063,7 @@ snapshots:
micromark-factory-mdx-expression@2.0.3:
dependencies:
'@types/estree': 1.0.8
'@types/estree': 1.0.9
devlop: 1.1.0
micromark-factory-space: 2.0.1
micromark-util-character: 2.1.1
@@ -9150,7 +9179,7 @@ snapshots:
micromark-util-events-to-acorn@2.0.3:
dependencies:
'@types/estree': 1.0.8
'@types/estree': 1.0.9
'@types/unist': 3.0.3
devlop: 1.1.0
estree-util-visit: 2.0.0
@@ -9936,23 +9965,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
react-markdown@9.0.3(@types/react@19.2.14)(react@19.2.4):
dependencies:
'@types/hast': 3.0.4
'@types/react': 19.2.14
devlop: 1.1.0
hast-util-to-jsx-runtime: 2.3.6
html-url-attributes: 3.0.1
mdast-util-to-hast: 13.2.1
react: 19.2.4
remark-parse: 11.0.0
remark-rehype: 11.1.2
unified: 11.0.5
unist-util-visit: 5.1.0
vfile: 6.0.3
transitivePeerDependencies:
- supports-color
react-medium-image-zoom@5.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
react: 19.2.4
@@ -10078,7 +10090,7 @@ snapshots:
recma-parse@1.0.0:
dependencies:
'@types/estree': 1.0.8
'@types/estree': 1.0.9
esast-util-from-js: 2.0.1
unified: 11.0.5
vfile: 6.0.3
@@ -10120,7 +10132,7 @@ snapshots:
dependencies:
regex-utilities: 2.3.0
rehype-attr@3.0.3:
rehype-attr@4.0.0:
dependencies:
unified: 11.0.5
unist-util-visit: 5.0.0
@@ -10201,6 +10213,16 @@ snapshots:
unified: 11.0.5
unist-util-visit: 5.1.0
rehype-slug@5.1.0:
dependencies:
'@types/hast': 2.3.10
github-slugger: 2.0.0
hast-util-has-property: 2.0.1
hast-util-heading-rank: 2.1.1
hast-util-to-string: 2.0.0
unified: 10.1.2
unist-util-visit: 4.1.2
rehype-slug@6.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -10696,9 +10718,9 @@ snapshots:
util-deprecate@1.0.2: {}
uuid@11.1.0: {}
uuid@11.1.1: {}
uuid@13.0.0: {}
uuid@13.0.2: {}
uvu@0.5.6:
dependencies:
@@ -10741,7 +10763,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3):
vite@6.4.2(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)

View File

@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "BiliNote",
"version": "2.0.0",
"version": "2.3.4",
"identifier": "com.jefferyhuang.bilinote",
"build": {
"frontendDist": "../dist",

View File

@@ -1,6 +1,6 @@
import './App.css'
import { lazy, Suspense, useEffect } from 'react'
import { BrowserRouter, Navigate, Routes, Route } from 'react-router-dom'
import { BrowserRouter, HashRouter, Navigate, Routes, Route } from 'react-router-dom'
import { useTaskPolling } from '@/hooks/useTaskPolling.ts'
import { useCheckBackend } from '@/hooks/useCheckBackend.ts'
import { systemCheck } from '@/services/system.ts'
@@ -57,12 +57,16 @@ function App() {
)
}
// 桌面端使用 HashRouter 避免刷新 404Web 端继续使用 BrowserRouter
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
const Router = isTauri ? HashRouter : BrowserRouter
// 后端已初始化,渲染主应用
return (
<>
<StartupBanner />
<BackendHealthIndicator />
<BrowserRouter>
<Router>
<Suspense fallback={<div className="flex h-screen items-center justify-center"></div>}>
<Routes>
<Route path="/onboarding" element={<Onboarding />} />
@@ -86,7 +90,7 @@ function App() {
</Route>
</Routes>
</Suspense>
</BrowserRouter>
</Router>
</>
)
}

View File

@@ -14,6 +14,7 @@ import 'react-medium-image-zoom/dist/styles.css'
import gfm from 'remark-gfm'
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import rehypeSlug from 'rehype-slug'
import 'katex/dist/katex.min.css'
import 'github-markdown-css/github-markdown-light.css'
import { ScrollArea } from '@/components/ui/scroll-area.tsx'
@@ -47,7 +48,7 @@ const steps = [
]
const remarkPlugins = [gfm, remarkMath]
const rehypePlugins = [rehypeKatex]
const rehypePlugins = [rehypeKatex, rehypeSlug]
/**
* 构建 ReactMarkdown components 对象baseURL 用于修正图片路径。
@@ -117,6 +118,51 @@ function createMarkdownComponents(baseURL: string) {
)
}
// 处理笔记内部锚点链接(如目录跳转)
if (href?.startsWith('#')) {
const handleAnchorClick = (e: React.MouseEvent) => {
e.preventDefault()
const id = decodeURIComponent(href.slice(1))
// 1. 优先精确匹配 id
let target = document.getElementById(id)
// 2. 精确失败时按 heading 文本模糊匹配
// LLM 生成的目录锚点可能和 heading 实际文本不完全一致
//(例如 heading 带 *Content-[00:00]* 后缀,目录链接里没有)
if (!target) {
const normalize = (s: string) =>
s.replace(/[-:\s*\[\]]/g, '').toLowerCase()
const search = normalize(id)
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6')
for (const h of headings) {
const text = h.textContent || ''
if (normalize(text).includes(search) || search.includes(normalize(text))) {
target = h
break
}
}
}
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' })
} else {
toast.error('未找到对应章节')
}
}
return (
<a
href={href}
onClick={handleAnchorClick}
className="text-primary hover:text-primary/80 inline-flex items-center gap-0.5 font-medium underline underline-offset-4"
{...props}
>
{children}
</a>
)
}
return (
<a
href={href}

View File

@@ -5,9 +5,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Github, Star, ExternalLink, Download } from 'lucide-react'
import { ScrollArea } from '@/components/ui/scroll-area.tsx'
import logo from '@/assets/icon.svg'
import wechatQr from '@/assets/wechat.png'
// 二维码统一走 GitHub 在线地址:后续在仓库替换图片后,已打包的 App 会自动更新,无需重新发版
const GITHUB_DOC_RAW = 'https://raw.githubusercontent.com/JefferyHcool/BiliNote/master/doc'
const wechatQr = `${GITHUB_DOC_RAW}/wechat-group-1.png`
const remoteInstallQr = `${GITHUB_DOC_RAW}/remote-install-wechat.png`
export default function AboutPage() {
const appVersion = __APP_VERSION__
const images = [
'https://common-1304618721.cos.ap-chengdu.myqcloud.com/20250504102850.png',
'https://common-1304618721.cos.ap-chengdu.myqcloud.com/20250504103028.png',
@@ -27,7 +32,7 @@ export default function AboutPage() {
height={50}
className="rounded-lg"
/>
<h1 className="text-4xl font-bold">BiliNote v2.0.0</h1>
<h1 className="text-4xl font-bold">BiliNote v{appVersion}</h1>
</div>
<p className="text-muted-foreground mb-6 text-xl italic">
AI AI
@@ -197,12 +202,28 @@ export default function AboutPage() {
<section className="mb-16">
<h2 className="mb-8 text-center text-3xl font-bold"></h2>
<div className="mx-auto max-w-3xl">
<div className="flex flex-col items-center justify-center gap-8">
<div className="flex flex-col items-center justify-center gap-10 md:flex-row md:items-start">
<div className="text-center">
<h3 className="mb-3 text-xl font-semibold">BiliNote </h3>
<div className="bg-muted mx-auto flex h-52 w-52 items-center justify-center rounded-md">
<img src={wechatQr} alt="BiliNote 交流微信群" className="h-full w-full object-contain" />
</div>
<p className="text-muted-foreground mt-3 text-sm">使</p>
</div>
<div className="text-center">
<h3 className="mb-3 text-xl font-semibold">BiliNote AI笔记系统一对一搭建服务</h3>
<div className="bg-muted mx-auto flex h-52 w-52 items-center justify-center rounded-md">
<img
src={remoteInstallQr}
alt="BiliNote AI笔记系统一对一搭建服务"
className="h-full w-full object-contain"
/>
</div>
<p className="text-muted-foreground mt-3 text-sm">
<br />
</p>
</div>
</div>
</div>

View File

@@ -1 +1,3 @@
/// <reference types="vite/client" />
declare const __APP_VERSION__: string

View File

@@ -1,11 +1,25 @@
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import tailwindcss from '@tailwindcss/vite'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
function readAppVersion() {
const fallbackVersion = '0.0.0'
try {
const tauriConfigPath = path.resolve(__dirname, 'src-tauri/tauri.conf.json')
const tauriConfig = JSON.parse(fs.readFileSync(tauriConfigPath, 'utf-8')) as { version?: string }
return tauriConfig.version || fallbackVersion
}
catch {
return fallbackVersion
}
}
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
// 在 Docker 环境中,父目录可能没有 .env 文件,使用当前目录
@@ -14,9 +28,13 @@ export default defineConfig(({ mode }) => {
const apiBaseUrl = env.VITE_API_BASE_URL || 'http://127.0.0.1:8483'
const port = parseInt(env.VITE_FRONTEND_PORT || '3015', 10)
const appVersion = env.VITE_APP_VERSION || process.env.VITE_APP_VERSION || readAppVersion()
return {
base: './',
define: {
__APP_VERSION__: JSON.stringify(appVersion),
},
plugins: [react(), tailwindcss()],
resolve: {
alias: {

View File

@@ -2,6 +2,20 @@
本项目所有重要变更记录于此。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [2.3.3] - 2026-05-22
### Fixed
- **预构建 Docker 镜像数据持久化**:文档的 `docker run` 只挂了 `data/`(媒体缓存),而 SQLite 数据库LLM 供应商配置 + 笔记历史)和笔记文件不在该卷下,导致删除 / 升级容器时丢失配置与历史。现将数据库重定向到 `/app/backend/data/bili_note.db`、笔记到 `data/note_results`(随 data 卷持久化README 更新为挂载 `data` / `config` / `static` / `models` 四个数据卷,并提示**勿**挂整个 `/app/backend`(命名卷会固化镜像内代码,导致 `docker pull` 升级不生效)。`docker-compose` 路径本就正确(`./backend:/app` 整目录绑挂),未受影响。
## [2.3.2] - 2026-05-22
### Fixed
- **后端启动崩溃Docker**`python:3.11-slim` 基础镜像升级到 Debian 13 / glibc 2.41 后,`ctranslate2` 4.5.0 预编译库带「可执行栈」标记被 glibc 拒绝加载(`cannot enable executable stack ... Invalid argument`)。由于 `from faster_whisper import WhisperModel` 在顶层 importimport 失败直接拖垮整个后端启动 → 容器反复重启。升级 `ctranslate2` 4.5.0→4.6.0wheel 加入 `noexecstack` 链接标志,从二进制层根治)
- **whisper 模型误报「离线模式找不到模型」**下载modelscope 自定义目录与加载faster-whisper HF cache布局不一致导致命不中缓存。统一为下载 / 加载 / 完整性检测 / 损坏自愈都走 HF cache 布局,并向后兼容老 modelscope 目录
- **桌面端构建产物版本恒为 2.0.0**Release 工作流在 `pnpm tauri build` 前从 git tag 注入版本号到 `tauri.conf.json`,使产物版本与 Release 版本对齐
## [2.3.1] - 2026-05-22
### Changed

View File

@@ -35,6 +35,10 @@ COPY ./backend /tmp/backend
ARG BASE_REGISTRY=docker.io
FROM ${BASE_REGISTRY}/library/node:20-alpine AS frontend-builder
# 可由发布 workflow 从 git tag 注入,用于前端 About 页展示版本;未传时由 Vite 回退读取 tauri.conf.json。
ARG VITE_APP_VERSION=
ENV VITE_APP_VERSION=${VITE_APP_VERSION}
# pnpm 版本 pin 到 9 系列:
# - lockfile (BillNote_frontend/pnpm-lock.yaml) 是 lockfileVersion '9.0',由 pnpm 9 生成
# - pnpm 11+ 要求 Node 22+,与 node:20 不兼容ERR_UNKNOWN_BUILTIN_MODULE
@@ -95,14 +99,16 @@ COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
# 在 [supervisord] 块用 environment= 设兜底默认值;在 [program:backend] 用
# %(ENV_*)s 显式引用,等价于「把 host 通过 docker run -e 或 env_file 传进来的
# 变量再透传给 python main.py」。漏掉这一步就是用户「改 .env 没反应」的根因。
RUN mkdir -p /var/log/supervisor
# /app/backend/data 用于持久化数据库与笔记(见下方 DATABASE_URL / NOTE_OUTPUT_DIR
# 预建好目录,避免不挂卷启动时 sqlite 因父目录不存在而创建库失败。
RUN mkdir -p /var/log/supervisor /app/backend/data
COPY <<EOF /etc/supervisor/conf.d/supervisord.conf
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
environment=BACKEND_PORT="8483",BACKEND_HOST="0.0.0.0",TRANSCRIBER_TYPE="fast-whisper",WHISPER_MODEL_SIZE="tiny",FFMPEG_BIN_PATH="",HF_ENDPOINT="https://hf-mirror.com",STATIC="/static",OUT_DIR="./static/screenshots",DATA_DIR="data",NOTE_OUTPUT_DIR="note_results",IMAGE_BASE_URL="/static/screenshots",ENV="production",GROQ_TRANSCRIBER_MODEL="whisper-large-v3-turbo"
environment=BACKEND_PORT="8483",BACKEND_HOST="0.0.0.0",TRANSCRIBER_TYPE="fast-whisper",WHISPER_MODEL_SIZE="tiny",FFMPEG_BIN_PATH="",HF_ENDPOINT="https://hf-mirror.com",STATIC="/static",OUT_DIR="./static/screenshots",DATA_DIR="data",NOTE_OUTPUT_DIR="data/note_results",DATABASE_URL="sqlite:////app/backend/data/bili_note.db",IMAGE_BASE_URL="/static/screenshots",ENV="production",GROQ_TRANSCRIBER_MODEL="whisper-large-v3-turbo"
[program:nginx]
command=nginx -g "daemon off;"
@@ -118,7 +124,7 @@ stdout_logfile=/var/log/supervisor/backend.log
stderr_logfile=/var/log/supervisor/backend.log
autorestart=true
priority=20
environment=BACKEND_PORT="%(ENV_BACKEND_PORT)s",BACKEND_HOST="%(ENV_BACKEND_HOST)s",TRANSCRIBER_TYPE="%(ENV_TRANSCRIBER_TYPE)s",WHISPER_MODEL_SIZE="%(ENV_WHISPER_MODEL_SIZE)s",FFMPEG_BIN_PATH="%(ENV_FFMPEG_BIN_PATH)s",HF_ENDPOINT="%(ENV_HF_ENDPOINT)s",STATIC="%(ENV_STATIC)s",OUT_DIR="%(ENV_OUT_DIR)s",DATA_DIR="%(ENV_DATA_DIR)s",NOTE_OUTPUT_DIR="%(ENV_NOTE_OUTPUT_DIR)s",IMAGE_BASE_URL="%(ENV_IMAGE_BASE_URL)s",ENV="%(ENV_ENV)s",GROQ_TRANSCRIBER_MODEL="%(ENV_GROQ_TRANSCRIBER_MODEL)s"
environment=BACKEND_PORT="%(ENV_BACKEND_PORT)s",BACKEND_HOST="%(ENV_BACKEND_HOST)s",TRANSCRIBER_TYPE="%(ENV_TRANSCRIBER_TYPE)s",WHISPER_MODEL_SIZE="%(ENV_WHISPER_MODEL_SIZE)s",FFMPEG_BIN_PATH="%(ENV_FFMPEG_BIN_PATH)s",HF_ENDPOINT="%(ENV_HF_ENDPOINT)s",STATIC="%(ENV_STATIC)s",OUT_DIR="%(ENV_OUT_DIR)s",DATA_DIR="%(ENV_DATA_DIR)s",NOTE_OUTPUT_DIR="%(ENV_NOTE_OUTPUT_DIR)s",DATABASE_URL="%(ENV_DATABASE_URL)s",IMAGE_BASE_URL="%(ENV_IMAGE_BASE_URL)s",ENV="%(ENV_ENV)s",GROQ_TRANSCRIBER_MODEL="%(ENV_GROQ_TRANSCRIBER_MODEL)s"
EOF
# 修改 nginx 配置以使用本地 backend

View File

@@ -3,7 +3,7 @@
<p align="center">
<img src="./doc/icon.svg" alt="BiliNote Banner" width="50" height="50" />
</p>
<h1 align="center" > BiliNote v2.3.1</h1>
<h1 align="center" > BiliNote v2.3.4</h1>
</div>
<p align="center"><i>AI 视频笔记生成工具 让 AI 为你的视频做笔记</i></p>
@@ -53,6 +53,16 @@ BiliNote 是一个开源的 AI 视频笔记助手支持通过哔哩哔哩、Y
> Windows 用户请注意:一定要在没有中文路径的环境下运行。
## 💎 BiliNote AI笔记系统一对一搭建服务
提供 **BiliNote AI笔记系统一对一搭建服务**:专人一对一远程协助,从环境部署、模型配置到上手使用全程陪跑,帮你快速跑通整套系统。扫码添加微信,备注「搭建服务」即可咨询:
<table align="center">
<tr>
<td align="center"><img src="./doc/remote-install-wechat.png" alt="BiliNote AI笔记系统一对一搭建服务" width="220" /><br/>BiliNote AI笔记系统一对一搭建服务</td>
</tr>
</table>
## 🔧 功能特性
- 支持多平台Bilibili、YouTube、本地视频、抖音、快手
@@ -163,10 +173,17 @@ docker pull ghcr.io/jefferyhcool/bilinote:latest
docker run -d -p 80:80 \
-v bilinote-data:/app/backend/data \
-v bilinote-config:/app/backend/config \
-v bilinote-static:/app/backend/static \
-v bilinote-models:/app/backend/models \
--name bilinote \
ghcr.io/jefferyhcool/bilinote:latest
```
上面四个卷分别持久化:`data`SQLite 数据库 + 生成的笔记)、`config`LLM 供应商配置 / Cookie / 转写设置)、`static`(笔记引用的视频截图)、`models`Whisper 模型缓存,可选,避免每次重新下载)。这样 `docker pull` 升级新镜像、删旧容器重建后,配置和历史都不会丢。
> ⚠️ **不要**用 `-v 卷名:/app/backend` 挂整个后端目录——命名卷会用首次启动时的镜像内容固化,之后 `docker pull` 升级也会被旧代码盖住,导致「升级不生效」。只挂上面这些数据子目录即可。
访问:`http://localhost`
也可以使用 docker-compose 本地构建:
@@ -302,10 +319,17 @@ docker pull ghcr.io/jefferyhcool/bilinote:latest
# 运行容器
docker run -d -p 80:80 \
-v bilinote-data:/app/backend/data \
-v bilinote-config:/app/backend/config \
-v bilinote-static:/app/backend/static \
-v bilinote-models:/app/backend/models \
--name bilinote \
ghcr.io/jefferyhcool/bilinote:latest
```
上面四个卷分别持久化:`data`SQLite 数据库 + 生成的笔记)、`config`LLM 供应商配置 / Cookie / 转写设置)、`static`(笔记引用的视频截图)、`models`Whisper 模型缓存,可选,避免每次重新下载)。这样 `docker pull` 升级新镜像、删旧容器重建后,配置和历史都不会丢。
> ⚠️ **不要**用 `-v 卷名:/app/backend` 挂整个后端目录——命名卷会用首次启动时的镜像内容固化,之后 `docker pull` 升级也会被旧代码盖住,导致「升级不生效」。只挂上面这些数据子目录即可。
访问:`http://localhost`
也可以使用 docker-compose 本地构建:

View File

@@ -119,12 +119,21 @@ _downloading: dict[str, str] = {} # model_size -> status ("downloading" | "done
def _check_whisper_model_exists(model_size: str, subdir: str = "whisper") -> bool:
"""检查指定 whisper 模型是否已下载完整到本地。
必须 model.bin 落盘才算完成,仅有空目录或半成品不能算「已下载」——
否则监控页会显示绿勾但加载时报「Unable to open file 'model.bin'」。
faster-whisper 把模型缓存在 HF cache 布局下:
<model_dir>/models--Systran--faster-whisper-{size}/snapshots/<hash>/model.bin
必须能在某个 snapshot 目录里找到 model.bin 才算完成。
(历史 modelscope 布局 <model_dir>/whisper-{size}/model.bin 也兼容识别。)
"""
model_dir = get_model_dir(subdir)
model_path = os.path.join(model_dir, f"whisper-{model_size}")
return (Path(model_path) / "model.bin").exists()
model_dir = Path(get_model_dir(subdir))
# HF cache 布局
hf_repo_dir = model_dir / f"models--Systran--faster-whisper-{model_size}" / "snapshots"
if hf_repo_dir.exists():
for snapshot in hf_repo_dir.iterdir():
if (snapshot / "model.bin").exists():
return True
# 历史 modelscope 布局(向后兼容老用户)
legacy = model_dir / f"whisper-{model_size}" / "model.bin"
return legacy.exists()
def _check_mlx_whisper_model_exists(model_size: str) -> bool:
@@ -189,24 +198,37 @@ class ModelDownloadRequest(BaseModel):
def _do_download_whisper(model_size: str):
"""后台下载 faster-whisper 模型。"""
from app.transcriber.whisper import MODEL_MAP
from modelscope import snapshot_download
"""后台下载 faster-whisper 模型。
直接走 huggingface_hub.snapshot_download把模型放到 HF cache 布局里——
这样 faster-whisper 加载时WhisperModel(model_size_or_path=size_name,
download_root=model_dir))能直接命中缓存,跟加载路径完全对齐。
"""
from huggingface_hub import snapshot_download
try:
_downloading[model_size] = "downloading"
model_dir = get_model_dir("whisper")
model_path = os.path.join(model_dir, f"whisper-{model_size}")
# 用 model.bin 判定而非目录存在:半成品目录不能算「已下载」
if (Path(model_path) / "model.bin").exists():
# 已经下好就不重复下
if _check_whisper_model_exists(model_size, "whisper"):
_downloading[model_size] = "done"
return
repo_id = MODEL_MAP.get(model_size)
if not repo_id:
_downloading[model_size] = "failed"
return
logger.info(f"开始下载 whisper 模型: {model_size}")
snapshot_download(repo_id, local_dir=model_path)
repo_id = f"Systran/faster-whisper-{model_size}"
logger.info(f"开始下载 whisper 模型: {repo_id}")
# 跟 faster-whisper utils.py 用同样的 allow_patterns避免多下无关文件
# 不传 local_dir 让它走 HF 默认 cache 布局(与加载逻辑对齐)
snapshot_download(
repo_id,
cache_dir=model_dir,
allow_patterns=[
"config.json",
"preprocessor_config.json",
"model.bin",
"tokenizer.json",
"vocabulary.*",
],
)
logger.info(f"whisper 模型下载完成: {model_size}")
_downloading[model_size] = "done"
except Exception as e:

View File

@@ -129,6 +129,10 @@ class ProviderService:
try:
# 过滤掉空值
filtered_data = {k: v for k, v in data.items() if v is not None and k != 'id'}
# 防御掩码污染:前端展示时 api_key 被 mask_key() 处理过(如 a92f****...2d3a
# 如果用户未重新输入直接保存,带星号的值不应覆盖原 key。
if 'api_key' in filtered_data and '*' in str(filtered_data.get('api_key', '')):
filtered_data.pop('api_key')
print('更新模型供应商',filtered_data)
update_provider(id, **filtered_data)
# 获取更新后的供应商信息

View File

@@ -11,8 +11,6 @@ from events import transcription_finished
from pathlib import Path
import os
import shutil
from tqdm import tqdm
from modelscope import snapshot_download
'''
@@ -20,19 +18,16 @@ from modelscope import snapshot_download
'''
logger=get_logger(__name__)
MODEL_MAP={
"tiny": "pengzhendong/faster-whisper-tiny",
'base':'pengzhendong/faster-whisper-base',
'small':'pengzhendong/faster-whisper-small',
'medium':'pengzhendong/faster-whisper-medium',
'large-v1':'pengzhendong/faster-whisper-large-v1',
'large-v2':'pengzhendong/faster-whisper-large-v2',
'large-v3':'pengzhendong/faster-whisper-large-v3',
'large-v3-turbo':'pengzhendong/faster-whisper-large-v3-turbo',
}
# 历史遗留:之前用 modelscope 下载到自定义目录然后把路径传给 WhisperModel。
# 但 faster-whisper 1.1.1 的 download_modelutils.py:76逻辑是
# 只要 size_or_id 里含 "/" 就当 HF repo_id 处理,没有「本地目录直接返回」分支。
# 我们传 /app/models/whisper/whisper-tiny 进去 → 被当成不存在的 HF repo →
# 在线请求失败 → fallback local_files_only=True → HF cache 找不到(因为是
# modelscope 目录布局不是 HF→ LocalEntryNotFoundError误导说"离线模式"。
# 解法:彻底让 faster-whisper 自己处理下载——传 size name配 download_root
# 作为 HF cache 根目录HF_ENDPOINT 已经在 Dockerfile 里指到 hf-mirror.com
# 国内能用。删掉 modelscope 那一套,避免布局不匹配。
class WhisperTranscriber(Transcriber):
# TODO:修改为可配置
def __init__(
self,
model_size: str = "base",
@@ -48,44 +43,40 @@ class WhisperTranscriber(Transcriber):
print('没有 cuda 使用 cpu进行计算')
self.compute_type = compute_type or ("float16" if self.device == "cuda" else "int8")
self.model_size = model_size
model_dir = get_model_dir("whisper")
model_path = os.path.join(model_dir, f"whisper-{model_size}")
repo_id = MODEL_MAP[model_size]
# 第一步:目录 / model.bin 不在 → 下载。
# 关键判据用 model.bin 而不是目录存在:首次下载若被打断(网络中断 / 磁盘满 /
# 容器被 kill会留下半成品目录只看目录存在会跳过下载。
model_bin = Path(model_path) / "model.bin"
if not model_bin.exists():
if Path(model_path).exists():
logger.warning(f"模型目录 {model_path} 存在但 model.bin 缺失(上次下载未完成),重新下载")
else:
logger.info(f"模型 whisper-{model_size} 不存在,开始下载...")
model_path = snapshot_download(repo_id, local_dir=model_path)
logger.info("模型下载完成")
# 第二步加载。model.bin 可能存在但【内容截断】(下载到一半被 kill
# 此时 WhisperModel() 会抛 "File model.bin is incomplete: failed to read a buffer..."。
# 捕获后删掉损坏目录、重新下载、再试一次——自愈,避免 500 死循环。
try:
self.model = WhisperModel(
model_size_or_path=model_path,
device=self.device,
compute_type=self.compute_type,
download_root=model_dir,
)
self.model = self._build_model(model_size, model_dir)
except Exception as e:
logger.warning(f"加载 whisper-{model_size} 失败(疑似模型文件损坏 / 截断):{e};删除后重新下载")
shutil.rmtree(model_path, ignore_errors=True)
model_path = snapshot_download(repo_id, local_dir=model_path)
logger.info("模型重新下载完成,重试加载")
self.model = WhisperModel(
model_size_or_path=model_path,
device=self.device,
compute_type=self.compute_type,
download_root=model_dir,
)
# 自愈:损坏 / 截断 / 半成品 cache → 删掉对应 HF cache 重下一次
logger.warning(f"加载 whisper-{model_size} 失败:{e};清理 cache 后重新下载")
self._purge_cache(model_dir, model_size)
self.model = self._build_model(model_size, model_dir)
def _build_model(self, model_size: str, model_dir: str) -> WhisperModel:
return WhisperModel(
model_size_or_path=model_size, # 传 size name让 faster-whisper 自己映射到 Systran/faster-whisper-*
device=self.device,
compute_type=self.compute_type,
download_root=model_dir,
)
@staticmethod
def _purge_cache(model_dir: str, model_size: str) -> None:
"""删掉 HF cache 里这个 size 对应的 snapshot 目录,强制下次重新下载。
HF cache 布局:<model_dir>/models--Systran--faster-whisper-{size}/
没找到也不报错——可能用户改了 endpoint 或者 cache 布局变了。
"""
candidates = [
Path(model_dir) / f"models--Systran--faster-whisper-{model_size}",
Path(model_dir) / f"whisper-{model_size}", # 历史 modelscope 目录,顺手清掉
]
for path in candidates:
if path.exists():
logger.info(f"清理损坏 cache: {path}")
shutil.rmtree(path, ignore_errors=True)
@staticmethod
def is_torch_installed() -> bool:
try:

View File

@@ -1,11 +1,41 @@
import os
import subprocess
import sys
from dotenv import load_dotenv
from app.utils.logger import get_logger
logger = get_logger(__name__)
load_dotenv()
def _load_dotenv_from_multiple_paths():
"""尝试多个位置加载 .env适配源码运行和 PyInstaller 打包场景。
PyInstaller 打包后当前工作目录是 EXE 所在目录,而源码运行时 .env
通常在项目根目录或 backend/ 同级。遍历常见候选路径确保能命中。
"""
candidates = []
# 1. 当前工作目录EXE 所在目录)
candidates.append(os.path.join(os.getcwd(), '.env'))
# 2. 本脚本所在目录backend/
script_dir = os.path.dirname(os.path.abspath(__file__))
candidates.append(os.path.join(script_dir, '.env'))
# 3. 项目根目录backend/../.env
candidates.append(os.path.join(script_dir, '..', '.env'))
# 4. PyInstaller 打包后的 _internal/ 子目录
if getattr(sys, 'frozen', False):
exe_dir = os.path.dirname(sys.executable)
candidates.append(os.path.join(exe_dir, '_internal', '.env'))
for path in candidates:
normalized = os.path.normpath(path)
if os.path.isfile(normalized):
load_dotenv(normalized)
return
# 都没找到fallback 到默认行为(从 CWD 找)
load_dotenv()
_load_dotenv_from_multiple_paths()
def check_ffmpeg_exists() -> bool:
"""
检查 ffmpeg 是否可用。优先使用 FFMPEG_BIN_PATH 环境变量指定的路径。

View File

@@ -24,7 +24,7 @@ click-repl==0.3.0
colorama==0.4.6
coloredlogs==15.0.1
cssselect2==0.8.0
ctranslate2==4.5.0
ctranslate2==4.6.0
distro==1.9.0
dnspython==2.7.0
email_validator==2.2.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB