11 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
20 changed files with 285 additions and 94 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

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.3.3",
"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

@@ -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

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.3</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、本地视频、抖音、快手

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

@@ -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 环境变量指定的路径。

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB