Skip to content

Commit c5c27cb

Browse files
hyglgithubclaude
andcommitted
feat: v2.0.0 - DeepSeek AI integration and UI improvements
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 49b032a commit c5c27cb

11 files changed

Lines changed: 659 additions & 220 deletions

File tree

README.md

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![GitHub release](https://img.shields.io/github/v/release/hyglgithub/BiViNote?style=flat-square&label=version)](https://github.com/hyglgithub/BiViNote/releases)
55
[![License](https://img.shields.io/github/license/hyglgithub/BiViNote?style=flat-square)](LICENSE)
66

7-
在 B 站视频页抓取字幕、截取视频帧画面,生成带有截图的 Markdown 笔记。
7+
在 B 站视频页抓取字幕、截取视频帧画面,生成带有截图的 Markdown 笔记。支持接入 DeepSeek AI 自动整理文档。
88

99
## 功能
1010

@@ -13,12 +13,13 @@
1313
- ✂️ **截图裁剪** — 基于 Cropper.js 的图片裁剪、缩放、旋转、翻转
1414
- 📋 **章节支持** — 展示视频章节,按章节分段导出
1515
- 📄 **多格式导出** — SRT 字幕、Markdown 笔记(含截图时打包 ZIP)
16+
- 🤖 **AI 文档整理** — 接入 DeepSeek 自动整理字幕文档,流式输出思考过程和整理结果
1617
- 🔄 **自动刷新** — 视频切换时自动获取新字幕
1718
- 🎯 **字幕同步** — 播放时高亮当前字幕,支持自动滚动
1819
- 🌙 **夜间模式** — 日/夜两套配色全局切换
1920
- ⚙️ **自定义设置** — 字体大小、行高、帧步长、自动滚动等
2021
- 🖼️ **图标状态** — 视频页图标正常显示,非视频页图标变暗
21-
- 💬 **提示词**AI 笔记整理提示词,根据有无截图自动切换
22+
- 💬 **提示词管理**手动/自动模式提示词,全屏编辑,支持恢复默认
2223

2324
## 功能演示
2425

@@ -60,8 +61,19 @@ git clone https://github.com/hyglgithub/BiViNote.git
6061
| 字幕 | 字幕列表、添加截屏、复制、跳转、高亮同步 |
6162
| 章节 | 章节列表、添加截屏、复制、跳转 |
6263
| 视频信息 | 勾选需要写入笔记的视频属性(标题/作者/日期/时长/地址/简介/时间戳) |
63-
| 提示词 | AI 笔记整理提示词,可配置路径前缀,支持复制/修改/恢复默认 |
64-
| 设置 | 字幕语言、字体大小、行高、帧步长、自动滚动、夜间模式 |
64+
| 文档整理 | DeepSeek AI 自动整理(流式输出思考+结果),手动整理模式,一键下载/复制 |
65+
| 设置 | 文档整理方式、提示词管理(全屏编辑)、字幕语言、字体大小、行高、帧步长 |
66+
67+
### 文档整理(AI)
68+
69+
选择「自动」模式后,点击「文档整理」标签页会自动连接 DeepSeek:
70+
71+
1. 首次使用需登录 DeepSeek(点击「打开 DeepSeek 登录」跳转)
72+
2. 登录后点击「开始整理」,AI 自动清理口语化内容、修正错别字、保留截图标记
73+
3. 整理过程中可点击「停止整理」终止
74+
4. 整理完成后可下载 Markdown(含截图时打包 ZIP)、复制文本、继续在 DeepSeek 追问
75+
76+
提示词可在设置页「提示词管理」中自定义,支持分别配置手动/自动模式的提示词。
6577

6678
### 底部按钮
6779

@@ -122,24 +134,28 @@ note.zip
122134
```
123135
BiViNote/
124136
├── manifest.json # 扩展配置 (Manifest V3)
125-
├── background.js # Service Worker - API 代理、图标状态管理
126-
├── content.js # 入口脚本 - 面板注入、路由监听
137+
├── background.js # Service Worker - API 代理、图标状态、SSE 处理、DeepSeek 通信
138+
├── content.js # 入口脚本 - 面板注入、路由监听、视频切换检测
127139
├── js/
128140
│ ├── state.js # 全局状态管理
129-
│ ├── panel.js # 面板 UI - 标签页、折叠、拖动、设置、提示词
141+
│ ├── panel.js # 面板 UI - 标签页、折叠、拖动、设置、提示词管理、文档整理
130142
│ ├── subtitle.js # 字幕 - 获取、渲染、高亮同步、跳转
131143
│ ├── chapter.js # 章节 - 获取、渲染、跳转
132144
│ ├── video-info.js # 视频信息展示
133145
│ ├── capture.js # 截图 - OffscreenCanvas、保存、剪贴板
134146
│ ├── crop-viewer.js # 截图浏览 - Cropper.js 裁剪、缩放、旋转、翻转
135147
│ ├── export.js # 导出 - SRT、Markdown、ZIP
148+
│ ├── deepseek.js # DeepSeek 通信模块 - 状态机、chunk 处理、请求生命周期
136149
│ └── settings.js # 设置 - chrome.storage.local 持久化
137150
├── css/
138151
│ └── panel.css # 面板样式(含暗色主题)
139152
├── libs/
140-
│ ├── jszip.min.js # ZIP 打包库
141-
│ ├── cropper.min.js # Cropper.js 裁剪库
142-
│ └── cropper.min.css
153+
│ ├── jszip.min.js # ZIP 打包库
154+
│ ├── cropper.min.js # Cropper.js 裁剪库
155+
│ ├── cropper.min.css
156+
│ ├── deepseek-api.js # DeepSeek MAIN world - PoW、completion、stop_stream
157+
│ ├── deepseek-bridge.js # DeepSeek ISOLATED world - 消息桥接
158+
│ └── wasm-solver.js # DeepSeek PoW WASM 求解器
143159
└── icons/ # 扩展图标(正常 + 变暗状态)
144160
```
145161

@@ -155,6 +171,8 @@ BiViNote/
155171
- **请求取消**:fetchRunId 机制防止过期请求污染状态
156172
- **截图裁剪**:基于 Cropper.js,支持裁剪、缩放、旋转、翻转
157173
- **折叠面板**:可拖动圆形图标,点击展开,功能菜单快捷操作
174+
- **DeepSeek 集成**:多世界脚本注入(MAIN + ISOLATED)、SSE 流式解析(7 种事件格式)、PoW 挑战求解、stop_stream 终止、cookie 降级检测
175+
- **自动滚动控制**:用户上滑暂停自动滚动,回到底部恢复
158176

159177
## 兼容性
160178

background.js

Lines changed: 129 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,27 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
100100
return true;
101101
}
102102

103+
if (message.type === 'ds-abort') {
104+
// 获取当前请求的 chatId 和 messageId,发送 stop_stream 到 DeepSeek 标签页
105+
const processorIds = Object.keys(dsSseProcessors);
106+
if (processorIds.length > 0) {
107+
const processor = dsSseProcessors[processorIds[0]];
108+
const chatId = processor.getChatId();
109+
const messageId = processor.getMessageId();
110+
chrome.tabs.query({ url: '*://chat.deepseek.com/*' }, (tabs) => {
111+
if (tabs[0]?.id) {
112+
chrome.tabs.sendMessage(tabs[0].id, {
113+
type: 'ds-abort-stop',
114+
chatId,
115+
messageId,
116+
}).catch(() => {});
117+
}
118+
});
119+
}
120+
dsSseProcessors = {};
121+
return false;
122+
}
123+
103124
if (message.type === 'ds-send') {
104125
const requestId = message.requestId || crypto.randomUUID();
105126
dsHandleSend(message.markdown, message.prompt, requestId);
@@ -118,6 +139,21 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
118139
return false;
119140
}
120141

142+
if (message.type === 'ds-open-chat') {
143+
const url = message.url || 'https://chat.deepseek.com';
144+
const isStreaming = Object.keys(dsSseProcessors).length > 0;
145+
chrome.tabs.query({ url: '*://chat.deepseek.com/*' }, (tabs) => {
146+
if (isStreaming || tabs.length === 0) {
147+
// 流式处理中或无标签页:新建标签页,避免中断正在运行的脚本
148+
chrome.tabs.create({ url });
149+
} else {
150+
// 空闲:复用已有标签页
151+
chrome.tabs.update(tabs[0].id, { url, active: true });
152+
}
153+
});
154+
return false;
155+
}
156+
121157
// DeepSeek bridge → bilibili tab 转发
122158
if (message.type === 'DEEPSEEK_CHUNK') {
123159
const rid = message.requestId;
@@ -395,6 +431,27 @@ function formatDate(timestamp) {
395431
// ── DeepSeek 文档整理 ──
396432

397433
const DS_URL = 'https://chat.deepseek.com';
434+
435+
const DS_DEFAULT_PROMPT = `你是一个视频笔记整理助手,将视频导出的 Markdown 文档整理为简洁、高质量、适合长期保存的 Markdown 学习笔记。
436+
437+
要求:
438+
439+
1. 删除口语化内容、重复内容、无意义过渡语句,例如:好的、然后、这里呢、兄弟、就是说等。
440+
2. 删除所有字幕时间戳,例如 \`00:12\`、\`05:30\`。
441+
3. 不要逐句输出字幕,将连续字幕整理为简洁、连贯、易阅读的知识内容。
442+
4. 字幕可能由 AI 识别生成,存在错别字、同音字、术语错误、英文大小写错误,请结合上下文修正,并统一技术术语写法。
443+
5. 若文档存在章节结构,严格按原始章节整理;若无章节,则按内容自然分段。
444+
6. 不要新增原文不存在的标题、章节或目录层级,不要改变原始内容顺序。
445+
7. 文档中形如 ![xxx](assets/数字.png) 的 Markdown 标记属于特殊文本块,() 内为相对资源路径且默认与前一句字幕内容关联;必须保留全部此类标记,禁止修改语法、alt 文本、路径或文件名,不得遗漏,可根据整理后的内容适当调整其在当前语义块中的位置。
446+
8. 保留所有技术名词、工具名、框架名、产品名,不要删除、替换或省略。
447+
9. 若存在 Frontmatter(文档开头 YAML),必须完整原样保留,禁止修改字段、字段值和字段顺序。
448+
10. 仅整理原文,禁止总结、解释、扩展原文不存在的信息或补充额外知识。
449+
450+
待整理文档:
451+
452+
{markdown}
453+
454+
直接输出整理后的 Markdown 文档,不要输出任何额外内容。`;
398455
let dsInjectedTabs = new Set();
399456
let dsChatId = null;
400457
let dsSseProcessors = {};
@@ -441,10 +498,11 @@ chrome.tabs.onRemoved.addListener((tabId) => { dsInjectedTabs.delete(tabId); });
441498

442499
async function dsCheckLogin() {
443500
try {
444-
const tabs = await chrome.tabs.query({ url: '*://chat.deepseek.com/*' });
445-
const tab = tabs.length > 0 ? tabs[0] : null;
501+
// 自动获取或创建 DeepSeek 标签页
502+
const tab = await dsEnsureTab();
446503
if (!tab?.id) return { loggedIn: false };
447504

505+
// 主要方式:通过 localStorage token 验证
448506
try {
449507
const results = await chrome.scripting.executeScript({
450508
target: { tabId: tab.id },
@@ -483,6 +541,12 @@ async function dsCheckLogin() {
483541
if (r && typeof r.loggedIn === 'boolean') return r;
484542
} catch {}
485543

544+
// 降级:cookie 检测
545+
const cookies = await chrome.cookies.getAll({ domain: '.deepseek.com' });
546+
const cookieMap = Object.fromEntries(cookies.map(c => [c.name, c.value]));
547+
const hasSession = ['ds_session_id', 'HWSID'].some(name => !!cookieMap[name]);
548+
if (hasSession) return { loggedIn: true, key: 'cookie' };
549+
486550
return { loggedIn: false };
487551
} catch (e) {
488552
return { loggedIn: false, reason: String(e) };
@@ -519,16 +583,14 @@ async function dsHandleSend(markdown, prompt, requestId) {
519583
let fullPrompt = prompt;
520584
try {
521585
const stored = await chrome.storage.local.get('deepseekPrompt');
522-
const sysPrompt = stored.deepseekPrompt || '';
523-
if (sysPrompt && sysPrompt.includes('{markdown}')) {
586+
const sysPrompt = stored.deepseekPrompt || DS_DEFAULT_PROMPT;
587+
if (sysPrompt.includes('{markdown}')) {
524588
fullPrompt = sysPrompt.replace('{markdown}', markdown);
525-
} else if (sysPrompt) {
526-
fullPrompt = sysPrompt + '\n\n' + markdown;
527589
} else {
528-
fullPrompt = markdown;
590+
fullPrompt = sysPrompt + '\n\n' + markdown;
529591
}
530592
} catch {
531-
fullPrompt = markdown;
593+
fullPrompt = DS_DEFAULT_PROMPT.replace('{markdown}', markdown);
532594
}
533595

534596
chrome.tabs.sendMessage(tab.id, {
@@ -552,25 +614,48 @@ async function dsHandleSend(markdown, prompt, requestId) {
552614
function dsCreateSSEProcessor() {
553615
let inThink = false;
554616
let chatId = null;
617+
let messageId = null;
618+
let dataLineBuf = '';
555619

556620
function processChunk(chunk) {
557621
let text = '';
558-
for (const line of chunk.split('\n')) {
559-
if (!line.startsWith('data: ')) continue;
560-
const jsonStr = line.slice(6).trim();
561-
if (!jsonStr || jsonStr === '[DONE]') continue;
562-
try {
563-
const data = JSON.parse(jsonStr);
564-
if (data.type === 'deepseek:chat_session_id') { chatId = data.chat_session_id; continue; }
565-
const result = processEvent(data);
566-
if (result) text += result;
567-
} catch {}
568-
}
622+
try {
623+
for (const line of chunk.split('\n')) {
624+
if (line.startsWith('data: ')) {
625+
const jsonStr = line.slice(6).trim();
626+
if (!jsonStr || jsonStr === '[DONE]') { dataLineBuf = ''; continue; }
627+
try {
628+
const data = JSON.parse(jsonStr);
629+
dataLineBuf = '';
630+
if (data.type === 'deepseek:chat_session_id') { chatId = data.chat_session_id; continue; }
631+
if (data.response_message_id != null) messageId = data.response_message_id;
632+
const result = processEvent(data);
633+
if (result) text += result;
634+
} catch {
635+
dataLineBuf = line;
636+
}
637+
} else {
638+
if (dataLineBuf) {
639+
dataLineBuf += '\n' + line;
640+
try {
641+
const jsonStr = dataLineBuf.slice(6).trim();
642+
const data = JSON.parse(jsonStr);
643+
dataLineBuf = '';
644+
if (data.type === 'deepseek:chat_session_id') { chatId = data.chat_session_id; continue; }
645+
if (data.response_message_id != null) messageId = data.response_message_id;
646+
const result = processEvent(data);
647+
if (result) text += result;
648+
} catch {}
649+
}
650+
}
651+
}
652+
} catch {}
569653
return text;
570654
}
571655

572656
function processEvent(data) {
573657
if (data.o === 'SET' || data.o === 'BATCH') return null;
658+
const path = Array.isArray(data.p) ? data.p.join('/') : data.p;
574659
const resp = data.v?.response;
575660

576661
if (resp?.fragments && Array.isArray(resp.fragments)) {
@@ -606,7 +691,22 @@ function dsCreateSSEProcessor() {
606691
return parts.length > 0 ? parts.join('') : null;
607692
}
608693

609-
if (data.o === 'APPEND' && typeof data.v === 'string') return data.v || null;
694+
if (data.o === 'APPEND' && typeof data.v === 'string') {
695+
return data.v || null;
696+
}
697+
698+
if (path?.includes('reasoning') && typeof data.v === 'string') {
699+
if (!data.v) return null;
700+
if (!inThink) { inThink = true; return `<think>${data.v}`; }
701+
return data.v;
702+
}
703+
704+
if (data.type === 'thinking') {
705+
const content = typeof data.v === 'string' ? data.v : data.content || '';
706+
if (!content) return null;
707+
if (!inThink) { inThink = true; return `<think>${content}`; }
708+
return content;
709+
}
610710

611711
const delta = data.choices?.[0]?.delta;
612712
if (delta) {
@@ -624,10 +724,18 @@ function dsCreateSSEProcessor() {
624724

625725
if (typeof data.v === 'string') {
626726
if (!data.v) return null;
727+
if (!path && inThink) return data.v;
627728
if (inThink) { inThink = false; return `</think>${data.v}`; }
628729
return data.v;
629730
}
630731

732+
if (data.type === 'text' && typeof data.content === 'string') {
733+
const content = data.content.trim();
734+
if (!content) return null;
735+
if (inThink) { inThink = false; return `</think>${content}`; }
736+
return content;
737+
}
738+
631739
return null;
632740
}
633741

@@ -636,6 +744,6 @@ function dsCreateSSEProcessor() {
636744
return '';
637745
}
638746

639-
return { processChunk, flush, getChatId: () => chatId };
747+
return { processChunk, flush, getChatId: () => chatId, getMessageId: () => messageId };
640748
}
641749

content.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
setTimeout(async () => {
7373
if (BN.state.panelVisible) {
7474
await BN.subtitle.refresh();
75+
BN.panel.resetDocAuto();
7576
BN.panel.renderDoc();
7677
}
7778
}, 1000);

0 commit comments

Comments
 (0)