
人跟 AI 工具竞争注意力其实是个不对等的游戏中AI 占绝对上风。算法知道你什么时候会打开 Linkedin feed清楚你上一次为什么点开一条裁员新闻也了解你对“某某拿到了融资”这类字眼的点击偏好。它会不断调整信号让你再刷几分钟。于是你真正用于思考、写作和构建的时间被一段段与自己无关的信息切碎。这件事最荒谬的地方在于今天想解决它几乎不再需要毅力。做一个只给自己用的小软件成本已经便宜到可以随时动手。你可以把“看 feed”变成“看任务清单”把算法替你挑选的信息流换成自己定义的数据管道。有人会把这个动作叫作杀死 feed但其实它更像是亲手接管了 feed 的决策权。本文会先梳理个人软件成本下降背后的关键变化再以“一个人脉联系提醒工具”为例带你从零实现一个能替代社交信息流的本地个人软件。读完以后你应该能跑通一条完整链路导入联系人数据、定时生成摘要、按自己的节奏推送给自己而不是让社交平台替你决定今天该看什么。1. 这篇文章真正要解决的问题你不是缺信息你是缺一个可控的输入管道1.1 信息流把“人脉维护”包装成了“信息消费”很多开发者在 LinkedIn 上的真实需求其实很具体知道某位前同事现在去了哪家公司记住某位合作方最近有没有新动作或者在换工作时能够快速想起谁可以给我推荐。满足这些需求原本只需要一套联系人数据不需要每天消耗半小时到一个小时刷 feed。但社交平台不能这样设计。它的商业模式是让你的注意力停留而不是让你完成任务。于是你会看到“XX 升级为 CTO”“XX 公司完成了新一轮融资”这些信息本身不坏问题是平台会持续生成更多类似的内容让你不知不觉进入“防御性浏览”明明没有明确目标却怕错过重要动态。从工程角度看我们不是要切断所有人际连接而是要切断“信息流这种低信噪比的输入方式”。真正值得留下来的是那些与你存在明确关系、明确时间节点的联系人提醒。1.2 开发者最该改变的不是卸载应用而是构建替代品过去十年里只要谈到戒掉信息流普遍的建议是卸载 App、删除书签、规定刷手机时间。这些方法不是没有用但它们都在消耗意志力。而意志力恰恰是针对算法攻击最脆弱的防线。更好的方案是把原来靠 feed 被动完成的信息收集改成由自己的小工具主动输出。你不必每天去浏览页面只需要在固定时间收到一份由本地数据库生成的摘要。这份摘要没有热门排序、没有广告、没有无限下滑只包含你在意的联系人、你设置的下一联系时间以及你曾经标注过的标签。这也是我这篇文章想强调的核心判断程序员改变注意力习惯的最有效手段通常是写一个只服务自己的软件而不是逼自己做到“不看手机”。1.3 读完本文你能获得什么本文示例会实现一个叫 People Digest 的小工具。它基于 SQLite、Node.js 和本地定时任务运行核心能力包括从 CSV 文件中导入联系人数据根据联系人设置的“下次联系日期”自动生成摘要通过本地 cron 每天或每周运行一次把摘要输出到 Markdown 文件可选地通过 SMTP 把摘要发到自己的邮箱。整套系统不依赖任何付费后端不调用社交平台非官方 API不采集别人的数据。它只用你自己导出的数据、自己维护的数据就可以解决问题。环境不需要复杂的云服务Node.js 可以跑SQLite 文件可以长期保留cron 是操作系统自带能力。这本身就是当前个人软件的典型形态本地优先、单一流程、运行成本几乎为零。2. 个人软件为什么正在变便宜不只是 AI 的功劳先说结论个人软件变便宜是多重成本同时下降的结果AI 只是其中最显眼的一块催化层。2.1 从需求到原型的时间成本被压缩过去写一个带数据库的小应用最少步骤包括创建项目、配置路由、设计表结构、写 API、写前端、部署。对不熟悉全栈的开发者来说最容易卡住的是细节而不是“业务逻辑本身”。现在 AI 辅助编码工具可以在几分钟内生成可运行的骨架代码把“从零开始”的成本降到“从对话开始”。这不意味着开发者不再需要理解原理。恰恰相反当个人软件必须长期维护理解数据模型和部署边界就比理解某个框架的 API 更重要。AI 降低的是编码摩擦而不是思考成本。2.2 基础设施从“租服务器”走向“免费额度”很长一段时间里个人软件要跑到公网至少需要一台服务器和一个域名。现在个人项目可以选择静态部署平台、Serverless 函数、边缘数据库、对象存储等方案。很多服务在小流量下都有免费额度个人使用几乎不产生费用。如果工具只是给自己用甚至不需要公网。一个 SQLite 文件加一段脚本每天在本地运行就足够了。很多开发者容易陷入“先上云再看”的思路但个人软件最佳策略往往是先本地跑起来等真正需要异地同步再引入服务端。2.3 开源生态和模板化开发降低了“重复造轮子”的代价登录、支付、后台管理这些通用能力开源社区已经给出了大量成熟方案。你不需要为每一个个人工具重新发明一套权限系统也不需要维护庞大的前端工程。找一个没有复杂依赖的模板改一改数据模型通常一两天就能完成一个能用的版本。所以更准确地说个人软件的成本下降体现在过去做一个“只服务一个人的工具”要承担与商业软件差不多的工程成本现在则可以极度缩小范围。你可以只聚焦自己的数据流和最少的操作界面。由于应用只需要一个用户很多工程复杂性问题都不存在不需要并发均衡、不需要高可用、不需要多租户隔离。这种“单用户软件”反而比许多团队项目更容易设计得长期干净。2.4 从“做一个产品”转向“写一段自动化”成本下降的真正意义不是每个人都必须去创业做独立产品而是“用代码解决自己生活中的具体问题”变成了一种很自然的选择。过去你觉得“为了记住联系人而写一套 CRM”太重现在你已经可以只写一个联系人提醒脚本。反过来正是这种微型自动化的积累让你逐渐不再依赖大平台的信息投放。当你能把“查看联系人动态”变成“查看自己生成的日报”你当然不会再心甘情愿被 feed 的排序逻辑长期消耗。3. 一个替代 LinkedIn feed 的最小产品方案3.1 先明确需求边界这里的重点是“最小”不要把方案设计成 LinkedIn 克隆。产品只需要回答三个问题我有哪些人脉谁最近需要联系我要记录哪些备注对应的功能边界维护联系人基础字段姓名、公司、职位、邮箱、标签维护下次联系日期每天生成“该联系的人”清单定期把清单发送给自己不在系统里追新闻、不做好友动态流、不展示别人的点赞和评论。这套设计明显是“任务驱动”而不是“信息驱动”。Info feed 的本质是尽最大可能让你产生新的好奇心联系人 digest 的本质则是用最短时间告诉你当前要处理的动作。这也是杀死 feed 的核心把无边界的信息消费变成有明确终点的任务队列。3.2 数据从哪里来有几种合法的数据来源社交平台的数据导出功能。很多平台都允许用户下载自己的数据档案里面包含联系人列表、好友关系、历史互动等信息。自己维护的 CSV 表格。这是最保险的方案。邮箱通讯录导出。适合做初步导入再手动补充。手动录入。对最重要的联系人花一个小时整理比刷一个礼拜 feed 更有价值。不建议也不应该在公众号或教程里推广写爬虫去抓取公开页面。一旦脚本开始频繁请求平台接口既可能违反平台服务条款也可能影响别人的服务稳定性。个人软件的成本优势应该建立在数据所有权明确的前提下。你自己导出的数据怎么处理都没有问题你通过爬虫抓来的数据边界会模糊很多。3.3 产品输出形态最简单的输出就是一份 Markdown 报告# People Digest - 2026-06-15 ## 本周需要联系 - [ ] 张伟 / 某云厂商 / 高级架构师 —— 约定联系日期 2026-06-13逾期 2 天 - [ ] 李敏 / 某创业公司 / HRD —— 约定联系日期 2026-06-17还有 2 天这份报告可以直接在终端查看也可以由脚本转成邮件。关键点在于你控制内容的粒度你决定多久看一次。这不是“信息流”而是你的联系人待办清单。4. 环境准备与项目初始化4.1 运行环境本文的示例依赖以下环境版本不需要严格固定Node.js 18 或更高版本建议使用当前 LTSSQLite通过 Node.js 的better-sqlite3驱动访问不需要单独安装数据库服务一个支持 cron 的 Linux/macOS 环境Windows 用户可以在 WSL 或 Git Bash 中模拟。如果你不想使用真实联系人数据可以准备一个测试用 CSV。下面代码假设你具备基础命令行操作能力。4.2 创建项目目录并安装依赖mkdir -p people-digest/{data,reports,scripts,db} cd people-digest npm init -y npm install better-sqlite3 csv-parse dotenv nodemailer安装过程中如果better-sqlite3编译较慢请先确认 Node.js 版本与本地编译工具链。多数情况下安装包会直接使用预编译二进制不需要额外操作。项目目录结构如下people-digest/ ├── .env.example ├── db/ │ └── schema.sql ├── data/ │ └── contacts.csv ├── reports/ ├── scripts/ │ ├── import-contacts.js │ ├── generate-digest.js │ └── send-digest.js └── package.jsondata/contacts.csv是你自己的联系人数据建议不要放进 Git 仓库。reports/用来存放每次运行生成的摘要.env用于保存 SMTP 配置同样不要提交到仓库。5. 核心实现从 CSV 导入到自动生成摘要5.1 初始化数据库表结构在db/schema.sql中写入基础表结构。这个结构足够小却已经能支持日常联系人管理。-- file: db/schema.sql PRAGMA journal_mode WAL; CREATE TABLE IF NOT EXISTS contacts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, company TEXT DEFAULT , title TEXT DEFAULT , email TEXT DEFAULT , tags TEXT DEFAULT , next_contact_date TEXT, last_contacted_at TEXT, created_at TEXT DEFAULT (date(now)), source TEXT DEFAULT csv ); CREATE INDEX IF NOT EXISTS idx_contacts_next_contact_date ON contacts(next_contact_date);字段说明next_contact_date是下一次主动联系联系人日期格式建议使用YYYY-MM-DD。tags用逗号分隔适合存“前同事、校友、技术合作”这类标签。source字段用于区分数据来源后续重复导入时可以用它控制覆盖范围。创建数据库并执行 schemanode -e const Database require(better-sqlite3); const fs require(fs); const db new Database(data/people.db); db.exec(fs.readFileSync(db/schema.sql, utf8)); console.log(database ready); 5.2 准备一份 CSV常见 CSV 包含下面字段name,company,title,email,tags,next_contact_date 张伟,某云厂商,高级架构师,zhangweiexample.com,技术合作,2026-06-17 李敏,某创业公司,HRD,liminexample.com,校友,2026-06-19 王芳,某研究院,研究员,wangfangexample.com,行业专家,2026-06-25如果是从社交平台导出的联系人数据字段名可能不同。你需要先做一次字段映射把这几个关键字段对应到 CSV 中。建议不要直接依赖原始导出文件先整理成干净的标准 CSV再导入系统。5.3 编写联系人导入脚本在scripts/import-contacts.js中实现导入逻辑。设计原则是可重复执行每次执行会先清空sourcecsv的历史数据再全量写入当前 CSV。这比逐条 upsert 简单也符合个人工具“一表一来源”的场景。#!/usr/bin/env node // file: scripts/import-contacts.js require(dotenv).config(); const fs require(fs); const path require(path); const Database require(better-sqlite3); const { parse } require(csv-parse/sync); const DB_PATH process.env.DB_PATH || path.join(__dirname, .., data, people.db); const csvPath process.argv[2]; if (!csvPath) { console.error(用法: node scripts/import-contacts.js contacts.csv); process.exit(1); } if (!fs.existsSync(csvPath)) { console.error(文件不存在: ${csvPath}); process.exit(1); } if (!fs.existsSync(path.dirname(DB_PATH))) { fs.mkdirSync(path.dirname(DB_PATH), { recursive: true }); } const db new Database(DB_PATH); db.exec(PRAGMA journal_mode WAL;); const raw fs.readFileSync(csvPath, utf8); const records parse(raw, { columns: true, skip_empty_lines: true, trim: true, }); const insert db.prepare( INSERT INTO contacts ( name, company, title, email, tags, next_contact_date, source ) VALUES (?, ?, ?, ?, ?, ?, csv) ); const replaceAll db.transaction((rows) { db.prepare(DELETE FROM contacts WHERE source csv).run(); for (const row of rows) { if (!row.name) continue; insert.run( row.name || , row.company || , row.title || , row.email || , row.tags || , row.next_contact_date || ); } }); replaceAll(records); console.log(导入完成共处理 ${records.length} 条 CSV 行);这段代码的关键逻辑并不复杂csv-parse/sync会直接把 CSV 解析成对象数组数据库连接使用better-sqlite3这是 Node.js 生态较流行的同步 SQLite 驱动事务保证了“先清空再写入”过程不会中断到一半避免留下脏数据如果 CSV 中name为空直接跳过。注意这里默认使用sourcecsv来标记数据。之后如果还手动录入了联系人再次执行 CSV 导入不会覆盖手动数据只会覆盖 CSV 来源的数据。这样可以避免把某个来源的数据全量重写时误删其他数据。执行命令node scripts/import-contacts.js data/contacts.csv预期输出导入完成共处理 3 条 CSV 行5.4 编写摘要生成脚本在scripts/generate-digest.js中编写每日摘要逻辑。核心是查询所有next_contact_date小于等于今天日期的联系人。#!/usr/bin/env node // file: scripts/generate-digest.js require(dotenv).config(); const fs require(fs); const path require(path); const Database require(better-sqlite3); const DB_PATH process.env.DB_PATH || path.join(__dirname, .., data, people.db); const REPORT_DIR path.join(__dirname, .., reports); if (!fs.existsSync(path.dirname(DB_PATH))) { console.error(数据库不存在请先执行导入脚本); process.exit(1); } const db new Database(DB_PATH); function formatDate(date) { const y date.getFullYear(); const m String(date.getMonth() 1).padStart(2, 0); const d String(date.getDate()).padStart(2, 0); return ${y}-${m}-${d}; } const today new Date(); const todayStr formatDate(today); const rows db .prepare( SELECT * FROM contacts WHERE next_contact_date IS NOT NULL AND next_contact_date ! AND next_contact_date ? ORDER BY next_contact_date ASC ) .all(todayStr); const lines []; lines.push(# People Digest - ${todayStr}); lines.push(); lines.push(## 需要主动联系的人); lines.push(); if (rows.length 0) { lines.push(今天没有需要主动联系的人。); } else { for (const contact of rows) { const contactDate new Date(contact.next_contact_date T00:00:00); const diff Math.ceil((new Date(todayStr T00:00:00) - contactDate) / 86400000); const overdueText diff 0 ? 已逾期 ${diff} 天 : ; lines.push( - [ ] ${contact.name} / ${contact.company || 未知公司} / ${contact.title || } —— 约定日期 ${contact.next_contact_date} ${overdueText} ); } } lines.push(); lines.push( 如果想清除提醒请在本周联系后更新该联系人的 next_contact_date。); fs.mkdirSync(REPORT_DIR, { recursive: true }); const reportPath path.join(REPORT_DIR, digest-${todayStr}.md); fs.writeFileSync(reportPath, lines.join(\n), utf8); console.log(已生成摘要, reportPath); console.log(lines.join(\n));运行命令node scripts/generate-digest.js如果当前日期等于或超过 CSV 中的任意next_contact_date终端会打印类似下面的内容已生成摘要 reports/digest-2026-06-15.md # People Digest - 2026-06-15 ## 需要主动联系的人 - [ ] 张伟 / 某云厂商 / 高级架构师 —— 约定日期 2026-06-15这样你每天要做的不是打开信息流而是先查看这一行待办然后去微信、邮件或电话中完成一次真实联系。联系完之后更新next_contact_date摘要里就不会再出现这条提醒。5.5 可选通过邮件发送摘要如果你希望摘要直接发到邮箱需要配置 SMTP。在项目根目录创建.env.example# .env.example SMTP_HOSTsmtp.example.com SMTP_PORT465 SMTP_SECUREtrue SMTP_USERyour-accountexample.com SMTP_PASSWORDyour-app-password MAIL_FROMPeople Digest youexample.com MAIL_TOyouexample.com DB_PATH./data/people.db然后把.env.example复制为.env并填写真实配置。注意不要提交.env现代邮箱通常要求使用应用专用密码而不是登录密码。scripts/send-digest.js#!/usr/bin/env node // file: scripts/send-digest.js require(dotenv).config(); const fs require(fs); const path require(path); const nodemailer require(nodemailer); const filePath process.argv[2]; if (!filePath) { console.error(用法: node scripts/send-digest.js digest-file.md); process.exit(1); } fs.accessSync(filePath); const transporter nodemailer.createTransport({ host: process.env.SMTP_HOST, port: Number(process.env.SMTP_PORT || 465), secure: process.env.SMTP_SECURE true, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASSWORD, }, }); async function main() { const content fs.readFileSync(filePath, utf8); const subject [People Digest] ${path.basename(filePath, .md)}; const info await transporter.sendMail({ from: process.env.MAIL_FROM, to: process.env.MAIL_TO, subject, text: content, }); console.log(邮件发送成功:, info.messageId); } main().catch((err) { console.error(发送失败:, err.message); process.exit(1); });这里需要注意端口 465 通常配合SMTP_SECUREtrue端口 587 通常配合SMTP_SECUREfalse建议先在命令行手动发送一次确认邮箱服务商允许 SMTP 发起方如果只是给自己看也可以不配置邮件直接查看reports/下的文件。5.6 用 cron 每天自动运行个人软件的关键是自动化。Linux 和 macOS 下可以用crontab -e添加任务。示例是每天上午 9:00 生成摘要每周四上午 9:30 发邮件。0 9 * * * cd /absolute/path/to/people-digest /usr/bin/node scripts/generate-digest.js reports/cron.log 21 30 9 * * 4 cd /absolute/path/to/people-digest /usr/bin/node scripts/generate-digest.js /usr/bin/node scripts/send-digest.js reports/$(date \%F).md reports/cron.log 21首次配置 cron 前先手动运行一次完整命令确认路径正确。date \%F在 cron 中需要用百分号转义否则会报错。Windows 用户可以使用 Windows 任务计划程序或者在 WSL 内配置 cron原理相同。6. 运行结果与效果验证6.1 检查数据库内容导入完成后可以用下列命令检查数据库node -e const Database require(better-sqlite3); const db new Database(data/people.db); console.log(db.prepare(SELECT name, company, next_contact_date FROM contacts).all()); 预期输出是一个包含联系人信息的数组。如果输出为空说明导入逻辑没有执行成功。6.2 验证摘要文件每次运行摘要脚本后在reports/目录下应该能看到一个新的 Markdown 文件ls -la reports/ cat reports/digest-$(date %F).md如果你把未来联系人日期故意设置为昨天摘要中应该出现这条逾期提醒。如果不出现请检查数据库中的next_contact_date是否真的存在或者查询逻辑中的比较日期是否与当前日期一致。6.3 验证邮件通知执行发送脚本时如果看到邮件发送成功: messageId说明 SMTP 已打通。此时去邮箱检查收件箱和垃圾箱有些自建邮箱或测试服务会把邮件归入回收站。如果发送失败最常见的错误信息有三个Invalid login用户名或应用专用密码错误Invalid login: 535 Authentication failed需要检查邮箱设置里是否开启 SMTP 服务Timeout可能是 SMTP 域名或端口不通需要检查防火墙或网络环境。7. 常见问题与排查方法问题现象可能原因排查方式解决方案导入时提示 CSV 中文乱码文件不是 UTF-8 编码或带有 BOM用编辑器查看编码格式另存为 UTF-8禁止使用 GBKbetter-sqlite3安装失败Node 版本过低或本地缺少编译工具查看 npm 错误日志升级 Node.js或重新安装依赖今天需要联系的人没有出现在摘要中日期比较逻辑或时区不对手动查看数据库中记录的实际日期用本地日期格式化函数避免直接依赖toISOString邮件发送失败SMTP 授权码错误、端口不通打印错误详情查看邮箱服务商的 SMTP 文档改用应用专用密码或切换端口cron 执行后没有生成报告脚本路径或 Node 路径错误查看reports/cron.log中的完整错误在脚本中使用绝对路径重复导入后出现多份相同联系人schema 没有唯一索引导入逻辑不清空检查source字段和导入事务统一按“一个 source 一个数据源”管理我误把联系人数据提交到了 Git仓库中包含隐私数据用.gitignore忽略data/*.csv移除历史记录改为本地维护注意个人软件没有客服帮你排查问题。遇到异常时先看数据库里的数据再看日志最后确认代码路径和运行环境。绝大多数问题都出在路径、环境变量、时区这三类地方。8. 个人软件的正确工程姿势与安全边界8.1 数据本地优先不要无脑上云个人软件第一原则是数据在自己手里。一个 SQLite 文件可以存活十几年只要你有备份它不会因为某个供应商调整策略而消失。你完全可以把整个data/目录放进网盘做同步也可以使用sqlite3 .backup做定期备份。先本地运行再考虑同步。只有当你在多台设备之间需要共享才值得引入后端服务和数据库。即便如此也优先选择开放标准的数据格式避免把自己锁死在某个平台上。8.2 不要把私密内容提交到公开仓库联系人 CSV、SMTP 密码都属于敏感信息。项目初始化时就要写好.gitignore# .gitignore node_modules/ data/ reports/ .env如果未来确实要用 Git 管理代码请在私有仓库中保存不要因为一时方便把联系人信息传上公开平台。8.3 定时任务要设计成幂等重复运行摘要脚本不应该产生副作用。本文示例中脚本每次重新生成同名 Markdown 文件因此可以反复执行如果换成邮件发送就要设计“只发送最新文件”或“记录上次发送状态”减少重复打扰。8.4 联系提醒是辅助不是自动骚扰这套工具的边界很清晰它负责提醒你但不能代替你去发消息。不要做“到日期自动给联系人发私信”的功能。人际关系一旦被自动化很容易因为内容生硬或发送频率失控造成反效果。最好的方式是在报告中留下记录由你亲自发送一次真实沟通。8.5 保持单一数据源保持简单随着时间推移你可能会想加入更多功能记录联系人的生日、跟进招聘进度、同步邮件往来、生成关系图谱。这些功能听起来都有用但对单用户工具来说每增加一个模块都意味着更多维护成本。更好的策略是让每个领域只有一种来源。例如联系人信息以 CSV 为准日常任务记录放在日历沟通记录放在邮件里这个脚本只读取“下次联系日期”这一个关键字段。不要让个人软件变成承载所有数据的大前端否则迟早会演变成新一代复杂系统最终放弃维护。9. 总结与后续实践建议回到最初的场景当刷 LinkedIn feed 变成一种被动消费你应该意识到平台提供的不是“人脉管理方案”而是“注意力消费方案”。真正的人脉管理需要定期主动联系而对“该联系谁”的判断完全可以由自己的数据来计算。本文设计了一个最小个人软件 People Digest。你只需要一份 CSV、一个 SQLite 文件和一个 Node.js 脚本就能在几分钟内拥有自己的后台。这个后台不显示无限信息流只罗列你设置的履约任务。跑通这个流程之后你可以继续思考几个扩展方向一在摘要中加入联系人的上下文备注。联系完一个人后把聊天中提到的关键信息记录到notes字段下次联系前查看备注沟通质量会明显提升。二使用本地 AI 模型或开源 NLP 工具对联系人标签做自动聚类。但我更建议先手动维护两个月标签再判断是否真的需要自动化。多数情况下几十条联系人数据根本不需要模型。三把同样的模式复制到其他信息消费场景。你还可以用 RSS 替代新闻 feed用本地书签管理器替代收藏夹用自动构建的周报替代每天刷项目看板。不要从“做一个大系统”开始。最好的起点是从一个你已经不想再看的 feed 开始。先想清楚这个 feed 原本想帮你完成的功能再写一个最小工具把它替换掉。当你能看到自己的数据、控制自己的节奏时你会意识到杀掉一个 feed 很简单难的是建立一个可信的个人数据管道。而后者恰恰是当下所有开发者最值得投入的工程方向。