# 万物收藏 — 产品需求文档 & 开发计划 **版本:** 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. 抓取页面 ``、`<meta name="description">`、`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<Item & { highlight: string }> // highlight 为带 <mark> 标签的匹配片段 articles: Array<Article & { highlight: string }> } } ``` #### `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. 搜索词在结果列表中用 `<mark>` 高亮 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<typeof drizzle> 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, '<mark>', '</mark>', '...', 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*