mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-06-20 23:14:26 +08:00
feat(extension): 浏览器插件 P1 MVP
新建 BillNote_extension/ 工作空间(基于 vitesse-webext 骨架,Vue 3 + Vite + UnoCSS + MV3)。 P1 MVP 范围: - popup:自动读当前 tab URL,识别 Bilibili / YouTube / 抖音 / 快手;提交 /generate_note 后轮询 /task_status;展示 markdown,复制 + 下载 .md - options:后端地址输入与连通性测试;从 /get_all_providers + /get_models_by_provider 拉供应商/模型列表;默认画质、截图/跳转、笔记风格 - chrome.storage.local 持久化设置与最近 30 个任务,popup 重开恢复进行中任务 - markdown 里的 /static/screenshots 路径在渲染前重写为绝对地址 后端:CORS 改用 regex,新增允许 chrome-extension:// 与 moz-extension:// 源(同时保留 localhost / 127.0.0.1 / tauri.localhost)。无新增 backend endpoint。 P2-P4(content script 悬浮按钮、cookie 直通、side panel、思维导图、RAG 问答)保留 stub 文件,不在本次范围。 去掉 vitesse-webext 自带的 simple-git-hooks postinstall 配置——它会在仓库根装 pre-commit 钩子去跑 pnpm lint-staged,但仓库根没有 package.json,会破坏所有提交流。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
69
BillNote_extension/src/logic/api.ts
Normal file
69
BillNote_extension/src/logic/api.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { GenerateRequest, Model, Provider, TaskStatusResponse } from './types'
|
||||
import { settings } from './storage'
|
||||
|
||||
interface ApiEnvelope<T> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
}
|
||||
|
||||
function backendUrl(): string {
|
||||
return (settings.value?.backendUrl || 'http://localhost:8483').replace(/\/$/, '')
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${backendUrl()}${path}`, {
|
||||
headers: { 'Content-Type': 'application/json', ...(init?.headers || {}) },
|
||||
...init,
|
||||
})
|
||||
if (!res.ok)
|
||||
throw new Error(`HTTP ${res.status}: ${await res.text()}`)
|
||||
const body = (await res.json()) as ApiEnvelope<T> | T
|
||||
// 后端 ResponseWrapper 包了 {code, msg, data};非 0 视为业务错
|
||||
if (body && typeof body === 'object' && 'code' in body) {
|
||||
const env = body as ApiEnvelope<T>
|
||||
if (env.code !== 0)
|
||||
throw new Error(env.msg || '后端返回失败')
|
||||
return env.data
|
||||
}
|
||||
return body as T
|
||||
}
|
||||
|
||||
export async function getProviders(): Promise<Provider[]> {
|
||||
return request<Provider[]>('/api/get_all_providers')
|
||||
}
|
||||
|
||||
export async function getModelsByProvider(providerId: string): Promise<Model[]> {
|
||||
return request<Model[]>(`/api/get_models_by_provider/${providerId}`)
|
||||
}
|
||||
|
||||
export async function generateNote(payload: GenerateRequest): Promise<{ task_id: string }> {
|
||||
return request<{ task_id: string }>('/api/generate_note', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getTaskStatus(taskId: string): Promise<TaskStatusResponse> {
|
||||
// /task_status/{id} 返回的是裸对象(非 ResponseWrapper 包装),见 routers/note.py
|
||||
const res = await fetch(`${backendUrl()}/api/task_status/${taskId}`)
|
||||
if (!res.ok)
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
return (await res.json()) as TaskStatusResponse
|
||||
}
|
||||
|
||||
export async function ping(): Promise<boolean> {
|
||||
try {
|
||||
await getProviders()
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// markdown 里的 /static/screenshots/xxx 是相对路径,extension 渲染时需要拼绝对地址
|
||||
export function absolutizeMarkdownImages(md: string): string {
|
||||
const base = backendUrl()
|
||||
return md.replace(/!\[([^\]]*)\]\((\/static\/[^)]+)\)/g, (_, alt, path) => ``)
|
||||
}
|
||||
15
BillNote_extension/src/logic/common-setup.ts
Normal file
15
BillNote_extension/src/logic/common-setup.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { App } from 'vue'
|
||||
|
||||
export function setupApp(app: App) {
|
||||
// Inject a globally available `$app` object in template
|
||||
app.config.globalProperties.$app = {
|
||||
context: '',
|
||||
}
|
||||
|
||||
// Provide access to `app` in script setup with `const app = inject('app')`
|
||||
app.provide('app', app.config.globalProperties.$app)
|
||||
|
||||
// Here you can install additional plugins for all contexts: popup, options page and content-script.
|
||||
// example: app.use(i18n)
|
||||
// example excluding content-script context: if (context !== 'content-script') app.use(i18n)
|
||||
}
|
||||
1
BillNote_extension/src/logic/index.ts
Normal file
1
BillNote_extension/src/logic/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './storage'
|
||||
24
BillNote_extension/src/logic/platform.ts
Normal file
24
BillNote_extension/src/logic/platform.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Platform } from './types'
|
||||
|
||||
// 与 backend/app/validators/video_url_validator.py 保持一致
|
||||
export function detectPlatform(url: string | undefined | null): Platform | null {
|
||||
if (!url)
|
||||
return null
|
||||
if (/bilibili\.com\/video\//.test(url))
|
||||
return 'bilibili'
|
||||
if (/(youtube\.com\/watch|youtu\.be\/)/.test(url))
|
||||
return 'youtube'
|
||||
if (url.includes('douyin'))
|
||||
return 'douyin'
|
||||
if (url.includes('kuaishou'))
|
||||
return 'kuaishou'
|
||||
return null
|
||||
}
|
||||
|
||||
export const PLATFORM_LABELS: Record<Platform, string> = {
|
||||
bilibili: '哔哩哔哩',
|
||||
youtube: 'YouTube',
|
||||
douyin: '抖音',
|
||||
kuaishou: '快手',
|
||||
local: '本地',
|
||||
}
|
||||
44
BillNote_extension/src/logic/storage.ts
Normal file
44
BillNote_extension/src/logic/storage.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage'
|
||||
import type { Settings, TaskRecord } from './types'
|
||||
|
||||
export const DEFAULT_BACKEND_URL = 'http://localhost:8483'
|
||||
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
backendUrl: DEFAULT_BACKEND_URL,
|
||||
providerId: '',
|
||||
modelName: '',
|
||||
quality: 'medium',
|
||||
screenshot: false,
|
||||
link: false,
|
||||
style: '',
|
||||
}
|
||||
|
||||
// 全局共享设置(popup / options / sidepanel / background 都读这一份)
|
||||
export const { data: settings, dataReady: settingsReady } = useWebExtensionStorage<Settings>(
|
||||
'bilinote-settings',
|
||||
DEFAULT_SETTINGS,
|
||||
{ mergeDefaults: true },
|
||||
)
|
||||
|
||||
// 历史任务列表,最近的在前
|
||||
export const { data: tasks, dataReady: tasksReady } = useWebExtensionStorage<TaskRecord[]>(
|
||||
'bilinote-tasks',
|
||||
[],
|
||||
)
|
||||
|
||||
export const MAX_TASKS = 30
|
||||
|
||||
export function upsertTask(record: TaskRecord) {
|
||||
const list = tasks.value ?? []
|
||||
const idx = list.findIndex(t => t.taskId === record.taskId)
|
||||
if (idx >= 0)
|
||||
list.splice(idx, 1, { ...list[idx], ...record })
|
||||
else
|
||||
list.unshift(record)
|
||||
tasks.value = list.slice(0, MAX_TASKS)
|
||||
}
|
||||
|
||||
export function removeTask(taskId: string) {
|
||||
const list = tasks.value ?? []
|
||||
tasks.value = list.filter(t => t.taskId !== taskId)
|
||||
}
|
||||
82
BillNote_extension/src/logic/types.ts
Normal file
82
BillNote_extension/src/logic/types.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
// 与 backend/app/routers/note.py / provider.py / model.py 对齐
|
||||
export type Platform = 'bilibili' | 'youtube' | 'douyin' | 'kuaishou' | 'local'
|
||||
export type Quality = 'fast' | 'medium' | 'slow'
|
||||
|
||||
export type TaskStatus =
|
||||
| 'PENDING'
|
||||
| 'PARSING'
|
||||
| 'DOWNLOADING'
|
||||
| 'TRANSCRIBING'
|
||||
| 'SUMMARIZING'
|
||||
| 'FORMATTING'
|
||||
| 'SAVING'
|
||||
| 'SUCCESS'
|
||||
| 'FAILED'
|
||||
|
||||
export interface Provider {
|
||||
id: string
|
||||
name: string
|
||||
logo: string
|
||||
type: string
|
||||
enabled: number
|
||||
base_url?: string
|
||||
api_key?: string
|
||||
}
|
||||
|
||||
export interface Model {
|
||||
id: string
|
||||
model_name: string
|
||||
provider_id: string
|
||||
}
|
||||
|
||||
export interface GenerateRequest {
|
||||
video_url: string
|
||||
platform: Platform
|
||||
quality: Quality
|
||||
model_name: string
|
||||
provider_id: string
|
||||
screenshot?: boolean
|
||||
link?: boolean
|
||||
format?: string[]
|
||||
style?: string
|
||||
extras?: string
|
||||
}
|
||||
|
||||
export interface NoteResult {
|
||||
markdown: string
|
||||
transcript?: unknown
|
||||
audio_meta?: {
|
||||
title?: string
|
||||
duration?: number
|
||||
cover_url?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export interface TaskStatusResponse {
|
||||
status: TaskStatus
|
||||
message: string
|
||||
task_id: string
|
||||
result?: NoteResult
|
||||
}
|
||||
|
||||
export interface TaskRecord {
|
||||
taskId: string
|
||||
videoUrl: string
|
||||
platform: Platform
|
||||
status: TaskStatus
|
||||
message: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
result?: NoteResult
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
backendUrl: string
|
||||
providerId: string
|
||||
modelName: string
|
||||
quality: Quality
|
||||
screenshot: boolean
|
||||
link: boolean
|
||||
style: string
|
||||
}
|
||||
Reference in New Issue
Block a user