diff --git a/.env.example b/.env.example index e2ca898..3589634 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,28 @@ +# ============================================================================= +# BiliNote 环境变量示例 +# Docker 部署:cp .env.example .env,按需修改,然后 docker-compose up --build -d +# +# 注意区分两类变量: +# 1) VITE_* 是【构建时】变量,会被烘进前端 JS bundle。改完必须 +# docker-compose build frontend && docker-compose up -d 才会生效, +# 只 docker-compose restart 不行。 +# 2) 其他后端变量是【运行时】变量,改完 docker-compose up -d 即可。 +# +# 提醒:LLM API key 不要写在这里!请部署完成后,从前端「模型供应商」页面录入, +# 这些 key 会保存到 SQLite 数据库(./backend/bili_note.db)并随容器持久化。 +# ============================================================================= + # 通用端口配置 BACKEND_PORT=8483 # 后端端口 FRONTEND_PORT=3015 BACKEND_HOST=0.0.0.0 # 默认为 0.0.0.0,表示监听所有 IP 地址 不建议动 -APP_PORT= 3015 # docker 部署时用 -# 前端访问后端用 (开发环境使用) -VITE_API_BASE_URL=http://127.0.0.1:8000 +APP_PORT=3015 # docker 部署时对外暴露端口(浏览器访问的端口) + +# 前端访问后端用(开发环境直连;Docker 部署下走 nginx 代理,此值仅作回退) +VITE_API_BASE_URL=http://127.0.0.1:8483 VITE_SCREENSHOT_BASE_URL=http://127.0.0.1:8483/static/screenshots VITE_FRONTEND_PORT=3015 + # 生产环境配置 ENV=production STATIC=/static @@ -14,11 +30,16 @@ OUT_DIR=./static/screenshots NOTE_OUTPUT_DIR=note_results IMAGE_BASE_URL=/static/screenshots DATA_DIR=data -# FFMPEG 配置 + +# FFMPEG 配置(Docker 镜像已内置 ffmpeg,留空即可;自建/桌面端可填绝对路径) FFMPEG_BIN_PATH= -# transcriber 相关配置 -TRANSCRIBER_TYPE=fast-whisper # fast-whisper/bcut/kuaishou/mlx-whisper(仅Apple平台)/groq -WHISPER_MODEL_SIZE=medium +# 转写器配置 +# TRANSCRIBER_TYPE 可选:fast-whisper / bcut / kuaishou / mlx-whisper(仅 Apple Silicon) / groq +TRANSCRIBER_TYPE=fast-whisper +# WHISPER_MODEL_SIZE 默认 tiny (~75MB),首次启动快;想要更高识别质量可在前端 +# 「音频转写配置」页切到 base/small/medium/large。直接在这里改大尺寸会触发 +# 首次启动下载 ~1.5GB 文件,慢网络或 4GB 内存的容器容易 OOM。 +WHISPER_MODEL_SIZE=tiny GROQ_TRANSCRIBER_MODEL=whisper-large-v3-turbo # groq提供的faster-whisper 默认为 whisper-large-v3-turbo diff --git a/BillNote_frontend/Dockerfile b/BillNote_frontend/Dockerfile index 16e62a0..9e54ab9 100644 --- a/BillNote_frontend/Dockerfile +++ b/BillNote_frontend/Dockerfile @@ -1,6 +1,9 @@ # === 前端构建阶段 === # Tailwind v4 / Vite 6 需要 Node 20+,alpine + pnpm 会按 lockfile 拉 musl native binary。 -FROM node:20-alpine AS builder +# BASE_REGISTRY 默认 docker.io,国内拉不到可换 daocloud / 阿里云镜像: +# docker-compose build --build-arg BASE_REGISTRY=docker.m.daocloud.io +ARG BASE_REGISTRY=docker.io +FROM ${BASE_REGISTRY}/library/node:20-alpine AS builder # pnpm pin 到 9.x:lockfile 是 v9 生成;pnpm 11 要求 Node 22+ 与 node:20 不兼容 RUN corepack enable && corepack prepare pnpm@9.15.0 --activate @@ -16,7 +19,9 @@ COPY ./BillNote_frontend/ ./ RUN pnpm run build # --- 阶段2:使用 nginx 作为静态服务器 --- -FROM nginx:1.25-alpine +# 重新声明 ARG —— buildkit 跨阶段不自动继承 +ARG BASE_REGISTRY=docker.io +FROM ${BASE_REGISTRY}/library/nginx:1.25-alpine RUN rm -rf /etc/nginx/conf.d/default.conf COPY ./BillNote_frontend/deploy/default.conf /etc/nginx/conf.d/default.conf diff --git a/BillNote_frontend/package.json b/BillNote_frontend/package.json index 8435b87..b2c9ee8 100644 --- a/BillNote_frontend/package.json +++ b/BillNote_frontend/package.json @@ -25,8 +25,8 @@ "@radix-ui/react-tabs": "^1.1.9", "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/vite": "^4.1.3", - "@tauri-apps/api": "^2.10.1", - "@tauri-apps/plugin-shell": "~2.2.2", + "@tauri-apps/api": "^2.11.0", + "@tauri-apps/plugin-shell": "~2.3.5", "@uiw/react-markdown-preview": "^5.1.3", "antd": "^5.24.8", "axios": "^1.8.4", diff --git a/BillNote_frontend/pnpm-lock.yaml b/BillNote_frontend/pnpm-lock.yaml index 6162429..9c272ec 100644 --- a/BillNote_frontend/pnpm-lock.yaml +++ b/BillNote_frontend/pnpm-lock.yaml @@ -54,11 +54,11 @@ importers: 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)) '@tauri-apps/api': - specifier: ^2.10.1 - version: 2.10.1 + specifier: ^2.11.0 + version: 2.11.0 '@tauri-apps/plugin-shell': - specifier: ~2.2.2 - version: 2.2.2 + specifier: ~2.3.5 + 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) @@ -1590,8 +1590,8 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@tauri-apps/api@2.10.1': - resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==} + '@tauri-apps/api@2.11.0': + resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} '@tauri-apps/cli-darwin-arm64@2.10.1': resolution: {integrity: sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==} @@ -1669,8 +1669,8 @@ packages: engines: {node: '>= 10'} hasBin: true - '@tauri-apps/plugin-shell@2.2.2': - resolution: {integrity: sha512-fg9XKWfzRQsN8p+Zrk82WeHvXFvGVnG0/mTlujQdLWNnO5cM6WD9qCrHbFytScVS+WhmRAkuypQPcxeKKl3VBg==} + '@tauri-apps/plugin-shell@2.3.5': + resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==} '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -6463,7 +6463,7 @@ snapshots: 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) - '@tauri-apps/api@2.10.1': {} + '@tauri-apps/api@2.11.0': {} '@tauri-apps/cli-darwin-arm64@2.10.1': optional: true @@ -6512,9 +6512,9 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.10.1 '@tauri-apps/cli-win32-x64-msvc': 2.10.1 - '@tauri-apps/plugin-shell@2.2.2': + '@tauri-apps/plugin-shell@2.3.5': dependencies: - '@tauri-apps/api': 2.10.1 + '@tauri-apps/api': 2.11.0 '@types/babel__core@7.20.5': dependencies: diff --git a/BillNote_frontend/src-tauri/Cargo.lock b/BillNote_frontend/src-tauri/Cargo.lock index 43f5aae..84a0834 100644 --- a/BillNote_frontend/src-tauri/Cargo.lock +++ b/BillNote_frontend/src-tauri/Cargo.lock @@ -165,6 +165,21 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -216,7 +231,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.4", ] [[package]] @@ -244,9 +259,9 @@ dependencies = [ [[package]] name = "brotli" -version = "7.0.0" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -255,9 +270,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.3" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -387,7 +402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" dependencies = [ "serde", - "toml", + "toml 0.8.23", ] [[package]] @@ -448,7 +463,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -461,12 +476,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "cookie" version = "0.18.1" @@ -506,6 +515,19 @@ dependencies = [ "libc", ] +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -562,19 +584,15 @@ dependencies = [ [[package]] name = "cssparser" -version = "0.27.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" dependencies = [ "cssparser-macros", "dtoa-short", - "itoa 0.4.8", - "matches", - "phf 0.8.0", - "proc-macro2", - "quote", + "itoa", + "phf", "smallvec", - "syn 1.0.109", ] [[package]] @@ -589,14 +607,20 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.9" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.103", + "ctor-proc-macro", + "dtor", ] +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "darling" version = "0.20.11" @@ -632,6 +656,17 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + [[package]] name = "deranged" version = "0.4.0" @@ -644,11 +679,19 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.20" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case", "proc-macro2", "quote", "rustc_version", @@ -686,12 +729,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "dispatch2" version = "0.3.0" @@ -699,7 +736,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.9.1", - "objc2 0.6.1", + "block2 0.6.1", + "libc", + "objc2 0.6.4", ] [[package]] @@ -715,9 +754,9 @@ dependencies = [ [[package]] name = "dlopen2" -version = "0.7.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" dependencies = [ "dlopen2_derive", "libc", @@ -736,6 +775,21 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + [[package]] name = "dpi" version = "0.1.2" @@ -760,6 +814,21 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dunce" version = "1.0.5" @@ -781,7 +850,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml", + "toml 0.8.23", "vswhom", "winreg", ] @@ -827,6 +896,12 @@ dependencies = [ "typeid", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "fdeflate" version = "0.3.7" @@ -871,6 +946,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.5.0" @@ -913,16 +994,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures-channel" version = "0.3.31" @@ -995,15 +1066,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "gdk" version = "0.18.2" @@ -1113,17 +1175,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - [[package]] name = "getrandom" version = "0.2.16" @@ -1336,16 +1387,12 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "html5ever" -version = "0.26.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ "log", - "mac", "markup5ever", - "proc-macro2", - "quote", - "syn 1.0.109", ] [[package]] @@ -1356,7 +1403,7 @@ checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", - "itoa 1.0.15", + "itoa", ] [[package]] @@ -1400,7 +1447,7 @@ dependencies = [ "http", "http-body", "httparse", - "itoa 1.0.15", + "itoa", "pin-project-lite", "smallvec", "tokio", @@ -1457,12 +1504,12 @@ dependencies = [ [[package]] name = "ico" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -1615,16 +1662,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -1644,12 +1681,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.15" @@ -1703,10 +1734,12 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1744,25 +1777,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "kuchikiki" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" -dependencies = [ - "cssparser", - "html5ever", - "indexmap 1.9.3", - "matches", - "selectors", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libappindicator" version = "0.9.0" @@ -1793,6 +1807,15 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -1838,32 +1861,17 @@ dependencies = [ "value-bag", ] -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - [[package]] name = "markup5ever" -version = "0.11.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" dependencies = [ "log", - "phf 0.10.1", - "phf_codegen 0.10.0", - "string_cache", - "string_cache_codegen", "tendril", + "web_atoms", ] -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "memchr" version = "2.7.5" @@ -1908,23 +1916,23 @@ dependencies = [ [[package]] name = "muda" -version = "0.16.1" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492" +checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" dependencies = [ "crossbeam-channel", "dpi", "gtk", "keyboard-types", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.12", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1942,12 +1950,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -1963,12 +1965,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - [[package]] name = "num-conv" version = "0.1.0" @@ -2032,9 +2028,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -2048,15 +2044,9 @@ checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ "bitflags 2.9.1", "block2 0.6.1", - "libc", - "objc2 0.6.1", - "objc2-cloud-kit", - "objc2-core-data", + "objc2 0.6.4", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", "objc2-foundation 0.3.1", - "objc2-quartz-core 0.3.1", ] [[package]] @@ -2066,7 +2056,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" dependencies = [ "bitflags 2.9.1", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-foundation 0.3.1", ] @@ -2076,8 +2066,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-foundation 0.3.1", ] @@ -2089,7 +2078,7 @@ checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ "bitflags 2.9.1", "dispatch2", - "objc2 0.6.1", + "objc2 0.6.4", ] [[package]] @@ -2100,7 +2089,7 @@ checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ "bitflags 2.9.1", "dispatch2", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] @@ -2111,7 +2100,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.4", + "objc2-foundation 0.3.1", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac0f75792558aa9d618443bbb5db7426a7a0b6fddf96903f86ef9ad02e135740" +dependencies = [ + "objc2 0.6.4", "objc2-foundation 0.3.1", ] @@ -2150,8 +2149,7 @@ checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ "bitflags 2.9.1", "block2 0.6.1", - "libc", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -2162,7 +2160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ "bitflags 2.9.1", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -2198,7 +2196,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" dependencies = [ "bitflags 2.9.1", - "objc2 0.6.1", + "objc2 0.6.4", + "objc2-core-foundation", "objc2-foundation 0.3.1", ] @@ -2209,8 +2208,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" dependencies = [ "bitflags 2.9.1", - "objc2 0.6.1", + "block2 0.6.1", + "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.3.1", + "objc2-quartz-core 0.3.1", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3f5ec77a81d9e0c5a0b32159b0cb143d7086165e79708351e02bf37dfc65cd" +dependencies = [ + "objc2 0.6.4", "objc2-foundation 0.3.1", ] @@ -2222,7 +2239,7 @@ checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" dependencies = [ "bitflags 2.9.1", "block2 0.6.1", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", @@ -2333,106 +2350,43 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "phf" -version = "0.8.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros 0.8.0", - "phf_shared 0.8.0", - "proc-macro-hack", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_shared 0.10.0", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", + "phf_macros", + "phf_shared", + "serde", ] [[package]] name = "phf_codegen" -version = "0.8.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", + "phf_generator", + "phf_shared", ] [[package]] name = "phf_generator" -version = "0.8.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ - "phf_shared 0.8.0", - "rand 0.7.3", -] - -[[package]] -name = "phf_generator" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" -dependencies = [ - "phf_shared 0.10.0", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", + "fastrand", + "phf_shared", ] [[package]] name = "phf_macros" -version = "0.8.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator", + "phf_shared", "proc-macro2", "quote", "syn 2.0.103", @@ -2440,29 +2394,11 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.8.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher 0.3.11", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher 1.0.1", + "siphasher", ] [[package]] @@ -2509,6 +2445,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.9.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "potential_utf" version = "0.1.2" @@ -2591,12 +2540,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" version = "1.0.95" @@ -2656,20 +2599,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - [[package]] name = "rand" version = "0.8.5" @@ -2677,18 +2606,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", + "rand_chacha", + "rand_core", ] [[package]] @@ -2698,16 +2617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", + "rand_core", ] [[package]] @@ -2719,24 +2629,6 @@ dependencies = [ "getrandom 0.2.16", ] -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2823,9 +2715,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.20" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes", @@ -2842,7 +2734,6 @@ dependencies = [ "pin-project-lite", "serde", "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", "tokio-util", @@ -2895,7 +2786,7 @@ dependencies = [ "borsh", "bytes", "num-traits", - "rand 0.8.5", + "rand", "rkyv", "serde", "serde_json", @@ -2907,6 +2798,12 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -2990,22 +2887,21 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "selectors" -version = "0.22.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", "cssparser", "derive_more", - "fxhash", "log", - "matches", - "phf 0.8.0", - "phf_codegen 0.8.0", + "new_debug_unreachable", + "phf", + "phf_codegen", "precomputed-hash", + "rustc-hash", "servo_arc", "smallvec", - "thin-slice", ] [[package]] @@ -3019,10 +2915,11 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -3038,10 +2935,19 @@ dependencies = [ ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -3065,7 +2971,7 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ - "itoa 1.0.15", + "itoa", "memchr", "ryu", "serde", @@ -3092,15 +2998,12 @@ dependencies = [ ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "serde_spanned" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "form_urlencoded", - "itoa 1.0.15", - "ryu", - "serde", + "serde_core", ] [[package]] @@ -3136,9 +3039,9 @@ dependencies = [ [[package]] name = "serialize-to-javascript" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" dependencies = [ "serde", "serde_json", @@ -3147,22 +3050,21 @@ dependencies = [ [[package]] name = "serialize-to-javascript-impl" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.103", ] [[package]] name = "servo_arc" -version = "0.1.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" dependencies = [ - "nodrop", "stable_deref_trait", ] @@ -3236,12 +3138,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.1" @@ -3278,7 +3174,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ "bytemuck", "cfg_aliases", - "core-graphics", + "core-graphics 0.24.0", "foreign-types", "js-sys", "log", @@ -3326,25 +3222,24 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "string_cache" -version = "0.8.9" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.11.3", + "phf_shared", "precomputed-hash", - "serde", ] [[package]] name = "string_cache_codegen" -version = "0.5.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] @@ -3417,40 +3312,41 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml", + "toml 0.8.23", "version-compare", ] [[package]] name = "tao" -version = "0.33.0" +version = "0.35.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" +checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" dependencies = [ "bitflags 2.9.1", + "block2 0.6.1", "core-foundation", - "core-graphics", + "core-graphics 0.25.0", "crossbeam-channel", - "dispatch", + "dbus", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni", - "lazy_static", "libc", "log", "ndk", - "ndk-context", "ndk-sys", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-foundation 0.3.1", + "objc2-ui-kit", "once_cell", "parking_lot", + "percent-encoding", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -3485,17 +3381,17 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.5.1" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7b0bc1aec81bda6bc455ea98fcaed26b3c98c1648c627ad6ff1c704e8bf8cbc" +checksum = "b93bd86d231f0a8138f11a02a584769fe4b703dc36ae133d783228dbc4801405" dependencies = [ "anyhow", "bytes", + "cookie", "dirs", "dunce", "embed_plist", - "futures-util", - "getrandom 0.2.16", + "getrandom 0.3.3", "glob", "gtk", "heck 0.5.0", @@ -3505,10 +3401,11 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-foundation 0.3.1", "objc2-ui-kit", + "objc2-web-kit", "percent-encoding", "plist", "raw-window-handle", @@ -3527,7 +3424,6 @@ dependencies = [ "tokio", "tray-icon", "url", - "urlpattern", "webkit2gtk", "webview2-com", "window-vibrancy", @@ -3536,9 +3432,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.2.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a0350f0df1db385ca5c02888a83e0e66655c245b7443db8b78a70da7d7f8fc" +checksum = "3a318b234cc2dea65f575467bafcfb76286bce228ebc3778e337d61d03213007" dependencies = [ "anyhow", "cargo_toml", @@ -3552,22 +3448,21 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.2.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93f035551bf7b11b3f51ad9bc231ebbe5e085565527991c16cf326aa38cdf47" +checksum = "6bd11644962add2549a60b7e7c6800f17d7020156e02f516021d8103e80cc528" dependencies = [ "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -3585,9 +3480,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.2.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8db4df25e2d9d45de0c4c910da61cd5500190da14ae4830749fee3466dddd112" +checksum = "fed9d3742a37a355d2e47c9af924e9fbc112abb76f9835d35d4780e318419502" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3599,9 +3494,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.2.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a5ebe6a610d1b78a94650896e6f7c9796323f408800cef436e0fa0539de601" +checksum = "eefb2c18e8a605c23edb48fc56bb77381199e1a1e7f6ff0c9b970afe7b3cb8ee" dependencies = [ "anyhow", "glob", @@ -3610,7 +3505,6 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml", "walkdir", ] @@ -3624,7 +3518,7 @@ dependencies = [ "byte-unit", "fern", "log", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-foundation 0.3.1", "serde", "serde_json", @@ -3638,9 +3532,9 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" -version = "2.2.2" +version = "2.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34e525a448b80ad5d906fcbd93838ac3ba37985b29ac699a045b5da9b0a1a22" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" dependencies = [ "encoding_rs", "log", @@ -3659,39 +3553,41 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.6.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f004905d549854069e6774533d742b03cacfd6f03deb08940a8677586cbe39" +checksum = "8fef478ba1d2ac21c2d528740b24d0cb315e1e8b1111aae53fafac34804371fc" dependencies = [ "cookie", "dpi", "gtk", "http", "jni", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-ui-kit", + "objc2-web-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", "thiserror 2.0.12", "url", + "webkit2gtk", + "webview2-com", "windows", ] [[package]] name = "tauri-runtime-wry" -version = "2.6.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f85d056f4d4b014fe874814034f3416d57114b617a493a4fe552580851a3f3a2" +checksum = "a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0" dependencies = [ "gtk", "http", "jni", "log", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation 0.3.1", "once_cell", "percent-encoding", "raw-window-handle", @@ -3708,24 +3604,24 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.4.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2900399c239a471bcff7f15c4399eb1a8c4fe511ba2853e07c996d771a5e0a4" +checksum = "d57200389a2f82b4b0a40ae29ca19b6978116e8f4d4e974c3234ce40c0ffbdec" dependencies = [ "anyhow", "brotli", "cargo_metadata", "ctor", + "dom_query", "dunce", "glob", - "html5ever", "http", "infer", "json-patch", - "kuchikiki", "log", "memchr", - "phf 0.11.3", + "phf", + "plist", "proc-macro2", "quote", "regex", @@ -3737,7 +3633,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.12", - "toml", + "toml 0.9.6", "url", "urlpattern", "uuid", @@ -3752,26 +3648,19 @@ checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4" dependencies = [ "embed-resource", "indexmap 2.9.0", - "toml", + "toml 0.8.23", ] [[package]] name = "tendril" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" dependencies = [ - "futf", - "mac", + "new_debug_unreachable", "utf-8", ] -[[package]] -name = "thin-slice" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" - [[package]] name = "thiserror" version = "1.0.69" @@ -3819,7 +3708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", - "itoa 1.0.15", + "itoa", "libc", "num-conv", "num_threads", @@ -3905,11 +3794,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit 0.22.27", ] +[[package]] +name = "toml" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae2a4cf385da23d1d53bc15cdfa5c2109e93d8d362393c801e87da2f72f0e201" +dependencies = [ + "indexmap 2.9.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.11", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -3919,6 +3823,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" @@ -3926,7 +3839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.9.0", - "toml_datetime", + "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -3937,7 +3850,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ "indexmap 2.9.0", - "toml_datetime", + "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -3949,18 +3862,33 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.9.0", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", "winnow 0.7.11", ] +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tower" version = "0.5.2" @@ -3978,20 +3906,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" dependencies = [ "bitflags 2.9.1", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -4027,24 +3955,24 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.20.1" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" dependencies = [ "crossbeam-channel", "dirs", "libappindicator", "muda", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.1", "once_cell", - "png", + "png 0.18.1", "serde", "thiserror 2.0.12", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4229,12 +4157,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -4252,48 +4174,32 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.103", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4301,31 +4207,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.103", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -4336,19 +4242,31 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "webkit2gtk" -version = "2.0.1" +name = "web_atoms" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" dependencies = [ "bitflags 1.3.2", "cairo-rs", @@ -4370,9 +4288,9 @@ dependencies = [ [[package]] name = "webkit2gtk-sys" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", @@ -4390,9 +4308,9 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.37.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b542b5cfbd9618c46c2784e4d41ba218c336ac70d44c55e47b251033e7d85601" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", @@ -4404,9 +4322,9 @@ dependencies = [ [[package]] name = "webview2-com-macros" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", @@ -4415,9 +4333,9 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.37.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae2d11c4a686e4409659d7891791254cf9286d3cfe0eef54df1523533d22295" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.12", "windows", @@ -4461,7 +4379,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", @@ -4479,7 +4397,7 @@ dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -4500,7 +4418,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -4512,7 +4430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -4544,6 +4462,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -4551,7 +4475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4560,7 +4484,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4569,7 +4493,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4608,6 +4532,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -4661,7 +4594,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4670,7 +4603,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4829,6 +4762,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" + [[package]] name = "winreg" version = "0.55.0" @@ -4856,26 +4795,26 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wry" -version = "0.51.2" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886a0a9d2a94fd90cfa1d929629b79cfefb1546e2c7430c63a47f0664c0e4e2" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", "block2 0.6.1", "cookie", "crossbeam-channel", + "dirs", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", "http", "javascriptcore-rs", "jni", - "kuchikiki", "libc", "ndk", - "objc2 0.6.1", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", diff --git a/BillNote_frontend/src-tauri/Cargo.toml b/BillNote_frontend/src-tauri/Cargo.toml index 3aed5bb..612cc61 100644 --- a/BillNote_frontend/src-tauri/Cargo.toml +++ b/BillNote_frontend/src-tauri/Cargo.toml @@ -15,14 +15,16 @@ name = "app_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] -tauri-build = { version = "2.2.0", features = [] } +# tauri-build / tauri crate 与 @tauri-apps/api 大版本必须对齐(CLI 在 build 前会校验)。 +# @tauri-apps/api 已升 2.10(commit bb9a70e),这里同步到 2.x 最新让 cargo 解析到匹配版本。 +tauri-build = { version = "2", features = [] } [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } log = "0.4" -tauri = { version = "2.5.0", features = ["devtools"] } -tauri-plugin-log = "2.0.0-rc" +tauri = { version = "2", features = ["devtools"] } +tauri-plugin-log = "2" tauri-plugin-shell = "2" [package.metadata.tauri.bundle.macOS] diff --git a/BillNote_frontend/src-tauri/src/lib.rs b/BillNote_frontend/src-tauri/src/lib.rs index 2c8be15..a623437 100644 --- a/BillNote_frontend/src-tauri/src/lib.rs +++ b/BillNote_frontend/src-tauri/src/lib.rs @@ -3,10 +3,19 @@ use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use std::env; use std::collections::HashMap; +use std::io::{Read, Write}; +use std::net::{SocketAddr, TcpStream}; use std::path::Path; use std::sync::Mutex; +use std::time::{Duration, Instant}; use serde::Serialize; +// Sidecar 启动期内前端不该看到「加载中」无限转。 +// 总等待上限 = 启动期 PyInstaller 解压 + uvicorn bind 时间的最坏估计, +// 实测 macOS / Windows 慢盘大概 5-20s,设 45s 留余量但不至于让用户绝望。 +const BACKEND_STARTUP_TIMEOUT_SECS: u64 = 45; +const BACKEND_DEFAULT_PORT: u16 = 8483; + // Sidecar 子进程句柄,用 Mutex 包裹方便 restart 时杀旧进程 struct SidecarHandle(Mutex>); @@ -50,6 +59,10 @@ pub fn run() { })?; app.manage(SidecarHandle(Mutex::new(Some(child)))); + // 启动 ready probe:异步轮询本地 BACKEND_PORT 是否在监听, + // 解决前端 useCheckBackend 在 PyInstaller 解压期瞎猜后端起没起的问题。 + spawn_backend_ready_probe(app.handle().clone()); + Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -60,8 +73,33 @@ pub fn run() { get_install_path_diagnostics, restart_backend_sidecar ]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .build(tauri::generate_context!()) + .expect("error while building tauri application") + // 用 build()+run() 拿到 RunEvent 流,关键诉求:app 退出前必须 kill 掉 PyInstaller + // sidecar,否则它会变成持有 8483 端口的孤儿进程,下次启动 BiliNote 直接 bind 失败。 + // 之前漏掉这一步导致用户 PID 96739 那种「上次没关干净 → 这次起不来」的死循环。 + .run(|app_handle, event| { + match event { + // ExitRequested 在用户 Cmd-Q / 点关闭 / Dock 退出时触发,先于实际进程结束。 + // Exit 是兜底——任何走到 Tauri 主循环结束的路径都会经过它。 + tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => { + kill_backend_sidecar(app_handle); + } + _ => {} + } + }); +} + +// 关闭期统一杀 sidecar,take() 把 child 从 state 拿走避免重复 kill。 +fn kill_backend_sidecar(app_handle: &tauri::AppHandle) { + if let Some(state) = app_handle.try_state::() { + if let Ok(mut guard) = state.0.lock() { + if let Some(child) = guard.take() { + eprintln!("[shutdown] killing backend sidecar before app exit"); + let _ = child.kill(); + } + } + } } // 获取额外的二进制路径 @@ -306,6 +344,12 @@ fn restart_backend_sidecar( state: State<'_, SidecarHandle>, app: tauri::AppHandle, ) -> Result<(), String> { + // 0. 先告诉前端「我们要重启了」。前端可以借此忽略接下来 N 秒内的 backend-terminated + // 事件——那是我们主动 kill 老 sidecar 的副作用,不是真异常。否则会出现: + // terminated 事件延迟到达 → 覆盖掉 'running' 状态 → 面板永远显示「已退出」。 + if let Some(window) = app.get_webview_window("main") { + let _ = window.emit("backend-restarting", ()); + } // 1. 拿出旧 child 并 kill(kill 失败也继续,可能进程已经退了) { let mut guard = state.0.lock().map_err(|e| format!("锁 sidecar state 失败: {}", e))?; @@ -323,9 +367,88 @@ fn restart_backend_sidecar( if let Some(window) = app.get_webview_window("main") { let _ = window.emit("backend-restarted", ()); } + // 4. 重启后同样起一次 ready probe,让前端能及时退出失败态 + spawn_backend_ready_probe(app); Ok(()) } +// 后端就绪探测:异步轮询 GET /api/sys_check,要求 HTTP 200 才算就绪。 +// +// 旧实现只做 TcpStream::connect_timeout——但端口被另一个孤儿 sidecar 占着时也会 +// 连得通,导致 emit('backend-ready') 误判:前端进入主界面,但真正的新 sidecar +// 没 bind 上立刻就死,banner 永远停在「后端进程已退出」。 +// +// 真发一个 HTTP 请求拿 200 才算「这是我们的后端在响应」。 +fn spawn_backend_ready_probe(app: tauri::AppHandle) { + let port: u16 = env::var("BACKEND_PORT") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(BACKEND_DEFAULT_PORT); + let addr: SocketAddr = format!("127.0.0.1:{}", port).parse().expect("invalid backend addr"); + let timeout = Duration::from_secs(BACKEND_STARTUP_TIMEOUT_SECS); + + std::thread::spawn(move || { + let start = Instant::now(); + let probe_interval = Duration::from_millis(500); + loop { + if probe_sys_check(&addr) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.emit("backend-ready", port); + println!("Backend ready on port {} after {:?}", port, start.elapsed()); + } + return; + } + if start.elapsed() >= timeout { + if let Some(window) = app.get_webview_window("main") { + let payload = format!( + "后端在 {}s 内 /api/sys_check 未返回 200,疑似启动失败或端口 {} 被其他进程占用", + timeout.as_secs(), + port + ); + let _ = window.emit("backend-startup-timeout", payload); + eprintln!( + "Backend startup timeout: /api/sys_check did not return 200 on 127.0.0.1:{} after {:?}", + port, + start.elapsed() + ); + } + return; + } + std::thread::sleep(probe_interval); + } + }); +} + +// 极简 HTTP/1.0 GET /api/sys_check —— 用 std::net 手写避免引 reqwest/ureq 的重依赖。 +// 任何错都视为「还没就绪」,下次 tick 再试。 +fn probe_sys_check(addr: &SocketAddr) -> bool { + let connect_timeout = Duration::from_millis(800); + let rw_timeout = Duration::from_millis(1500); + let mut stream = match TcpStream::connect_timeout(addr, connect_timeout) { + Ok(s) => s, + Err(_) => return false, + }; + let _ = stream.set_read_timeout(Some(rw_timeout)); + let _ = stream.set_write_timeout(Some(rw_timeout)); + // HTTP/1.0 + Connection: close 让服务端发完响应就关,免去 chunked / keep-alive 解析 + let req = format!( + "GET /api/sys_check HTTP/1.0\r\nHost: 127.0.0.1:{}\r\nConnection: close\r\n\r\n", + addr.port() + ); + if stream.write_all(req.as_bytes()).is_err() { + return false; + } + // 只要 status line,64 字节够了 + let mut buf = [0u8; 64]; + let n = match stream.read(&mut buf) { + Ok(n) => n, + Err(_) => return false, + }; + let head = std::str::from_utf8(&buf[..n]).unwrap_or(""); + // 兼容 HTTP/1.0 / 1.1 起始行 + head.starts_with("HTTP/1.1 200") || head.starts_with("HTTP/1.0 200") +} + // 安装路径诊断:PyInstaller 在含非 ASCII / 空格的路径下加载 _internal/* 经常炸; // 父目录不可写时模型 / 配置 / 日志也无法落盘 #[derive(Serialize, Clone)] diff --git a/BillNote_frontend/src/App.tsx b/BillNote_frontend/src/App.tsx index 682a75a..d871506 100644 --- a/BillNote_frontend/src/App.tsx +++ b/BillNote_frontend/src/App.tsx @@ -33,7 +33,7 @@ const NotFoundPage = lazy(() => import('@/pages/NotFoundPage')) function App() { useTaskPolling(3000) // 每 3 秒轮询一次 - const { loading, initialized } = useCheckBackend() + const { loading, initialized, failed, lastError, retry } = useCheckBackend() // 在后端初始化完成后执行系统检查 useEffect(() => { @@ -42,12 +42,17 @@ function App() { } }, [initialized]) - // 如果后端还未初始化,显示初始化对话框 + // 如果后端还未初始化,显示初始化对话框(loading 或 failed 都展示,由 dialog 内部决定渲染哪一态) if (!initialized) { return ( <> - + ) } diff --git a/BillNote_frontend/src/components/BackendHealth/BackendHealthIndicator.tsx b/BillNote_frontend/src/components/BackendHealth/BackendHealthIndicator.tsx index ce8217e..cddc5c1 100644 --- a/BillNote_frontend/src/components/BackendHealth/BackendHealthIndicator.tsx +++ b/BillNote_frontend/src/components/BackendHealth/BackendHealthIndicator.tsx @@ -10,12 +10,14 @@ import BackendLogPanel from './BackendLogPanel' type Health = 'green' | 'yellow' | 'red' | 'unknown' const HEALTH_POLL_MS = 5000 -const SYS_HEALTH_PATH = '/api/sys_health' +// 路径不带 /api/,因为 backendBase() 已经把它包进 baseURL 了(同 axios 实例的语义)。 +// 之前写 '/api/sys_health' + base='http://host/api' = 双 /api → 一直 404。 +const SYS_HEALTH_PATH = '/sys_health' function backendBase(): string { - // 与 services/request.ts 用的一致 + // 与 utils/request.ts 的 baseURL 计算保持一致:env 没设走 '/api' 兜底。 const fromEnv = (import.meta as any).env?.VITE_API_BASE_URL as string | undefined - return (fromEnv ?? '').replace(/\/$/, '') + return ((fromEnv && fromEnv.length > 0) ? fromEnv : '/api').replace(/\/$/, '') } const BackendHealthIndicator = () => { diff --git a/BillNote_frontend/src/components/BackendHealth/useBackendEvents.ts b/BillNote_frontend/src/components/BackendHealth/useBackendEvents.ts index cd2a5d2..ca423f7 100644 --- a/BillNote_frontend/src/components/BackendHealth/useBackendEvents.ts +++ b/BillNote_frontend/src/components/BackendHealth/useBackendEvents.ts @@ -35,6 +35,9 @@ export function useBackendEvents(): BackendEvents { const [logs, setLogs] = useState([]) // 用 ref 持有最新 logs 数组,append 时不被闭包陷阱卡到旧值 const logsRef = useRef([]) + // 主动重启期:Rust 在 kill 老 sidecar 前会 emit 'backend-restarting'。 + // 期间到达的 'backend-terminated' 是我们自己造成的,不要污染状态。 + const ignoreNextTerminatedRef = useRef(false) function append(entry: LogEntry) { const next = logsRef.current.concat(entry) @@ -58,7 +61,23 @@ export function useBackendEvents(): BackendEvents { const offErr = await listen('backend-error', event => { append({ level: 'error', text: stripQuotes(event.payload), ts: Date.now() }) }) + const offRestarting = await listen('backend-restarting', () => { + // 紧接着到达的 terminated 是我们主动 kill 老 sidecar 引发的,跳过 3s + ignoreNextTerminatedRef.current = true + setTimeout(() => { ignoreNextTerminatedRef.current = false }, 3000) + append({ level: 'info', text: '[Backend restarting]', ts: Date.now() }) + }) const offTerm = await listen('backend-terminated', event => { + // 主动重启窗口内的 terminated 是预期副作用,仅记日志、不改状态 + if (ignoreNextTerminatedRef.current) { + ignoreNextTerminatedRef.current = false + append({ + level: 'info', + text: `[Backend terminated, restart in progress] code=${event.payload ?? 'unknown'}`, + ts: Date.now(), + }) + return + } setStatus('terminated') setExitCode(event.payload ?? null) append({ @@ -73,7 +92,7 @@ export function useBackendEvents(): BackendEvents { append({ level: 'info', text: '[Backend restarted]', ts: Date.now() }) }) - unlisteners = [offMsg, offErr, offTerm, offRestart] + unlisteners = [offMsg, offErr, offRestarting, offTerm, offRestart] })() return () => { diff --git a/BillNote_frontend/src/components/BackendInitDialog.tsx b/BillNote_frontend/src/components/BackendInitDialog.tsx index 989888f..079ca4d 100644 --- a/BillNote_frontend/src/components/BackendInitDialog.tsx +++ b/BillNote_frontend/src/components/BackendInitDialog.tsx @@ -1,13 +1,141 @@ +import { useMemo, useState } from 'react' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { Loader2 } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { Loader2, AlertTriangle, RotateCcw, Clipboard } from 'lucide-react' +import { useBackendEvents } from '@/components/BackendHealth/useBackendEvents' + +// 失败态预览里最多展示几行 stderr。比这还多就请用户去 copyLogs() 拷出来。 +const STDERR_PREVIEW_LINES = 6 interface Props { + /** 加载中:显示转圈对话框 */ open: boolean + /** 启动失败:显示错误 + 重启/复制日志按钮 */ + failed?: boolean + /** 失败原因(来自 useCheckBackend.lastError 或 Tauri 事件 payload) */ + lastError?: string | null + /** 重新走一遍 useCheckBackend 的轮询(不重启 sidecar) */ + onRetry?: () => void } - function BackendInitDialog({ open }: Props) { +// 加载中 + 启动失败两个状态合并在一个 dialog 里。 +// 失败态比加载态更紧急:用户能看到具体原因 + 一键重启 + 一键复制日志去 issue, +// 而不是面对一个永远转圈的对话框。 +function BackendInitDialog({ open, failed = false, lastError = null, onRetry }: Props) { + const { isTauri, restart, copyLogs, logs } = useBackendEvents() + const [restarting, setRestarting] = useState(false) + const [copyResult, setCopyResult] = useState<'idle' | 'ok' | 'fail'>('idle') + + // 从 ring buffer 里挑最后几行 stderr —— 它们比 lastError(hook 自己总结的那句)信息密度更高, + // 通常 Python traceback 的最后一行就是真正的错误类型 + 消息 + const stderrPreview = useMemo(() => { + if (!failed || !logs?.length) return [] + return logs + .filter((l) => l.level === 'error') + .slice(-STDERR_PREVIEW_LINES) + .map((l) => l.text) + }, [failed, logs]) + + // 任一态需要展示就保持 dialog 开着,关掉只在两个 flag 都熄灭时发生 + const isOpen = open || failed + + const handleRestart = async () => { + setRestarting(true) + try { + if (isTauri) await restart() + onRetry?.() + } catch { + // restart 内部已经 append 到 log,这里不再 toast + } finally { + setRestarting(false) + } + } + + const handleCopy = async () => { + const ok = await copyLogs() + setCopyResult(ok ? 'ok' : 'fail') + setTimeout(() => setCopyResult('idle'), 2000) + } + + if (failed) { + return ( + + + + + + 后端启动失败 + + +
+

+ {lastError || '后端在预计时间内未就绪。'} +

+ {stderrPreview.length > 0 && ( +
+

+ 后端最近 {stderrPreview.length} 行错误日志 + (完整日志请用「复制启动日志」): +

+
+                  {stderrPreview.join('\n')}
+                
+
+ )} +
+

常见原因:

+
    +
  • 安装路径含中文 / 空格(PyInstaller 在这种路径下经常起不来)
  • +
  • 没装 ffmpeg / 端口 8483 被占用
  • +
  • 首次启动时 whisper 模型下载未完成
  • +
+
+
+ + {isTauri && ( + + )} +
+

+ 仍然无法解决?复制日志去  + + GitHub Issues + +  反馈。 +

+
+
+
+ ) + } + + // 默认加载态 return ( - + @@ -15,9 +143,12 @@ interface Props { 后端正在初始化中… -

请稍候,系统正在启动后端服务,出现报错属于正常现象

+

+ 请稍候,系统正在启动后端服务。首次启动可能需要 10-30 秒解压依赖。 +

) } -export default BackendInitDialog \ No newline at end of file + +export default BackendInitDialog diff --git a/BillNote_frontend/src/components/Form/DownloaderForm/ProxyConfig.tsx b/BillNote_frontend/src/components/Form/DownloaderForm/ProxyConfig.tsx new file mode 100644 index 0000000..c256c27 --- /dev/null +++ b/BillNote_frontend/src/components/Form/DownloaderForm/ProxyConfig.tsx @@ -0,0 +1,89 @@ +import { useEffect, useState } from 'react' +import toast from 'react-hot-toast' +import { Switch } from '@/components/ui/switch' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { getProxyConfig, updateProxyConfig } from '@/services/proxy' + +// 全局代理配置:作用于 LLM API + 转写 API(Groq 等)+ yt-dlp 视频下载。 +// 国内访问 OpenAI / Groq / YouTube 基本都要靠它。 +const ProxyConfig = () => { + const [enabled, setEnabled] = useState(false) + const [url, setUrl] = useState('') + const [effective, setEffective] = useState('') + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + + useEffect(() => { + ;(async () => { + try { + const cfg = await getProxyConfig() + setEnabled(cfg.enabled) + setUrl(cfg.url) + setEffective(cfg.effective) + } catch { + /* 拦截器已 toast */ + } finally { + setLoading(false) + } + })() + }, []) + + const handleSave = async () => { + if (enabled && !url.trim()) { + toast.error('请填写代理地址,或关闭代理开关') + return + } + setSaving(true) + try { + const cfg = await updateProxyConfig({ enabled, url: url.trim() }) + setEnabled(cfg.enabled) + setUrl(cfg.url) + setEffective(cfg.effective) + toast.success('代理配置已保存') + } catch { + /* 拦截器已 toast */ + } finally { + setSaving(false) + } + } + + if (loading) { + return
加载代理配置…
+ } + + // env 兜底:配置没开但 effective 有值,说明来自 HTTP_PROXY 环境变量 + const fromEnv = !enabled && !!effective + + return ( +
+
+ 全局代理 + +
+

+ 作用于 AI 模型接口、转写接口(Groq 等)、YouTube 下载。 +

+ setUrl(e.target.value)} + className="text-sm" + /> + {fromEnv && ( +

+ 当前生效(来自环境变量):{effective} +

+ )} + {enabled && effective && ( +

当前生效:{effective}

+ )} + +
+ ) +} + +export default ProxyConfig diff --git a/BillNote_frontend/src/components/Form/modelForm/components/providerCard.tsx b/BillNote_frontend/src/components/Form/modelForm/components/providerCard.tsx index 4092af8..93f8dae 100644 --- a/BillNote_frontend/src/components/Form/modelForm/components/providerCard.tsx +++ b/BillNote_frontend/src/components/Form/modelForm/components/providerCard.tsx @@ -41,21 +41,22 @@ const ProviderCard: FC = ({
navigate(`/settings/model/${id}`)} > -
navigate(`/settings/model/${id}`)} - > +
{providerName}
-
+ {/* Switch 自己的点击不应该冒泡触发整行跳转 */} +
e.stopPropagation()}> { }) }) + // 后端被「重启后端」按钮拉起来后 / Rust ready-probe 检测到新 sidecar 真的就绪后, + // 自动清掉 terminated 横幅。之前 dismissible:false + 没自动清逻辑 = banner 永远卡。 + const offRestarted = await listen('backend-restarted', () => { + setBanner(b => (b?.severity === 'error' ? null : b)) + }) + const offReady = await listen('backend-ready', () => { + setBanner(b => (b?.severity === 'error' ? null : b)) + }) + // backend-error 是 sidecar stderr,量大噪音多,这里不直接展示,留给 P2 的日志面板。 - unlisteners = [offWarning, offTerminated] + unlisteners = [offWarning, offTerminated, offRestarted, offReady] })() return () => { diff --git a/BillNote_frontend/src/hooks/useCheckBackend.ts b/BillNote_frontend/src/hooks/useCheckBackend.ts index f05ce70..8b8ee50 100644 --- a/BillNote_frontend/src/hooks/useCheckBackend.ts +++ b/BillNote_frontend/src/hooks/useCheckBackend.ts @@ -1,52 +1,156 @@ -import { useEffect, useState } from 'react' -import request from '@/utils/request' +import { useCallback, useEffect, useRef, useState } from 'react' -const MAX_RETRIES = 3 -const RETRY_INTERVAL = 10000 // 10秒 +// 后端就绪检测的几个时间常量 +// - 总等待上限 60s:超过这个时间没就绪就切「启动失败」UI, +// 不再像旧实现 while(true) 无限转 +// - 轮询间隔 2s:比旧的 10s 更敏感,桌面端 sidecar 5-15s 解压期内能尽快感知就绪 +// - 单次请求超时 5s,避免连接 hang 拖到下一轮 +const TOTAL_TIMEOUT_MS = 60_000 +const POLL_INTERVAL_MS = 2_000 +const PROBE_TIMEOUT_MS = 5_000 -export const useCheckBackend = () => { - const [loading, setLoading] = useState(false) - const [initialized, setInitialized] = useState(false) +const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window - useEffect(() => { - let retries = 0 +// 直接用 fetch 而非 utils/request 的共享 axios:那个 axios 装了全局 toast 拦截器, +// 启动期每次 /sys_check 失败都会弹一个红色 toast,2s 一次轮询会叠出十几个。 +function getBackendBase(): string { + const fromEnv = (import.meta as any).env?.VITE_API_BASE_URL as string | undefined + return ((fromEnv && fromEnv.length > 0) ? fromEnv : '/api').replace(/\/$/, '') +} - const check = async () => { - try { - await request.get('/sys_check') - setInitialized(true) - setLoading(false) - } catch { - if (retries === 0) { - // 第一次失败时开始显示加载状态 - setLoading(true) - } +async function probeSysCheck(): Promise { + const url = `${getBackendBase()}/sys_check` + const ctrl = new AbortController() + const t = setTimeout(() => ctrl.abort(), PROBE_TIMEOUT_MS) + try { + const res = await fetch(url, { signal: ctrl.signal }) + if (!res.ok) return false + const json = await res.json().catch(() => null) + return json?.code === 0 + } + catch { + return false + } + finally { + clearTimeout(t) + } +} - if (retries < MAX_RETRIES) { - retries++ - setTimeout(check, RETRY_INTERVAL) - } else { - // 达到重试上限,继续轮询直到后端就绪 - waitUntilBackendReady() - } - } - } +interface Status { + loading: boolean + initialized: boolean + failed: boolean + lastError: string | null +} - const waitUntilBackendReady = async () => { - while (true) { - try { - await request.get('/sys_health') - setInitialized(true) - setLoading(false) - break - } catch { - await new Promise(res => setTimeout(res, RETRY_INTERVAL)) - } - } - } +interface BackendCheck extends Status { + retry: () => void +} - check() +const initialStatus: Status = { + loading: true, + initialized: false, + failed: false, + lastError: null, +} + +/** + * 后端就绪检测。 + * + * 三路信号汇聚: + * 1. HTTP 轮询 /sys_check —— 所有平台通用 + * 2. Tauri 'backend-ready' 事件 —— 桌面端 sidecar 探测器先于 HTTP 一步触达 + * 3. Tauri 'backend-terminated' / 'backend-startup-timeout' 事件 —— sidecar 死了或超时 + * 立即进失败态,不再继续轮询(旧实现的 while(true) 就是死在这里) + * + * 任何一路报「ready」即成功;任何一路报「失败」立即停掉所有轮询。 + */ +export const useCheckBackend = (): BackendCheck => { + const [status, setStatus] = useState(initialStatus) + // tick 用来强制重启 useEffect(retry 时 +1),不引入 ref 互斥逻辑的复杂性 + const [tick, setTick] = useState(0) + // 标记当前 effect 是否已 settle(避免后到的事件覆盖已确定的成功/失败态) + const settledRef = useRef(false) + + const retry = useCallback(() => { + settledRef.current = false + setStatus(initialStatus) + setTick((t) => t + 1) }, []) - return { loading, initialized } -} \ No newline at end of file + useEffect(() => { + let timeoutId: ReturnType | null = null + let pollTimerId: ReturnType | null = null + let cancelled = false + const tauriUnsubs: Array<() => void> = [] + + const markReady = () => { + if (cancelled || settledRef.current) return + settledRef.current = true + setStatus({ loading: false, initialized: true, failed: false, lastError: null }) + } + + const markFailed = (msg: string) => { + if (cancelled || settledRef.current) return + settledRef.current = true + setStatus({ loading: false, initialized: false, failed: true, lastError: msg }) + } + + const poll = async () => { + if (cancelled || settledRef.current) return + const ok = await probeSysCheck() + if (cancelled || settledRef.current) return + if (ok) { + markReady() + return + } + // 单次失败不报 toast、不抛错,继续轮询 + setStatus((s) => ({ ...s, lastError: '后端尚未响应' })) + pollTimerId = setTimeout(poll, POLL_INTERVAL_MS) + } + + // 总超时兜底 + timeoutId = setTimeout(() => { + markFailed(`后端 ${TOTAL_TIMEOUT_MS / 1000}s 内未就绪,请检查后端日志或重启`) + }, TOTAL_TIMEOUT_MS) + + // 桌面端订阅 Tauri 事件(动态 import 避免 web 端打包报错) + if (isTauri) { + import('@tauri-apps/api/event') + .then(async ({ listen }) => { + if (cancelled) return + const offReady = await listen('backend-ready', () => markReady()) + const offTimeout = await listen('backend-startup-timeout', (e) => { + markFailed(typeof e.payload === 'string' ? e.payload : '后端启动超时') + }) + const offTerm = await listen('backend-terminated', (e) => { + const code = e.payload + markFailed(`后端进程已退出 (code=${code ?? 'unknown'})`) + }) + tauriUnsubs.push(offReady, offTimeout, offTerm) + }) + .catch((err) => { + // 拿不到 @tauri-apps/api/event 不致命,继续走 HTTP 轮询 + console.warn('[useCheckBackend] 无法订阅 Tauri 事件:', err) + }) + } + + // 立刻开始第一轮轮询 + poll() + + return () => { + cancelled = true + if (timeoutId) clearTimeout(timeoutId) + if (pollTimerId) clearTimeout(pollTimerId) + tauriUnsubs.forEach((off) => { + try { + off() + } catch { + /* noop */ + } + }) + } + }, [tick]) + + return { ...status, retry } +} diff --git a/BillNote_frontend/src/pages/HomePage/components/NoteForm.tsx b/BillNote_frontend/src/pages/HomePage/components/NoteForm.tsx index 1a1f6e5..9e9cbfa 100644 --- a/BillNote_frontend/src/pages/HomePage/components/NoteForm.tsx +++ b/BillNote_frontend/src/pages/HomePage/components/NoteForm.tsx @@ -39,6 +39,7 @@ import { Textarea } from '@/components/ui/textarea.tsx' import { noteStyles, noteFormats, videoPlatforms } from '@/constant/note.ts' import { fetchModels } from '@/services/model.ts' import { useNavigate } from 'react-router-dom' +import toast from 'react-hot-toast' /* -------------------- 校验 Schema -------------------- */ const formSchema = z @@ -229,8 +230,25 @@ const NoteForm = () => { } // message.success('已提交任务') - const data = await generateNote(payload) - addPendingTask(data.task_id, values.platform, payload) + try { + const data = await generateNote(payload) + addPendingTask(data.task_id, values.platform, payload) + } catch (e: any) { + // 就绪门禁:本地转写模型还没下载好。后端返回 reason='transcriber_model_not_ready', + // 引导用户去「设置 → 音频转写配置」下载,而不是留一个静默失败的任务。 + if (e?.data?.reason === 'transcriber_model_not_ready') { + const downloading = e?.data?.downloading + toast.error( + downloading + ? '转写模型正在下载中,请稍候再提交' + : '转写模型尚未下载,请先去「音频转写配置」页下载', + ) + if (!downloading) navigate('/settings/transcriber') + return + } + // 其余错误:axios 拦截器已经弹过 toast,这里只兜底不让 promise 变成未处理 rejection + console.error('提交任务失败:', e) + } } const onInvalid = (errors: FieldErrors) => { console.warn('表单校验失败:', errors) diff --git a/BillNote_frontend/src/pages/Onboarding/index.tsx b/BillNote_frontend/src/pages/Onboarding/index.tsx index d60a6c1..7cebcb2 100644 --- a/BillNote_frontend/src/pages/Onboarding/index.tsx +++ b/BillNote_frontend/src/pages/Onboarding/index.tsx @@ -1,9 +1,36 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { addProvider, addModel, getProviderList, testConnection } from '@/services/model' +import { addProvider, addModel, testConnection, getProviderList, updateProviderById } from '@/services/model' import { getTranscriberConfig, updateTranscriberConfig } from '@/services/transcriber' import logo from '@/assets/icon.svg' +// 后端 R.error / ProviderError 的形状是 { code, msg, data },没有 .message。 +// 直接 ${e} 会渲染成 [object Object],这里统一抽取可读文案。 +function errText(e: any): string { + if (!e) return '未知错误' + if (typeof e === 'string') return e + return e.msg || e.message || JSON.stringify(e) +} + +const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window + +// 后端连通性自检不走共享 axios(会弹 toast),用裸 fetch 避免启动期 toast 叠堆 +function getBackendBase(): string { + const fromEnv = (import.meta as any).env?.VITE_API_BASE_URL as string | undefined + return ((fromEnv && fromEnv.length > 0) ? fromEnv : '/api').replace(/\/$/, '') +} +async function pingBackend(): Promise { + try { + const res = await fetch(`${getBackendBase()}/sys_check`) + if (!res.ok) return false + const json = await res.json().catch(() => null) + return json?.code === 0 + } + catch { + return false + } +} + // 桌面端首启 4 步引导。完成后写 localStorage('bilinote-onboarded') = '1',路由守卫不再拦。 // // 1. 后端连通性自检 @@ -52,24 +79,52 @@ const Onboarding = () => { } // step 1: ping 后端 + // 关键点:旧实现 useEffect 只在 step===1 时 ping 一次。失败后 backendOk=false 永远卡死, + // 即便后端随后就绪了也不会刷新。现在改成: + // - 手动重试按钮调用 doPing + // - Tauri backend-ready / backend-restarted 事件触发 doPing + // - 初次失败后 2s 自动再 ping 一次(覆盖 sidecar 慢热场景) + const doPing = useCallback(async () => { + setPinging(true) + const ok = await pingBackend() + setBackendOk(ok) + setPinging(false) + return ok + }, []) + useEffect(() => { if (step !== 1) return let cancelled = false + let timerId: ReturnType | null = null + let offReady: (() => void) | null = null + let offRestarted: (() => void) | null = null + ;(async () => { - setPinging(true) - try { - await getProviderList() - if (!cancelled) setBackendOk(true) + const ok = await doPing() + if (cancelled) return + if (!ok) { + // 后端可能正在解压/启动,2s 后再试一次 + timerId = setTimeout(() => { if (!cancelled) doPing() }, 2000) } - catch { - if (!cancelled) setBackendOk(false) - } - finally { - if (!cancelled) setPinging(false) + + // 桌面端订阅 Tauri 事件:后端真正就绪 / 重启完成时立刻再检查一次 + if (isTauri) { + try { + const { listen } = await import('@tauri-apps/api/event') + offReady = await listen('backend-ready', () => { if (!cancelled) doPing() }) + offRestarted = await listen('backend-restarted', () => { if (!cancelled) doPing() }) + } + catch { /* 拿不到事件 API 不致命 */ } } })() - return () => { cancelled = true } - }, [step]) + + return () => { + cancelled = true + if (timerId) clearTimeout(timerId) + offReady?.() + offRestarted?.() + } + }, [step, doPing]) async function saveProvider() { setError('') @@ -79,31 +134,61 @@ const Onboarding = () => { if (!modelName.trim()) { setError('请填模型名'); return } setSavingProvider(true) try { - // 复用桌面 web 端的 add_provider;type 必须是 'custom'(backend 强制) - const res: any = await addProvider({ - name: providerName.trim(), - api_key: apiKey.trim(), - base_url: baseUrl.trim(), - type: 'custom', - logo: 'custom', - }) - const newId = (res?.data ?? res) as string | undefined - if (!newId) throw new Error('后端未返回 provider id') - setProviderId(newId) + const name = providerName.trim() + let pid: string | undefined - // 加一个默认 model - await addModel({ provider_id: newId, model_name: modelName.trim() }) + // 后端 seed_default_providers() 会预置 OpenAI / DeepSeek / Qwen 等同名供应商, + // 直接 add_provider 撞名会报「供应商名称已存在」。所以:撞名时改为 + // 「找到那个已存在的同名供应商 → 更新它的 key / base_url」而不是新建。 + // 这些调用都带 silent:true —— 撞名是预期内的,不弹全局红 toast。 + try { + const res: any = await addProvider({ + name, + api_key: apiKey.trim(), + base_url: baseUrl.trim(), + type: 'custom', + logo: 'custom', + }, { silent: true }) + pid = (res?.data ?? res) as string | undefined + if (!pid) throw new Error('后端未返回 provider id') + } + catch (addErr: any) { + const msg = errText(addErr) + if (!msg.includes('已存在')) throw addErr + // 撞名:复用已存在的同名供应商 + const list: any[] = (await getProviderList({ silent: true })) || [] + const existing = list.find(p => p?.name === name) + if (!existing?.id) throw new Error(`供应商「${name}」已存在但无法定位,请换个名字`) + pid = existing.id + await updateProviderById({ + id: pid, + api_key: apiKey.trim(), + base_url: baseUrl.trim(), + enabled: 1, + }, { silent: true }) + } - // 测试连通 - try { await testConnection({ id: newId }) } + setProviderId(pid!) + + // 加一个默认 model(同名 model 已存在时后端会报错,这里也容错) + try { + await addModel({ provider_id: pid!, model_name: modelName.trim() }, { silent: true }) + } + catch (modelErr: any) { + const msg = errText(modelErr) + if (!msg.includes('已存在')) throw modelErr + // 模型已存在,直接继续 + } + + // 测试连通(失败不阻断流程,让用户自己决定继续) + try { await testConnection({ id: pid!, model: modelName.trim() }, { silent: true }) } catch (e: any) { - // 测试失败不阻断流程,让用户自己决定继续 - console.warn('测试连接失败:', e?.message ?? e) + console.warn('测试连接失败:', errText(e)) } next() } catch (e: any) { - setError(`保存失败:${e?.message ?? e}`) + setError(`保存失败:${errText(e)}`) } finally { setSavingProvider(false) @@ -123,7 +208,7 @@ const Onboarding = () => { next() } catch (e: any) { - setError(`保存失败:${e?.message ?? e}`) + setError(`保存失败:${errText(e)}`) } finally { setSavingTranscriber(false) @@ -171,6 +256,15 @@ const Onboarding = () => {
)}
+ {backendOk !== true && ( + + )} diff --git a/BillNote_frontend/src/pages/SettingPage/Downloader.tsx b/BillNote_frontend/src/pages/SettingPage/Downloader.tsx index cb80a8c..dfa58c1 100644 --- a/BillNote_frontend/src/pages/SettingPage/Downloader.tsx +++ b/BillNote_frontend/src/pages/SettingPage/Downloader.tsx @@ -1,10 +1,11 @@ -import Provider from '@/components/Form/modelForm/Provider.tsx' import { Outlet } from 'react-router-dom' import Options from '@/components/Form/DownloaderForm/Options.tsx' +import ProxyConfig from '@/components/Form/DownloaderForm/ProxyConfig.tsx' const Downloader = () => { return (
-
+
+
diff --git a/BillNote_frontend/src/pages/SettingPage/Monitor.tsx b/BillNote_frontend/src/pages/SettingPage/Monitor.tsx index f806606..76d74b8 100644 --- a/BillNote_frontend/src/pages/SettingPage/Monitor.tsx +++ b/BillNote_frontend/src/pages/SettingPage/Monitor.tsx @@ -174,7 +174,11 @@ export default function Monitor() { Whisper 模型 - {status && } + {status && (() => { + const isLocal = status.whisper.transcriber_type === 'fast-whisper' || status.whisper.transcriber_type === 'mlx-whisper' + if (!isLocal) return + return + })()} {loading && !status ? ( @@ -192,6 +196,14 @@ export default function Monitor() { 转写引擎: {status.whisper.transcriber_type}
+ {(status.whisper.transcriber_type === 'fast-whisper' || status.whisper.transcriber_type === 'mlx-whisper') && ( +
+ 本地下载: + + {status.whisper.downloaded ? '已就绪' : '未下载(首次转写会触发下载)'} + +
+ )}
) : null} diff --git a/BillNote_frontend/src/services/model.ts b/BillNote_frontend/src/services/model.ts index ec91d94..0a00199 100644 --- a/BillNote_frontend/src/services/model.ts +++ b/BillNote_frontend/src/services/model.ts @@ -1,21 +1,26 @@ import request from '@/utils/request.ts' -export const getProviderList = async () => { - return await request.get('/get_all_providers') +// opts.silent: 让本次请求失败时不弹全局红 toast(调用方自行 catch 处理, +// 比如 onboarding 撞名重试这种预期内失败) +interface CallOpts { silent?: boolean } +const cfg = (o?: CallOpts) => (o?.silent ? { suppressToast: true } : undefined) + +export const getProviderList = async (opts?: CallOpts) => { + return await request.get('/get_all_providers', cfg(opts)) } export const getProviderById = async (id: string) => { return await request.get(`/get_provider_by_id/${id}`) } -export const updateProviderById = async (data: any) => { - return await request.post('/update_provider', data) +export const updateProviderById = async (data: any, opts?: CallOpts) => { + return await request.post('/update_provider', data, cfg(opts)) } -export const addProvider = async (data: any) => { - return await request.post('/add_provider', data) +export const addProvider = async (data: any, opts?: CallOpts) => { + return await request.post('/add_provider', data, cfg(opts)) } -export const testConnection = async (data: any) => { - return await request.post('/connect_test', data) +export const testConnection = async (data: any, opts?: CallOpts) => { + return await request.post('/connect_test', data, cfg(opts)) } export const fetchModels = async (providerId: string) => { @@ -26,8 +31,11 @@ export const fetchEnableModelById = async (id: string) => { return await request.get('/model_enable/' + id) } -export async function addModel(data: { provider_id: string; model_name: string }) { - return request.post('/models', data) +export async function addModel( + data: { provider_id: string; model_name: string }, + opts?: CallOpts, +) { + return request.post('/models', data, cfg(opts)) } export const fetchEnableModels = async () => { diff --git a/BillNote_frontend/src/services/proxy.ts b/BillNote_frontend/src/services/proxy.ts new file mode 100644 index 0000000..8911b5a --- /dev/null +++ b/BillNote_frontend/src/services/proxy.ts @@ -0,0 +1,19 @@ +import request from '@/utils/request' + +export interface ProxyConfig { + enabled: boolean + url: string + /** 后端实际生效的代理(可能来自配置,也可能来自 HTTP_PROXY 环境变量兜底) */ + effective: string +} + +export const getProxyConfig = async (): Promise => { + return await request.get('/proxy_config') +} + +export const updateProxyConfig = async (data: { + enabled: boolean + url?: string +}): Promise => { + return await request.post('/proxy_config', data) +} diff --git a/BillNote_frontend/src/services/system.ts b/BillNote_frontend/src/services/system.ts index 4f54c97..f51d295 100644 --- a/BillNote_frontend/src/services/system.ts +++ b/BillNote_frontend/src/services/system.ts @@ -1,9 +1,29 @@ import request from '@/utils/request' -export const systemCheck = async () => { +export interface SysHealth { + backend: 'ok' | 'error' + ffmpeg: 'ok' | 'missing' + db: 'ok' | 'error' + whisper_model: { + /** 当前选中的模型 size,例如 'tiny' / 'base' / 'large-v3' */ + size: string | null + /** 转写器类型 */ + type: string | null + /** 是否已完整下载到本地(仅本地引擎有意义) */ + downloaded: boolean + /** 是否实际检查过 —— 在线引擎跳过检查时为 false */ + checked: boolean + } +} + +/** 详细健康状态:用于设置页 / 启动诊断。后端始终返回 200,按字段判断各项。 */ +export const getSysHealth = async (): Promise => { return await request.get('/sys_health') } +/** 保留旧 systemCheck 函数名(App.tsx 启动时仍调用),返回值同 getSysHealth。 */ +export const systemCheck = getSysHealth + export interface DeployStatus { backend: { status: string @@ -11,12 +31,16 @@ export interface DeployStatus { } cuda: { available: boolean + /** 新增:torch 是否安装。轻量部署没装 torch 时为 false,避免误判为 CUDA 故障 */ + torch_installed?: boolean version: string | null gpu_name: string | null } whisper: { model_size: string transcriber_type: string + /** 新增:模型是否已完整下载(fast-whisper 看 model.bin / mlx 看 config.json) */ + downloaded: boolean } ffmpeg: { available: boolean diff --git a/BillNote_frontend/src/store/taskStore/index.ts b/BillNote_frontend/src/store/taskStore/index.ts index 678b0c5..f511f78 100644 --- a/BillNote_frontend/src/store/taskStore/index.ts +++ b/BillNote_frontend/src/store/taskStore/index.ts @@ -169,10 +169,25 @@ export const useTaskStore = create()( if (!task) return const newFormData = payload || task.formData - await generateNote({ - ...newFormData, - task_id: id, - }) + try { + await generateNote({ + ...newFormData, + task_id: id, + }) + } catch (e: any) { + // 就绪门禁:转写模型未下载好。不要把任务标成 PENDING(会一直转), + // 给提示让用户先去下载。 + if (e?.data?.reason === 'transcriber_model_not_ready') { + toast.error( + e?.data?.downloading + ? '转写模型正在下载中,请稍候再重试' + : '转写模型尚未下载,请先去「设置 → 音频转写配置」页下载', + ) + return + } + console.error('重试任务失败:', e) + return + } set(state => ({ tasks: state.tasks.map(t => diff --git a/BillNote_frontend/src/utils/request.ts b/BillNote_frontend/src/utils/request.ts index ccaed46..c44693c 100644 --- a/BillNote_frontend/src/utils/request.ts +++ b/BillNote_frontend/src/utils/request.ts @@ -8,6 +8,14 @@ export interface IResponse { data: T; } +// 允许调用方在 axios 配置里带 suppressToast: true,让拦截器对【预期内的失败】 +// 不弹全局红 toast(例如 onboarding 撞名重试、轮询健康检查)。业务代码自己 catch 处理。 +declare module 'axios' { + export interface AxiosRequestConfig { + suppressToast?: boolean + } +} + // 模拟一个消息提示函数 (实际项目中会使用UI库的组件,如 Ant Design 的 message 或 Element UI 的 ElMessage) // This function simulates a message display (in real projects, you'd use a UI library's component) @@ -28,25 +36,24 @@ request.interceptors.response.use( // showMessage('success', res.msg || '操作成功'); // 如果需要显示成功消息 return res.data; // 返回data部分,简化后续业务代码 } else { - // 业务错误,统一显示后端返回的错误消息 - // Business error, uniformly display the error message returned from the backend - toast.error(res.msg || '操作失败,请稍后再试'); + // 业务错误,统一显示后端返回的错误消息(除非调用方显式 suppressToast) + if (!response.config?.suppressToast) { + toast.error(res.msg || '操作失败,请稍后再试'); + } return Promise.reject(res); // 拒绝Promise,让业务代码可以捕获并处理 } }, (error) => { + const suppress = error?.config?.suppressToast === true // 网络/服务器错误 const res = error?.response?.data as IResponse | undefined; if (res) { // 如果后端有返回错误信息,则显示后端信息 - // If the backend returns an error message, display it - - toast.error(res.msg || '服务器错误,请稍后再试'); + if (!suppress) toast.error(res.msg || '服务器错误,请稍后再试'); return Promise.reject(res); } else { // 没有响应数据(如网络中断),显示通用网络错误 - // No response data (e.g., network disconnected), display generic network error - toast.error( '请求失败,请检查网络连接或稍后再试') + if (!suppress) toast.error('请求失败,请检查网络连接或稍后再试') return Promise.reject({ code: -1, msg: '请求失败,请检查网络连接', diff --git a/CHANGELOG.md b/CHANGELOG.md index af51b91..5dadb41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,39 @@ 本项目所有重要变更记录于此。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 +## [2.3.0] - 2026-05-14 + +主线:一波部署与运行时韧性专项——Docker / 桌面端 / 在线引擎三端的"装不上、起不来、跑一半挂"问题集中清理,并新增全局代理与转写模型就绪门禁。 + +### Added + +- **全局代理**:新增 `ProxyConfigManager`(`config/proxy.json` 持久化 + `HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY` 环境变量兜底)。一处配置同时作用于 LLM API、转写 API(Groq 等)、yt-dlp 视频下载、youtube-transcript-api 字幕拉取。前端「设置 → 下载配置」页新增代理卡片,会显示当前实际生效值(含 env 兜底来源提示) +- **转写模型就绪门禁**:`/generate_note` 在排队前检查本地转写引擎(fast-whisper / mlx-whisper)的模型是否已下载完整,未就绪直接拦截并返回 `reason=transcriber_model_not_ready`,不再让任务静默卡在首次大文件下载;前端引导用户去「音频转写配置」页下载 +- **桌面端后端健康监控韧性**:Tauri 侧 spawn sidecar 后以 HTTP 探针轮询 `/api/sys_check` 判就绪并 emit `backend-ready`;`RunEvent::Exit` 钩子在 app 退出前 kill sidecar,杜绝孤儿进程占用 8483 端口;启动失败对话框展示原因 + 最近 stderr + 一键重启 / 复制日志 +- `/sys_health` 重构为结构化健康响应 `{backend, ffmpeg, db, whisper_model}`;部署监控页显示 Whisper 模型本地下载状态 +- 所有 Dockerfile 新增 `BASE_REGISTRY` build-arg,国内拉不到 docker.io 时可换 daocloud 等镜像源 + +### Fixed + +- **whisper 模型损坏自愈**:`model.bin` 截断 / 损坏导致 `Unable to open file 'model.bin'` 死循环——加载失败时删除损坏目录、重新下载、重试一次;mlx-whisper 同样按 `config.json` 判定完整性 +- **空 API Key 天书报错**:空 key 会让 httpx 拼出非法 header `Bearer ` 并抛 `LocalProtocolError: Illegal header value b'Bearer '`。新增 `build_openai_client` 在入口校验,给出「xxx 的 API Key 未配置」的清晰提示 +- **新模型 temperature 不兼容**:OpenAI o1 / o3 / gpt-5 系列拒绝自定义 `temperature`,命中后就地去掉该参数重试,不消耗重试预算 +- **桌面端「后端加载中」死循环**:`useCheckBackend` 重写——60s 总超时取代 `while(true)` 无限轮询,订阅 Tauri `backend-ready` / `backend-terminated` / `backend-startup-timeout` 事件;裸 `fetch` 探测避免启动期 toast 叠堆 +- **CORS 漏配桌面端 origin**:补全 `tauri://localhost` / `https://tauri.localhost`,修桌面端 fetch 拿到 200 却被浏览器 CORS 拒绝读响应(表现为"连不上后端"但后端日志全 200) +- `/api/api/sys_health` 双 `/api` 前缀导致健康检查 404 +- `docker-compose` 的 `restart: on-failure:3` 改为 `unless-stopped`,避免短暂崩溃后容器被永久打死;GPU compose 补齐 `healthcheck` / `restart` / `mem_limit` +- `Dockerfile.complete` 的 supervisord 用 `%(ENV_*)s` 透传环境变量给 backend 子进程(此前只白名单 2 个,`docker run -e` 配的变量后端看不到) +- `.env.example`:修正 `VITE_API_BASE_URL` 端口(8000→8483)、`WHISPER_MODEL_SIZE`(medium→tiny,首次启动不被 ~1.5GB 下载卡住) +- Onboarding:第 1 步后端连通检测改为自动重试 + Tauri 事件触发 + 手动重检按钮;第 2 步撞预置供应商名时改为更新已存在供应商而非报错 +- 模型供应商列表卡片整行可点击切换(此前仅 icon 区域响应) +- `connect_test` 改用真实 chat completion 探测而非 `/v1/models`(后者在 key 无 inference 权限 / 供应商不实现该端点时会误判) + +### Internal + +- `backend/main.py` lifespan 拆为 `[startup 1/5]…[startup 5/5]` 分段日志,启动期异常可一眼定位死在哪一步 +- `request.ts` 新增 `suppressToast` 配置位,预期内的失败(如 onboarding 撞名重试)不弹全局红 toast +- `CLAUDE.md` 勘误:移除不存在的 `app/messaging/` / `app/i18n/` / `worker_registry.py` 描述,修正 `events/` 路径,补 `pytest` / 前端 `typecheck` 命令 + ## [2.2.3] - 2026-05-09 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index f9cf392..dc7d6f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,8 @@ BiliNote is an AI video note generation tool. It extracts content from video lin cd backend pip install -r requirements.txt python main.py # Starts on 0.0.0.0:8483 +pytest # Run tests in backend/tests/ +pytest tests/test_request_chunker.py::test_name # Run a single test ``` ### Frontend (React 19 + Vite + TypeScript) @@ -43,6 +45,8 @@ pnpm install pnpm dev # watch mode → ./extension/ pnpm build # production build → ./extension/ pnpm typecheck +pnpm test # Vitest unit tests +pnpm test:e2e # Playwright e2e ``` Load unpacked at `chrome://extensions/` → select `BillNote_extension/extension/`. Talks to the same backend at `http://localhost:8483` (configurable in the options page). CORS in `backend/main.py` already accepts `chrome-extension://` and `moz-extension://` via regex. @@ -56,15 +60,15 @@ Load unpacked at `chrome://extensions/` → select `BillNote_extension/extension - `chat_service.py` + `chat_tools.py` + `vector_store.py` — RAG-based AI Q&A with Function Calling, indexing transcripts and video metadata - `cookie_manager.py` — per-platform cookie storage; injected into yt-dlp by downloaders (e.g. Bilibili) - `transcriber_config_manager.py` — persisted transcriber settings - - `worker_registry.py` — **optional** Nacos registration + heartbeat for distributed worker mode (no-op when `NACOS_SERVER_ADDR` unset) -- `app/messaging/` — **optional** RabbitMQ producer/consumer publishing task progress/results to `bilinote.task.feedback` exchange. Silently degrades when `RABBITMQ_URL` is unset; always import-safe. - `app/downloaders/` — Platform adapters (bilibili, youtube, douyin, kuaishou, local) with shared `base.py` interface - `app/transcriber/` — Speech-to-text engines (fast-whisper, groq, bcut, kuaishou, mlx-whisper) with factory in `transcriber_provider.py`. YouTube path prefers existing subtitles and skips audio download when available. - `app/gpt/` — LLM integration with factory pattern (`gpt_factory.py`), prompt templates (`prompt.py`, `prompt_builder.py`), and `request_chunker.py` for long transcripts - `app/db/` — SQLite + SQLAlchemy: DAO pattern (`provider_dao.py`, `model_dao.py`, `video_task_dao.py`), models in `models/` - `app/utils/` — `response.py` (ResponseWrapper for consistent JSON), `video_helper.py` (screenshots via FFmpeg), `export.py` (PDF/DOCX), `ppt_generator.py`, `minio_client.py` -- `app/i18n/` — backend localization -- `events/` (root level) — Blinker signal system for post-processing (e.g., temp file cleanup after transcription) +- `app/validators/video_url_validator.py` — URL → platform detection (mirrored client-side in the extension) +- `app/exceptions/` — `BizException` + handlers wired in `main.py` via `register_exception_handlers` +- `backend/events/` — Blinker signal system for post-processing (e.g., temp file cleanup after transcription); registered in `lifespan` startup +- `backend/ffmpeg_helper.py` — `ensure_ffmpeg_or_raise` is called at startup; respects `FFMPEG_BIN_PATH` **Frontend** (`BillNote_frontend/src/`) — React 19 + Vite + Tailwind + shadcn/ui: - `pages/HomePage/` — Main note generation UI: `NoteForm.tsx` (input), `MarkdownViewer.tsx` (preview), `MarkmapComponent.tsx` (mind map) @@ -94,8 +98,8 @@ Load unpacked at `chrome://extensions/` → select `BillNote_extension/extension - **Environment**: Root `.env` (copy from `.env.example`). LLM API keys are configured through the UI, not env vars. - **Database**: SQLite at `backend/app/db/bili_note.db`, auto-initialized on first run - **FFmpeg**: Required system dependency for video/audio processing -- **Vite proxy**: Dev server proxies `/api` and `/static` to backend (configured in `vite.config.ts`, reads env from parent dir) -- **Distributed mode (optional)**: Setting `NACOS_SERVER_ADDR` enables Nacos worker registration; setting `RABBITMQ_URL` enables MQ feedback. Both are no-ops when unset — single-node deployment works without either. Other knobs: `WORKER_ID`, `WORKER_SELF_URL`, `WORKER_MAX_CONCURRENT`, `TASK_MAX_WORKERS`. +- **Vite proxy**: Dev server proxies `/api` and `/static` to backend (configured in `vite.config.ts`, reads env from parent dir; falls back to current dir when `DOCKER_BUILD` is set) +- **CORS**: `backend/main.py` uses a regex (`CORS_ORIGIN_REGEX`) that allows localhost, `tauri.localhost`, and `chrome-extension://` / `moz-extension://` origins — required for the desktop app and the browser extension. ## Code Style diff --git a/Dockerfile.complete b/Dockerfile.complete index d7f6e29..4ad647e 100644 --- a/Dockerfile.complete +++ b/Dockerfile.complete @@ -1,5 +1,9 @@ +# BASE_REGISTRY 默认 docker.io;国内拉不到可换镜像源: +# docker build --build-arg BASE_REGISTRY=docker.m.daocloud.io -f Dockerfile.complete . +ARG BASE_REGISTRY=docker.io + # === 阶段1:构建 Backend === -FROM python:3.11-slim AS backend-builder +FROM ${BASE_REGISTRY}/library/python:3.11-slim AS backend-builder ARG APT_MIRROR=mirrors.tuna.tsinghua.edu.cn ARG PIP_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple @@ -28,7 +32,8 @@ COPY ./backend /tmp/backend # === 阶段2:构建 Frontend === # Node 18-alpine 跑不动 Tailwind v4 / Vite 6(前者要求 Node 20+,后者推荐 Node 20+), # 升到 node:20-alpine。alpine 走 musl,pnpm 会按 lockfile 拉 *-linux-x64-musl native binary。 -FROM node:20-alpine AS frontend-builder +ARG BASE_REGISTRY=docker.io +FROM ${BASE_REGISTRY}/library/node:20-alpine AS frontend-builder # pnpm 版本 pin 到 9 系列: # - lockfile (BillNote_frontend/pnpm-lock.yaml) 是 lockfileVersion '9.0',由 pnpm 9 生成 @@ -50,7 +55,8 @@ ENV DOCKER_BUILD=1 RUN pnpm run build # === 阶段3:完整应用镜像 === -FROM python:3.11-slim +ARG BASE_REGISTRY=docker.io +FROM ${BASE_REGISTRY}/library/python:3.11-slim ARG APT_MIRROR=mirrors.tuna.tsinghua.edu.cn @@ -85,6 +91,10 @@ RUN rm -rf /etc/nginx/conf.d/default.conf COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf # 创建 supervisor 配置 +# 关键点:supervisord 默认 *不* 把自己的环境变量传给子进程。 +# 在 [supervisord] 块用 environment= 设兜底默认值;在 [program:backend] 用 +# %(ENV_*)s 显式引用,等价于「把 host 通过 docker run -e 或 env_file 传进来的 +# 变量再透传给 python main.py」。漏掉这一步就是用户「改 .env 没反应」的根因。 RUN mkdir -p /var/log/supervisor COPY < BiliNote Banner

-

BiliNote v2.2.3

+

BiliNote v2.3.0

AI 视频笔记生成工具 让 AI 为你的视频做笔记

@@ -23,13 +23,10 @@ ## ✨ 项目简介 BiliNote 是一个开源的 AI 视频笔记助手,支持通过哔哩哔哩、YouTube、抖音等视频链接,自动提取内容并生成结构清晰、重点明确的 Markdown 格式笔记。支持插入截图、原片跳转、AI 问答等功能。 - +## 在线使用 +可以通过访问 [这里](https://www.bilinote.app/) 进行使用。 ## 📝 使用文档 详细文档可以查看[这里](https://docs.bilinote.app/) - -## 体验地址 -可以通过访问 [这里](https://www.bilinote.app/) 进行体验,速度略慢,不支持长视频。 - ## 📦 桌面版下载 本项目提供了 Windows 和 macOS 桌面客户端,可在 [Releases](https://github.com/JefferyHcool/BiliNote/releases) 页面下载最新版本。 @@ -53,6 +50,14 @@ BiliNote 是一个开源的 AI 视频笔记助手,支持通过哔哩哔哩、Y - 笔记顶部视频封面 Banner 展示 - 工作区和生成历史面板支持折叠/展开 +### v2.3.0 新增 + +- 全局代理:一处配置同时作用于 AI 模型接口、转写接口(Groq 等)、YouTube 下载(设置 → 下载配置页),支持 `HTTP_PROXY` 环境变量兜底 +- 转写模型就绪门禁:本地引擎模型没下载好时拦截视频任务,引导先去下载,不再静默卡在首次下载 +- 桌面端后端健康监控韧性:退出自动清理 sidecar、启动失败展示原因 + 日志、不再无限「加载中」 +- whisper 模型损坏自愈:`model.bin` 截断时自动删除重下;空 API Key / 新模型 temperature 不兼容给出清晰提示 +- Docker 部署韧性:`BASE_REGISTRY` 可换国内镜像源、restart 策略修正、`.env.example` 端口与默认模型修正、新增部署 FAQ + ### v2.2.3 修订 - 修:vite build 在 CI 中报 'Rollup failed to resolve import @tauri-apps/api/event'(缺直接依赖声明) @@ -146,13 +151,74 @@ docker run -d -p 80:80 \ 也可以使用 docker-compose 本地构建: ```bash -# 标准部署 -docker-compose up -d +cp .env.example .env # 第一次部署务必先创建 .env,否则 BACKEND_PORT/APP_PORT 等变量为空会启动失败 +docker-compose up --build -d -# GPU 加速部署(需要 NVIDIA GPU) -docker-compose -f docker-compose.gpu.yml up -d +# GPU 加速部署(需要 NVIDIA GPU + NVIDIA Container Toolkit) +docker-compose -f docker-compose.gpu.yml up --build -d ``` +#### Docker 部署常见问题(FAQ) + +社区反馈最集中的几个坑,遇到先按下面排查: + +**0. 国内拉不到 docker.io(build 阶段报 `dial tcp ... i/o timeout`)** + +`docker-compose build` 拉 `python:3.11-slim` / `node:20-alpine` / `nginx:1.25-alpine` 时连 `auth.docker.io` 超时。三种解法,按推荐顺序: + +- **方法 A:直接用预构建镜像(最省事)**——不要本地 build,跳到上面的 `docker pull ghcr.io/jefferyhcool/bilinote:latest` 路径,ghcr.io 在国内通常比 docker.io 顺。 +- **方法 B:配置 Docker daemon 镜像加速器**——编辑 `~/.docker/daemon.json`(Linux 在 `/etc/docker/daemon.json`),加: + ```json + { + "registry-mirrors": ["https://docker.m.daocloud.io"] + } + ``` + 然后重启 Docker Desktop / `sudo systemctl restart docker`。这是一劳永逸的做法。 +- **方法 C:临时切换 base image 镜像源**——本项目所有 Dockerfile 都暴露了 `BASE_REGISTRY` build-arg: + ```bash + BASE_REGISTRY=docker.m.daocloud.io docker-compose build + docker-compose up -d + ``` + 或永久写到 `.env`:`echo 'BASE_REGISTRY=docker.m.daocloud.io' >> .env`。 + +注意:Chinese 公共 docker 镜像源时常被关停,2025-2026 之间可用的列表会变;如果 `docker.m.daocloud.io` 不通,搜一下"Docker 镜像加速 可用"找最新可用源即可。 + +**1. 容器一直 restart / unhealthy** + +先看后端日志: +```bash +docker logs -f bilinote-backend +``` +后端启动会按顺序打印 `[startup 1/5] ... [startup 5/5] 启动完成`。若日志卡在某一步或出现 `[startup FAILED]`,就是那一步的问题,常见: +- **卡在 `[startup 3/5]`**:转写器配置读不到。检查 `.env` 里 `TRANSCRIBER_TYPE` 是否写错,`mlx-whisper` 只能在 Apple Silicon 用,Linux/Docker 请用 `fast-whisper` 或 `groq`。 +- **首次跑视频时容器被 kill**:whisper 模型下载触发 OOM。先把 `.env` 里 `WHISPER_MODEL_SIZE` 改成 `tiny`,跑通后再去前端「音频转写配置」里逐档升。 + +**2. 改了 `.env` 没生效** + +区分两类变量: +- `VITE_*` 是**构建时**变量(前端 bundle 里硬编码),改完必须 `docker-compose build frontend && docker-compose up -d`。只 `restart` 不会重新打包。 +- 其他后端变量(`TRANSCRIBER_TYPE`、`WHISPER_MODEL_SIZE`、`FFMPEG_BIN_PATH` 等)是**运行时**变量,改完 `docker-compose up -d` 即可。 + +注意:**LLM API key 不要写 `.env`**,从前端「模型供应商」页面录入,会保存到 SQLite 数据库并持久化。 + +**3. 数据存在哪?删容器会丢吗?** + +`docker-compose` 用的是 `./backend:/app` 绑挂,下面这些文件都在宿主机的 `./backend/` 目录里、删容器不会丢: +- `./backend/bili_note.db` —— SQLite 库(含 LLM 供应商配置、笔记历史) +- `./backend/config/transcriber.json` —— 转写器运行时配置 +- `./backend/static/screenshots/` —— 视频截图 +- `./backend/uploads/` —— 上传的本地视频 + +要彻底重置就 `docker-compose down && rm backend/bili_note.db backend/config/transcriber.json`。 + +**4. 前端打开是空白页 / 报 502** + +通常是 nginx 起来了但 backend 还没 healthy。`docker ps` 看 backend 容器 STATUS 是不是 `(healthy)`;若长期 `(unhealthy)`,按问题 1 排查后端日志。 + +**5. 不要用 `restart: on-failure:N`** + +如果你 fork 后改过 compose 文件、把 restart 策略改成了 `on-failure:3`:任何 3 次连续崩溃都会让容器永远不再启动,之后改 `.env` 也没用。本项目自带的 compose 已经统一用 `unless-stopped`。 + ### 方式二:源码部署 #### 1. 克隆仓库 diff --git a/backend/Dockerfile b/backend/Dockerfile index 9e899bf..d08fb37 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,8 @@ -FROM python:3.11-slim +# BASE_REGISTRY 默认走 docker.io;国内拉不到 docker.io 时可换 daocloud / 阿里云 / 自建镜像源: +# docker-compose build --build-arg BASE_REGISTRY=docker.m.daocloud.io +# 或写到 docker-compose.yml 的 build.args / 环境变量里 +ARG BASE_REGISTRY=docker.io +FROM ${BASE_REGISTRY}/library/python:3.11-slim ARG APT_MIRROR=mirrors.tuna.tsinghua.edu.cn ARG PIP_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple diff --git a/backend/Dockerfile.gpu b/backend/Dockerfile.gpu index d74a3cb..aff0073 100644 --- a/backend/Dockerfile.gpu +++ b/backend/Dockerfile.gpu @@ -1,4 +1,6 @@ -FROM nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04 +# BASE_REGISTRY 默认走 docker.io;国内可换 daocloud / 阿里云镜像(注意所选镜像需支持 nvidia/cuda 命名空间) +ARG BASE_REGISTRY=docker.io +FROM ${BASE_REGISTRY}/nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04 ARG APT_MIRROR=mirrors.tuna.tsinghua.edu.cn ARG PIP_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple diff --git a/backend/app/downloaders/youtube_downloader.py b/backend/app/downloaders/youtube_downloader.py index bb8ed8a..d3de680 100644 --- a/backend/app/downloaders/youtube_downloader.py +++ b/backend/app/downloaders/youtube_downloader.py @@ -9,12 +9,22 @@ from app.downloaders.base import Downloader, DownloadQuality from app.downloaders.youtube_subtitle import YouTubeSubtitleFetcher from app.models.notes_model import AudioDownloadResult from app.models.transcriber_model import TranscriptResult +from app.services.proxy_config_manager import ProxyConfigManager from app.utils.path_helper import get_data_dir from app.utils.url_parser import extract_video_id logger = logging.getLogger(__name__) +def _apply_proxy(ydl_opts: dict) -> dict: + """YouTube 在国内需要代理。配置了全局代理就塞进 yt-dlp opts。""" + proxy = ProxyConfigManager().get_proxy_url() + if proxy: + ydl_opts['proxy'] = proxy + logger.info(f"yt-dlp 走代理: {proxy}") + return ydl_opts + + class YoutubeDownloader(Downloader, ABC): def __init__(self): @@ -46,6 +56,7 @@ class YoutubeDownloader(Downloader, ABC): if skip_download: ydl_opts['skip_download'] = True + _apply_proxy(ydl_opts) with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(video_url, download=not skip_download) video_id = info.get("id") @@ -91,6 +102,7 @@ class YoutubeDownloader(Downloader, ABC): 'merge_output_format': 'mp4', # 确保合并成 mp4 } + _apply_proxy(ydl_opts) with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(video_url, download=True) video_id = info.get("id") diff --git a/backend/app/downloaders/youtube_subtitle.py b/backend/app/downloaders/youtube_subtitle.py index d59a871..81a559a 100644 --- a/backend/app/downloaders/youtube_subtitle.py +++ b/backend/app/downloaders/youtube_subtitle.py @@ -8,6 +8,7 @@ from typing import Optional, List from youtube_transcript_api import YouTubeTranscriptApi from app.models.transcriber_model import TranscriptResult, TranscriptSegment +from app.services.proxy_config_manager import ProxyConfigManager from app.utils.logger import get_logger logger = get_logger(__name__) @@ -17,7 +18,21 @@ class YouTubeSubtitleFetcher: """通过 youtube-transcript-api 获取 YouTube 字幕。""" def __init__(self): - self._api = YouTubeTranscriptApi() + # 配了全局代理就给 youtube-transcript-api 套一个带 proxies 的 requests.Session, + # 否则国内拉字幕同样会超时。代理未配置时退回默认无代理客户端。 + proxy = ProxyConfigManager().get_proxy_url() + if proxy: + try: + import requests + session = requests.Session() + session.proxies = {"http": proxy, "https": proxy} + self._api = YouTubeTranscriptApi(http_client=session) + logger.info(f"YouTube 字幕走代理: {proxy}") + except Exception as e: + logger.warning(f"为 youtube-transcript-api 注入代理失败,回退无代理: {e}") + self._api = YouTubeTranscriptApi() + else: + self._api = YouTubeTranscriptApi() def fetch_subtitles( self, diff --git a/backend/app/gpt/deepseek_gpt.py b/backend/app/gpt/deepseek_gpt.py index 2341425..1c230b8 100644 --- a/backend/app/gpt/deepseek_gpt.py +++ b/backend/app/gpt/deepseek_gpt.py @@ -1,6 +1,6 @@ from typing import List from app.gpt.base import GPT -from openai import OpenAI +from app.utils.openai_client import build_openai_client from app.gpt.prompt import BASE_PROMPT, AI_SUM, SCREENSHOT from app.gpt.utils import fix_markdown from app.models.gpt_model import GPTSource @@ -15,7 +15,7 @@ class DeepSeekGPT(GPT): self.base_url = getenv("DEEP_SEEK_API_BASE_URL") self.model=getenv('DEEP_SEEK_MODEL') print(self.model) - self.client = OpenAI(api_key=self.api_key, base_url=self.base_url) + self.client = build_openai_client(self.api_key, self.base_url, key_label="DeepSeek 的 API Key") self.screenshot = False def _format_time(self, seconds: float) -> str: diff --git a/backend/app/gpt/provider/OpenAI_compatible_provider.py b/backend/app/gpt/provider/OpenAI_compatible_provider.py index a3af2d1..5ed279e 100644 --- a/backend/app/gpt/provider/OpenAI_compatible_provider.py +++ b/backend/app/gpt/provider/OpenAI_compatible_provider.py @@ -1,13 +1,13 @@ from typing import Optional, Union -from openai import OpenAI - from app.utils.logger import get_logger +from app.utils.openai_client import build_openai_client logging= get_logger(__name__) class OpenAICompatibleProvider: def __init__(self, api_key: str, base_url: str, model: Union[str, None]=None): - self.client = OpenAI(api_key=api_key, base_url=base_url) + # build_openai_client:注入全局代理 + 校验 api_key 非空 + self.client = build_openai_client(api_key, base_url, key_label="模型供应商的 API Key") self.model = model @property @@ -15,17 +15,27 @@ class OpenAICompatibleProvider: return self.client @staticmethod - def test_connection(api_key: str, base_url: str) -> bool: + def test_connection(api_key: str, base_url: str, model: str) -> bool: + """发一条最小化 chat completion 验证 key / base_url / model 三方都通。 + + 为什么不用 client.models.list(): + - 部分代理 / 自建供应商不实现 /v1/models(如某些 OpenAI 兼容网关) + - 部分供应商 key 在没有 inference 权限时 /v1/models 仍返回 200 + 最终用户跑的就是 chat.completions.create,所以直接测它最忠实。 + max_tokens=1 + temperature=0 让请求开销 < 0.0001 美元、延迟 < 2s。 + """ try: - client = OpenAI(api_key=api_key, base_url=base_url) - model = client.models.list() - # for segment in model: - # print(segment) - # print(model) - logging.info("连通性测试成功") + client = build_openai_client( + api_key, base_url, key_label="模型供应商的 API Key", timeout=15.0, + ) + client.chat.completions.create( + model=model, + messages=[{"role": "user", "content": "ping"}], + max_tokens=1, + temperature=0, + ) + logging.info(f"连通性测试成功(model={model})") return True except Exception as e: - logging.info(f"连通性测试失败:{e}") - - # print(f"Error connecting to OpenAI API: {e}") + logging.warning(f"连通性测试失败(model={model}):{e}") return False \ No newline at end of file diff --git a/backend/app/gpt/universal_gpt.py b/backend/app/gpt/universal_gpt.py index 739225f..514bf4a 100644 --- a/backend/app/gpt/universal_gpt.py +++ b/backend/app/gpt/universal_gpt.py @@ -185,15 +185,40 @@ class UniversalGPT(GPT): status = getattr(exc, "status_code", None) or getattr(exc, "status", None) return status in {408, 409, 429, 500, 502, 503, 504, 524} + @staticmethod + def _is_temperature_unsupported_error(exc: Exception) -> bool: + """OpenAI o1/o3/gpt-5 系列等新模型不接受自定义 temperature, + 只允许默认值 1,传 0.7 会报 `'temperature' does not support 0.7 ...`。""" + raw = str(exc).lower() + return "temperature" in raw and ( + "does not support" in raw + or "unsupported_value" in raw + or "only the default" in raw + ) + + def _do_create(self, messages: list): + """单次调用。如果模型拒绝自定义 temperature,就地去掉该参数再试一次 + (不消耗外层的重试次数预算),仍失败则把异常抛给外层重试逻辑。""" + try: + return self.client.chat.completions.create( + model=self.model, + messages=messages, + temperature=self.temperature, + ) + except Exception as exc: + if self._is_temperature_unsupported_error(exc): + print(f"[universal_gpt] 模型 {self.model} 不支持自定义 temperature,改用默认值重试") + return self.client.chat.completions.create( + model=self.model, + messages=messages, + ) + raise + def _chat_completion_create(self, messages: list): last_exc = None for attempt in range(self._max_retry_attempts): try: - return self.client.chat.completions.create( - model=self.model, - messages=messages, - temperature=self.temperature - ) + return self._do_create(messages) except Exception as exc: last_exc = exc if attempt == self._max_retry_attempts - 1 or not self._is_retryable_error(exc): diff --git a/backend/app/routers/config.py b/backend/app/routers/config.py index 0fecc48..97687f6 100644 --- a/backend/app/routers/config.py +++ b/backend/app/routers/config.py @@ -80,6 +80,36 @@ def update_transcriber_config(data: TranscriberConfigRequest): return R.success(data=config) +# ---- 全局代理配置(作用于 LLM API + 转写 API + yt-dlp 下载)---- + +class ProxyConfigRequest(BaseModel): + enabled: bool + url: Optional[str] = None + + +@router.get("/proxy_config") +def get_proxy_config(): + from app.services.proxy_config_manager import ProxyConfigManager + mgr = ProxyConfigManager() + cfg = mgr.get_config() + # effective 给前端展示「当前实际生效的代理」——可能来自配置,也可能来自 env 兜底 + return R.success(data={ + **cfg, + "effective": mgr.get_proxy_url() or "", + }) + + +@router.post("/proxy_config") +def update_proxy_config(data: ProxyConfigRequest): + from app.services.proxy_config_manager import ProxyConfigManager + mgr = ProxyConfigManager() + cfg = mgr.update_config(enabled=data.enabled, url=data.url) + return R.success(data={ + **cfg, + "effective": mgr.get_proxy_url() or "", + }) + + # ---- Whisper 模型下载状态 & 下载触发 ---- # 用于跟踪正在进行的下载任务 @@ -87,10 +117,33 @@ _downloading: dict[str, str] = {} # model_size -> status ("downloading" | "done def _check_whisper_model_exists(model_size: str, subdir: str = "whisper") -> bool: - """检查指定 whisper 模型是否已下载到本地。""" + """检查指定 whisper 模型是否已下载完整到本地。 + + 必须 model.bin 落盘才算完成,仅有空目录或半成品不能算「已下载」—— + 否则监控页会显示绿勾但加载时报「Unable to open file 'model.bin'」。 + """ model_dir = get_model_dir(subdir) model_path = os.path.join(model_dir, f"whisper-{model_size}") - return Path(model_path).exists() + return (Path(model_path) / "model.bin").exists() + + +def _check_mlx_whisper_model_exists(model_size: str) -> bool: + """检查 mlx-whisper 模型是否已下载完整到本地。 + + 与 fast-whisper 的目录布局不同:mlx 模型按 HuggingFace repo_id + (如 mlx-community/whisper-tiny-mlx)落盘,且没有 model.bin, + 用 config.json 作为「下载完成」的判据,和 mlx_whisper_transcriber.py 保持一致。 + """ + try: + from app.transcriber.mlx_whisper_transcriber import MLX_MODEL_MAP + except Exception: + return False + repo_id = MLX_MODEL_MAP.get(model_size) + if not repo_id: + return False + model_dir = get_model_dir("mlx-whisper") + model_path = os.path.join(model_dir, repo_id) + return (Path(model_path) / "config.json").exists() @router.get("/transcriber_models_status") @@ -113,11 +166,9 @@ def get_transcriber_models_status(): from app.transcriber.mlx_whisper_transcriber import MLX_MODEL_MAP for size in WHISPER_MODEL_SIZES: mlx_key = f"mlx-{size}" - model_dir = get_model_dir("mlx-whisper") repo_id = MLX_MODEL_MAP.get(size) - # 模型在本地按 repo_id(如 mlx-community/whisper-small-mlx)落盘 - model_path = os.path.join(model_dir, repo_id) if repo_id else None - downloaded = bool(model_path and Path(model_path).exists()) + # 用 config.json 判定,和 _check_mlx_whisper_model_exists / 加载逻辑保持一致 + downloaded = _check_mlx_whisper_model_exists(size) mlx_statuses.append({ "model_size": size, "downloaded": downloaded, @@ -146,7 +197,8 @@ def _do_download_whisper(model_size: str): _downloading[model_size] = "downloading" model_dir = get_model_dir("whisper") model_path = os.path.join(model_dir, f"whisper-{model_size}") - if Path(model_path).exists(): + # 用 model.bin 判定而非目录存在:半成品目录不能算「已下载」 + if (Path(model_path) / "model.bin").exists(): _downloading[model_size] = "done" return repo_id = MODEL_MAP.get(model_size) @@ -179,7 +231,8 @@ def _do_download_mlx_whisper(model_size: str): model_dir = get_model_dir("mlx-whisper") model_path = os.path.join(model_dir, repo_id) - if Path(model_path).exists(): + # 用 config.json 判定而非目录存在:半成品目录不能算「已下载」 + if (Path(model_path) / "config.json").exists(): _downloading[key] = "done" return logger.info(f"开始下载 mlx-whisper 模型: {model_size} ← {repo_id}") @@ -214,46 +267,119 @@ def download_transcriber_model(data: ModelDownloadRequest, background_tasks: Bac @router.get("/sys_health") async def sys_health(): + """结构化健康状态——任何子项异常都不应让整个 endpoint 5xx。 + + 每个字段:'ok' | 'missing' | 'error'。 + 前端 useCheckBackend 用 /sys_check 做存活判定(不依赖外部依赖), + /sys_health 用来在设置页区分「后端没起」vs「后端起了但 ffmpeg 缺」vs「DB 写不进去」等更细的状态。 + """ + ffmpeg_status = "ok" try: ensure_ffmpeg_or_raise() - return R.success() - except EnvironmentError: - return R.error(msg="系统未安装 ffmpeg 请先进行安装") + except Exception: + ffmpeg_status = "missing" + + db_status = "ok" + try: + from app.db.engine import engine + from sqlalchemy import text + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + except Exception: + db_status = "error" + + # 当前转写器配置 + 模型是否已下载(用 model.bin 落盘判定,与 transcriber 加载逻辑一致) + whisper_info: dict = {"size": None, "type": None, "downloaded": False, "checked": False} + try: + cfg = transcriber_config_manager.get_config() + size = cfg["whisper_model_size"] + ttype = cfg["transcriber_type"] + whisper_info["size"] = size + whisper_info["type"] = ttype + # 只有本地引擎才有「下载」概念;groq / bcut / kuaishou 在线引擎跳过 + if ttype == "fast-whisper": + whisper_info["downloaded"] = _check_whisper_model_exists(size, "whisper") + whisper_info["checked"] = True + elif ttype == "mlx-whisper": + whisper_info["downloaded"] = _check_mlx_whisper_model_exists(size) + whisper_info["checked"] = True + except Exception: + pass + + return R.success(data={ + "backend": "ok", + "ffmpeg": ffmpeg_status, + "db": db_status, + "whisper_model": whisper_info, + }) + @router.get("/sys_check") async def sys_check(): + """轻量存活判定:后端进程能响应这个 endpoint 就算「起来了」,不查外部依赖。 + + 给桌面端 useCheckBackend / Tauri ready-probe 用。 + """ return R.success() @router.get("/deploy_status") async def deploy_status(): - """返回部署监控所需的所有状态信息""" - import torch + """返回部署监控所需的所有状态信息。 + + 所有子项都用 try 包起来——监控页本身不应该被任何一个子项打死。 + 特别是 torch:它只在 fast-whisper 路径用得到,用 Groq / 必剪 / 快手在线 + 引擎的轻量部署完全可以不装,那种情况这个 endpoint 不应该 500。 + """ import os - + # CUDA 状态 - cuda_available = torch.cuda.is_available() - cuda_info = { - "available": cuda_available, - "version": torch.version.cuda if cuda_available else None, - "gpu_name": torch.cuda.get_device_name(0) if cuda_available else None, - } - - # Whisper 模型状态(从配置文件读取,与前端设置同步) - transcriber_cfg = transcriber_config_manager.get_config() - model_size = transcriber_cfg["whisper_model_size"] - transcriber_type = transcriber_cfg["transcriber_type"] - + try: + import torch + cuda_available = torch.cuda.is_available() + cuda_info = { + "available": cuda_available, + "torch_installed": True, + "version": torch.version.cuda if cuda_available else None, + "gpu_name": torch.cuda.get_device_name(0) if cuda_available else None, + } + except Exception: + cuda_info = { + "available": False, + "torch_installed": False, + "version": None, + "gpu_name": None, + } + + # Whisper 模型 / 转写器配置 + 本地下载状态 + try: + transcriber_cfg = transcriber_config_manager.get_config() + size = transcriber_cfg["whisper_model_size"] + ttype = transcriber_cfg["transcriber_type"] + if ttype == "fast-whisper": + downloaded = _check_whisper_model_exists(size, "whisper") + elif ttype == "mlx-whisper": + downloaded = _check_mlx_whisper_model_exists(size) + else: + downloaded = False # 在线引擎无下载概念 + whisper_info = { + "model_size": size, + "transcriber_type": ttype, + "downloaded": downloaded, + } + except Exception: + whisper_info = {"model_size": None, "transcriber_type": None, "downloaded": False} + # FFmpeg 状态 try: ensure_ffmpeg_or_raise() ffmpeg_ok = True - except: + except Exception: ffmpeg_ok = False - + return R.success(data={ "backend": {"status": "running", "port": int(os.getenv("BACKEND_PORT", 8483))}, "cuda": cuda_info, - "whisper": {"model_size": model_size, "transcriber_type": transcriber_type}, + "whisper": whisper_info, "ffmpeg": {"available": ffmpeg_ok}, }) \ No newline at end of file diff --git a/backend/app/routers/note.py b/backend/app/routers/note.py index 3bf08b4..80033c8 100644 --- a/backend/app/routers/note.py +++ b/backend/app/routers/note.py @@ -180,6 +180,24 @@ async def upload(file: UploadFile = File(...)): @router.post("/generate_note") def generate_note(data: VideoRequest, background_tasks: BackgroundTasks): try: + # 就绪门禁:本地转写引擎(fast-whisper / mlx-whisper)必须等模型下载完才能跑视频, + # 否则任务会卡在首次下载(慢 / OOM / 截断),用户只看到一个静默失败的任务。 + # 客户端已抓好字幕(prefetched_transcript)则不需要转写,跳过检查。 + if not data.prefetched_transcript: + from app.services.transcriber_config_manager import TranscriberConfigManager + readiness = TranscriberConfigManager().is_model_ready() + if not readiness["ready"]: + logger.warning(f"拒绝 generate_note:{readiness['reason']}") + return R.error( + msg=readiness["reason"], + code=300102, + data={ + "reason": "transcriber_model_not_ready", + "transcriber_type": readiness["transcriber_type"], + "model_size": readiness["model_size"], + "downloading": readiness["downloading"], + }, + ) video_id = extract_video_id(data.video_url, data.platform) # if not video_id: diff --git a/backend/app/routers/provider.py b/backend/app/routers/provider.py index 0b3215a..1d11520 100644 --- a/backend/app/routers/provider.py +++ b/backend/app/routers/provider.py @@ -20,6 +20,8 @@ class ProviderRequest(BaseModel): class TestRequest(BaseModel): id: str + # 可选:指定用哪个 model 跑连通性测试;不传则用该 provider 在 DB 里的第一个模型 + model: Optional[str] = None class ProviderUpdateRequest(BaseModel): id: str name: Optional[str] = None @@ -91,5 +93,5 @@ def update_provider(data: ProviderUpdateRequest): @router.post('/connect_test') def gpt_connect_test(data: TestRequest): - ModelService().connect_test(data.id) + ModelService().connect_test(data.id, model=data.model) return R.success(msg='连接成功') diff --git a/backend/app/services/model.py b/backend/app/services/model.py index 8bc73a3..576a2e1 100644 --- a/backend/app/services/model.py +++ b/backend/app/services/model.py @@ -100,23 +100,46 @@ class ModelService: logger.error(f"[{provider_id}] 获取模型失败: {e}") return [] @staticmethod - def connect_test(id: str) -> bool: + def connect_test(id: str, model: str | None = None) -> bool: + """连通性测试:发一条最小化 chat completion。 + model 优先级: + 1. 调用方显式传入(前端可在「模型选择」UI 里挑一个再测) + 2. DB 中该 provider 已保存的第一个模型 + 3. 都没有 → 抛错让用户先加一个模型 + """ provider = ProviderService.get_provider_by_id(id) - - if provider: - if not provider.get('api_key'): - raise ProviderError(code=ProviderErrorEnum.NOT_FOUND.code, message=ProviderErrorEnum.NOT_FOUND.message) - result = OpenAICompatibleProvider.test_connection( - api_key=provider.get('api_key'), - base_url=provider.get('base_url') + if not provider: + raise ProviderError( + code=ProviderErrorEnum.NOT_FOUND.code, + message=ProviderErrorEnum.NOT_FOUND.message, + ) + if not provider.get('api_key'): + raise ProviderError( + code=ProviderErrorEnum.NOT_FOUND.code, + message=ProviderErrorEnum.NOT_FOUND.message, ) - if result: - return True - else: - raise ProviderError(code=ProviderErrorEnum.WRONG_PARAMETER.code,message=ProviderErrorEnum.WRONG_PARAMETER.message) - raise ProviderError(code=ProviderErrorEnum.NOT_FOUND.code, message=ProviderErrorEnum.NOT_FOUND.message) + if not model: + saved_models = ModelService.get_enabled_models_by_provider(provider["id"]) + if not saved_models: + raise ProviderError( + code=ProviderErrorEnum.WRONG_PARAMETER.code, + message="请先为该供应商添加至少一个模型再测试连通性", + ) + model = saved_models[0]["model_name"] + + ok = OpenAICompatibleProvider.test_connection( + api_key=provider.get('api_key'), + base_url=provider.get('base_url'), + model=model, + ) + if ok: + return True + raise ProviderError( + code=ProviderErrorEnum.WRONG_PARAMETER.code, + message=ProviderErrorEnum.WRONG_PARAMETER.message, + ) diff --git a/backend/app/services/proxy_config_manager.py b/backend/app/services/proxy_config_manager.py new file mode 100644 index 0000000..abbc636 --- /dev/null +++ b/backend/app/services/proxy_config_manager.py @@ -0,0 +1,60 @@ +import json +import os +from pathlib import Path +from typing import Any, Dict, Optional + + +class ProxyConfigManager: + """全局代理配置,存 JSON 文件,支持前端动态修改。 + + 作用范围:LLM API + 转写 API(Groq 等)+ yt-dlp 视频下载。 + 优先级:配置文件里 enabled=true 的 url > 环境变量 HTTP_PROXY/HTTPS_PROXY/ALL_PROXY。 + 这样桌面端/web 用户在设置页填,docker/服务器部署用环境变量兜底。 + """ + + def __init__(self, filepath: str = "config/proxy.json"): + self.path = Path(filepath) + self.path.parent.mkdir(parents=True, exist_ok=True) + + def _read(self) -> Dict[str, Any]: + if not self.path.exists(): + return {} + try: + with self.path.open("r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return {} + + def _write(self, data: Dict[str, Any]): + with self.path.open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + def get_config(self) -> Dict[str, Any]: + data = self._read() + return { + "enabled": bool(data.get("enabled", False)), + "url": data.get("url", "") or "", + } + + def update_config(self, enabled: bool, url: Optional[str] = None) -> Dict[str, Any]: + data = self._read() + data["enabled"] = bool(enabled) + if url is not None: + data["url"] = url.strip() + self._write(data) + return self.get_config() + + def get_proxy_url(self) -> Optional[str]: + """返回当前生效的代理 URL;没有则 None。 + + - 配置文件 enabled=true 且 url 非空 → 用配置的 url + - 否则回退到环境变量(标准的 HTTP_PROXY / HTTPS_PROXY / ALL_PROXY,大小写都认) + """ + cfg = self.get_config() + if cfg["enabled"] and cfg["url"]: + return cfg["url"] + for key in ("HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy"): + val = os.environ.get(key) + if val: + return val + return None diff --git a/backend/app/services/transcriber_config_manager.py b/backend/app/services/transcriber_config_manager.py index 8205372..5d1f73c 100644 --- a/backend/app/services/transcriber_config_manager.py +++ b/backend/app/services/transcriber_config_manager.py @@ -61,3 +61,54 @@ class TranscriberConfigManager: def get_whisper_model_size(self) -> str: return self.get_config()["whisper_model_size"] + + def is_model_ready(self) -> Dict[str, Any]: + """当前转写器是否就绪可用。 + + 返回 {ready, transcriber_type, model_size, downloading, reason}: + - 在线引擎 (groq/bcut/kuaishou):永远 ready(不需要本地模型) + - fast-whisper:检查 whisper-{size}/model.bin 落盘 + - mlx-whisper:检查 {repo_id}/config.json 落盘 + 给 /generate_note 入口做「开始视频前先确认模型下载好」的门禁用。 + """ + cfg = self.get_config() + ttype = cfg["transcriber_type"] + size = cfg["whisper_model_size"] + result = { + "ready": True, + "transcriber_type": ttype, + "model_size": size, + "downloading": False, + "reason": "", + } + if ttype not in ("fast-whisper", "mlx-whisper"): + return result # 在线引擎无需本地模型 + + # 延迟 import 避免与 routers.config 的循环依赖;只取纯函数,不触发路由副作用 + try: + from app.routers.config import ( + _check_whisper_model_exists, + _check_mlx_whisper_model_exists, + _downloading, + ) + except Exception as e: + # 拿不到检查函数时保守放行,不要把用户卡死 + result["reason"] = f"无法检查模型状态: {e}" + return result + + if ttype == "fast-whisper": + downloaded = _check_whisper_model_exists(size, "whisper") + downloading = _downloading.get(size) == "downloading" + else: # mlx-whisper + downloaded = _check_mlx_whisper_model_exists(size) + downloading = _downloading.get(f"mlx-{size}") == "downloading" + + result["downloading"] = downloading + if downloaded: + return result + result["ready"] = False + result["reason"] = ( + f"转写模型 {ttype} / {size} 尚未下载就绪" + + (",正在下载中,请稍候" if downloading else ",请先在「设置 → 音频转写配置」页下载") + ) + return result diff --git a/backend/app/transcriber/groq.py b/backend/app/transcriber/groq.py index 15a103a..033ffa6 100644 --- a/backend/app/transcriber/groq.py +++ b/backend/app/transcriber/groq.py @@ -5,7 +5,7 @@ from app.decorators.timeit import timeit from app.models.transcriber_model import TranscriptResult, TranscriptSegment from app.services.provider import ProviderService from app.transcriber.base import Transcriber -from openai import OpenAI +from app.utils.openai_client import build_openai_client import ffmpeg import tempfile from dotenv import load_dotenv @@ -30,12 +30,14 @@ class GroqTranscriber(Transcriber, ABC): print(f"压缩完成,临时路径:{file_path}") provider = ProviderService.get_provider_by_id('groq') - if not provider: raise Exception("Groq 供应商未配置,请配置以后使用。") - client = OpenAI( + # build_openai_client 会校验 api_key 非空(空 key 会抛天书般的 + # `Illegal header value b'Bearer '`),并自动注入全局代理 + client = build_openai_client( api_key=provider.get('api_key'), - base_url=provider.get('base_url') + base_url=provider.get('base_url'), + key_label="Groq 转写引擎的 API Key", ) filename = file_path diff --git a/backend/app/transcriber/mlx_whisper_transcriber.py b/backend/app/transcriber/mlx_whisper_transcriber.py index 8c0a456..bd4fedd 100644 --- a/backend/app/transcriber/mlx_whisper_transcriber.py +++ b/backend/app/transcriber/mlx_whisper_transcriber.py @@ -58,9 +58,16 @@ class MLXWhisperTranscriber(Transcriber): # 设置模型路径 model_dir = get_model_dir("mlx-whisper") self.model_path = os.path.join(model_dir, self.model_name) - # 检查并下载模型 - if not Path(self.model_path).exists(): - logger.info(f"模型 {self.model_name} 不存在,开始下载...") + # 用 config.json 而非目录存在作为「下载完成」的判据, + # 同 fast-whisper 的 model.bin:避免半成品目录把后续下载吞掉 + config_file = Path(self.model_path) / "config.json" + if not config_file.exists(): + if Path(self.model_path).exists(): + logger.warning( + f"MLX 模型目录 {self.model_path} 存在但 config.json 缺失(上次下载未完成),重新下载" + ) + else: + logger.info(f"模型 {self.model_name} 不存在,开始下载...") snapshot_download( self.model_name, local_dir=self.model_path, diff --git a/backend/app/transcriber/whisper.py b/backend/app/transcriber/whisper.py index fca49f6..e212861 100644 --- a/backend/app/transcriber/whisper.py +++ b/backend/app/transcriber/whisper.py @@ -10,6 +10,7 @@ from app.utils.path_helper import get_model_dir from events import transcription_finished from pathlib import Path import os +import shutil from tqdm import tqdm from modelscope import snapshot_download @@ -50,22 +51,41 @@ class WhisperTranscriber(Transcriber): model_dir = get_model_dir("whisper") model_path = os.path.join(model_dir, f"whisper-{model_size}") - if not Path(model_path).exists(): - logger.info(f"模型 whisper-{model_size} 不存在,开始下载...") - repo_id = MODEL_MAP[model_size] - model_path = snapshot_download( - repo_id, + repo_id = MODEL_MAP[model_size] - local_dir=model_path, - ) + # 第一步:目录 / 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("模型下载完成") - self.model = WhisperModel( - model_size_or_path=model_path, - device=self.device, - compute_type=self.compute_type, - download_root=model_dir - ) + # 第二步:加载。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, + ) + 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, + ) @staticmethod def is_torch_installed() -> bool: try: diff --git a/backend/app/utils/openai_client.py b/backend/app/utils/openai_client.py new file mode 100644 index 0000000..3a0e9b1 --- /dev/null +++ b/backend/app/utils/openai_client.py @@ -0,0 +1,45 @@ +"""统一构造 OpenAI 兼容客户端:注入全局代理 + 校验 api_key。 + +为什么要这一层: + - 代理:openai SDK 默认只认进程级 HTTP_PROXY 环境变量,桌面端用户在 UI 里 + 填的代理需要显式塞进 httpx.Client 才生效。 + - api_key 校验:空 key 会让 httpx 拼出非法 header `Bearer `,抛出 + `httpx.LocalProtocolError: Illegal header value b'Bearer '` 这种天书报错。 + 在入口挡掉,给用户「xxx 的 API Key 未配置」这种能看懂的提示。 +""" +from typing import Optional + +from openai import OpenAI + +from app.services.proxy_config_manager import ProxyConfigManager +from app.utils.logger import get_logger + +logger = get_logger(__name__) + + +def build_openai_client( + api_key: Optional[str], + base_url: Optional[str], + *, + key_label: str = "API Key", + timeout: Optional[float] = None, +) -> OpenAI: + """构造 OpenAI 客户端。api_key 为空直接抛清晰错误;代理已配置则注入。 + + key_label 用于错误提示,例如 "Groq 的 API Key" / "OpenAI 供应商的 API Key"。 + """ + if not api_key or not str(api_key).strip(): + raise ValueError(f"{key_label} 未配置,请先在「设置」里填写后再使用") + + kwargs = {"api_key": str(api_key).strip(), "base_url": base_url} + if timeout is not None: + kwargs["timeout"] = timeout + + proxy_url = ProxyConfigManager().get_proxy_url() + if proxy_url: + # 延迟 import httpx:仅在确实要走代理时才需要 + import httpx + kwargs["http_client"] = httpx.Client(proxy=proxy_url, timeout=timeout or 600.0) + logger.info(f"OpenAI 客户端走代理: {proxy_url}") + + return OpenAI(**kwargs) diff --git a/backend/main.py b/backend/main.py index 31640e2..9f21a64 100644 --- a/backend/main.py +++ b/backend/main.py @@ -39,24 +39,50 @@ if not os.path.exists(out_dir): @asynccontextmanager async def lifespan(app: FastAPI): - register_handler() - init_db() - # 转写器不再在启动时强制初始化,而是在首次生成笔记时按需创建 - # 如果配置了不可用的类型(如 mlx-whisper 未安装),会在使用时报错而非静默回退 - _cfg = TranscriberConfigManager().get_config() - logger.info(f"当前转写器配置: type={_cfg['transcriber_type']}, model_size={_cfg['whisper_model_size']}") - seed_default_providers() + # 启动序列拆成 5 步、每步独立日志 + 异常时打明确的 [startup N/5 FAILED] 标记。 + # 目的:用户 docker logs 一眼能看出后端死在哪一步,避免「容器一直重启但看不出原因」。 + try: + logger.info("[startup 1/5] register_handler() — 注册事件处理器") + register_handler() + + logger.info("[startup 2/5] init_db() — 初始化 SQLite 数据库") + init_db() + + logger.info("[startup 3/5] TranscriberConfigManager — 读取转写器配置") + # 转写器不再在启动时强制初始化,而是在首次生成笔记时按需创建。 + # 如果配置了不可用的类型(如 mlx-whisper 未安装),会在使用时报错而非静默回退。 + _cfg = TranscriberConfigManager().get_config() + logger.info( + f" 当前转写器: type={_cfg['transcriber_type']}, " + f"model_size={_cfg['whisper_model_size']}" + ) + + logger.info("[startup 4/5] seed_default_providers() — 初始化默认 LLM 供应商") + seed_default_providers() + + logger.info("[startup 5/5] 启动完成,等待请求") + except Exception: + logger.exception("[startup FAILED] 后端启动期异常,详见堆栈;容器会退出并由 restart 策略决定是否重试") + raise + yield app = create_app(lifespan=lifespan) # 允许的源:本地 web 端 + Tauri 桌面端 + 浏览器扩展(chrome/edge/firefox) # 用 regex 是因为 chrome-extension:// 的 id 在每次开发版加载时不固定 +# Tauri 2 不同平台 webview origin 不一样,必须全列: +# - macOS: tauri://localhost (自定义协议) +# - Windows: https://tauri.localhost (Edge WebView2) +# - Linux: http://tauri.localhost (WebKitGTK) +# 漏掉哪个都会导致桌面端 fetch 返回 200 但 browser 因为 CORS 拒绝读响应, +# 表现为前端「连不上后端」但后端日志一片 200 OK。 CORS_ORIGIN_REGEX = ( r"^chrome-extension://[a-z]+$" r"|^moz-extension://.+$" r"|^http://(localhost|127\.0\.0\.1)(:\d+)?$" - r"|^http://tauri\.localhost$" + r"|^tauri://localhost$" + r"|^https?://tauri\.localhost$" ) app.add_middleware( diff --git a/docker-compose.gpu.yml b/docker-compose.gpu.yml index 1b1fdbc..327bed2 100644 --- a/docker-compose.gpu.yml +++ b/docker-compose.gpu.yml @@ -6,6 +6,8 @@ services: context: . dockerfile: backend/Dockerfile.gpu args: + # 国内拉不到 docker.io 时设置 BASE_REGISTRY;注意所选镜像需要支持 nvidia/cuda 命名空间 + BASE_REGISTRY: ${BASE_REGISTRY:-docker.io} APT_MIRROR: ${APT_MIRROR:-mirrors.tuna.tsinghua.edu.cn} PIP_INDEX: ${PIP_INDEX:-https://pypi.tuna.tsinghua.edu.cn/simple} env_file: @@ -14,9 +16,20 @@ services: - BACKEND_PORT=${BACKEND_PORT} - BACKEND_HOST=${BACKEND_HOST} volumes: + # 同 docker-compose.yml:./backend 绑到 /app,DB / 转写器配置 / 截图 / 上传都持久化 - ./backend:/app expose: - "${BACKEND_PORT}" # 不再对外暴露,用于 nginx 内部通信 + # 用 unless-stopped 避免短暂崩溃把容器永久打死后再也读不到 .env 修改 + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:${BACKEND_PORT}/api/sys_health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s # GPU 镜像首次加载 CUDA 比 CPU 慢,给久一点 + # GPU 部署默认跑较大模型,把内存限制提到 8g 避免 host OOM + mem_limit: 8g deploy: resources: reservations: @@ -30,10 +43,14 @@ services: build: context: . dockerfile: BillNote_frontend/Dockerfile + args: + BASE_REGISTRY: ${BASE_REGISTRY:-docker.io} env_file: - .env expose: - "80" # 不暴露给宿主机,只供 nginx 访问 + restart: unless-stopped + mem_limit: 512m nginx: container_name: bilinote-nginx @@ -43,5 +60,9 @@ services: volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf depends_on: - - backend - - frontend + backend: + condition: service_healthy + frontend: + condition: service_started + restart: unless-stopped + mem_limit: 256m diff --git a/docker-compose.yml b/docker-compose.yml index 4f5383a..bcb022d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,8 @@ services: context: . dockerfile: backend/Dockerfile args: + # 国内拉不到 docker.io 时设置 BASE_REGISTRY=docker.m.daocloud.io(或其他可用镜像) + BASE_REGISTRY: ${BASE_REGISTRY:-docker.io} APT_MIRROR: ${APT_MIRROR:-mirrors.tuna.tsinghua.edu.cn} PIP_INDEX: ${PIP_INDEX:-https://pypi.tuna.tsinghua.edu.cn/simple} env_file: @@ -14,16 +16,25 @@ services: - BACKEND_PORT=${BACKEND_PORT} - BACKEND_HOST=${BACKEND_HOST} volumes: + # 把整个 backend/ 目录绑到 /app,意味着这些都持久化到宿主机、删容器不丢: + # ./backend/bili_note.db — SQLite 数据库(含 LLM 供应商配置、笔记历史) + # ./backend/config/transcriber.json — 转写器运行时配置 + # ./backend/static/screenshots/ — 视频截图 + # ./backend/uploads/ — 上传的本地视频 - ./backend:/app expose: - "${BACKEND_PORT}" # 不再对外暴露,用于 nginx 内部通信 - restart: on-failure:3 + # 用 unless-stopped 而非 on-failure:N,避免任何短暂崩溃把容器永久打死后 + # 再也接收不到用户修过的 .env。手动 docker-compose stop 仍可正常停下。 + restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:${BACKEND_PORT}/api/sys_health"] interval: 30s timeout: 10s retries: 3 start_period: 15s + # WHISPER_MODEL_SIZE 选 medium 及以上请把这里调到 8g+, + # 否则首次模型加载时容易被 host OOM-killer 干掉。 mem_limit: 4g frontend: @@ -31,11 +42,13 @@ services: build: context: . dockerfile: BillNote_frontend/Dockerfile + args: + BASE_REGISTRY: ${BASE_REGISTRY:-docker.io} env_file: - .env expose: - "80" # 不暴露给宿主机,只供 nginx 访问 - restart: on-failure:3 + restart: unless-stopped mem_limit: 512m nginx: @@ -50,5 +63,5 @@ services: condition: service_healthy frontend: condition: service_started - restart: on-failure:3 + restart: unless-stopped mem_limit: 256m