Mini Mom 系列 01|30分钟复刻:链接抓取 -> inbox 落盘 -> Daily 回链
这一篇只做一件事: 把“看到一个链接”变成“Obsidian 里有一条结构化笔记”。
目标闭环:
URL -> 抓正文 ->(可选 AI 摘要)-> 生成 Markdown -> 写入 inbox
第 0 步:新建项目目录
mkdir -p ~/mini-mom && cd ~/mini-mom
mkdir -p tools config content/inbox
bun init -y
安装最小依赖(可选):
bun add dotenv
第 1 步:准备配置
config/local.json
{
"obsidianInboxDir": "/Users/<你>/Documents/YourVault/00 Inbox",
"aiBaseUrl": "https://ai.chen.rs/v1",
"aiModel": "gpt-5-mini",
"useAiDigest": true
}
环境变量:
export AI_API_KEY="sk-xxx"
# 或 OPENAI_API_KEY
第 2 步:创建抓取脚本
新建 tools/link-capture.ts(最小可运行版):
#!/usr/bin/env bun
import { mkdir, writeFile, readFile } from "node:fs/promises";
import { join } from "node:path";
type Config = {
obsidianInboxDir: string;
aiBaseUrl?: string;
aiModel?: string;
useAiDigest?: boolean;
};
function arg(name: string): string | undefined {
const i = Bun.argv.indexOf(name);
return i >= 0 ? Bun.argv[i + 1] : undefined;
}
function slugify(input: string): string {
return input
.toLowerCase()
.replace(/https?:\/\//g, "")
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 60);
}
async function readViaJina(url: string): Promise<{ title: string; content: string }> {
const res = await fetch(`https://r.jina.ai/${url}`);
if (!res.ok) throw new Error(`Jina failed: ${res.status}`);
const raw = await res.text();
const title = (raw.match(/^Title:\s*(.+)$/m)?.[1] || "Untitled").trim();
const marker = "Markdown Content:";
const idx = raw.indexOf(marker);
const content = idx >= 0 ? raw.slice(idx + marker.length).trim() : raw.trim();
return { title, content };
}
async function digest(content: string, cfg: Config): Promise<string> {
if (!cfg.useAiDigest) return "(已关闭 AI 摘要)";
const key = process.env.AI_API_KEY || process.env.OPENAI_API_KEY;
if (!key) return "(未配置 AI API Key,跳过摘要)";
const base = (cfg.aiBaseUrl || "https://ai.chen.rs/v1").replace(/\/$/, "");
const model = cfg.aiModel || "gpt-5-mini";
const res = await fetch(`${base}/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model,
messages: [
{ role: "system", content: "你是摘要助手。输出 3-5 句中文摘要。" },
{ role: "user", content: content.slice(0, 10000) },
],
temperature: 0.2,
}),
});
if (!res.ok) return `(摘要失败: ${res.status})`;
const json = await res.json() as any;
return json.choices?.[0]?.message?.content?.trim() || "(模型未返回摘要)";
}
async function main() {
const url = arg("--url");
const configPath = arg("--config") || "config/local.json";
if (!url) throw new Error("缺少 --url");
const cfg = JSON.parse(await readFile(configPath, "utf-8")) as Config;
const now = new Date();
const iso = now.toISOString();
const captured = await readViaJina(url);
const summary = await digest(captured.content, cfg);
const filename = `${iso.slice(0, 10)}-${slugify(captured.title || url)}.md`;
await mkdir(cfg.obsidianInboxDir, { recursive: true });
const md = `---
source_url: "${url}"
captured_at: "${iso}"
tags: ["link-clip", "inbox"]
---
# ${captured.title || "Untitled"}
## 摘要
${summary}
## 原文
${captured.content.slice(0, 15000)}
`;
const output = join(cfg.obsidianInboxDir, filename);
await writeFile(output, md, "utf-8");
console.log(`saved: ${output}`);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
第 3 步:运行与验证
bun tools/link-capture.ts --url "https://example.com/some-article"
你应该看到:
- 终端输出
saved: ... - Obsidian
00 Inbox出现新文件 - 文件里有 frontmatter + 摘要 + 原文正文
想更像 Mom:三个增强
- Jina 失败自动回退浏览器抓取(Playwriter)
- 英文正文自动做 EN/ZH 段落对照
- 自动回链 Daily Note(避免内容孤岛)
这三个就是 Mom 链接工作流的“体验关键点”。
常见坑
- 抓取正文很短:目标站点有反爬,需浏览器回退
- 摘要为空:
AI_API_KEY没配好 - Obsidian 没看到文件:
obsidianInboxDir路径写错
本篇完成定义
- 你可以在任何网页 URL 上稳定产出一条 inbox 笔记
- 不依赖手工复制粘贴
- 至少 80% 链接能一次成功落盘
下一篇我们把“写好的 Markdown”接到 share 服务,变成可访问页面。
→ 2026-02-09-Mini-Mom-02-Share服务复刻