mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-06-28 19:21:24 +08:00
feat(extension): P2 视频页悬浮按钮 + 右键菜单 + cookie 直通;P3 侧边栏首版
- contentScripts: 仅在支持的视频平台(B 站 / YouTube / 抖音 / 快手)注入悬浮 BiliNote 按钮,点击通过 webext-bridge 发 'bilinote-start' 给 background - background: 处理 bilinote-start 与右键菜单点击;调 /api/generate_note;写 chrome.storage;自动打开侧边栏。logic/storage 是 Vue 反应式版本,service worker 不能 import,因此把常量抽到 logic/constants.ts - contextMenus: onInstalled 时注册"用 BiliNote 总结此视频",限定 4 个支持平台域名 - 浏览器 Cookie 同步:options 页加按钮,按平台读 chrome.cookies.getAll,序列化为 'name=value; ...' 后 POST 给后端 /api/update_downloader_cookie。chrome.cookies + contextMenus 权限补到 manifest - 侧边栏(P3 首版):从 storage 读最近任务并轮询,复用 TaskProgress + MarkdownView。markmap 思维导图与 RAG 问答推到后续 - 修 P1 endpoint 拼错的 bug:/api/get_models_by_provider 实际是 /api/model_enable,404 来自这里 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
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'
|
||||
|
||||
// only on dev mode
|
||||
if (import.meta.hot) {
|
||||
@@ -8,16 +11,162 @@ if (import.meta.hot) {
|
||||
import('./contentScriptHMR')
|
||||
}
|
||||
|
||||
// 工具栏图标点击 → 打开 popup(默认行为,无需配置)
|
||||
// side panel 留给 P3 阶段:在那时把 popup 替换成"action 行为"或加单独命令
|
||||
// 此处不开启 openPanelOnActionClick,否则会绕过 default_popup
|
||||
// ---------- 直接操作 chrome.storage(service worker 里别用 Vue 反应式)----------
|
||||
|
||||
browser.runtime.onInstalled.addListener((): void => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('BiliNote extension installed')
|
||||
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): Promise<{ ok: boolean, taskId?: string, error?: string }> {
|
||||
const platform = detectPlatform(url)
|
||||
if (!platform)
|
||||
return { ok: false, error: '当前链接不是支持的视频平台' }
|
||||
|
||||
const settings = await readSettings()
|
||||
if (!settings.providerId || !settings.modelName)
|
||||
return { ok: false, error: '请先在设置页选择供应商与模型' }
|
||||
|
||||
const backend = settings.backendUrl.replace(/\/$/, '')
|
||||
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,
|
||||
screenshot: settings.screenshot,
|
||||
link: settings.link,
|
||||
style: settings.style || undefined,
|
||||
format: [
|
||||
...(settings.screenshot ? ['screenshot'] : []),
|
||||
...(settings.link ? ['link'] : []),
|
||||
],
|
||||
}),
|
||||
})
|
||||
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(),
|
||||
})
|
||||
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 }, 'bilinote-start'>('bilinote-start', async ({ data, sender }) => {
|
||||
const result = await startTask(data.url)
|
||||
// 成功就把侧边栏拉起来给用户看进度
|
||||
if (result.ok)
|
||||
await openSidePanelInTab(sender?.tabId)
|
||||
return result
|
||||
})
|
||||
|
||||
// 占位:未来 content script 通过 webext-bridge 触发任务时,由这里转发到后端
|
||||
// ---------- 安装时事件 ----------
|
||||
|
||||
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)
|
||||
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 })
|
||||
@@ -27,4 +176,3 @@ onMessage('get-current-tab', async () => {
|
||||
return { title: undefined, url: undefined }
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user