Files
BiliNote/BillNote_frontend/src/pages/HomePage/components/NoteForm.tsx
黄建武 04dad3b72a feat(downloaders): 添加抖音视频识别功能
- 新增 abogus.py 文件,实现 a_bogus 参数的生成逻辑
- 代码源自 JoeanAmier/TikTokDownloader 项目,并进行了适配和优化
- 功能包括生成用户代理字符串、加密 URL 参数和生成最终的 a_bogus值
- 提供了详细的注释和函数说明,便于理解和维护
2025-05-02 14:00:29 +08:00

544 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form.tsx'
import { useEffect } from 'react'
import { Input } from '@/components/ui/input.tsx'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select.tsx'
import { Button } from '@/components/ui/button.tsx'
import { Checkbox } from '@/components/ui/checkbox.tsx'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { Info, Clock, Loader2 } from 'lucide-react'
import { message } from 'antd'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip.tsx'
import { generateNote } from '@/services/note.ts'
import { useTaskStore } from '@/store/taskStore'
import NoteHistory from '@/pages/HomePage/components/NoteHistory.tsx'
import { useModelStore } from '@/store/modelStore'
import { Alert } from 'antd'
import { Textarea } from '@/components/ui/textarea.tsx'
import { ScrollArea } from '@/components/ui/scroll-area.tsx'
import { uploadFile } from '@/services/upload.ts'
// ✅ 定义表单 schema
const formSchema = z
.object({
video_url: z.string(),
platform: z.string().nonempty('请选择平台'),
quality: z.enum(['fast', 'medium', 'slow'], {
required_error: '请选择音频质量',
}),
screenshot: z.boolean().optional(),
link: z.boolean().optional(),
model_name: z.string().nonempty('请选择模型'),
format: z.array(z.string()).default([]),
style: z.string().nonempty('请选择笔记生成风格'),
extras: z.string().optional(),
})
.superRefine((data, ctx) => {
const { video_url, platform } = data
if (platform === 'local' || platform === 'douyin') {
if (!video_url || typeof video_url !== 'string') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '本地视频路径不能为空',
path: ['video_url'],
})
}
} else {
try {
const url = new URL(video_url)
if (!(url.protocol === 'http:' || url.protocol === 'https:')) {
throw new Error()
}
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '请输入正确的视频链接',
path: ['video_url'],
})
}
}
})
type NoteFormValues = z.infer<typeof formSchema>
const noteFormats = [
{
label: '目录',
value: 'toc',
},
{ label: '原片跳转', value: 'link' },
{ label: '原片截图', value: 'screenshot' },
{ label: 'AI总结', value: 'summary' },
]
const noteStyles = [
{
label: '精简',
value: 'minimal', // 简洁、快速呈现要点
},
{
label: '详细',
value: 'detailed', // 详细记录,包含时间戳、关键点
},
{
label: '教程',
value: 'tutorial', // 详细记录,包含时间戳、关键点
},
{
label: '学术',
value: 'academic', // 适合学术报告,正式且结构化
},
{
label: '小红书',
value: 'xiaohongshu', // 适合社交平台分享,亲切、口语化
},
{
label: '生活向',
value: 'life_journal', // 记录个人生活感悟,情感化表达
},
{
label: '任务导向',
value: 'task_oriented', // 强调任务、目标,适合工作和待办事项
},
{
label: '商业风格',
value: 'business', // 适合商业报告、会议纪要,正式且精准
},
{
label: '会议纪要',
value: 'meeting_minutes', // 适合商业报告、会议纪要,正式且精准
},
]
const NoteForm = () => {
useTaskStore(state => state.tasks)
const setCurrentTask = useTaskStore(state => state.setCurrentTask)
const currentTaskId = useTaskStore(state => state.currentTaskId)
const getCurrentTask = useTaskStore(state => state.getCurrentTask)
const loadEnabledModels = useModelStore(state => state.loadEnabledModels)
const modelList = useModelStore(state => state.modelList)
const showFeatureHint = useModelStore(state => state.showFeatureHint)
const setShowFeatureHint = useModelStore(state => state.setShowFeatureHint)
const form = useForm<NoteFormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
video_url: '',
platform: 'bilibili',
quality: 'medium', // 默认中等质量
screenshot: false,
model_name: modelList[0]?.model_name || '', // 确保有值
format: [], // 初始化为空数组
style: 'minimal', // 默认选择精简风格
extras: '', // 初始化为空字符串
},
})
const platform = form.watch('platform')
const onClose = () => {
setShowFeatureHint(false)
}
const isGenerating = () => {
console.log('🚀 isGenerating', getCurrentTask()?.status)
return (
getCurrentTask()?.status != 'SUCCESS' &&
getCurrentTask()?.status != 'FAILED' &&
getCurrentTask()?.status != undefined
)
}
const handleFileUpload = async (file: File, onSuccess: (url: string) => void) => {
if (!file) return
const formData = new FormData()
formData.append('file', file)
try {
const res = await uploadFile(formData)
if (res.data.code === 0) {
const uploadedUrl = res.data.data.url
console.log('✅ 上传成功', uploadedUrl)
onSuccess(uploadedUrl)
}
} catch (error) {
console.error('上传失败', error)
// 可以弹个 toast 或者提示上传失败
}
}
// TODO 修复选择其他视频平台以后再选择本地视频还可以选择 Link 的问题
const onSubmit = async (data: NoteFormValues) => {
console.log('🎯 提交内容:', data)
message.success('提交任务')
const payload = {
video_url: data.video_url,
platform: data.platform,
quality: data.quality,
model_name: data.model_name,
provider_id: modelList.find(model => model.model_name === data.model_name).provider_id,
format: data.format,
style: data.style,
extras: data.extras,
}
const res = await generateNote(payload)
const taskId = res.data.task_id
useTaskStore.getState().addPendingTask(taskId, data.platform, payload)
}
useEffect(() => {
loadEnabledModels()
}, [])
return (
<>
<ScrollArea className="sm:h-[400px] md:h-[800px]">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
<div className="flex w-full items-center gap-2 py-1.5">
<Button
type="submit"
className="bg-primary w-full sm:w-full"
disabled={isGenerating()}
>
{isGenerating() && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isGenerating() ? '正在生成…' : '生成笔记'}
</Button>
</div>
<div className="space-y-2">
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">YouTube等平台</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex gap-2">
{/* 平台选择 */}
<FormField
control={form.control}
name="platform"
render={({ field }) => (
<FormItem>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-32">
<SelectValue placeholder="选择平台" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="bilibili"></SelectItem>
<SelectItem value="youtube">Youtube</SelectItem>
<SelectItem value="douyin"></SelectItem>
<SelectItem value="local"></SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* 视频地址 */}
<FormField
control={form.control}
name="video_url"
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
{form.watch('platform') === 'local' ? (
<div className="flex flex-col gap-2">
{/* 第一行:本地路径输入框 */}
<Input placeholder="请输入本地视频路径" {...field} className="w-full" />
</div>
) : (
<Input placeholder="请输入视频网站链接" {...field} />
)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="video_url"
render={({ field }) => (
<FormItem className="w-full">
<FormControl>
{form.watch('platform') === 'local' ? (
<div
className="hover:border-primary flex h-40 w-full cursor-pointer items-center justify-center rounded-md border-2 border-dashed border-gray-300 transition-colors"
onDragOver={e => {
e.preventDefault()
e.stopPropagation()
}}
onDrop={e => {
e.preventDefault()
e.stopPropagation()
const file = e.dataTransfer.files?.[0]
if (file) {
handleFileUpload(file, uploadedUrl => {
field.onChange(uploadedUrl)
})
}
}}
onClick={() => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'video/*'
input.onchange = (e: any) => {
const file = e.target.files?.[0]
if (file) {
handleFileUpload(file, uploadedUrl => {
field.onChange(uploadedUrl)
})
}
}
input.click()
}}
>
<div className="text-center text-sm text-gray-500">
<p className="mb-2"></p>
<p className="text-xs text-gray-400"></p>
</div>
</div>
) : (
<></>
)}
</FormControl>
{/* ❗可以不要FormMessage不然重复两次报错提示 */}
</FormItem>
)}
/>
{/*<p className="text-xs text-neutral-500">*/}
{/* 支持哔哩哔哩视频链接,例如:*/}
{/* https://www.bilibili.com/video/BV1vc25YQE9X/*/}
{/*</p>*/}
{/*<FormField*/}
{/* control={form.control}*/}
{/* name="quality"*/}
{/* render={({ field }) => (*/}
{/* <FormItem>*/}
{/* <div className="my-3 flex items-center justify-between">*/}
{/* <h2 className="block">音频质量</h2>*/}
{/* <TooltipProvider>*/}
{/* <Tooltip>*/}
{/* <TooltipTrigger asChild>*/}
{/* <Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />*/}
{/* </TooltipTrigger>*/}
{/* <TooltipContent>*/}
{/* <p className="max-w-[200px] text-xs">*/}
{/* 质量越高,下载体积越大,速度越慢*/}
{/* </p>*/}
{/* </TooltipContent>*/}
{/* </Tooltip>*/}
{/* </TooltipProvider>*/}
{/* </div>*/}
{/* <Select onValueChange={field.onChange} defaultValue={field.value}>*/}
{/* <FormControl>*/}
{/* <SelectTrigger className="w-full">*/}
{/* <SelectValue placeholder="选择质量" />*/}
{/* </SelectTrigger>*/}
{/* </FormControl>*/}
{/* <SelectContent>*/}
{/* <SelectItem value="fast">快速(压缩)</SelectItem>*/}
{/* <SelectItem value="medium">中等(推荐)</SelectItem>*/}
{/* <SelectItem value="slow">高质量(清晰)</SelectItem>*/}
{/* </SelectContent>*/}
{/* </Select>*/}
{/* /!*<FormDescription className="text-xs text-neutral-500">*!/*/}
{/* /!* 质量越高,下载体积越大,速度越慢*!/*/}
{/* /!*</FormDescription>*!/*/}
{/* <FormMessage />*/}
{/* </FormItem>*/}
{/* )}*/}
{/*/>*/}
<FormField
control={form.control}
name="model_name"
render={({ field }) => (
<FormItem>
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-[200px] text-xs">
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择配置好的模型" />
</SelectTrigger>
</FormControl>
<SelectContent>
{modelList.map(item => {
return <SelectItem value={item.model_name}>{item.model_name}</SelectItem>
})}
</SelectContent>
</Select>
{/*<FormDescription className="text-xs text-neutral-500">*/}
{/* 质量越高,下载体积越大,速度越慢*/}
{/*</FormDescription>*/}
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="style"
render={({ field }) => (
<FormItem>
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-[200px] text-xs"></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择笔记风格" />
</SelectTrigger>
</FormControl>
<SelectContent>
{noteStyles.map(item => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="format"
render={({ field }) => (
<FormItem>
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<div className="flex flex-wrap space-x-1.5">
{noteFormats.map(item => (
<label key={item.value} className="flex items-center space-x-2">
<Checkbox
checked={field.value?.includes(item.value)}
disabled={item.value === 'link' && platform === 'local'}
onCheckedChange={checked => {
const currentValue = field.value ?? [] // ✨ 保底是数组
if (checked) {
field.onChange([...currentValue, item.value])
} else {
field.onChange(currentValue.filter(v => v !== item.value))
}
}}
/>
<span>{item.label}</span>
</label>
))}
</div>
</FormControl>
<FormField
control={form.control}
name="extras"
render={({ field }) => (
<FormItem>
<div className="my-3 flex items-center justify-between">
<h2 className="block"></h2>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="hover:text-primary h-4 w-4 cursor-pointer text-neutral-400" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">Prompt最后 </p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Textarea placeholder={'笔记需要罗列出 xxx 关键点'} {...field} />
{/*<FormDescription className="text-xs text-neutral-500">*/}
{/* 质量越高,下载体积越大,速度越慢*/}
{/*</FormDescription>*/}
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
{/* 添加一些额外的说明或功能介绍 */}
{/*<div className="bg-primary-light mt-6 rounded-lg p-4"></div>*/}
</>
)
}
export default NoteForm