Files
BiliNote/BillNote_extension/src/background/main.ts

191 lines
6.0 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 { onMessage } from 'webext-bridge/background'
import type { Settings, TaskRecord } from '~/logic/types'
import { DEFAULT_SETTINGS, MAX_TASKS, SETTINGS_KEY, TASKS_KEY } from '~/logic/constants'
import { detectPlatform } from '~/logic/platform'
import { fetchBilibiliSubtitle } from '~/logic/bilibili-subtitle'
import { normalizeVideoTitle } from '~/logic/task-display'
// only on dev mode
if (import.meta.hot) {
// @ts-expect-error for background HMR
import('/@vite/client')
// load latest content script
import('./contentScriptHMR')
}
// ---------- 直接操作 chrome.storageservice worker 里别用 Vue 反应式)----------
async function readSettings(): Promise<Settings> {
const obj = await browser.storage.local.get(SETTINGS_KEY)
const raw = obj[SETTINGS_KEY] as string | undefined
if (!raw)
return { ...DEFAULT_SETTINGS }
try {
return { ...DEFAULT_SETTINGS, ...(JSON.parse(raw) as Partial<Settings>) }
}
catch {
return { ...DEFAULT_SETTINGS }
}
}
async function readTasks(): Promise<TaskRecord[]> {
const obj = await browser.storage.local.get(TASKS_KEY)
const raw = obj[TASKS_KEY] as string | undefined
if (!raw)
return []
try {
return JSON.parse(raw) as TaskRecord[]
}
catch {
return []
}
}
async function writeTasks(tasks: TaskRecord[]) {
await browser.storage.local.set({ [TASKS_KEY]: JSON.stringify(tasks.slice(0, MAX_TASKS)) })
}
async function upsertTask(record: TaskRecord) {
const tasks = await readTasks()
const idx = tasks.findIndex(t => t.taskId === record.taskId)
if (idx >= 0)
tasks.splice(idx, 1, { ...tasks[idx], ...record })
else
tasks.unshift(record)
await writeTasks(tasks)
}
// ---------- 启动任务 ----------
async function startTask(url: string, title?: string): Promise<{ ok: boolean, taskId?: string, error?: string }> {
const platform = detectPlatform(url)
const displayTitle = normalizeVideoTitle(title)
if (!platform)
return { ok: false, error: '当前链接不是支持的视频平台' }
const settings = await readSettings()
if (!settings.providerId || !settings.modelName)
return { ok: false, error: '请先在设置页选择供应商与模型' }
const backend = settings.backendUrl.replace(/\/$/, '')
// B 站:先在浏览器里抓字幕(带本地登录态 cookie随提交带过去
const prefetched = platform === 'bilibili' ? await fetchBilibiliSubtitle(url) : null
const formats = settings.formats || []
try {
const res = await fetch(`${backend}/api/generate_note`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
video_url: url,
platform,
quality: settings.quality,
provider_id: settings.providerId,
model_name: settings.modelName,
// backend 同时接受 format 数组与 screenshot/link 单独布尔;从 formats 派生保持单一真相源
format: [...formats],
screenshot: formats.includes('screenshot'),
link: formats.includes('link'),
style: settings.style || undefined,
extras: settings.extras || undefined,
video_understanding: settings.video_understanding || undefined,
video_interval: settings.video_understanding ? settings.video_interval : undefined,
grid_size: settings.video_understanding ? settings.grid_size : undefined,
prefetched_transcript: prefetched ?? undefined,
}),
})
if (!res.ok)
return { ok: false, error: `HTTP ${res.status}` }
const body = await res.json() as { code: number, msg: string, data: { task_id: string } }
if (body.code !== 0)
return { ok: false, error: body.msg }
await upsertTask({
taskId: body.data.task_id,
videoUrl: url,
platform,
status: 'PENDING',
message: '已提交',
createdAt: Date.now(),
updatedAt: Date.now(),
title: displayTitle,
})
return { ok: true, taskId: body.data.task_id }
}
catch (e) {
return { ok: false, error: (e as Error).message }
}
}
async function openSidePanelInTab(tabId?: number) {
try {
// @ts-expect-error chrome.sidePanel 类型在 webextension-polyfill 中尚未补全
if (typeof chrome !== 'undefined' && chrome.sidePanel?.open && tabId !== undefined)
// @ts-expect-error see above
await chrome.sidePanel.open({ tabId })
}
catch (err) {
console.warn('打开侧边栏失败:', err)
}
}
// ---------- 消息桥 ----------
onMessage<{ url: string; title?: string }, 'bilinote-start'>('bilinote-start', async ({ data, sender }) => {
const result = await startTask(data.url, data.title)
// 成功就把侧边栏拉起来给用户看进度
if (result.ok)
await openSidePanelInTab(sender?.tabId)
return result
})
// ---------- 安装时事件 ----------
browser.runtime.onInstalled.addListener(() => {
console.log('BiliNote extension installed')
// 右键菜单:在视频页或视频链接上"用 BiliNote 总结"
try {
browser.contextMenus.create({
id: 'bilinote-summarize-page',
title: '用 BiliNote 总结此视频',
contexts: ['page', 'link', 'video'],
documentUrlPatterns: [
'*://*.bilibili.com/*',
'*://*.youtube.com/*',
'*://youtu.be/*',
'*://*.douyin.com/*',
'*://*.kuaishou.com/*',
],
})
}
catch (e) {
console.warn('注册右键菜单失败:', e)
}
})
browser.contextMenus?.onClicked.addListener(async (info, tab) => {
if (info.menuItemId !== 'bilinote-summarize-page')
return
const url = info.linkUrl || tab?.url
if (!url)
return
const result = await startTask(url, tab?.title)
if (result.ok)
await openSidePanelInTab(tab?.id)
else
console.warn('右键启动失败:', result.error)
})
// content script 占位握手 —— 未来可扩展为查询当前任务等
onMessage('get-current-tab', async () => {
try {
const [tab] = await browser.tabs.query({ active: true, currentWindow: true })
return { title: tab?.title, url: tab?.url }
}
catch {
return { title: undefined, url: undefined }
}
})