mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-05-07 04:52:42 +08:00
Merge branch 'master' into master
This commit is contained in:
@@ -4,7 +4,7 @@ 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:8483
|
||||
VITE_API_BASE_URL=http://127.0.0.1:8000
|
||||
VITE_SCREENSHOT_BASE_URL=http://127.0.0.1:8483/static/screenshots
|
||||
VITE_FRONTEND_PORT=3015
|
||||
# 生产环境配置
|
||||
@@ -19,6 +19,6 @@ FFMPEG_BIN_PATH=
|
||||
|
||||
# transcriber 相关配置
|
||||
TRANSCRIBER_TYPE=fast-whisper # fast-whisper/bcut/kuaishou/mlx-whisper(仅Apple平台)/groq
|
||||
WHISPER_MODEL_SIZE=base
|
||||
WHISPER_MODEL_SIZE=medium
|
||||
|
||||
GROQ_TRANSCRIBER_MODEL=whisper-large-v3-turbo # groq提供的faster-whisper 默认为 whisper-large-v3-turbo
|
||||
|
||||
@@ -7,12 +7,9 @@ import { Route } from 'react-router-dom'
|
||||
import Index from '@/pages/Index.tsx'
|
||||
import NotFoundPage from '@/pages/NotFoundPage'
|
||||
import Model from '@/pages/SettingPage/Model.tsx'
|
||||
import Transcriber from '@/pages/SettingPage/transcriber.tsx'
|
||||
import ProviderForm from '@/components/Form/modelForm/Form.tsx'
|
||||
import StepBar from '@/pages/HomePage/components/StepBar.tsx'
|
||||
import Downloading from '@/components/Lottie/download.tsx'
|
||||
import Prompt from '@/pages/SettingPage/Prompt.tsx'
|
||||
import AboutPage from '@/pages/SettingPage/about.tsx'
|
||||
import Monitor from '@/pages/SettingPage/Monitor.tsx'
|
||||
import Downloader from '@/pages/SettingPage/Downloader.tsx'
|
||||
import DownloaderForm from '@/components/Form/DownloaderForm/Form.tsx'
|
||||
import { useEffect } from 'react'
|
||||
@@ -56,6 +53,7 @@ function App() {
|
||||
<Route path="download" element={<Downloader />}>
|
||||
<Route path=":id" element={<DownloaderForm />} />
|
||||
</Route>
|
||||
<Route path="monitor" element={<Monitor />}></Route>
|
||||
<Route path="about" element={<AboutPage />}></Route>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import {
|
||||
BotMessageSquare,
|
||||
SquareChevronRight,
|
||||
Captions,
|
||||
HardDriveDownload,
|
||||
Wrench,
|
||||
Info,
|
||||
Activity,
|
||||
} from 'lucide-react'
|
||||
import MenuBar, { IMenuProps } from '@/pages/SettingPage/components/menuBar.tsx'
|
||||
|
||||
@@ -37,6 +35,12 @@ const Menu = () => {
|
||||
// icon: <SquareChevronRight />,
|
||||
// path: '/settings/prompt',
|
||||
// },
|
||||
{
|
||||
id: 'monitor',
|
||||
name: '部署监控',
|
||||
icon: <Activity />,
|
||||
path: '/settings/monitor',
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
name: '关于',
|
||||
|
||||
241
BillNote_frontend/src/pages/SettingPage/Monitor.tsx
Normal file
241
BillNote_frontend/src/pages/SettingPage/Monitor.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import {
|
||||
Server,
|
||||
Cpu,
|
||||
AudioLines,
|
||||
Film,
|
||||
RefreshCw,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Loader2
|
||||
} from 'lucide-react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { getDeployStatus, DeployStatus } from '@/services/system'
|
||||
|
||||
export default function Monitor() {
|
||||
const [status, setStatus] = useState<DeployStatus | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
|
||||
|
||||
const fetchStatus = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const data = await getDeployStatus()
|
||||
setStatus(data)
|
||||
setLastUpdated(new Date())
|
||||
} catch (err) {
|
||||
setError('无法连接到后端服务')
|
||||
setStatus(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus()
|
||||
// 自动刷新(每 30 秒)
|
||||
const interval = setInterval(fetchStatus, 30000)
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchStatus])
|
||||
|
||||
const StatusBadge = ({ ok, label }: { ok: boolean; label?: string }) => (
|
||||
<Badge
|
||||
variant={ok ? 'default' : 'destructive'}
|
||||
className={ok ? 'bg-green-500 hover:bg-green-600' : ''}
|
||||
>
|
||||
{ok ? (
|
||||
<><CheckCircle2 className="mr-1 h-3 w-3" />{label || '正常'}</>
|
||||
) : (
|
||||
<><XCircle className="mr-1 h-3 w-3" />{label || '异常'}</>
|
||||
)}
|
||||
</Badge>
|
||||
)
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full overflow-y-auto bg-white">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">部署监控</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
实时监控系统各组件运行状态
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{lastUpdated && (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
最后更新: {lastUpdated.toLocaleTimeString()}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={fetchStatus}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 rounded-lg border border-red-200 bg-red-50 p-4 text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status Cards */}
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
{/* Backend FastAPI */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-lg font-medium">
|
||||
<Server className="mr-2 inline h-5 w-5 text-blue-500" />
|
||||
后端 FastAPI
|
||||
</CardTitle>
|
||||
{status && <StatusBadge ok={status.backend.status === 'running'} label="运行中" />}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading && !status ? (
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
加载中...
|
||||
</div>
|
||||
) : status ? (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">状态:</span>
|
||||
<span className={status.backend.status === 'running' ? 'font-medium text-green-600' : 'font-medium text-red-600'}>
|
||||
{status.backend.status === 'running' ? '运行中' : status.backend.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">端口:</span>
|
||||
<span className="font-mono">{status.backend.port}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* CUDA GPU */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-lg font-medium">
|
||||
<Cpu className="mr-2 inline h-5 w-5 text-green-500" />
|
||||
CUDA GPU
|
||||
</CardTitle>
|
||||
{status && <StatusBadge ok={status.cuda.available} label={status.cuda.available ? '已启用' : '未启用'} />}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading && !status ? (
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
加载中...
|
||||
</div>
|
||||
) : status ? (
|
||||
<div className="space-y-2 text-sm">
|
||||
{status.cuda.available ? (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">GPU:</span>
|
||||
<span className="font-medium">{status.cuda.gpu_name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">CUDA 版本:</span>
|
||||
<span className="font-mono">{status.cuda.version}</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-muted-foreground">
|
||||
CUDA 不可用,将使用 CPU 模式
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Whisper Model */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-lg font-medium">
|
||||
<AudioLines className="mr-2 inline h-5 w-5 text-purple-500" />
|
||||
Whisper 模型
|
||||
</CardTitle>
|
||||
{status && <StatusBadge ok={true} label="已配置" />}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading && !status ? (
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
加载中...
|
||||
</div>
|
||||
) : status ? (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">模型大小:</span>
|
||||
<span className="font-medium">{status.whisper.model_size}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">转写引擎:</span>
|
||||
<span className="font-mono">{status.whisper.transcriber_type}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* FFmpeg */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-lg font-medium">
|
||||
<Film className="mr-2 inline h-5 w-5 text-orange-500" />
|
||||
FFmpeg
|
||||
</CardTitle>
|
||||
{status && <StatusBadge ok={status.ffmpeg.available} label={status.ffmpeg.available ? '可用' : '不可用'} />}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading && !status ? (
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
加载中...
|
||||
</div>
|
||||
) : status ? (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">状态:</span>
|
||||
<span className={status.ffmpeg.available ? 'font-medium text-green-600' : 'font-medium text-red-600'}>
|
||||
{status.ffmpeg.available ? '已安装' : '未安装'}
|
||||
</span>
|
||||
</div>
|
||||
{!status.ffmpeg.available && (
|
||||
<div className="text-xs text-red-500">
|
||||
请安装 FFmpeg 并添加到系统 PATH
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="mt-8 text-center text-xs text-gray-400">
|
||||
状态每 30 秒自动刷新
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,29 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const systemCheck=async()=>{
|
||||
export const systemCheck = async () => {
|
||||
return await request.get('/sys_health')
|
||||
}
|
||||
|
||||
export interface DeployStatus {
|
||||
backend: {
|
||||
status: string
|
||||
port: number
|
||||
}
|
||||
cuda: {
|
||||
available: boolean
|
||||
version: string | null
|
||||
gpu_name: string | null
|
||||
}
|
||||
whisper: {
|
||||
model_size: string
|
||||
transcriber_type: string
|
||||
}
|
||||
ffmpeg: {
|
||||
available: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const getDeployStatus = async (): Promise<DeployStatus> => {
|
||||
return await request.get('/deploy_status')
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import tailwindcss from '@tailwindcss/vite'
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd() + '/../')
|
||||
|
||||
const apiBaseUrl = env.VITE_API_BASE_URL || 'http://localhost:8000'
|
||||
const apiBaseUrl = env.VITE_API_BASE_URL || 'http://127.0.0.1:8483'
|
||||
const port = parseInt(env.VITE_FRONTEND_PORT || '3015', 10)
|
||||
|
||||
return {
|
||||
|
||||
@@ -42,4 +42,37 @@ async def sys_health():
|
||||
|
||||
@router.get("/sys_check")
|
||||
async def sys_check():
|
||||
return R.success()
|
||||
return R.success()
|
||||
|
||||
|
||||
@router.get("/deploy_status")
|
||||
async def deploy_status():
|
||||
"""返回部署监控所需的所有状态信息"""
|
||||
import torch
|
||||
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 模型状态
|
||||
model_size = os.getenv("WHISPER_MODEL_SIZE", "base")
|
||||
transcriber_type = os.getenv("TRANSCRIBER_TYPE", "fast-whisper")
|
||||
|
||||
# FFmpeg 状态
|
||||
try:
|
||||
ensure_ffmpeg_or_raise()
|
||||
ffmpeg_ok = True
|
||||
except:
|
||||
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},
|
||||
"ffmpeg": {"available": ffmpeg_ok},
|
||||
})
|
||||
@@ -1,44 +1,9 @@
|
||||
import re
|
||||
|
||||
|
||||
import re
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def prepend_source_link(markdown: str | None, source_url: str) -> str | None:
|
||||
"""
|
||||
在笔记开头添加来源链接;若首个非空行已包含来源链接,则更新该行并避免重复。
|
||||
"""
|
||||
if markdown is None:
|
||||
return None
|
||||
|
||||
source = (source_url or "").strip()
|
||||
if not source:
|
||||
return markdown
|
||||
|
||||
header = f"> 来源链接:{source}"
|
||||
lines = markdown.splitlines()
|
||||
first_non_empty_idx = None
|
||||
for idx, line in enumerate(lines):
|
||||
if line.strip():
|
||||
first_non_empty_idx = idx
|
||||
break
|
||||
|
||||
if first_non_empty_idx is not None:
|
||||
first_line = lines[first_non_empty_idx].strip()
|
||||
if first_line.startswith("> 来源链接:") or first_line.startswith("来源链接:"):
|
||||
lines[first_non_empty_idx] = header
|
||||
return "\n".join(lines)
|
||||
|
||||
if markdown.strip():
|
||||
return f"{header}\n\n{markdown}"
|
||||
return header
|
||||
|
||||
|
||||
def replace_content_markers(markdown: str, video_id: str, platform: str = 'bilibili') -> str:
|
||||
"""
|
||||
替换 *Content-04:16*、Content-04:16 或 Content-[04:16] 为超链接,跳转到对应平台视频的时间位置
|
||||
替换 *Content-04:16*、Content-04:16 或 Content-[04:16] 为超链接
|
||||
目标格式:- [04:16](https://www.bilibili.com/video/BVxxx?t=256#t=04:16)
|
||||
"""
|
||||
# 匹配三种形式:*Content-04:16*、Content-04:16、Content-[04:16]
|
||||
pattern = r"(?:\*?)Content-(?:\[(\d{2}):(\d{2})\]|(\d{2}):(\d{2}))"
|
||||
@@ -49,18 +14,28 @@ def replace_content_markers(markdown: str, video_id: str, platform: str = 'bilib
|
||||
mm = match.group(1) or match.group(3)
|
||||
ss = match.group(2) or match.group(4)
|
||||
total_seconds = int(mm) * 60 + int(ss)
|
||||
time_str = f"{mm}:{ss}"
|
||||
|
||||
if platform == 'bilibili':
|
||||
parsed_video_id = safe_video_id.replace("_p", "?p=")
|
||||
url = f"https://www.bilibili.com/video/{parsed_video_id}&t={total_seconds}"
|
||||
# 处理多 P 情况,如果是 BV123_p3 转换为 BV123?p=3
|
||||
actual_video_id = video_id.replace("_p", "?p=")
|
||||
|
||||
# 判断连接符是 ? 还是 & (如果 video_id 里已经有了 ?p=,则时间参数用 &t=)
|
||||
connector = "&t=" if "?" in actual_video_id else "?t="
|
||||
|
||||
# 拼接最终 URL,并在末尾加上 #t=MM:SS 锚点
|
||||
url = f"https://www.bilibili.com/video/{actual_video_id}{connector}{total_seconds}#t={time_str}"
|
||||
return f"- [{time_str}]({url})"
|
||||
|
||||
elif platform == 'youtube':
|
||||
url = f"https://www.youtube.com/watch?v={safe_video_id}&t={total_seconds}s"
|
||||
url = f"https://www.youtube.com/watch?v={video_id}&t={total_seconds}s"
|
||||
return f"- [{time_str}]({url})"
|
||||
|
||||
elif platform == 'douyin':
|
||||
url = f"https://www.douyin.com/video/{safe_video_id}"
|
||||
return f"[原片 @ {mm}:{ss}]({url})"
|
||||
url = f"https://www.douyin.com/video/{video_id}"
|
||||
return f"[原片 @ {time_str}]({url})"
|
||||
|
||||
else:
|
||||
return f"({mm}:{ss})"
|
||||
return f"({time_str})"
|
||||
|
||||
return f"[原片 @ {mm}:{ss}]({url})"
|
||||
|
||||
return re.sub(pattern, replacer, markdown)
|
||||
return re.sub(pattern, replacer, markdown)
|
||||
Reference in New Issue
Block a user