Initial commit
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# B站插件调研任务计划
|
||||
|
||||
## 任务概览
|
||||
- **任务名称**: B站插件技术调研
|
||||
- **目标**: 完成B站第三方插件开发的可行性分析、原型验证和功能规格定义
|
||||
- **预计工期**: 2-3天
|
||||
- **创建时间**: 2026-02-06
|
||||
|
||||
---
|
||||
|
||||
## 调研范围
|
||||
1. 授权登录方案(OAuth2/私信鉴权)
|
||||
2. 数据获取能力(用户数据、视频数据、互动数据)
|
||||
3. 功能限制与风险评估
|
||||
4. 对标现有方案分析
|
||||
|
||||
---
|
||||
|
||||
## Checkpoint 清单
|
||||
|
||||
### 🔍 Phase 1: 授权方案调研(Day 1 上午)
|
||||
- [x] 1.1 官方开放平台调研 ✅
|
||||
- [x] 访问 open.bilibili.com 了解官方API
|
||||
- [x] 调研开发者申请流程和门槛
|
||||
- [x] 记录OAuth2授权流程
|
||||
- **关键发现**: 官方平台暂不开放个人开发者申请
|
||||
- **文档**: [docs/research-notes/phase-1.1-official-platform.md](docs/research-notes/phase-1.1-official-platform.md)
|
||||
- [x] 1.2 第三方方案调研(bili-auth)✅
|
||||
- [x] 研究 bili-auth 实现原理
|
||||
- [x] 分析私信鉴权流程
|
||||
- [x] 评估账号封禁风险
|
||||
- **关键发现**: 个人可行,但有封号风险;适合小规模使用
|
||||
- **文档**: [docs/research-notes/phase-1.2-bili-auth.md](docs/research-notes/phase-1.2-bili-auth.md)
|
||||
- [ ] 研究 bili-auth 实现原理
|
||||
- [ ] 分析私信鉴权流程
|
||||
- [ ] 评估账号封禁风险
|
||||
- [x] 1.3 授权方案对比文档 ✅
|
||||
- [x] 输出方案对比表(官方 vs 第三方)
|
||||
- [x] 给出推荐方案及理由
|
||||
- **关键结论**: 推荐 bili-auth 方案,风险可控,Go决策
|
||||
- **文档**: [docs/research-notes/phase-1.3-auth-comparison.md](docs/research-notes/phase-1.3-auth-comparison.md)
|
||||
|
||||
### 📊 Phase 2: 数据获取能力调研(Day 1 下午 - Day 2 上午)
|
||||
- [x] 2.1 用户数据API调研 ✅
|
||||
- [x] 用户信息获取
|
||||
- [x] 关注列表获取
|
||||
- [x] 观看历史/收藏列表(受限)
|
||||
- [x] 2.2 视频数据API调研 ✅
|
||||
- [x] 视频详情获取
|
||||
- [x] 视频搜索能力
|
||||
- [x] 热门榜单获取
|
||||
- [x] 2.3 互动数据API调研 ✅
|
||||
- [x] 评论获取
|
||||
- [x] 弹幕获取(如有)
|
||||
- [x] 点赞/投币/收藏统计
|
||||
- [x] 2.4 API限制与频率调研 ✅
|
||||
- [x] 调用频率限制
|
||||
- [x] 反爬机制分析
|
||||
- [x] 数据缓存策略建议
|
||||
- **文档**: [docs/research-notes/phase-2-data-api.md](docs/research-notes/phase-2-data-api.md)
|
||||
- **关键结论**: MVP功能(关注列表、UP主动态、视频搜索)均可实现
|
||||
|
||||
### ⚠️ Phase 3: 风险评估(Day 2 下午)
|
||||
- [x] 3.1 账号安全风险 ✅
|
||||
- [x] 被封号/封私信的可能性
|
||||
- [x] 风控触发条件分析
|
||||
- [x] 降低风险的策略
|
||||
- [x] 3.2 API稳定性风险 ✅
|
||||
- [x] 接口变更频率
|
||||
- [x] 非官方接口失效风险
|
||||
- [x] 备选方案设计
|
||||
- [x] 3.3 法律合规风险 ✅
|
||||
- [x] 用户数据隐私合规
|
||||
- [x] B站服务条款分析
|
||||
- [x] 免责声明建议
|
||||
|
||||
### 🎯 Phase 4: 对标分析(Day 2 下午 - Day 3 上午)
|
||||
- [x] 4.1 现有B站工具调研 ✅
|
||||
- [x] bilibili-API-collect(已归档,分析原因)
|
||||
- [x] 其他开源B站工具
|
||||
- [x] 4.2 OpenClaw生态对标 ✅
|
||||
- [x] 现有技能模式分析
|
||||
|
||||
### 📝 Phase 6: 文档输出(Day 3 下午)
|
||||
- [x] 6.1 可行性分析报告 ✅
|
||||
- [x] 6.2 功能规格文档 ✅
|
||||
- [x] 6.3 调研笔记整理 ✅
|
||||
- **主文档**: [docs/bilibili-plugin-feasibility.md](docs/bilibili-plugin-feasibility.md)
|
||||
- **调研笔记**: `docs/research-notes/` 目录下 4 份文档
|
||||
|
||||
---
|
||||
|
||||
## 当前状态
|
||||
🟢 **全部完成** - 调研工作已结束,产出完整可行性报告
|
||||
|
||||
## 主要结论
|
||||
- **推荐方案**: bili-auth 第三方OAuth
|
||||
- **可行性**: ✅ Go
|
||||
- **风险等级**: 🟡 中(可控)
|
||||
- **建议**: 使用小号,控制频率,准备降级方案
|
||||
|
||||
---
|
||||
|
||||
## 交付物清单
|
||||
1. `docs/bilibili-plugin-feasibility.md` - 可行性分析报告
|
||||
2. `docs/bilibili-plugin-spec.md` - 功能规格文档
|
||||
3. `prototype/` - 原型代码目录
|
||||
4. `docs/research-notes/` - 调研过程笔记
|
||||
|
||||
---
|
||||
|
||||
## 当前状态
|
||||
🟢 **Phase 1 已完成** - 授权方案调研完成,推荐 bili-auth 方案
|
||||
|
||||
## 阻塞项
|
||||
暂无
|
||||
|
||||
## 下一步行动
|
||||
开始 Phase 1.1 - 官方开放平台调研
|
||||
@@ -0,0 +1,343 @@
|
||||
# obsidian-kb 技能改进待办事项
|
||||
|
||||
**创建时间**: 2026-02-06 20:15
|
||||
**技能版本**: 1.0.0
|
||||
**状态**: 🟡 进行中
|
||||
|
||||
---
|
||||
|
||||
## 问题记录
|
||||
|
||||
### 事件背景
|
||||
|
||||
用户通过 WhatsApp 发送 B站视频短链接 `https://b23.tv/oLBZZfK`,期望技能自动处理。实际流程:
|
||||
|
||||
1. ❌ 技能未自动触发
|
||||
2. ⚠️ 短链接解析失败(B站 412 反爬虫)
|
||||
3. 📝 手动提取 BV 号 `BV1jcFqz7EFW` 成功处理
|
||||
4. ⚠️ 视频无字幕,生成笔记内容为空
|
||||
|
||||
用户反馈的四个问题:
|
||||
1. 技能应该立刻触发
|
||||
2. 应该有短链接到长链接的解析过程(手机用户无法获取长链)
|
||||
3. 为技能增加单个视频链接分析功能
|
||||
4. 针对没有字幕的,看看有没有本地生成字幕的方法
|
||||
|
||||
---
|
||||
|
||||
## 待办任务
|
||||
|
||||
### ✅ 任务1:实现自动触发机制
|
||||
|
||||
**状态**: 已完成 ✓
|
||||
**完成时间**: 2026-02-06
|
||||
**优先级**: P0(最高)
|
||||
**实际工时**: 2 小时
|
||||
|
||||
#### 实施结果
|
||||
|
||||
**方案A: SKILL.md 触发规则 + Handler 脚本**
|
||||
在 SKILL.md 中添加了 frontmatter 元数据和触发规则说明,创建了 handler.py 脚本处理链接检测和预览。
|
||||
|
||||
**完成的修改**:
|
||||
- [x] 1.1 研究 OpenClaw 技能触发机制
|
||||
- [x] 1.2 在 SKILL.md 中添加触发规则(frontmatter + 文档)
|
||||
- [x] 1.3 创建 handler.py 处理链接检测
|
||||
- [x] 1.4 测试所有 URL 模式
|
||||
- [x] 1.5 优化用户体验(确认提示、短链接友好提示)
|
||||
|
||||
**文件变更**:
|
||||
- `skills/obsidian-kb/SKILL.md` - 添加 frontmatter 和触发规则章节
|
||||
- `skills/obsidian-kb/handler.py` - 新增自动触发处理器
|
||||
- `skills/obsidian-kb/AUTO_TRIGGER.md` - 新增实现文档
|
||||
|
||||
**支持的模式**:
|
||||
- B站视频: `https://www.bilibili.com/video/BV*`, `BV*`, `https://b23.tv/*`
|
||||
- YouTube 视频: `https://www.youtube.com/watch?v=*`, `https://youtu.be/*`
|
||||
- 知乎文章: `https://zhuanlan.zhihu.com/p/*`
|
||||
|
||||
**测试结果**:
|
||||
✅ 所有 URL 模式检测正确
|
||||
✅ 短链接显示友好提示
|
||||
✅ 支持批量检测多个链接
|
||||
✅ 智能去重避免重复显示
|
||||
|
||||
详细文档: `skills/obsidian-kb/AUTO_TRIGGER.md`
|
||||
|
||||
---
|
||||
|
||||
### ✅ 任务2:短链接解析功能
|
||||
|
||||
**状态**: 已完成 ✓
|
||||
**完成时间**: 2026-02-06
|
||||
**优先级**: P2(中等)
|
||||
**实际工时**: 1.5 小时
|
||||
|
||||
#### 实施结果
|
||||
|
||||
**方案A: 用户引导(已实现)**
|
||||
在检测到短链接时显示友好的用户引导信息,分别针对手机用户和桌面用户提供建议。
|
||||
|
||||
**方案B: 浏览器自动化(已实现框架)**
|
||||
提供了 PlaywrightResolver 类,支持可选的浏览器自动化解析。
|
||||
|
||||
**完成的修改**:
|
||||
- [x] 2.1 评估各方案可行性
|
||||
- [x] 2.2 实施方案B(用户引导)- 主功能
|
||||
- [x] 2.3 研究 Playwright 集成 - 可选功能
|
||||
- [x] 2.4 测试各种短链接场景
|
||||
|
||||
#### 文件变更
|
||||
- `skills/obsidian-kb/shortlink_resolver.py` - 新增短链接解析模块
|
||||
- `skills/obsidian-kb/handler.py` - 集成短链接检测
|
||||
- `skills/obsidian-kb/add_video.py` - 支持短链接解析和 Playwright
|
||||
- `skills/obsidian-kb/SKILL.md` - 更新文档
|
||||
- `skills/obsidian-kb/AUTO_TRIGGER.md` - 添加短链接章节
|
||||
|
||||
#### 核心功能
|
||||
|
||||
1. **ShortLinkResolver 类**
|
||||
- 检测 B站短链接(b23.tv)
|
||||
- 支持其他短链接服务(bit.ly, t.cn, dwz.cn 等)
|
||||
- 格式化友好的帮助信息
|
||||
|
||||
2. **PlaywrightResolver 类(可选)**
|
||||
- 浏览器自动化解析短链接
|
||||
- 需要安装:`pip install playwright && playwright install chromium`
|
||||
- 在 `add_video.py` 中使用 `--playwright` 参数启用
|
||||
|
||||
3. **集成到现有流程**
|
||||
- `handler.py` 自动触发时检测短链接
|
||||
- `add_video.py` 手动添加时支持短链接
|
||||
- 智能区分短链接和完整 URL
|
||||
|
||||
#### 测试结果
|
||||
✅ 单个 B站短链接检测
|
||||
✅ 多个短链接检测
|
||||
✅ 友好的用户引导信息
|
||||
✅ 集成到 handler.py
|
||||
✅ add_video.py 支持短链接
|
||||
✅ 完整 URL 和 BV 号正常工作
|
||||
|
||||
#### 验收标准
|
||||
- [x] 短链接能够友好处理(显示引导信息)
|
||||
- [x] 用户不会感到困惑
|
||||
- [x] 支持 b23.tv 和其他短链接服务(检测)
|
||||
- [x] 集成到 handler.py
|
||||
- [x] 测试各种短链接场景(单个、多个、混合)
|
||||
|
||||
详细文档: `skills/obsidian-kb/AUTO_TRIGGER.md`(短链接章节)
|
||||
|
||||
---
|
||||
|
||||
### 🟡 任务3:单个视频快速添加功能
|
||||
|
||||
**优先级**: P1(高)
|
||||
**预计工时**: 1 小时
|
||||
|
||||
#### 问题描述
|
||||
当前技能是批量订阅模式,缺少快速添加单个视频的命令。用户需要手动编写脚本。
|
||||
|
||||
#### 技术方案
|
||||
|
||||
**新增命令: `/kb-add`**
|
||||
```bash
|
||||
/kb-add <URL> # 添加单个视频
|
||||
/kb-add <URL> --process # 添加并立即处理
|
||||
/kb-add <URL> --yes # 跳过确认
|
||||
```
|
||||
|
||||
**实现位置**: `src/cli.py`
|
||||
|
||||
#### 实施步骤
|
||||
- [ ] 3.1 在 cli.py 中添加 `cmd_add()` 函数
|
||||
- [ ] 3.2 调用现有的 `BilibiliAdapter.extract_content()`
|
||||
- [ ] 3.3 调用现有的 `Storage.save_content()`
|
||||
- [ ] 3.4 支持可选的 `--process` 参数
|
||||
- [ ] 3.5 更新 SKILL.md 文档
|
||||
|
||||
#### 代码示例
|
||||
```python
|
||||
def cmd_add(args: argparse.Namespace):
|
||||
"""添加单个视频到收件箱"""
|
||||
adapter = BilibiliAdapter()
|
||||
|
||||
# 解析 URL
|
||||
url = resolve_url(args.url) # 可选:短链接解析
|
||||
|
||||
# 提取内容
|
||||
content = adapter.extract_content(url)
|
||||
if not content:
|
||||
print(f"❌ 无法提取视频: {url}")
|
||||
return 1
|
||||
|
||||
# 获取字幕
|
||||
content.transcript = adapter.get_transcript(content)
|
||||
|
||||
# 保存
|
||||
storage = Storage()
|
||||
content.status = ContentStatus.CONFIRMED
|
||||
storage.save_content(content)
|
||||
|
||||
print(f"✅ 已添加: {content.title}")
|
||||
print(f" ID: {content.id}")
|
||||
|
||||
# 可选:立即处理
|
||||
if args.process:
|
||||
pipeline = ProcessingPipeline()
|
||||
result = pipeline.process_full(content)
|
||||
print(f"📄 笔记: {result.output_path}")
|
||||
```
|
||||
|
||||
#### 验收标准
|
||||
- [ ] `/kb-add <URL>` 命令可用
|
||||
- [ ] 支持短链接解析(配合任务2)
|
||||
- [ ] 支持立即处理选项
|
||||
- [ ] 更新 SKILL.md 文档
|
||||
|
||||
---
|
||||
|
||||
### 🔵 任务4:本地生成字幕功能
|
||||
|
||||
**优先级**: P3(低)
|
||||
**预计工时**: 3-5 小时
|
||||
|
||||
#### 问题描述
|
||||
部分视频没有字幕,导致 AI 分析结果为空。
|
||||
|
||||
#### 技术方案
|
||||
|
||||
**方案A: OpenAI Whisper(本地)**
|
||||
```python
|
||||
import whisper
|
||||
|
||||
model = whisper.load_model("base")
|
||||
result = model.transcribe("audio.mp3")
|
||||
print(result["text"])
|
||||
```
|
||||
|
||||
**方案B: 阿里云语音识别(API)**
|
||||
使用 Aliyun TTS/ASR API(如果已配置)
|
||||
|
||||
**方案C: 腾讯云语音识别(API)**
|
||||
使用腾讯云 ASR API
|
||||
|
||||
#### 实施步骤
|
||||
- [ ] 4.1 评估各方案:Whisper(本地免费)vs 云API(更准确)
|
||||
- [ ] 4.2 下载视频音频(使用 yt-dlp)
|
||||
- [ ] 4.3 实现字幕生成
|
||||
- [ ] 4.4 支持多语言识别
|
||||
- [ ] 4.5 添加缓存机制(避免重复生成)
|
||||
|
||||
#### 代码示例
|
||||
```python
|
||||
def generate_subtitles(content: Content) -> str:
|
||||
"""为无字幕视频生成字幕"""
|
||||
|
||||
# 1. 下载音频
|
||||
audio_path = download_audio(content.url)
|
||||
|
||||
# 2. 语音识别
|
||||
model = whisper.load_model("base")
|
||||
result = model.transcribe(
|
||||
audio_path,
|
||||
language="zh", # 自动检测
|
||||
task="transcribe"
|
||||
)
|
||||
|
||||
# 3. 转换为 SRT 格式
|
||||
srt_text = to_srt(result["segments"])
|
||||
|
||||
# 4. 保存到缓存
|
||||
cache_path = f"cache/subtitles/{content.id}.srt"
|
||||
save_subtitle(cache_path, srt_text)
|
||||
|
||||
return srt_text
|
||||
```
|
||||
|
||||
#### 验收标准
|
||||
- [ ] 无字幕视频能生成字幕
|
||||
- [ ] 字幕准确度可接受
|
||||
- [ ] 处理时间合理(5分钟视频 < 1分钟)
|
||||
- [ ] 支持中文字幕
|
||||
|
||||
---
|
||||
|
||||
## 技术依赖
|
||||
|
||||
### Whisper(本地方案)
|
||||
```bash
|
||||
pip install openai-whisper
|
||||
pip install ffmpeg # 系统依赖
|
||||
```
|
||||
|
||||
### Playwright(短链接解析)
|
||||
```bash
|
||||
pip install playwright
|
||||
playwright install chromium
|
||||
```
|
||||
|
||||
### yt-dlp(音频下载)
|
||||
```bash
|
||||
pip install yt-dlp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实施路线图
|
||||
|
||||
### Phase 1: 快速修复(1-2天)
|
||||
- ✅ 任务3:单个视频添加功能(1h)
|
||||
- 🔲 任务1:自动触发机制(1-2h)
|
||||
|
||||
### Phase 2: 体验优化(3-5天)
|
||||
- 🔲 任务2:短链接解析(2-3h)
|
||||
- 🔲 测试与文档更新
|
||||
|
||||
### Phase 3: 功能增强(可选)
|
||||
- 🔲 任务4:本地生成字幕(3-5h)
|
||||
- 🔲 性能优化与错误处理
|
||||
|
||||
---
|
||||
|
||||
## 测试用例
|
||||
|
||||
### 自动触发测试
|
||||
- 发送完整 URL → 触发
|
||||
- 发送短链接 → 触发或引导
|
||||
- 发送 BV 号 → 触发
|
||||
- 发送非视频链接 → 不触发
|
||||
|
||||
### 单个添加测试
|
||||
```bash
|
||||
python3 src/cli.py add "https://www.bilibili.com/video/BVxxxxx"
|
||||
python3 src/cli.py add "BVxxxxx"
|
||||
python3 src/cli.py add "https://b23.tv/xxxxx"
|
||||
python3 src/cli.py add "https://www.bilibili.com/video/BVxxxxx" --process
|
||||
```
|
||||
|
||||
### 字幕生成测试
|
||||
- 有字幕视频 → 跳过生成
|
||||
- 无字幕视频 → 生成字幕
|
||||
- 长视频 → 分段识别
|
||||
- 多语言视频 → 正确识别
|
||||
|
||||
---
|
||||
|
||||
## 备注
|
||||
|
||||
### 当前临时方案
|
||||
- `skills/obsidian-kb/add_video.py` - 单个视频添加脚本
|
||||
- 手动用 curl 解析短链接
|
||||
- 跳过无字幕视频
|
||||
|
||||
### 相关文件
|
||||
- 技能目录: `skills/obsidian-kb/`
|
||||
- CLI 入口: `src/cli.py`
|
||||
- B站适配器: `src/sources/bilibili.py`
|
||||
- 存储管理: `src/storage.py`
|
||||
- 处理管道: `src/pipeline.py`
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2026-02-06 20:15*
|
||||
@@ -0,0 +1,762 @@
|
||||
# obsidian-kb 本地生成字幕功能 - 详细规划
|
||||
|
||||
**创建时间**: 2026-02-07 01:26
|
||||
**任务优先级**: 🔵 P3(低,可选增强)
|
||||
**预计总工时**: 4-6 小时
|
||||
|
||||
---
|
||||
|
||||
## 🎯 目标
|
||||
|
||||
为无字幕视频自动生成字幕,使 AI 分析功能不受字幕限制,覆盖更多视频内容。
|
||||
|
||||
---
|
||||
|
||||
## 📋 技术方案对比
|
||||
|
||||
### 方案对比表
|
||||
|
||||
| 方案 | 准确度 | 速度 | 成本 | 复杂度 | 推荐度 |
|
||||
|------|--------|------|------|--------|--------|
|
||||
| **Whisper 本地** | ⭐⭐⭐⭐ | ⭐⭐⭐ | 免费 | 低 | ⭐⭐⭐⭐⭐ |
|
||||
| **阿里云 ASR** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 按量付费 | 低 | ⭐⭐⭐⭐ |
|
||||
| **腾讯云 ASR** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 按量付费 | 低 | ⭐⭐⭐⭐ |
|
||||
| **百度云 ASR** | ⭐⭐⭐ | ⭐⭐⭐⭐ | 按量付费 | 低 | ⭐⭐⭐ |
|
||||
|
||||
### 推荐:Whisper 本地方案
|
||||
|
||||
**理由:**
|
||||
- ✅ 完全免费,无 API 费用
|
||||
- ✅ 数据本地处理,隐私安全
|
||||
- ✅ 准确度足够(中英文识别效果良好)
|
||||
- ✅ 可离线使用
|
||||
- ✅ 社区活跃,持续更新
|
||||
|
||||
**权衡:**
|
||||
- ⚠️ 需要下载模型文件(~150MB-3GB)
|
||||
- ⚠️ CPU 速度较慢(10分钟视频约需 2-5 分钟)
|
||||
- ⚠️ 需要安装 ffmpeg 依赖
|
||||
|
||||
**优化方向:**
|
||||
- 使用 GPU 加速(如果有 CUDA)
|
||||
- 选择合适的模型大小(base vs small vs medium)
|
||||
- 添加进度反馈和取消功能
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
### 核心模块
|
||||
|
||||
```
|
||||
subtitle_generator.py ← 新建
|
||||
├── AudioDownloader # 音频下载器
|
||||
├── WhisperTranscriber # Whisper 识别器
|
||||
├── SubtitleFormatter # SRT 格式化器
|
||||
├── SubtitleCache # 缓存管理器
|
||||
└── SubtitleGenerator # 统一接口
|
||||
```
|
||||
|
||||
### 工作流程
|
||||
|
||||
```
|
||||
用户请求视频处理
|
||||
↓
|
||||
检查是否有字幕
|
||||
↓ [有字幕]
|
||||
使用现有字幕
|
||||
↓ [无字幕]
|
||||
下载音频 (yt-dlp)
|
||||
↓
|
||||
语音识别 (Whisper)
|
||||
↓
|
||||
转换为 SRT 格式
|
||||
↓
|
||||
保存到缓存
|
||||
↓
|
||||
继续 AI 分析
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 详细实施步骤
|
||||
|
||||
### Step 1: 环境准备(30分钟)
|
||||
|
||||
#### 1.1 安装依赖
|
||||
```bash
|
||||
# Python 包
|
||||
pip install openai-whisper
|
||||
pip install yt-dlp
|
||||
|
||||
# 系统依赖(检查)
|
||||
ffmpeg -v
|
||||
```
|
||||
|
||||
#### 1.2 验证安装
|
||||
```bash
|
||||
# 测试 Whisper
|
||||
whisper --help
|
||||
|
||||
# 测试 yt-dlp
|
||||
yt-dlp --version
|
||||
```
|
||||
|
||||
#### 1.3 创建缓存目录
|
||||
```bash
|
||||
mkdir -p cache/audio
|
||||
mkdir -p cache/subtitles
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 音频下载模块(1小时)
|
||||
|
||||
#### 2.1 创建 `subtitle_generator.py`
|
||||
|
||||
```python
|
||||
class AudioDownloader:
|
||||
"""使用 yt-dlp 下载视频音频"""
|
||||
|
||||
def download_audio(self, url: str, output_path: str) -> str:
|
||||
"""
|
||||
下载视频音频
|
||||
|
||||
Args:
|
||||
url: 视频URL
|
||||
output_path: 输出音频路径
|
||||
|
||||
Returns:
|
||||
str: 实际保存的音频文件路径
|
||||
"""
|
||||
import yt_dlp
|
||||
|
||||
ydl_opts = {
|
||||
'format': 'bestaudio/best',
|
||||
'outtmpl': output_path,
|
||||
'postprocessors': [{
|
||||
'key': 'FFmpegExtractAudio',
|
||||
'preferredcodec': 'mp3',
|
||||
'preferredquality': '192',
|
||||
}],
|
||||
'quiet': True,
|
||||
'no_warnings': True,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
|
||||
return output_path.replace('.%(ext)s', '.mp3')
|
||||
```
|
||||
|
||||
#### 2.2 测试下载功能
|
||||
```bash
|
||||
python3 -c "
|
||||
from subtitle_generator import AudioDownloader
|
||||
downloader = AudioDownloader()
|
||||
downloader.download_audio('https://www.bilibili.com/video/BVxxxxx', 'test_audio.mp3')
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Whisper 识别模块(1.5小时)
|
||||
|
||||
#### 3.1 实现 WhisperTranscriber
|
||||
|
||||
```python
|
||||
import whisper
|
||||
import time
|
||||
|
||||
class WhisperTranscriber:
|
||||
"""Whisper 语音识别器"""
|
||||
|
||||
# 模型选择
|
||||
MODELS = {
|
||||
'tiny': 39M, # 最快,准确度最低
|
||||
'base': 74M, # 推荐,速度/准确度平衡
|
||||
'small': 244M, # 准确度更高
|
||||
'medium': 769M, # 接近专业级
|
||||
'large': 1550M, # 最高准确度
|
||||
}
|
||||
|
||||
def __init__(self, model_size: str = 'base'):
|
||||
"""
|
||||
初始化识别器
|
||||
|
||||
Args:
|
||||
model_size: 模型大小,默认 'base'
|
||||
"""
|
||||
self.model_size = model_size
|
||||
self.model = None
|
||||
self.device = None
|
||||
|
||||
def load_model(self):
|
||||
"""加载 Whisper 模型"""
|
||||
if self.model is None:
|
||||
print(f"加载 Whisper 模型 ({self.model_size})...")
|
||||
start_time = time.time()
|
||||
|
||||
# 自动检测 GPU
|
||||
import torch
|
||||
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
|
||||
self.model = whisper.load_model(
|
||||
self.model_size,
|
||||
device=self.device
|
||||
)
|
||||
|
||||
load_time = time.time() - start_time
|
||||
print(f"模型加载完成 ({self.device}) - 耗时: {load_time:.1f}秒")
|
||||
|
||||
def transcribe(
|
||||
self,
|
||||
audio_path: str,
|
||||
language: str = None,
|
||||
progress_callback = None
|
||||
) -> dict:
|
||||
"""
|
||||
语音识别
|
||||
|
||||
Args:
|
||||
audio_path: 音频文件路径
|
||||
language: 语言代码('zh'、'en'、None=自动检测)
|
||||
progress_callback: 进度回调函数
|
||||
|
||||
Returns:
|
||||
dict: Whisper 识别结果
|
||||
"""
|
||||
self.load_model()
|
||||
|
||||
print(f"开始识别: {audio_path}")
|
||||
start_time = time.time()
|
||||
|
||||
result = self.model.transcribe(
|
||||
audio_path,
|
||||
language=language,
|
||||
task='transcribe',
|
||||
verbose=False
|
||||
)
|
||||
|
||||
duration = time.time() - start_time
|
||||
print(f"识别完成 - 耗时: {duration:.1f}秒")
|
||||
|
||||
return result
|
||||
|
||||
def get_segments(self, result: dict) -> list:
|
||||
"""获取分段信息"""
|
||||
return result.get('segments', [])
|
||||
```
|
||||
|
||||
#### 3.2 测试识别功能
|
||||
```bash
|
||||
python3 -c "
|
||||
from subtitle_generator import WhisperTranscriber
|
||||
transcriber = WhisperTranscriber('base')
|
||||
result = transcriber.transcribe('test_audio.mp3', language='zh')
|
||||
print(f'识别文本: {result[\"text\"][:100]}...')
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: SRT 格式化模块(30分钟)
|
||||
|
||||
#### 4.1 实现 SubtitleFormatter
|
||||
|
||||
```python
|
||||
class SubtitleFormatter:
|
||||
"""字幕格式化器(SRT 格式)"""
|
||||
|
||||
@staticmethod
|
||||
def to_srt(segments: list) -> str:
|
||||
"""
|
||||
将 Whisper segments 转换为 SRT 格式
|
||||
|
||||
Args:
|
||||
segments: Whisper segments 列表
|
||||
|
||||
Returns:
|
||||
str: SRT 格式文本
|
||||
"""
|
||||
srt_lines = []
|
||||
|
||||
for i, segment in enumerate(segments, 1):
|
||||
# SRT 格式:
|
||||
# 序号
|
||||
# 开始时间 --> 结束时间
|
||||
# 文本
|
||||
# 空行
|
||||
|
||||
start_time = SubtitleFormatter._seconds_to_srt_time(segment['start'])
|
||||
end_time = SubtitleFormatter._seconds_to_srt_time(segment['end'])
|
||||
text = segment['text'].strip()
|
||||
|
||||
srt_lines.append(str(i))
|
||||
srt_lines.append(f"{start_time} --> {end_time}")
|
||||
srt_lines.append(text)
|
||||
srt_lines.append("")
|
||||
|
||||
return "\n".join(srt_lines)
|
||||
|
||||
@staticmethod
|
||||
def _seconds_to_srt_time(seconds: float) -> str:
|
||||
"""
|
||||
将秒数转换为 SRT 时间格式 (HH:MM:SS,mmm)
|
||||
|
||||
Args:
|
||||
seconds: 秒数
|
||||
|
||||
Returns:
|
||||
str: SRT 时间格式
|
||||
"""
|
||||
hours = int(seconds // 3600)
|
||||
minutes = int((seconds % 3600) // 60)
|
||||
secs = int(seconds % 60)
|
||||
millis = int((seconds - int(seconds)) * 1000)
|
||||
|
||||
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
|
||||
```
|
||||
|
||||
#### 4.2 测试格式化
|
||||
```bash
|
||||
python3 -c "
|
||||
from subtitle_generator import SubtitleFormatter
|
||||
segments = [{'start': 0.0, 'end': 2.5, 'text': '测试字幕'}]
|
||||
srt = SubtitleFormatter.to_srt(segments)
|
||||
print(srt)
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: 缓存管理模块(30分钟)
|
||||
|
||||
#### 5.1 实现 SubtitleCache
|
||||
|
||||
```python
|
||||
import os
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
class SubtitleCache:
|
||||
"""字幕缓存管理器"""
|
||||
|
||||
def __init__(self, cache_dir: str = 'cache/subtitles'):
|
||||
"""
|
||||
初始化缓存
|
||||
|
||||
Args:
|
||||
cache_dir: 缓存目录
|
||||
"""
|
||||
self.cache_dir = Path(cache_dir)
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def get_cache_key(self, url: str) -> str:
|
||||
"""
|
||||
生成缓存键
|
||||
|
||||
Args:
|
||||
url: 视频URL
|
||||
|
||||
Returns:
|
||||
str: 缓存键(SHA256 哈希的前8位)
|
||||
"""
|
||||
return hashlib.sha256(url.encode()).hexdigest()[:8]
|
||||
|
||||
def get_cache_path(self, url: str) -> str:
|
||||
"""
|
||||
获取缓存文件路径
|
||||
|
||||
Args:
|
||||
url: 视频URL
|
||||
|
||||
Returns:
|
||||
str: 缓存文件路径
|
||||
"""
|
||||
cache_key = self.get_cache_key(url)
|
||||
return str(self.cache_dir / f"{cache_key}.srt")
|
||||
|
||||
def get(self, url: str) -> str:
|
||||
"""
|
||||
获取缓存字幕
|
||||
|
||||
Args:
|
||||
url: 视频URL
|
||||
|
||||
Returns:
|
||||
str | None: 缓存的字幕内容,如果不存在返回 None
|
||||
"""
|
||||
cache_path = self.get_cache_path(url)
|
||||
|
||||
if os.path.exists(cache_path):
|
||||
with open(cache_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
return None
|
||||
|
||||
def set(self, url: str, subtitle: str) -> str:
|
||||
"""
|
||||
保存缓存字幕
|
||||
|
||||
Args:
|
||||
url: 视频URL
|
||||
subtitle: 字幕内容
|
||||
|
||||
Returns:
|
||||
str: 缓存文件路径
|
||||
"""
|
||||
cache_path = self.get_cache_path(url)
|
||||
|
||||
with open(cache_path, 'w', encoding='utf-8') as f:
|
||||
f.write(subtitle)
|
||||
|
||||
return cache_path
|
||||
|
||||
def exists(self, url: str) -> bool:
|
||||
"""检查缓存是否存在"""
|
||||
return os.path.exists(self.get_cache_path(url))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6: 统一接口模块(30分钟)
|
||||
|
||||
#### 6.1 实现 SubtitleGenerator
|
||||
|
||||
```python
|
||||
class SubtitleGenerator:
|
||||
"""字幕生成器统一接口"""
|
||||
|
||||
def __init__(self, model_size: str = 'base', cache_dir: str = 'cache/subtitles'):
|
||||
"""
|
||||
初始化生成器
|
||||
|
||||
Args:
|
||||
model_size: Whisper 模型大小
|
||||
cache_dir: 缓存目录
|
||||
"""
|
||||
self.audio_downloader = AudioDownloader()
|
||||
self.transcriber = WhisperTranscriber(model_size)
|
||||
self.formatter = SubtitleFormatter()
|
||||
self.cache = SubtitleCache(cache_dir)
|
||||
|
||||
def generate(
|
||||
self,
|
||||
url: str,
|
||||
content_id: str,
|
||||
audio_cache_dir: str = 'cache/audio',
|
||||
language: str = None,
|
||||
use_cache: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
生成字幕
|
||||
|
||||
Args:
|
||||
url: 视频URL
|
||||
content_id: 内容ID
|
||||
audio_cache_dir: 音频缓存目录
|
||||
language: 语言代码(None=自动检测)
|
||||
use_cache: 是否使用缓存
|
||||
|
||||
Returns:
|
||||
str: SRT 格式字幕
|
||||
"""
|
||||
# 检查缓存
|
||||
if use_cache and self.cache.exists(url):
|
||||
print(f"使用缓存字幕: {url}")
|
||||
return self.cache.get(url)
|
||||
|
||||
print(f"开始生成字幕: {url}")
|
||||
|
||||
# 下载音频
|
||||
import os
|
||||
os.makedirs(audio_cache_dir, exist_ok=True)
|
||||
audio_path = os.path.join(audio_cache_dir, f"{content_id}.mp3")
|
||||
|
||||
print(f"下载音频...")
|
||||
audio_path = self.audio_downloader.download_audio(url, audio_path)
|
||||
|
||||
# 语音识别
|
||||
result = self.transcriber.transcribe(audio_path, language=language)
|
||||
segments = self.transcriber.get_segments(result)
|
||||
|
||||
# 转换为 SRT
|
||||
srt_text = self.formatter.to_srt(segments)
|
||||
|
||||
# 保存缓存
|
||||
cache_path = self.cache.set(url, srt_text)
|
||||
print(f"字幕已缓存: {cache_path}")
|
||||
|
||||
# 清理音频文件(可选)
|
||||
# os.remove(audio_path)
|
||||
|
||||
return srt_text
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 7: 集成到现有流程(1小时)
|
||||
|
||||
#### 7.1 修改 `pipeline.py`
|
||||
|
||||
在 `ProcessingPipeline` 中集成字幕生成:
|
||||
|
||||
```python
|
||||
from subtitle_generator import SubtitleGenerator
|
||||
|
||||
class ProcessingPipeline:
|
||||
|
||||
def __init__(self, config: Config = None):
|
||||
self.config = config or get_config()
|
||||
# ... 现有初始化
|
||||
|
||||
# 添加字幕生成器
|
||||
self.subtitle_generator = SubtitleGenerator(
|
||||
model_size=self.config.get('subtitle_model', 'base'),
|
||||
cache_dir=self.config.get('subtitle_cache_dir', 'cache/subtitles')
|
||||
)
|
||||
|
||||
def _ensure_subtitle(self, content: Content) -> str:
|
||||
"""
|
||||
确保内容有字幕,如果没有则生成
|
||||
|
||||
Args:
|
||||
content: 内容对象
|
||||
|
||||
Returns:
|
||||
str: 字幕文本
|
||||
"""
|
||||
# 1. 检查是否已有字幕
|
||||
if content.transcript and len(content.transcript) > 100:
|
||||
print("使用现有字幕")
|
||||
return content.transcript
|
||||
|
||||
# 2. 检查是否启用自动生成
|
||||
if not self.config.get('auto_generate_subtitles', False):
|
||||
print("自动生成字幕未启用,跳过")
|
||||
return ""
|
||||
|
||||
# 3. 生成字幕
|
||||
try:
|
||||
print(f"开始自动生成字幕...")
|
||||
transcript = self.subtitle_generator.generate(
|
||||
url=content.url,
|
||||
content_id=content.id,
|
||||
language=self.config.get('subtitle_language', None),
|
||||
use_cache=True
|
||||
)
|
||||
|
||||
# 更新内容
|
||||
content.transcript = transcript
|
||||
self.storage.save_content(content)
|
||||
|
||||
return transcript
|
||||
|
||||
except Exception as e:
|
||||
print(f"字幕生成失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return ""
|
||||
```
|
||||
|
||||
#### 7.2 更新配置文件
|
||||
|
||||
在 `config.yaml` 中添加字幕配置:
|
||||
|
||||
```yaml
|
||||
# 字幕生成配置
|
||||
subtitle:
|
||||
enabled: false # 是否启用自动生成
|
||||
model: base # Whisper 模型大小: tiny/base/small/medium/large
|
||||
language: null # 语言: zh/en/null(自动检测)
|
||||
cache_dir: cache/subtitles # 缓存目录
|
||||
audio_cache_dir: cache/audio # 音频缓存目录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 8: 测试与优化(1小时)
|
||||
|
||||
#### 8.1 创建测试脚本
|
||||
|
||||
```python
|
||||
# test_subtitle_generator.py
|
||||
|
||||
def test_full_workflow():
|
||||
"""测试完整工作流程"""
|
||||
|
||||
generator = SubtitleGenerator(model_size='base')
|
||||
|
||||
# 测试 URL(10分钟左右的视频)
|
||||
test_url = "https://www.bilibili.com/video/BV1xx411c7mD"
|
||||
|
||||
print("=" * 50)
|
||||
print("测试完整字幕生成流程")
|
||||
print("=" * 50)
|
||||
|
||||
srt_text = generator.generate(
|
||||
url=test_url,
|
||||
content_id='test001',
|
||||
language='zh',
|
||||
use_cache=True
|
||||
)
|
||||
|
||||
print(f"\n生成字幕预览(前500字符):")
|
||||
print(srt_text[:500])
|
||||
|
||||
print(f"\n总字符数: {len(srt_text)}")
|
||||
|
||||
# 验证缓存
|
||||
print("\n验证缓存...")
|
||||
cached = generator.generate(
|
||||
url=test_url,
|
||||
content_id='test002',
|
||||
use_cache=True
|
||||
)
|
||||
assert cached == srt_text, "缓存验证失败"
|
||||
print("✅ 缓存正常")
|
||||
|
||||
print("\n✅ 测试通过")
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_full_workflow()
|
||||
```
|
||||
|
||||
#### 8.2 性能测试
|
||||
|
||||
| 模型 | 10分钟视频时长 | 准确度 | 推荐场景 |
|
||||
|------|---------------|--------|----------|
|
||||
| tiny | ~30秒 | ⭐⭐ | 快速测试、批量处理 |
|
||||
| base | ~2分钟 | ⭐⭐⭐⭐ | 日常使用(推荐)|
|
||||
| small | ~5分钟 | ⭐⭐⭐⭐⭐ | 高质量需求 |
|
||||
| medium | ~10分钟 | ⭐⭐⭐⭐⭐ | 专业用途 |
|
||||
| large | ~15分钟+ | ⭐⭐⭐⭐⭐ | 最高准确度 |
|
||||
|
||||
---
|
||||
|
||||
## 📦 文件清单
|
||||
|
||||
### 新增文件
|
||||
```
|
||||
skills/obsidian-kb/
|
||||
├── subtitle_generator.py # 核心模块
|
||||
├── test_subtitle_generator.py # 测试脚本
|
||||
├── cache/
|
||||
│ ├── audio/ # 音频缓存
|
||||
│ └── subtitles/ # 字幕缓存
|
||||
```
|
||||
|
||||
### 修改文件
|
||||
```
|
||||
skills/obsidian-kb/
|
||||
├── src/pipeline.py # 集成字幕生成
|
||||
├── config.yaml.example # 添加字幕配置
|
||||
├── SKILL.md # 更新文档
|
||||
├── requirements.txt # 添加依赖
|
||||
```
|
||||
|
||||
### 依赖更新
|
||||
|
||||
`requirements.txt`:
|
||||
```
|
||||
openai-whisper>=20230314
|
||||
yt-dlp>=2023.1.6
|
||||
torch>=2.0.0 # Whisper 依赖
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 验收标准
|
||||
|
||||
### 功能验收
|
||||
- [ ] 无字幕视频能成功生成字幕
|
||||
- [ ] 字幕准确度可接受(测试 5 个视频)
|
||||
- [ ] 处理时间合理(10分钟视频 < 5 分钟,使用 base 模型)
|
||||
- [ ] 支持中文字幕
|
||||
- [ ] 支持英文识别
|
||||
- [ ] 缓存机制正常工作(避免重复生成)
|
||||
- [ ] 集成到 pipeline,无字幕视频自动生成
|
||||
|
||||
### 代码验收
|
||||
- [ ] 代码结构清晰,模块化设计
|
||||
- [ ] 有完整的错误处理
|
||||
- [ ] 有日志输出和进度反馈
|
||||
- [ ] 单元测试通过
|
||||
- [ ] 集成测试通过
|
||||
|
||||
### 文档验收
|
||||
- [ ] SKILL.md 更新,说明字幕生成功能
|
||||
- [ ] 配置文档说明(config.yaml.example)
|
||||
- [ ] 有使用示例和注意事项
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. 存储空间
|
||||
- Whisper 模型文件:~150MB (tiny) - 3GB (large)
|
||||
- 音频缓存:每分钟视频约 1-2MB
|
||||
- 字幕缓存:每分钟视频约 1-3KB
|
||||
|
||||
**建议:**
|
||||
- 定期清理 `cache/audio` 目录
|
||||
- 字幕缓存可以长期保留
|
||||
- 使用 `base` 模型作为默认
|
||||
|
||||
### 2. CPU/GPU
|
||||
- **无 GPU**: 使用 base 模型,10分钟视频约 2-5 分钟
|
||||
- **有 GPU**: 可使用 medium/large 模型,速度提升 5-10 倍
|
||||
|
||||
### 3. 隐私安全
|
||||
- Whisper 本地处理,不上传音频
|
||||
- 缓存文件在本地,不共享
|
||||
|
||||
### 4. 错误处理
|
||||
- 下载失败 → 记录错误,继续处理
|
||||
- 识别失败 → 记录错误,继续处理
|
||||
- 格式化失败 → 使用原始文本
|
||||
|
||||
---
|
||||
|
||||
## 🎯 预期效果
|
||||
|
||||
### 用户体验
|
||||
- 无字幕视频也能生成笔记
|
||||
- 等待时间可接受(显示进度)
|
||||
- 缓存加速重复处理
|
||||
|
||||
### 覆盖率提升
|
||||
- 当前:只能处理有字幕视频(~60%)
|
||||
- 目标:所有视频(~95%,部分音频质量问题除外)
|
||||
|
||||
### 成本
|
||||
- 软件成本:0 元(开源免费)
|
||||
- 硬件成本:0 元(复用现有机器)
|
||||
- 网络流量:下载音频(每次约 5-20MB)
|
||||
|
||||
---
|
||||
|
||||
## 📅 时间线
|
||||
|
||||
| 阶段 | 任务 | 预计时间 |
|
||||
|------|------|----------|
|
||||
| Phase 1 | 环境准备 + 音频下载 | 1.5h |
|
||||
| Phase 2 | Whisper 识别 + SRT 格式化 | 2h |
|
||||
| Phase 3 | 缓存 + 集成 + 测试 | 1.5h |
|
||||
| Phase 4 | 文档 + 优化 | 1h |
|
||||
| **总计** | | **4-6h** |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 后续优化方向
|
||||
|
||||
1. **GPU 加速** - 如果有 CUDA,可大幅提升速度
|
||||
2. **多语言支持** - 支持更多语言识别
|
||||
3. **批量处理** - 一次生成多个视频的字幕
|
||||
4. **字幕编辑** - 提供 Web 界面编辑生成的字幕
|
||||
5. **云端集成** - 可选切换到云 API(阿里云/腾讯云)
|
||||
|
||||
---
|
||||
|
||||
**规划完成!是否开始实施?**
|
||||
@@ -0,0 +1,35 @@
|
||||
# 进行中任务列表
|
||||
|
||||
## 活跃任务
|
||||
|
||||
### obsidian-kb 技能改进
|
||||
- **状态**: 🟡 进行中
|
||||
- **开始时间**: 2026-02-06
|
||||
- **预计完成**: 待定
|
||||
- **计划文件**: [memory/tasks/obsidian-kb-improvements.md](./obsidian-kb-improvements.md)
|
||||
- **当前阶段**: 需求记录与方案设计
|
||||
- **待办任务**:
|
||||
- 🔴 P0: 实现自动触发机制
|
||||
- 🟡 P1: 单个视频快速添加功能
|
||||
- 🟠 P2: 短链接解析功能
|
||||
- 🔵 P3: 本地生成字幕功能
|
||||
- **下一步**: 优先实现 P1(单个视频添加),1小时预计完成
|
||||
|
||||
### B站插件技术调研
|
||||
- **状态**: 🟡 进行中
|
||||
- **开始时间**: 2026-02-06
|
||||
- **预计完成**: 2026-02-08
|
||||
- **计划文件**: [memory/tasks/bilibili-plugin-research.md](./bilibili-plugin-research.md)
|
||||
- **当前阶段**: Phase 1 - 授权方案调研
|
||||
- **下一步**: 1.1 官方开放平台调研
|
||||
|
||||
## 历史任务
|
||||
|
||||
### 长视频分段分析功能
|
||||
- **状态**: 🟢 已完成
|
||||
- **完成时间**: 2026-02-06
|
||||
- **详情**: [memory/2026-02-06.md](../2026-02-06.md)
|
||||
|
||||
---
|
||||
|
||||
*此文件由 heartbeat 定期检查更新*
|
||||
Reference in New Issue
Block a user