# 万物收藏 — 产品需求文档 & 开发计划
**版本:** v1.0.0
**技术栈:** Nuxt 4 · SQLite · Drizzle ORM
**文档日期:** 2026-05-17
**状态:** 待评审
---
## 目录
1. [产品概述](#1-产品概述)
2. [用户角色与场景](#2-用户角色与场景)
3. [功能需求详述](#3-功能需求详述)
4. [数据库设计](#4-数据库设计)
5. [API 接口设计](#5-api-接口设计)
6. [前端页面与交互](#6-前端页面与交互)
7. [技术架构](#7-技术架构)
8. [开发计划](#8-开发计划)
9. [非功能需求](#9-非功能需求)
10. [验收标准](#10-验收标准)
---
## 1. 产品概述
### 1.1 产品定位
**万物收藏**是一款面向个人用户的知识收藏与管理工具,核心理念是打通「收藏 → 整理 → 深读 → 创作」的完整闭环,解决以下核心问题:
- 收藏内容分散在浏览器书签、手机备忘录、各类 App,无法统一管理
- 收藏了但忘了,找不到,形成信息黑洞
- 收藏只是「消费」,缺乏将其转化为个人输出的工具支持
### 1.2 产品愿景
让每一条收藏都能产生价值,成为用户的**第二大脑**。
### 1.3 核心功能概览
| 模块 | 功能 |
|------|------|
| 收藏录入 | 支持网页/文本/图片/文件/视频等多种类型,多入口采集 |
| 分类体系 | 树状分类(文件夹)+ 扁平标签,双轨管理 |
| 检索系统 | 全文搜索 + 多维过滤 + AI 语义搜索 |
| 批注系统 | 文字高亮、段落批注、整体备注、评分 |
| 文章创作 | Markdown 编辑器,引用收藏,导出与分享 |
| AI 辅助 | 摘要生成、标签推荐、相关推荐、对话检索 |
---
## 2. 用户角色与场景
### 2.1 目标用户
**主要用户:** 知识工作者(设计师、工程师、研究员、内容创作者)
**次要用户:** 有阅读习惯的普通用户
### 2.2 典型使用场景
**场景 A — 碎片化收藏**
用户在浏览器中看到好文章,通过浏览器插件一键保存,添加标签,收藏进入收件箱,稍后整理。
**场景 B — 深度整理**
用户每周花 20 分钟整理收件箱,给每条收藏补充分类、标签,读一遍后在重要段落高亮批注。
**场景 C — 创作输出**
用户要写一篇关于「AI 设计工具」的文章,搜索相关收藏,将多条内容引用到编辑器,形成文章,导出 Markdown 或分享链接。
**场景 D — 快速检索**
用户想找上周看到的一篇关于「设计系统」的文章,在搜索框输入关键词,1 秒内找到目标。
---
## 3. 功能需求详述
### 3.1 收件箱(Inbox)
**功能描述:** 所有新收藏的默认落点,未整理的收藏集中于此。
**需求明细:**
| ID | 需求 | 优先级 |
|----|------|--------|
| INB-01 | 收件箱显示所有未分配分类的收藏,按时间倒序排列 | P0 |
| INB-02 | 顶部显示待处理数量(红色角标) | P0 |
| INB-03 | 支持在收件箱直接操作:分配分类、打标签、删除 | P0 |
| INB-04 | 支持批量选择并批量操作(批量移动分类、批量删除) | P1 |
| INB-05 | 收件箱内容可以「稍后处理」标记,区别于已整理内容 | P2 |
**交互逻辑:**
- 收藏录入时,若未指定分类,自动归入收件箱
- 分配分类后,从收件箱消失,移入对应分类
- 收件箱数量 = `items WHERE category_id IS NULL`
---
### 3.2 收藏录入
#### 3.2.1 支持的内容类型
| 类型 | 标识 | 说明 |
|------|------|------|
| 网页 | `web` | URL 链接,自动抓取标题、描述、OG 图片、favicon |
| 文本 | `text` | 任意文字、语录、代码片段、想法 |
| 图片 | `image` | 本地上传,支持 jpg/png/webp/gif,最大 10MB |
| 文件 | `file` | PDF、Word、PPT 等文档,最大 50MB |
| 视频 | `video` | YouTube/B站等外链,抓取封面和标题 |
#### 3.2.2 录入入口
| ID | 需求 | 优先级 |
|----|------|--------|
| ADD-01 | 网页端「新增收藏」按钮,弹出 Modal 表单 | P0 |
| ADD-02 | 浏览器扩展(Chrome/Edge),一键保存当前页面 | P1 |
| ADD-03 | 系统分享菜单(iOS/Android App,后续计划) | P3 |
| ADD-04 | 剪贴板监听:复制 URL 后,页面弹出快捷保存提示 | P2 |
| ADD-05 | 快捷键 `Cmd/Ctrl + K` 打开快速收藏输入框 | P1 |
#### 3.2.3 网页类型自动抓取
当用户输入 URL 后,后端自动:
1. 抓取页面 `
`、``、`og:image`、`favicon`
2. 提取正文内容(使用 `@extractus/article-extractor`)
3. 如为视频链接(YouTube/bilibili),调用 oEmbed API 获取元数据
4. AI 自动生成摘要(异步,完成后更新记录)
| ID | 需求 | 优先级 |
|----|------|--------|
| FETCH-01 | URL 输入后,2 秒内自动填充标题、描述、封面图预览 | P0 |
| FETCH-02 | 抓取失败时,给出友好提示,允许手动填写标题 | P0 |
| FETCH-03 | 支持 User-Agent 伪装,绕过简单的反爬机制 | P1 |
| FETCH-04 | 抓取结果缓存 7 天,同一 URL 不重复抓取 | P1 |
| FETCH-05 | 抓取正文内容并存储,支持全文搜索 | P1 |
#### 3.2.4 录入表单字段
| 字段 | 类型 | 是否必填 | 说明 |
|------|------|----------|------|
| `type` | 枚举 | 是 | web/text/image/file/video |
| `url` | string | web/video 必填 | 原始链接 |
| `title` | string | 是 | 标题,自动抓取可覆盖 |
| `description` | text | 否 | 描述或摘录内容 |
| `cover_url` | string | 否 | 封面图 URL |
| `category_id` | integer | 否 | 分类 ID,空则入收件箱 |
| `tags` | string[] | 否 | 标签名数组 |
| `note` | text | 否 | 个人备注 |
| `rating` | integer | 否 | 1-5 评分,默认不评 |
---
### 3.3 分类管理
**功能描述:** 树状文件夹结构,最多 3 级嵌套。
| ID | 需求 | 优先级 |
|----|------|--------|
| CAT-01 | 创建分类,设置名称和图标(emoji) | P0 |
| CAT-02 | 编辑分类名称、图标 | P0 |
| CAT-03 | 删除分类(提示:其下收藏将移入收件箱) | P0 |
| CAT-04 | 拖拽排序分类 | P1 |
| CAT-05 | 分类支持颜色主题(6 种预设色) | P2 |
| CAT-06 | 分类支持移动(改变父级) | P1 |
| CAT-07 | 侧边栏显示每个分类的收藏数量 | P0 |
| CAT-08 | 最多支持 3 级嵌套,第 4 级创建时提示限制 | P0 |
**分类图标**:从预设 Emoji 集合选择,或直接输入 Emoji 字符。
---
### 3.4 标签管理
**功能描述:** 扁平化多维标记,一条内容可关联多个标签。
| ID | 需求 | 优先级 |
|----|------|--------|
| TAG-01 | 录入时直接输入标签名,自动创建 | P0 |
| TAG-02 | 标签输入支持 autocomplete(已有标签下拉提示) | P0 |
| TAG-03 | 标签管理页:列出所有标签及数量,支持重命名、删除、合并 | P1 |
| TAG-04 | 标签支持自定义颜色(从 8 种预设色中选) | P1 |
| TAG-05 | AI 智能推荐标签(基于内容分析,收藏时弹出建议) | P2 |
| TAG-06 | 侧边栏显示常用标签(按使用频率,最多显示 10 个) | P0 |
| TAG-07 | 点击标签名,过滤显示所有含该标签的收藏 | P0 |
---
### 3.5 检索系统
#### 3.5.1 全文搜索
| ID | 需求 | 优先级 |
|----|------|--------|
| SEARCH-01 | 支持对 `title`、`description`、`note`、`content`(正文)的全文搜索 | P0 |
| SEARCH-02 | 搜索结果高亮匹配关键词 | P0 |
| SEARCH-03 | 搜索响应时间 < 500ms(本地 SQLite FTS5) | P0 |
| SEARCH-04 | 支持短语搜索(加引号)和排除搜索(-keyword) | P1 |
| SEARCH-05 | 搜索历史记录,最近 20 条,可清除 | P2 |
**实现方案:** SQLite FTS5 全文索引,建立 `items_fts` 虚拟表,对 `title + description + content + note` 建联合索引。
#### 3.5.2 多维过滤
支持以下维度组合过滤,多条件为 AND 关系:
| 过滤维度 | 可选值 |
|----------|--------|
| 内容类型 | web / text / image / file / video |
| 分类 | 选中分类(含子分类) |
| 标签 | 选中标签(多选 AND/OR 切换) |
| 时间范围 | 今天 / 本周 / 本月 / 自定义范围 |
| 星标 | 是 / 否 |
| 评分 | ≥ N 星 |
| 排序 | 收藏时间倒序 / 更新时间倒序 / 评分倒序 |
| ID | 需求 | 优先级 |
|----|------|--------|
| FILTER-01 | 顶部过滤栏支持快速按类型切换 | P0 |
| FILTER-02 | 高级过滤面板(抽屉),组合多维度 | P1 |
| FILTER-03 | 过滤条件 URL 参数化,支持分享过滤状态 | P2 |
| FILTER-04 | 过滤条件组合可保存为「智能分类」 | P3 |
---
### 3.6 批注系统
**功能描述:** 在收藏内容上进行个人标注,沉淀思考。
| ID | 需求 | 优先级 |
|----|------|--------|
| ANN-01 | 整体备注:在收藏详情页写自由文本备注(Markdown 支持) | P0 |
| ANN-02 | 评分:1-5 星,可修改 | P0 |
| ANN-03 | 文字高亮:选中网页正文中的文字,标记颜色高亮(4 种色) | P1 |
| ANN-04 | 段落批注:在高亮文字处添加批注评论 | P2 |
| ANN-05 | 批注列表:详情页显示所有高亮和批注的汇总 | P2 |
| ANN-06 | 高亮内容支持导出(复制为 Markdown 格式) | P2 |
**高亮存储格式:**
```json
{
"highlights": [
{
"id": "uuid",
"text": "被高亮的原文",
"color": "yellow",
"start_offset": 120,
"end_offset": 180,
"note": "批注内容(可为空)",
"created_at": "2026-05-17T10:00:00Z"
}
]
}
```
---
### 3.7 文章创作
**功能描述:** 以收藏为素材,进行深度写作创作的工具。
#### 3.7.1 编辑器功能
| ID | 需求 | 优先级 |
|----|------|--------|
| ART-01 | Markdown 编辑器,支持实时预览(左右分栏) | P0 |
| ART-02 | 编辑器工具栏:标题/加粗/斜体/链接/图片/代码块/引用 | P0 |
| ART-03 | 自动保存(debounce 1 秒)并显示保存状态 | P0 |
| ART-04 | 支持插入收藏引用:输入 `[[` 触发收藏搜索面板 | P0 |
| ART-05 | 引用块显示:收藏卡片形式(标题 + 来源 + 摘录) | P1 |
| ART-06 | 文章封面图设置 | P1 |
| ART-07 | 文章字数统计、阅读时间预估 | P1 |
| ART-08 | 版本历史:每次保存自动记录快照,可恢复历史版本 | P2 |
#### 3.7.2 文章关联
| ID | 需求 | 优先级 |
|----|------|--------|
| ART-REL-01 | 文章与收藏的关联关系:一篇文章可关联多条收藏 | P0 |
| ART-REL-02 | 收藏详情页显示「被 N 篇文章引用」,点击可跳转 | P1 |
| ART-REL-03 | 关联关系在文章发布时自动建立(解析 `[[` 引用块) | P0 |
#### 3.7.3 发布与导出
| ID | 需求 | 优先级 |
|----|------|--------|
| ART-PUB-01 | 导出为 Markdown 文件(.md) | P0 |
| ART-PUB-02 | 导出为 PDF | P1 |
| ART-PUB-03 | 生成只读分享链接(公开访问,无需登录) | P1 |
| ART-PUB-04 | 分享链接可设置有效期(7 天 / 30 天 / 永久) | P2 |
| ART-PUB-05 | 分享页支持评论(访客可留言) | P3 |
---
### 3.8 AI 辅助功能
| ID | 功能 | 触发方式 | 优先级 |
|----|------|----------|--------|
| AI-01 | 收藏摘要生成 | 收藏录入后异步生成 | P1 |
| AI-02 | 标签智能推荐 | 录入时提示,可一键采纳 | P1 |
| AI-03 | 相关收藏推荐 | 详情页侧栏展示 | P2 |
| AI-04 | 对话式检索「问我的收藏」 | 底部 AI 对话框 | P2 |
| AI-05 | 写作辅助(续写/润色) | 文章编辑器内 AI 按钮 | P3 |
| AI-06 | 每周回顾生成 | 定时任务,每周一 | P3 |
**AI 接入方案:** 通过 API Key 方式接入,优先支持 OpenAI / Anthropic / 本地 Ollama,在设置页配置。摘要生成使用流式输出,避免等待时间过长。
---
### 3.9 用户与账户
**初版为单用户本地应用**,不需要注册登录。后续版本规划云同步和多账户。
| ID | 需求 | 优先级 |
|----|------|--------|
| USER-01 | 设置页:修改显示名称和头像 | P1 |
| USER-02 | 设置页:配置 AI API Key 和 Provider | P1 |
| USER-03 | 设置页:导出全量数据(JSON + 附件 ZIP) | P1 |
| USER-04 | 设置页:导入数据(从 JSON 恢复) | P2 |
| USER-05 | 设置页:配置浏览器插件 Token | P1 |
---
## 4. 数据库设计
**数据库:** SQLite(文件路径:`./data/wanwu.db`)
**ORM:** Drizzle ORM
**迁移管理:** Drizzle Kit
### 4.1 完整 Schema 定义
```typescript
// db/schema.ts
import { sqliteTable, text, integer, real, blob } from 'drizzle-orm/sqlite-core'
import { sql } from 'drizzle-orm'
// ─── 分类表 ───────────────────────────────────────────
export const categories = sqliteTable('categories', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
icon: text('icon').notNull().default('📁'), // emoji
color: text('color').default('#6B7280'), // hex 颜色
parentId: integer('parent_id').references(() => categories.id, { onDelete: 'set null' }),
sortOrder: integer('sort_order').notNull().default(0),
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`),
})
// ─── 标签表 ───────────────────────────────────────────
export const tags = sqliteTable('tags', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull().unique(),
color: text('color').default('#6B7280'),
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
})
// ─── 收藏主表 ─────────────────────────────────────────
export const items = sqliteTable('items', {
id: integer('id').primaryKey({ autoIncrement: true }),
type: text('type', { enum: ['web', 'text', 'image', 'file', 'video'] }).notNull(),
title: text('title').notNull(),
description: text('description'), // 简短描述/摘录
content: text('content'), // 完整正文(用于 FTS)
url: text('url'), // 原始链接
coverUrl: text('cover_url'), // 封面图 URL
faviconUrl: text('favicon_url'), // 网站图标
sourceHost: text('source_host'), // 来源域名
filePath: text('file_path'), // 本地文件路径(image/file 类型)
fileSize: integer('file_size'), // 文件大小 bytes
fileMime: text('file_mime'), // MIME type
// 组织
categoryId: integer('category_id').references(() => categories.id, { onDelete: 'set null' }),
// 个人标记
note: text('note'), // 个人备注(支持 Markdown)
rating: integer('rating'), // 1-5,null 表示未评
starred: integer('starred', { mode: 'boolean' }).notNull().default(false),
highlights: text('highlights', { mode: 'json' }), // 高亮批注 JSON
// AI 生成
aiSummary: text('ai_summary'), // AI 摘要
aiKeywords: text('ai_keywords', { mode: 'json' }), // AI 关键词数组
// 状态
isArchived: integer('is_archived', { mode: 'boolean' }).notNull().default(false),
// 时间戳
publishedAt: text('published_at'), // 原文发布时间
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`),
})
// ─── 收藏-标签 多对多关联表 ───────────────────────────
export const itemTags = sqliteTable('item_tags', {
itemId: integer('item_id').notNull().references(() => items.id, { onDelete: 'cascade' }),
tagId: integer('tag_id').notNull().references(() => tags.id, { onDelete: 'cascade' }),
})
// ─── 文章表 ───────────────────────────────────────────
export const articles = sqliteTable('articles', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull().default('无标题文章'),
content: text('content').notNull().default(''), // Markdown 原文
coverUrl: text('cover_url'),
status: text('status', { enum: ['draft', 'published'] }).notNull().default('draft'),
wordCount: integer('word_count').notNull().default(0),
// 分享
shareToken: text('share_token').unique(), // 公开分享 token
shareExpiresAt: text('share_expires_at'),
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`),
})
// ─── 文章-收藏 关联表 ─────────────────────────────────
export const articleItems = sqliteTable('article_items', {
articleId: integer('article_id').notNull().references(() => articles.id, { onDelete: 'cascade' }),
itemId: integer('item_id').notNull().references(() => items.id, { onDelete: 'cascade' }),
sortOrder: integer('sort_order').notNull().default(0),
})
// ─── 文章版本历史表 ───────────────────────────────────
export const articleVersions = sqliteTable('article_versions', {
id: integer('id').primaryKey({ autoIncrement: true }),
articleId: integer('article_id').notNull().references(() => articles.id, { onDelete: 'cascade' }),
content: text('content').notNull(),
wordCount: integer('word_count').notNull().default(0),
createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
})
// ─── FTS 虚拟表(全文搜索)────────────────────────────
// 使用 SQLite FTS5,通过迁移脚本手动创建:
// CREATE VIRTUAL TABLE items_fts USING fts5(
// id UNINDEXED,
// title,
// description,
// content,
// note,
// content=items,
// content_rowid=id,
// tokenize='unicode61 remove_diacritics 1'
// );
//
// 同步触发器:
// CREATE TRIGGER items_ai AFTER INSERT ON items BEGIN
// INSERT INTO items_fts(rowid, title, description, content, note)
// VALUES (new.id, new.title, new.description, new.content, new.note);
// END;
// CREATE TRIGGER items_au AFTER UPDATE ON items BEGIN
// INSERT INTO items_fts(items_fts, rowid, title, description, content, note)
// VALUES ('delete', old.id, old.title, old.description, old.content, old.note);
// INSERT INTO items_fts(rowid, title, description, content, note)
// VALUES (new.id, new.title, new.description, new.content, new.note);
// END;
// CREATE TRIGGER items_ad AFTER DELETE ON items BEGIN
// INSERT INTO items_fts(items_fts, rowid, title, description, content, note)
// VALUES ('delete', old.id, old.title, old.description, old.content, old.note);
// END;
// ─── 用户设置表 ───────────────────────────────────────
export const settings = sqliteTable('settings', {
key: text('key').primaryKey(),
value: text('value', { mode: 'json' }),
updatedAt: text('updated_at').notNull().default(sql`(datetime('now'))`),
})
// 预设 settings key:
// display_name : string
// avatar_url : string
// ai_provider : 'openai' | 'anthropic' | 'ollama'
// ai_api_key : string (加密存储)
// ai_model : string
// ai_base_url : string (Ollama 自定义地址)
// plugin_token : string (浏览器插件认证 token)
// theme : 'dark' | 'light' | 'system'
```
### 4.2 索引设计
```sql
-- 高频查询索引
CREATE INDEX idx_items_category ON items(category_id);
CREATE INDEX idx_items_created ON items(created_at DESC);
CREATE INDEX idx_items_starred ON items(starred) WHERE starred = 1;
CREATE INDEX idx_items_type ON items(type);
CREATE INDEX idx_items_rating ON items(rating);
CREATE INDEX idx_item_tags_item ON item_tags(item_id);
CREATE INDEX idx_item_tags_tag ON item_tags(tag_id);
CREATE INDEX idx_article_items ON article_items(article_id);
CREATE UNIQUE INDEX idx_item_tags_unique ON item_tags(item_id, tag_id);
```
### 4.3 Drizzle 配置
```typescript
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './db/schema.ts',
out: './db/migrations',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DATABASE_URL ?? './data/wanwu.db',
},
verbose: true,
strict: true,
})
```
---
## 5. API 接口设计
所有接口基于 Nuxt 4 的 `server/api/` 目录,使用 H3 框架,格式遵循 REST 约定。
**通用响应格式:**
```typescript
// 成功
{ data: T, total?: number, page?: number }
// 失败
{ error: { code: string, message: string } }
```
**通用错误码:**
| Code | HTTP Status | 说明 |
|------|-------------|------|
| `VALIDATION_ERROR` | 400 | 参数校验失败 |
| `NOT_FOUND` | 404 | 资源不存在 |
| `CONFLICT` | 409 | 资源冲突(如标签名重复) |
| `INTERNAL_ERROR` | 500 | 服务器内部错误 |
---
### 5.1 收藏接口
#### `GET /api/items`
获取收藏列表,支持分页和过滤。
**Query 参数:**
| 参数 | 类型 | 说明 |
|------|------|------|
| `page` | number | 页码,默认 1 |
| `limit` | number | 每页条数,默认 20,最大 100 |
| `q` | string | 搜索关键词(全文搜索) |
| `type` | string | 内容类型过滤 |
| `categoryId` | number \| 'inbox' | 分类 ID,inbox 表示无分类 |
| `tagIds` | string | 标签 ID 逗号分隔 |
| `tagMode` | 'and' \| 'or' | 多标签关系,默认 and |
| `starred` | boolean | 星标过滤 |
| `ratingGte` | number | 评分下限 |
| `dateFrom` | string | 开始时间 ISO 8601 |
| `dateTo` | string | 结束时间 ISO 8601 |
| `sort` | string | created_at_desc / updated_at_desc / rating_desc |
| `archived` | boolean | 是否已归档,默认 false |
**响应:**
```typescript
{
data: Item[],
total: number,
page: number,
limit: number,
hasMore: boolean
}
```
---
#### `POST /api/items`
创建新收藏。
**Request Body:**
```typescript
{
type: 'web' | 'text' | 'image' | 'file' | 'video'
url?: string
title: string
description?: string
categoryId?: number
tagNames?: string[] // 标签名数组,自动创建不存在的标签
note?: string
rating?: number
starred?: boolean
}
```
**响应:** `{ data: Item }`
**副作用:**
- 若 type=web,触发异步 URL 抓取任务(Server-Sent Events 通知进度)
- 触发异步 AI 摘要生成
---
#### `GET /api/items/:id`
获取单条收藏详情,包含完整字段(含 content、highlights)。
---
#### `PATCH /api/items/:id`
更新收藏。支持部分更新,仅传入需要修改的字段。
**可更新字段:** `title` `description` `note` `rating` `starred` `categoryId` `isArchived` `highlights` `coverUrl`
---
#### `DELETE /api/items/:id`
删除收藏。若有关联文章引用,保留关联记录但收藏标记为已删除。
---
#### `POST /api/items/:id/tags`
为收藏添加标签。
**Body:** `{ tagName: string }`
**响应:** `{ data: { itemId, tagId, tagName } }`
---
#### `DELETE /api/items/:id/tags/:tagId`
移除收藏的某个标签。
---
#### `POST /api/items/fetch-url`
预抓取 URL 元数据(用于录入表单实时预览)。
**Body:** `{ url: string }`
**响应:**
```typescript
{
data: {
title: string
description: string
coverUrl: string
faviconUrl: string
sourceHost: string
type: 'web' | 'video'
}
}
```
---
#### `POST /api/items/batch`
批量操作。
**Body:**
```typescript
{
ids: number[]
action: 'move' | 'delete' | 'star' | 'unstar' | 'archive'
categoryId?: number // action=move 时必填
}
```
---
### 5.2 分类接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/categories` | 获取分类树(嵌套结构) |
| POST | `/api/categories` | 创建分类 |
| PATCH | `/api/categories/:id` | 更新分类(名称/图标/颜色/父级/排序) |
| DELETE | `/api/categories/:id` | 删除分类 |
| POST | `/api/categories/reorder` | 批量更新排序 `{ ids: number[] }` |
**GET /api/categories 响应:**
```typescript
{
data: Array<{
id: number
name: string
icon: string
color: string
itemCount: number
children: Category[] // 递归
}>
}
```
---
### 5.3 标签接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/tags` | 获取所有标签及使用数量 |
| POST | `/api/tags` | 创建标签 |
| PATCH | `/api/tags/:id` | 更新标签名称/颜色 |
| DELETE | `/api/tags/:id` | 删除标签(解除所有关联) |
| POST | `/api/tags/:id/merge` | 合并标签 `{ targetTagId: number }` |
---
### 5.4 文章接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/articles` | 获取文章列表(分页) |
| POST | `/api/articles` | 创建新文章 |
| GET | `/api/articles/:id` | 获取文章详情(含关联收藏) |
| PATCH | `/api/articles/:id` | 更新文章内容 |
| DELETE | `/api/articles/:id` | 删除文章 |
| POST | `/api/articles/:id/publish` | 发布文章 |
| POST | `/api/articles/:id/share` | 生成分享链接 |
| DELETE | `/api/articles/:id/share` | 撤销分享 |
| GET | `/api/articles/:id/versions` | 获取版本历史 |
| POST | `/api/articles/:id/restore/:versionId` | 恢复历史版本 |
| GET | `/api/articles/:id/export` | 导出文章(query: format=md/pdf) |
| GET | `/api/share/:token` | 公开分享页数据(无需认证) |
---
### 5.5 搜索接口
#### `GET /api/search`
**Query:** `q` (必填), `limit` (默认 10)
**响应:**
```typescript
{
data: {
items: Array- // highlight 为带 标签的匹配片段
articles: Array
}
}
```
#### `GET /api/search/suggest`
搜索建议(用于输入框 autocomplete)。
**Query:** `q`, `limit` (默认 5)
**响应:** `{ data: string[] }` — 建议关键词数组
---
### 5.6 AI 接口
#### `POST /api/ai/summarize`
手动触发 AI 摘要(流式响应)。
**Body:** `{ itemId: number }`
**响应:** `text/event-stream`,逐 token 返回
#### `POST /api/ai/suggest-tags`
获取 AI 标签建议。
**Body:** `{ title: string, description?: string, content?: string }`
**响应:** `{ data: string[] }` — 建议标签名数组
#### `POST /api/ai/chat`
对话式检索「问我的收藏」。
**Body:**
```typescript
{
message: string
history?: Array<{ role: 'user' | 'assistant', content: string }>
}
```
**响应:** `text/event-stream`
---
### 5.7 设置接口
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/settings` | 获取所有设置 |
| PATCH | `/api/settings` | 批量更新设置 `{ [key]: value }` |
| GET | `/api/settings/export` | 导出全量数据 (ZIP) |
| POST | `/api/settings/import` | 导入数据 |
---
## 6. 前端页面与交互
### 6.1 页面路由结构
```
/ → 重定向到 /inbox
/inbox → 收件箱
/items → 全部收藏
/items/starred → 星标收藏
/items?categoryId=N → 分类视图
/items?tagId=N → 标签视图
/articles → 文章列表
/articles/new → 新建文章
/articles/:id → 文章编辑器
/share/:token → 公开分享页(无需认证,独立 layout)
/settings → 设置页
/settings/tags → 标签管理
```
### 6.2 全局布局(Layout)
**三栏结构:**
```
┌──────────────────────────────────────────────────────────────┐
│ Topbar(高度 48px):Logo | 搜索框 | 新增按钮 | 头像 │
├───────────┬──────────────────────────────────┬───────────────┤
│ │ │ │
│ Sidebar │ 主内容区(卡片/列表) │ 详情面板 │
│ 220px │ │ 380px │
│ │ │ (可收起) │
│ │ AI 对话条(固定底部) │ │
└───────────┴──────────────────────────────────┴───────────────┘
```
**Sidebar 区块:**
1. 视图导航(收件箱、全部、最近、星标、文章)
2. 分类树(可展开/折叠,带拖拽排序)
3. 常用标签(最多 10 个)
4. 底部:设置入口
**详情面板(抽屉):**
- 点击卡片时从右侧滑入(400px 宽)
- 不影响主列表区域(overlay 模式在移动端,push 模式在桌面端)
- 内容:封面/预览、标题、来源、AI 摘要、标签、备注编辑、评分、相关推荐、操作按钮
### 6.3 主内容区
#### 视图模式
| 模式 | 说明 |
|------|------|
| 卡片(Grid) | 2-3 列瀑布流,每卡含封面缩略图 |
| 列表(List) | 单列,含标题/来源/标签/时间,密度更高 |
#### 卡片组件规范
**网页类型卡片:**
```
┌─────────────────────────────┐
│ [封面图 / 120px 高] │
│ ┌类型badge┐ ⭐收藏按钮 │
├─────────────────────────────┤
│ favicon 来源域名 │
│ 标题文字(最多 2 行) │
│ 描述(最多 2 行,灰色) │
│ #标签1 #标签2 05-16 │
└─────────────────────────────┘
```
**文本类型卡片(无封面):**
- 顶部区域显示文本前 3 行内容(背景渐变)
#### 排序与分组
- 支持按时间/评分/标题排序
- 支持按日期分组(今天 / 本周 / 更早)
### 6.4 搜索体验
1. 聚焦搜索框,展示最近搜索记录
2. 输入时实时 autocomplete(debounce 150ms)
3. 回车执行全文搜索,结果页显示匹配高亮
4. 搜索词在结果列表中用 `` 高亮
5. 搜索结果分区:收藏 / 文章
### 6.5 新增收藏 Modal
```
┌──────────────────────────────────────────┐
│ 新增收藏 ✕ │
├──────────────────────────────────────────┤
│ 类型:[🌐 网页] [📝 文本] [🖼️ 图片] │
│ [🎬 视频] [📄 文件] │
│ │
│ 链接或内容 │
│ ┌────────────────────────────────────┐ │
│ │ https://... │ │
│ └────────────────────────────────────┘ │
│ │
│ [正在抓取元数据...] ← URL 输入后自动触发 │
│ │
│ 标题(已自动填充) │
│ ┌────────────────────────────────────┐ │
│ │ Vercel 设计系统 2024 更新 │ │
│ └────────────────────────────────────┘ │
│ │
│ 分类 标签 │
│ ┌──────────────┐ ┌──────────────────┐│
│ │ 📁 设计 ▾ │ │ #设计 #参考... + ││
│ └──────────────┘ └──────────────────┘│
│ │
│ [AI 建议标签:设计系统 Vercel 工具 +] │
│ │
│ 备注(可选) │
│ ┌────────────────────────────────────┐ │
│ │ │ │
│ └────────────────────────────────────┘ │
│ │
│ [取消] [保存收藏 →] │
└──────────────────────────────────────────┘
```
### 6.6 文章编辑器页面
**布局:**
```
┌─────────────────────────────────────────────────────────────┐
│ ← 返回 | 文章标题(inline 编辑) | 草稿已保存 发布 ▾ │
├──────────────────────────┬──────────────────────────────────┤
│ │ │
│ Markdown 编辑区 │ 实时预览区 │
│ (左半) │ (右半) │
│ │ │
│ 输入 [[ 触发 │ │
│ 收藏搜索面板 │ │
│ │ │
└──────────────────────────┴──────────────────────────────────┘
│ 字数:1,234 阅读时间:约 5 分钟 | 版本历史 导出 分享 │
└─────────────────────────────────────────────────────────────┘
```
**`[[` 收藏引用面板:**
- 弹出 Popover,输入搜索词实时过滤收藏
- 选中后插入引用块:
```markdown
> [!cite] Vercel 设计系统 2024 更新
> 来源:vercel.com · 2026-05-16
> Vercel 团队分享了最新的设计系统更新...
```
---
## 7. 技术架构
### 7.1 技术栈详情
| 层级 | 技术 | 版本 | 用途 |
|------|------|------|------|
| 框架 | Nuxt 4 | 4.x | 全栈框架,SSR/SPA |
| 运行时 | Node.js | 20 LTS | 服务端运行时 |
| 数据库 | SQLite | 3.x | 本地持久化存储 |
| ORM | Drizzle ORM | latest | 类型安全数据库操作 |
| 数据库驱动 | better-sqlite3 | latest | SQLite Node.js 绑定 |
| UI 组件 | Nuxt UI v3 | 3.x | 基础 UI 组件库 |
| CSS | Tailwind CSS | v4 | 样式 |
| 状态管理 | Pinia | latest | 客户端状态 |
| 编辑器 | CodeMirror 6 | 6.x | Markdown 编辑器 |
| MD 渲染 | markdown-it | latest | Markdown → HTML |
| 图标 | Nuxt Icon / Lucide | latest | 图标库 |
| 验证 | Zod | latest | API 参数校验 |
| 测试 | Vitest | latest | 单元/集成测试 |
### 7.2 项目目录结构
```
wanwu/
├── app/
│ ├── components/
│ │ ├── item/
│ │ │ ├── ItemCard.vue # 收藏卡片
│ │ │ ├── ItemCardList.vue # 列表视图行
│ │ │ ├── ItemDetail.vue # 详情面板
│ │ │ ├── ItemAddModal.vue # 新增 Modal
│ │ │ └── ItemHighlight.vue # 高亮批注组件
│ │ ├── category/
│ │ │ ├── CategoryTree.vue # 分类树
│ │ │ └── CategoryModal.vue # 分类编辑
│ │ ├── article/
│ │ │ ├── ArticleEditor.vue # 文章编辑器
│ │ │ ├── ArticleCard.vue # 文章卡片
│ │ │ └── ArticleCitePanel.vue # 引用收藏面板
│ │ ├── search/
│ │ │ ├── SearchBar.vue
│ │ │ └── SearchResults.vue
│ │ ├── common/
│ │ │ ├── TagBadge.vue
│ │ │ ├── StarRating.vue
│ │ │ ├── FilterBar.vue
│ │ │ └── AiChatStrip.vue
│ │ └── layout/
│ │ ├── AppSidebar.vue
│ │ └── AppTopbar.vue
│ ├── composables/
│ │ ├── useItems.ts # 收藏数据操作
│ │ ├── useCategories.ts
│ │ ├── useTags.ts
│ │ ├── useArticles.ts
│ │ ├── useSearch.ts
│ │ ├── useAi.ts
│ │ └── useInfiniteScroll.ts
│ ├── layouts/
│ │ ├── default.vue # 主布局(三栏)
│ │ └── share.vue # 分享页布局
│ ├── pages/
│ │ ├── index.vue # → redirect /inbox
│ │ ├── inbox.vue
│ │ ├── items.vue # 全部收藏 / 分类 / 标签视图
│ │ ├── articles/
│ │ │ ├── index.vue
│ │ │ ├── new.vue
│ │ │ └── [id].vue
│ │ ├── share/
│ │ │ └── [token].vue
│ │ └── settings/
│ │ ├── index.vue
│ │ └── tags.vue
│ └── stores/
│ ├── ui.ts # UI 状态(侧栏收起、详情面板等)
│ ├── items.ts
│ ├── categories.ts
│ └── search.ts
├── server/
│ ├── api/
│ │ ├── items/
│ │ │ ├── index.get.ts
│ │ │ ├── index.post.ts
│ │ │ ├── [id].get.ts
│ │ │ ├── [id].patch.ts
│ │ │ ├── [id].delete.ts
│ │ │ ├── [id]/tags.post.ts
│ │ │ ├── [id]/tags/[tagId].delete.ts
│ │ │ ├── fetch-url.post.ts
│ │ │ └── batch.post.ts
│ │ ├── categories/
│ │ │ ├── index.get.ts
│ │ │ ├── index.post.ts
│ │ │ ├── [id].patch.ts
│ │ │ ├── [id].delete.ts
│ │ │ └── reorder.post.ts
│ │ ├── tags/
│ │ │ ├── index.get.ts
│ │ │ ├── index.post.ts
│ │ │ ├── [id].patch.ts
│ │ │ ├── [id].delete.ts
│ │ │ └── [id]/merge.post.ts
│ │ ├── articles/
│ │ │ ├── index.get.ts
│ │ │ ├── index.post.ts
│ │ │ ├── [id].get.ts
│ │ │ ├── [id].patch.ts
│ │ │ ├── [id].delete.ts
│ │ │ ├── [id]/publish.post.ts
│ │ │ ├── [id]/share.post.ts
│ │ │ ├── [id]/export.get.ts
│ │ │ └── [id]/versions/
│ │ ├── search/
│ │ │ ├── index.get.ts
│ │ │ └── suggest.get.ts
│ │ ├── ai/
│ │ │ ├── summarize.post.ts
│ │ │ ├── suggest-tags.post.ts
│ │ │ └── chat.post.ts
│ │ ├── settings/
│ │ │ ├── index.get.ts
│ │ │ ├── index.patch.ts
│ │ │ ├── export.get.ts
│ │ │ └── import.post.ts
│ │ └── share/
│ │ └── [token].get.ts
│ ├── services/
│ │ ├── fetcher.ts # URL 抓取服务
│ │ ├── ai.ts # AI 接入层
│ │ ├── search.ts # FTS 搜索逻辑
│ │ └── exporter.ts # 导出服务
│ ├── utils/
│ │ ├── db.ts # Drizzle 实例(单例)
│ │ └── validate.ts # Zod schema 集合
│ └── plugins/
│ └── db-init.ts # 数据库初始化(运行迁移)
├── db/
│ ├── schema.ts
│ ├── migrations/ # Drizzle Kit 生成
│ └── seed.ts # 测试数据
├── public/
│ └── uploads/ # 本地上传文件(.gitignore)
├── data/ # SQLite 数据库文件(.gitignore)
├── drizzle.config.ts
├── nuxt.config.ts
├── package.json
└── .env.example
```
### 7.3 关键实现细节
#### 数据库连接(单例模式)
```typescript
// server/utils/db.ts
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import * as schema from '~/db/schema'
let db: ReturnType
export function useDB() {
if (!db) {
const sqlite = new Database(process.env.DATABASE_URL ?? './data/wanwu.db')
sqlite.pragma('journal_mode = WAL') // 提高并发性能
sqlite.pragma('foreign_keys = ON') // 启用外键约束
sqlite.pragma('synchronous = NORMAL') // 平衡安全与性能
db = drizzle(sqlite, { schema })
}
return db
}
```
#### URL 抓取服务
```typescript
// server/services/fetcher.ts
import { extract } from '@extractus/article-extractor'
import { load } from 'cheerio'
export async function fetchUrlMeta(url: string) {
const res = await fetch(url, {
headers: { 'User-Agent': 'Mozilla/5.0 ...' },
signal: AbortSignal.timeout(8000),
})
const html = await res.text()
const $ = load(html)
const meta = {
title: $('meta[property="og:title"]').attr('content') || $('title').text(),
description: $('meta[property="og:description"]').attr('content')
|| $('meta[name="description"]').attr('content'),
coverUrl: $('meta[property="og:image"]').attr('content'),
faviconUrl: resolveFavicon(url, $),
sourceHost: new URL(url).hostname,
}
// 提取正文
const article = await extract(url)
return { ...meta, content: article?.content }
}
```
#### FTS5 搜索
```typescript
// server/services/search.ts
export async function fullTextSearch(db: DB, query: string, limit = 20) {
const sanitized = query.replace(/['"*]/g, ' ').trim()
const ftsQuery = sanitized.split(/\s+/).map(t => `"${t}"`).join(' AND ')
return db.all(sql`
SELECT
i.*,
snippet(items_fts, 1, '', '', '...', 20) AS highlight
FROM items_fts
JOIN items i ON i.id = items_fts.rowid
WHERE items_fts MATCH ${ftsQuery}
AND i.is_archived = 0
ORDER BY rank
LIMIT ${limit}
`)
}
```
### 7.4 nuxt.config.ts
```typescript
export default defineNuxtConfig({
compatibilityDate: '2025-01-01',
future: { compatibilityVersion: 4 },
modules: [
'@nuxt/ui',
'@pinia/nuxt',
'@nuxt/icon',
'nuxt-file-storage',
],
nitro: {
experimental: { asyncContext: true },
storage: {
uploads: { driver: 'fs', base: './public/uploads' }
}
},
css: ['~/assets/css/main.css'],
runtimeConfig: {
databaseUrl: './data/wanwu.db',
aiProvider: '',
aiApiKey: '',
aiModel: '',
public: {}
}
})
```
---
## 8. 开发计划
### 8.1 里程碑总览
| 里程碑 | 内容 | 工期 | 目标完成日 |
|--------|------|------|-----------|
| M0 | 项目初始化与基础架构 | 3 天 | Week 1 |
| M1 | 数据库 + 核心 API | 5 天 | Week 2 |
| M2 | 收藏管理前端(增删改查) | 5 天 | Week 3 |
| M3 | 分类 + 标签系统 | 3 天 | Week 3-4 |
| M4 | 搜索与过滤系统 | 3 天 | Week 4 |
| M5 | 详情面板与批注 | 4 天 | Week 5 |
| M6 | 文章编辑器 | 5 天 | Week 6 |
| M7 | AI 功能接入 | 4 天 | Week 7 |
| M8 | 设置页 + 导入导出 | 3 天 | Week 8 |
| M9 | 浏览器扩展 | 4 天 | Week 9 |
| M10 | 测试 + 打磨 + 发布 | 5 天 | Week 10 |
**总工期:** 约 10 周(单人全职开发)
---
### 8.2 M0 — 项目初始化(Week 1,第 1-3 天)
**任务清单:**
- [ ] `npx nuxi init wanwu --template ui` 初始化项目
- [ ] 配置 Nuxt 4 兼容模式(`future.compatibilityVersion: 4`)
- [ ] 安装依赖:`better-sqlite3`, `drizzle-orm`, `drizzle-kit`, `zod`, `@extractus/article-extractor`, `cheerio`
- [ ] 创建 `db/schema.ts`,定义所有表结构
- [ ] 运行 `drizzle-kit generate` 生成迁移文件
- [ ] 实现 `server/utils/db.ts` 单例 + 数据库初始化插件
- [ ] 配置 `.env.example`,列出所有环境变量
- [ ] 设置 ESLint + Prettier + husky pre-commit
- [ ] 配置 Tailwind CSS 主题色(暗色系 + 琥珀色 accent)
- [ ] 建立全局 Layout(Sidebar + Topbar + 主内容区占位)
- [ ] 编写 `db/seed.ts`,插入 10 条测试收藏数据
- [ ] 验证:`npm run dev` 启动无报错,数据库文件正常创建
**交付物:** 可运行的开发环境,数据库结构已建立
---
### 8.3 M1 — 核心 API(Week 2,第 4-8 天)
**任务清单(按接口优先级):**
**收藏 CRUD(第 4-5 天):**
- [ ] `GET /api/items` — 列表查询(含分页、类型过滤、排序)
- [ ] `POST /api/items` — 创建收藏(Zod 校验)
- [ ] `GET /api/items/:id` — 单条详情
- [ ] `PATCH /api/items/:id` — 部分更新
- [ ] `DELETE /api/items/:id` — 删除
**分类 + 标签(第 6 天):**
- [ ] `GET/POST/PATCH/DELETE /api/categories` — 分类 CRUD(含递归树查询)
- [ ] `GET/POST/PATCH/DELETE /api/tags` — 标签 CRUD
- [ ] `POST /api/items/:id/tags`、`DELETE /api/items/:id/tags/:tagId`
**URL 抓取(第 7 天):**
- [ ] `POST /api/items/fetch-url` — URL 元数据抓取
- [ ] 实现 `server/services/fetcher.ts`
- [ ] 错误处理:超时 / 抓取失败 / 非 HTML 响应
**FTS 搜索(第 8 天):**
- [ ] 在迁移脚本中创建 FTS5 虚拟表和同步触发器
- [ ] `GET /api/search` — 全文搜索,返回带高亮片段
- [ ] `GET /api/search/suggest` — 搜索建议
**验证:** 用 Postman/Hoppscotch 测试所有接口,全部通过
---
### 8.4 M2 — 收藏管理前端(Week 2-3,第 6-10 天)
**任务清单:**
**基础列表(第 6-7 天):**
- [ ] `pages/items.vue` — 收藏列表页,接入 `GET /api/items`
- [ ] `components/item/ItemCard.vue` — 卡片组件(含 hover 效果、封面图、类型 badge)
- [ ] `components/item/ItemCardList.vue` — 列表视图行
- [ ] 卡片/列表视图切换
- [ ] 无限滚动加载(Intersection Observer + `useInfiniteScroll` composable)
**新增收藏(第 8 天):**
- [ ] `components/item/ItemAddModal.vue`
- 类型切换(5 种)
- URL 输入 + 自动抓取(loading 态)
- 标题、分类选择、标签输入(autocomplete)
- AI 标签推荐展示(后续接入)
- [ ] 接入 `POST /api/items`
- [ ] 成功后刷新列表(乐观更新)
**Pinia Store(第 9 天):**
- [ ] `stores/items.ts` — 收藏状态管理(列表、分页、加载状态)
- [ ] `stores/categories.ts`
- [ ] `stores/ui.ts` — 面板状态、视图模式
**快捷键(第 10 天):**
- [ ] `Cmd/Ctrl + K` 打开快速收藏输入框
- [ ] `Esc` 关闭 Modal / 详情面板
---
### 8.5 M3 — 分类 + 标签系统(Week 3-4,第 11-13 天)
**任务清单:**
**分类树(第 11 天):**
- [ ] `components/category/CategoryTree.vue` — 递归树形组件,展开/折叠
- [ ] 分类创建/编辑 Modal(名称、图标 Emoji、颜色)
- [ ] 拖拽排序(使用 `vue-draggable-plus`)
- [ ] 接入分类 CRUD API
**标签系统(第 12 天):**
- [ ] `components/common/TagBadge.vue` — 标签组件(含颜色、点击过滤)
- [ ] `pages/settings/tags.vue` — 标签管理页(列表、编辑、删除、合并)
- [ ] 侧边栏常用标签显示
**收件箱(第 13 天):**
- [ ] `pages/inbox.vue` — 收件箱视图(仅显示 `categoryId IS NULL`)
- [ ] 顶部 Topbar 收件箱数量角标
- [ ] 收件箱批量分类操作
---
### 8.6 M4 — 搜索与过滤(Week 4,第 14-16 天)
**任务清单:**
**搜索(第 14 天):**
- [ ] `components/search/SearchBar.vue` — 顶部搜索框,输入 autocomplete
- [ ] 搜索结果页,含高亮显示
- [ ] 搜索历史(localStorage 存储)
**过滤系统(第 15-16 天):**
- [ ] `components/common/FilterBar.vue` — 类型快速切换
- [ ] 高级过滤面板(抽屉 Drawer):时间范围、评分、星标
- [ ] 多标签 AND/OR 模式切换
- [ ] 过滤状态 URL 参数化(使用 `useRoute` + `navigateTo`)
---
### 8.7 M5 — 详情面板与批注(Week 5,第 17-20 天)
**任务清单:**
**详情面板(第 17-18 天):**
- [ ] `components/item/ItemDetail.vue` — 右侧详情面板
- 封面大图
- 标题(inline 编辑)
- 来源信息、发布时间
- AI 摘要(含 loading skeleton)
- 标签展示 + 添加标签
- 备注编辑区(Markdown 支持,自动保存)
- 星标评分
- 相关推荐(3 条)
- 操作按钮:写文章 / 复制链接 / 编辑 / 删除
- [ ] 面板打开/关闭动画(CSS transition)
- [ ] 移动端改为全屏 Drawer
**批注功能(第 19-20 天):**
- [ ] `components/item/ItemHighlight.vue` — 网页正文展示 + 高亮选择
- [ ] 选中文字触发颜色选择气泡
- [ ] 批注 CRUD(存储到 `items.highlights` JSON 字段)
- [ ] 批注汇总列表
---
### 8.8 M6 — 文章编辑器(Week 6,第 21-25 天)
**任务清单:**
**编辑器核心(第 21-22 天):**
- [ ] 集成 CodeMirror 6(Markdown 语法高亮、vi/emacs 键位可选)
- [ ] 实时预览(markdown-it 渲染)
- [ ] 工具栏(标题/加粗/斜体/链接/图片/代码块/引用)
- [ ] 自动保存(debounce 1s,显示保存状态)
- [ ] 字数统计、阅读时间预估
**收藏引用(第 23 天):**
- [ ] `components/article/ArticleCitePanel.vue` — `[[` 触发的收藏搜索面板
- [ ] 选中收藏后插入引用 Markdown 块
- [ ] 预览区渲染引用块为卡片样式
- [ ] 解析引用关系,保存到 `article_items`
**发布与导出(第 24 天):**
- [ ] 文章状态切换(草稿 → 发布)
- [ ] 导出 Markdown(直接下载)
- [ ] 生成分享链接(生成 token,写入 DB)
- [ ] 分享页 `pages/share/[token].vue`
**版本历史(第 25 天):**
- [ ] 每次保存记录快照到 `article_versions`
- [ ] 版本历史侧边栏(时间 + 字数)
- [ ] 点击恢复某版本
---
### 8.9 M7 — AI 功能(Week 7,第 26-29 天)
**任务清单:**
**AI 基础接入(第 26 天):**
- [ ] `server/services/ai.ts` — 统一 AI 接入层
- 支持 OpenAI / Anthropic / Ollama
- 流式输出封装
- [ ] 设置页:AI 配置(Provider / API Key / Model)
- [ ] 测试连接功能
**摘要生成(第 27 天):**
- [ ] 收藏录入后异步触发摘要(Server-Sent Events)
- [ ] 详情面板显示摘要 + loading 骨架
- [ ] 手动重新生成摘要按钮
**标签推荐(第 28 天):**
- [ ] 新增 Modal 中:URL 抓取完成后自动调用 AI 建议标签
- [ ] 建议标签一键添加
- [ ] 接口:`POST /api/ai/suggest-tags`
**对话检索(第 29 天):**
- [ ] `components/common/AiChatStrip.vue` — 底部 AI 对话条
- [ ] `POST /api/ai/chat` — RAG 检索:先 FTS 搜索相关收藏,再送入 AI 生成回答
- [ ] 对话历史(session 级)
- [ ] 引用相关收藏卡片在回答中展示
---
### 8.10 M8 — 设置页 + 导入导出(Week 8,第 30-32 天)
**任务清单:**
- [ ] `pages/settings/index.vue` — 设置主页
- 个人资料(名称、头像)
- AI 配置
- 浏览器扩展 Token 管理
- 主题切换(暗/亮/系统)
- 危险区域(清空数据)
- [ ] 全量数据导出(JSON + 附件 ZIP)
- [ ] 数据导入与校验
- [ ] `GET /api/settings/export` 实现
---
### 8.11 M9 — 浏览器扩展(Week 9,第 33-36 天)
**任务清单:**
- [ ] 创建 Chrome Extension(Manifest V3)项目 `extension/`
- [ ] Popup 页面:
- 显示当前页面标题/封面
- 选择分类、输入标签
- 「保存到万物收藏」按钮
- [ ] Background Service Worker:
- 调用本地 `http://localhost:3000/api/items`(或配置的 Host)
- Token 认证
- [ ] 右键菜单:「保存链接到万物收藏」
- [ ] 选中文字右键:「保存文字片段到万物收藏」
- [ ] 测试扩展在 Chrome / Edge 中正常工作
---
### 8.12 M10 — 测试与发布(Week 10,第 37-41 天)
**任务清单:**
**测试(第 37-38 天):**
- [ ] 核心 API 单元测试(Vitest):items / categories / tags CRUD
- [ ] 搜索功能测试
- [ ] 边界情况:超长标题、特殊字符、大文件上传
- [ ] 多浏览器兼容测试(Chrome / Firefox / Safari)
**性能优化(第 39 天):**
- [ ] 图片懒加载
- [ ] 虚拟滚动(收藏列表超过 500 条时)
- [ ] 数据库慢查询排查(EXPLAIN QUERY PLAN)
- [ ] Nuxt build 产物分析(`nuxt analyze`)
**打磨(第 40 天):**
- [ ] 空状态设计(收件箱为空、搜索无结果等)
- [ ] 错误状态设计(网络失败、抓取失败等)
- [ ] Loading skeleton 补全
- [ ] 响应式适配(iPad 布局)
**发布(第 41 天):**
- [ ] 编写 README(安装、配置、启动指南)
- [ ] `npm run build` 验证生产构建
- [ ] Docker 镜像打包(`Dockerfile` + `docker-compose.yml`)
- [ ] GitHub Release v1.0.0
---
## 9. 非功能需求
### 9.1 性能要求
| 指标 | 目标值 |
|------|--------|
| 收藏列表首屏加载 | < 1s(本地 SQLite) |
| 全文搜索响应 | < 500ms |
| URL 元数据抓取 | < 3s(含 timeout) |
| 页面路由切换 | < 200ms(客户端导航) |
| 数据库查询(单次) | < 100ms |
### 9.2 可靠性
- SQLite WAL 模式,防止写入崩溃数据损坏
- 文章自动保存,最小化数据丢失风险
- 导出功能作为数据备份手段
- URL 抓取失败不影响收藏创建
### 9.3 安全性
- AI API Key 不明文存储(本地应用采用系统 Keychain 或环境变量)
- 文件上传类型白名单校验(不允许可执行文件)
- 文件上传大小限制(图片 10MB,文档 50MB)
- 分享链接使用 UUID v4 Token,无法枚举
- 所有 API 输入通过 Zod 校验,防止注入
### 9.4 数据持久化
- 数据库文件:`./data/wanwu.db`
- 上传文件:`./public/uploads/`(按日期分目录)
- 建议用户定期使用「导出功能」备份数据
- 数据库迁移向后兼容,升级不丢数据
---
## 10. 验收标准
### 10.1 P0 功能验收(必须完成)
| 功能 | 验收标准 |
|------|----------|
| 新增网页收藏 | 输入 URL → 自动填充标题和描述 → 保存成功 → 列表显示 |
| 新增文本收藏 | 输入文字 → 保存 → 列表显示 |
| 分类创建 | 创建分类 → 侧边栏显示 → 收藏可分配该分类 |
| 标签打标 | 录入时添加标签 → 卡片显示 → 点击标签过滤有效 |
| 全文搜索 | 输入关键词 → 500ms 内返回结果 → 关键词高亮 |
| 编辑备注 | 详情面板编辑备注 → 1s 内自动保存 → 刷新后仍存在 |
| 写文章 | 新建文章 → 使用 `[[` 引用收藏 → 导出 Markdown 正确 |
| 删除收藏 | 删除后列表消失,数据库记录删除 |
| 收件箱 | 无分类收藏显示在收件箱,分配分类后消失 |
### 10.2 P1 功能验收(重要)
| 功能 | 验收标准 |
|------|----------|
| AI 摘要 | 保存收藏后,详情面板摘要在 10s 内显示 |
| 标签推荐 | 新增收藏时展示 AI 建议标签,点击可采纳 |
| 批注高亮 | 在正文中选中文字 → 选色 → 高亮持久化 |
| 文章分享 | 生成链接 → 无需登录访问 → 内容正确显示 |
| 浏览器扩展 | 扩展 Popup 可保存当前页面到收藏 |
| 数据导出 | 导出 ZIP 包含 JSON 数据和所有附件 |
### 10.3 已知限制(v1.0 不做)
- 不支持多用户 / 账户系统
- 不支持云同步(本地优先)
- 不支持移动 App(Web 响应式适配为主)
- AI 对话无跨 session 记忆
- 不支持 PDF 正文全文搜索(仅标题和备注)
---
*文档结束 — 万物收藏 PRD v1.0.0*