artshumm/Link-Saver

GitHub: artshumm/Link-Saver

一个基于 Node/Express 和 SQLite 的微型链接收藏工具,支持粘贴 URL 后自动抓取页面标题并提供列表、收藏、过滤和删除功能。

Stars: 0 | Forks: 0

# Link Saver 粘贴一个 URL,应用就会为你获取该页面的真实 ``,添加时间戳并将其存储。 支持列出、收藏、过滤和删除已保存的链接。数据持久化存储在 SQLite 中,因此重启后数据依然存在。 ## 技术栈及选择理由 - **Node + Express** 用于构建微型 REST API —— 零繁琐配置,且任务书的 Part B 使用的也是 Express,因此这能让两部分保持在同一个心智模型中。 - **通过内置的 `node:sqlite` 使用 SQLite** 进行持久化。我想要一个真正的数据库(持久化、可查询、能处理并发写入),从而避免 Part B 中 JSON 文件方法的文件竞争问题。我原本打算使用 `better-sqlite3`,但当前环境没有 C 语言工具链来编译原生插件,因此我改用了 Node 22.5+ 内置的 `node:sqlite` —— 同样是同步的 prepared-statement API,**无需构建步骤,没有额外依赖**。 - **原生 HTML/CSS/JS** 前端 —— 单个页面不需要框架,并且能保持 payload 极其小巧。 **如果未来需要扩展:** 我会将存储层提取到 repository 接口之后(在不触碰路由的情况下将 SQLite 替换为 Postgres),在 HTTP 边界添加验证层(例如 Zod),将路由拆分到各自的模块中,并将标题获取操作放入 job/queue 中处理,这样缓慢的网页就不会阻塞请求。 ## 环境要求 - **Node 22.5 或更高版本**(需要内置的 `node:sqlite` 模块)。已在 Node 24 上测试。 ## 运行 ``` npm install # installs express, jest, supertest (no native builds) npm start # serves http://localhost:3000 ``` 然后打开 <http://localhost:3000>,粘贴一个 URL,点击 **Save**。 通过环境变量进行配置(均为可选 —— 参见 `.env.example`): | 变量名 | 默认值 | 用途 | | ------------------ | ------------------- | -------------------------------- | | `PORT` | `3000` | HTTP 端口 | | `DB_PATH` | `./data/links.db` | SQLite 文件位置 | | `FETCH_TIMEOUT_MS` | `5000` | 获取标题超时时间 | ## 测试 ``` npm test # jest, run in-band ``` 涵盖存储层、URL 验证、标题提取和 HTTP API 的 60 个测试。`package.json` 中强制要求达到 80%(语句/行/函数/分支)的覆盖率阈值。 ## API | 方法 | 路径 | Body | 备注 | | -------- | -------------------- | --------------------- | ---------------------------------------- | | `POST` | `/links` | `{ "url": "..." }` | 获取标题并保存。`400` 表示 URL 错误,`502` 表示无法获取 | | `GET` | `/links` | — | `?favourite=1` 过滤为仅显示收藏内容 | | `DELETE` | `/links/:id` | — | 成功返回 `204`,未找到返回 `404` | | `PATCH` | `/links/:id` | `{ "favourite": bool }` | 切换收藏状态;未找到返回 `404` | 响应使用一致的包装格式:`{ success, data }` 或 `{ success: false, error }`。 ## 收藏功能 —— 涉及的文件 将链接标记为收藏以及过滤仅显示收藏内容涉及以下修改: - **`db.js`** —— `favourite` 列(默认为 0)、`setFavourite()` 以及 `list({ favourite })` 过滤器。 - **`app.js`** —— `PATCH /links/:id` 路由以及 `GET /links` 上的 `?favourite=1` 查询。 - **`public/index.html`** —— "Favourites only" 复选框以及每行的星标按钮标记。 - **`public/app.js`** —— 星标切换 (`toggleFavourite`) 以及将过滤复选框与重新加载关联。 - **`public/styles.css`** —— 星标和过滤器样式。 - **`tests/api.test.js`, `tests/db.test.js`** —— 针对切换和过滤功能的测试覆盖。 ## 假设与决策 - **在服务器端而非客户端获取 Title。** 用户绝不需要手动输入标题;由服务器获取页面并解析 `<title>`。如果页面没有可用的标题(缺少标签、非 HTML 内容),它会回退到主机名,而不是导致保存失败。 - **SSRF 防护。** 由于服务器会获取任意的用户提供的 URL,我会解析主机名并拒绝私有/回环/链路本地地址(例如 `http://127.0.0.1`、`169.254.169.254`)。仅接受 `http`/`https` 协议。错误的 URL → `400`,无法获取/被阻止 → `502`。 - **URL 规范化。** 以解析出的 `href` 形式存储(因此 `example.com` 会被拒绝 —— 必须包含协议 —— 而 `https://example.com` 会被规范化为 `https://example.com/`)。 - **按保存时间倒序排列**。 ## 刻意省略的内容(及原因) - **无认证 / 多用户支持。** 这是一个单用户本地工具;超出了时间盒的范围。 - **无编辑 endpoint。** 任务书只要求保存/列出/删除/收藏。 - **重定向重新验证。** SSRF 检查仅在初始主机上运行;如果服务器在第一跳之后重定向到私有地址,则不会重新检查。这是一个已知的漏洞 —— 如果此应用公开暴露,我会重新验证每一跳(或使用自定义 agent)。 - **无分页。** 对于个人清单来说足够了;在数据量变大之前我会添加 `LIMIT/OFFSET`。 - **未自动化浏览器点击测试。** API 已经实现了端到端覆盖,并且我使用 curl 对运行中的服务器进行了冒烟测试(真实标题获取、错误 URL、SSRF、列出)。我**没有**在此环境中对 UI 运行自动化浏览器测试。 ## 如果有更多时间我会改进的地方 - 边界处的 Repository 接口 + Zod 验证。 - 针对 UI 的 Playwright E2E 测试(保存 → 显示 → 收藏 → 过滤 → 删除)。 - 采用乐观 UI 更新,而不是在每次修改后进行完整重新加载。 - 严格化已实施的逐跳 SSRF 重新验证和响应大小限制。 ## 关键 AI 提示词 发挥最大作用的 2-3 个提示词: 1. *"构建一个 Link Saver:Express + SQLite。`POST /links` 验证 URL,通过超时和 SSRF 防护(阻止私有/回环 IP,仅限 http/https)在服务器端获取页面标题,当没有 `<title>` 时回退到主机名。外加 `GET /links?favourite=1`、`DELETE /links/:id`、`PATCH /links/:id`。注入 db 和 title-fetcher,以便我可以在不连接网络的情况下进行单元测试。"* 2. *"首先编写 Jest + supertest 测试 (TDD)。覆盖正常路径、缺失/错误的 URL、无法获取的页面、收藏切换和过滤器、删除缺失项 → 404。为 SSRF IP 分类器和 `<title>` 提取器(实体、大写、无标题回退)添加单元测试。"* 3. *"将这份手写的 `server.js` 作为代码审查进行审查。首先找出破坏性的 bug,解释是什么输入破坏了每一个 bug,并按严重程度排序,然后提供忠实于 Express 加 JSON 文件设计的更正代码。"*(Part B / [`REVIEW.md`](REVIEW.md) —— 参见 [更正代码片段的已知局限性](REVIEW.md#known-limitations-of-the-corrected-snippet) 了解 JSON 文件修复方案特意留给此 SQLite 应用的内容。) ## 如果我能先向你提问 我会问:(1) SSRF/私有 URL 防护是否在范围内,或者这是否是一个获取 `localhost` 也没问题的受信任的本地工具?(2) 收藏是一个基于单个链接的布尔值(我所构建的),还是一个独立的集合?(3) 你希望标题随着时间的推移被重新获取/刷新,还是在保存时捕获一次?(4) 对认证或多用户有什么期望,还是假定为单用户?</div><div><strong>标签:</strong>Express, GNU通用公共许可证, MITM代理, Node.js, RESTful API, SQLite, 单页应用, 提示词优化, 网络测绘, 自定义脚本, 链接管理</div></article></div> <!-- 人机验证 --> <script> (function () { var base = (document.querySelector('base') && document.querySelector('base').getAttribute('href')) || ''; var path = base.replace(/\/?$/, '') + '/cap-wasm/cap_wasm.min.js'; window.CAP_CUSTOM_WASM_URL = new URL(path, window.location.href).href; })(); </script> </body> </html>