Share

外观
风格

Mini Mom 系列 01|30分钟复刻:链接抓取 -> inbox 落盘 -> Daily 回链

2026年2月9日 · 技术

这一篇只做一件事: 把“看到一个链接”变成“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:三个增强

  1. Jina 失败自动回退浏览器抓取(Playwriter)
  2. 英文正文自动做 EN/ZH 段落对照
  3. 自动回链 Daily Note(避免内容孤岛)

这三个就是 Mom 链接工作流的“体验关键点”。


常见坑

  • 抓取正文很短:目标站点有反爬,需浏览器回退
  • 摘要为空:AI_API_KEY 没配好
  • Obsidian 没看到文件:obsidianInboxDir 路径写错

本篇完成定义

  • 你可以在任何网页 URL 上稳定产出一条 inbox 笔记
  • 不依赖手工复制粘贴
  • 至少 80% 链接能一次成功落盘

下一篇我们把“写好的 Markdown”接到 share 服务,变成可访问页面。

2026-02-09-Mini-Mom-02-Share服务复刻