Commit 1d7e3152 authored by 谢宇轩's avatar 谢宇轩

Merge branch 'feat/source-sharing-bundles'

parents 757910fe 57447c63
......@@ -15,6 +15,8 @@ Fetch latest articles from configured news sources, archive them to Obsidian vau
| Preview latest (read-only) | `node scripts/harvest.js <source_id> <count> --preview` |
| Preview with full body | `node scripts/harvest.js <source_id> <count> --preview --full` |
| Finalize Chinese summaries | `node scripts/finalize.js "<dayDir>"` |
| Merge a summary batch | `node scripts/summary-add.js "<dayDir>"` |
| Check summary coverage | `node scripts/summary-add.js "<dayDir>" --status` |
| Add source | `node scripts/source-manage.js add` |
| List sources | `node scripts/source-manage.js list` |
| Edit source | `node scripts/source-manage.js edit <id>` |
......@@ -235,19 +237,20 @@ It prints a **compact manifest** to stdout (no full body — only a `bodyPreview
> **Browser sources and any source with a recipe** are auto-handled: before fetching, `harvest.js` calls `ensure-chromium.js`, which health-checks the CDP port and launches Chromium itself (cross-platform binary discovery, headless by default) if it isn't up — a no-op if already running. No manual `chromium ... &` or `sleep` is needed. To debug launch failures: `node scripts/ensure-chromium.js` (prints binary path, port, and stderr-log tail on failure).
#### Step 2. Generate Chinese summaries → write `.summaries.json`
From the compact manifest, for each article write one entry to `<dayDir>/.summaries.json`:
#### Step 2. Generate Chinese summaries → write `.summaries.json` (batched)
**Why batch:** writing all summaries in a *single* response hits the model's output-token ceiling once you have ~10+ articles — the response is truncated and the remaining articles silently get no summary (their 原文 keeps the `__SUMMARY_<slug>__` sentinel forever). This failure is **not rare** — it reproduces every time a source yields many articles. Avoid it by working in small batches that each get merged into `.summaries.json` by `summary-add.js`.
Each summary entry has this shape:
```json
[
{
"url": "https://theconversation.com/...-286430",
"titleZh": "特朗普新规或拖慢联邦科研",
"category": "科技/AI",
"tags": ["科技/AI", "The Conversation"],
"summaryZh": "## 事件概述\n...\n\n## 背景\n...\n\n## 关键引语\n> \"...\"\n\n## 影响分析\n..."
}
]
{
"url": "https://theconversation.com/...-286430",
"titleZh": "特朗普新规或拖慢联邦科研",
"category": "科技/AI",
"tags": ["科技/AI", "The Conversation"],
"summaryZh": "## 事件概述\n...\n\n## 背景\n...\n\n## 关键引语\n> \"...\"\n\n## 影响分析\n..."
}
```
- `titleZh`: concise Chinese title (≤30 chars)
......@@ -255,6 +258,23 @@ From the compact manifest, for each article write one entry to `<dayDir>/.summar
- `summaryZh`: the 4-section markdown body (事件概述 / 背景 / 关键引语 / 影响分析)
- The `bodyPreview` in the manifest is enough to write the summary. **Do not re-read the full 原文.md** — if you need more, read a targeted slice.
**Procedure** (loop until every article in the manifest has a summary):
1. Let `N` = number of articles in the manifest. If `N ≤ 8`, you may write all entries to `.summaries.json` directly (one Write) and skip to Step 3. Otherwise work in batches of **≤4**.
2. For each batch: use `Write` to create `<dayDir>/.summaries-batch.json` — a JSON array of ≤4 summary entries — then merge it:
```bash
node scripts/summary-add.js "<dayDir>"
```
This merges the batch into `.summaries.json` (keyed by `url`, so re-supplying an entry overwrites it — no duplicates), then deletes the batch file. Repeat per batch.
3. Once all batches are done, verify coverage and fill any gaps:
```bash
node scripts/summary-add.js "<dayDir>" --status
```
Lists done vs. missing against `.manifest.json`. Write any missing articles as another batch and merge. When `--status` reports `missing: 0`, proceed to Step 3.
> **Interrupted run?** Do not re-run `harvest.js` (it dedups via the registry, so it won't re-fetch). Instead run `node scripts/summary-add.js "<dayDir>" --status` to see which articles still need summaries, then resume from there.
#### Step 3. Run finalize (script stitches summaries in)
```bash
node scripts/finalize.js "<dayDir>"
......@@ -276,6 +296,7 @@ No files are written. stdout is a compact JSON list (one call, no re-fetching /
- **`count` is the target number of NEW articles**, not raw links. `harvest.js` already accounts for registry dedup: it scrolls to collect extra candidate links (`count + seen + buffer`, capped) so the *new* count can still reach `count` even when most of the homepage was already archived. If the run returns **fewer than `count`**, the stderr prints `⚠️ … stream may be exhausted` — that's a genuine source limit (no fresh articles to fetch), **not** a bug; re-running won't help. Wait for new articles or broaden the source instead. Don't re-run `harvest.js` for the same source/day just to "get more" — it deduplicates via the registry, so a second run only fetches genuinely new articles (and merges them into the pending `.manifest.json`, which is safe).
- **Never `Read` the 原文.md files** to write summaries — the `bodyPreview` in the manifest is sufficient. Targeted slice reads are OK if truly needed.
- **Summaries in batches of ≤4, never one big response.** A single response holding 10+ summaries hits the output-token ceiling and is truncated — the tail articles silently get no summary and keep the `__SUMMARY_<slug>__` sentinel. Write each batch to `.summaries-batch.json` and merge via `node scripts/summary-add.js "<dayDir>"`; repeat per batch. `summary-add.js --status` shows what's left. This is mandatory for `N > 8`; recommended for any multi-article run.
- **One `finalize.js` call** after writing `.summaries.json`. Do not hand-write file I/O — the scripts handle all original/registry/index/summary writes.
---
......
{
"format": "news-harvester-source",
"version": 1,
"exportedAt": "2026-07-30T08:41:39.136Z",
"source": {
"id": "nikkei-markets",
"name": "日经亚洲市场",
"homepageUrl": "https://asia.nikkei.com/business/markets",
"method": "browser",
"vaultFolder": "日经亚洲/市场"
},
"files": {
"helpers/nikkei-markets.md": "---\nsource: nikkei-markets\nlistSelector: '[class*=\"StreamArticleCard\"], [class*=\"SpotlightArticleCard\"], [class*=\"SecondaryArticleCard\"]'\nlinkSelector: 'a[href]'\nurlPattern: 'asia\\.nikkei\\.com/(?:business|spotlight|economy|politics|editor-s-picks|location|opinion|features|life-arts|techasia)/[^/?#]+/.+'\nexcludeUrlPattern: '/(?:podcasts|static|infographics|member)/'\nscrollSteps: 3\nscrollWaitMs: 2000\nwaitMs: 15000\nvalidateArticle: true\nbodyMinChars: 500\nbodySelectors:\n - '[class*=\"ArticleBody\"] p'\n - '[class*=\"article-body\"] p'\n - 'article p'\nbodyMinParagraphs: 3\ndateSelector: 'time[datetime]'\nauthorSelector: '[class*=\"author\"] a, [rel=\"author\"]'\nimageSelector: 'img'\nimageMinWidth: 200\nexcludeImage:\n - logo\n - icon\n - avatar\n - profile\n - placeholder\n - outbrainimg.com\ntitleStripSuffix: ' - Nikkei Asia$'\nmaxBodyChars: 15000\n---\n# 日经亚洲市场 抓取备忘\n\n## 列表页链接收集(`/business/markets`)\n\n- **listSelector 用复合卡片选择器**:`[class*=\"StreamArticleCard\"], [class*=\"SpotlightArticleCard\"], [class*=\"SecondaryArticleCard\"]`。实测 `/business/markets` 页面文章卡片分散在这三类容器里(StreamArticleCard 24 篇 + SpotlightArticleCard 3 篇 + SecondaryArticleCard 3 篇 = 30 篇)。\n- 与 nikkei-tech 同属 Nikkei 模板,卡片容器类型一致,复用同一复合选择器。\n- **urlPattern** 覆盖所有可能的文章路径段:`business`、`spotlight`、`economy`、`politics`、`editor-s-picks`、`location`、`opinion`、`features`、`life-arts`、`techasia`。`/[^/?#]+/.+` 要求至少 3 段路径。\n- **excludeUrlPattern** 剔除 `/podcasts/`、`/static/`、`/infographics/`、`/member/` 等非文章路径。\n- **validateArticle: true** 在 fetchPage 阶段用 og:type + JSON-LD + 正文字符数剔除分类页(卡片 tag 链接如 `/business/markets/currencies`、`/business/markets/property` 会被判 isArticle=false 并跳过)。\n- `scrollSteps: 3` 触发懒加载;Nikkei 用 `?page=N` 分页,query strip 自动排除分页链接。\n\n## 详情页正文验证\n\n`validateArticle: true` 让 `fetchPage` 采集判别信号,返回 `isArticle` 布尔;harvest 对 `isArticle === false` 直接 skip:\n\n```\nisArticle = (og:type === 'article' || JSON-LD @type ∈ {NewsArticle, Article})\n && bodyChars >= 500\n && bodyParagraphs >= 3\n```\n\n## 其他\n\n- 正文优先 `[class*=\"ArticleBody\"]`(Nikkei CSS Modules 正文容器)。\n- 部分文章有付费墙,bodyMinChars=500 + bodyMinParagraphs=3 过滤被截断到极短的页面。\n"
}
}
\ No newline at end of file
{
"format": "news-harvester-source",
"version": 1,
"exportedAt": "2026-07-30T08:41:39.076Z",
"source": {
"id": "nikkei-world",
"name": "日经亚洲世界",
"homepageUrl": "https://asia.nikkei.com/location",
"method": "browser",
"vaultFolder": "日经亚洲/世界"
},
"files": {
"helpers/nikkei-world.md": "---\nsource: nikkei-world\nlistSelector: '[class*=\"StreamArticleCard\"], [class*=\"SpotlightArticleCard\"], [class*=\"SecondaryArticleCard\"]'\nlinkSelector: 'a[href]'\nurlPattern: 'asia\\.nikkei\\.com/(?:business|spotlight|economy|politics|editor-s-picks|location|opinion|features|life-arts|techasia)/[^/?#]+/.+'\nexcludeUrlPattern: '/(?:podcasts|static|infographics|member)/'\nscrollSteps: 3\nscrollWaitMs: 2000\nwaitMs: 15000\nvalidateArticle: true\nbodyMinChars: 500\nbodySelectors:\n - '[class*=\"ArticleBody\"] p'\n - '[class*=\"article-body\"] p'\n - 'article p'\nbodyMinParagraphs: 3\ndateSelector: 'time[datetime]'\nauthorSelector: '[class*=\"author\"] a, [rel=\"author\"]'\nimageSelector: 'img'\nimageMinWidth: 200\nexcludeImage:\n - logo\n - icon\n - avatar\n - profile\n - placeholder\n - outbrainimg.com\ntitleStripSuffix: ' - Nikkei Asia$'\nmaxBodyChars: 15000\n---\n# 日经亚洲世界 抓取备忘\n\n## 列表页链接收集(`/location`)\n\n- **listSelector 用复合卡片选择器**:`[class*=\"StreamArticleCard\"], [class*=\"SpotlightArticleCard\"], [class*=\"SecondaryArticleCard\"]`。实测 `/location` 页面文章卡片分散在这三类容器里(StreamArticleCard 26 篇 + SpotlightArticleCard 3 篇 + SecondaryArticleCard 3 篇 = 32 篇)。\n- 与 nikkei-tech 同属 Nikkei 模板,卡片容器类型一致,复用同一复合选择器。\n- **urlPattern** 覆盖所有可能的文章路径段:`business`、`spotlight`、`economy`、`politics`、`editor-s-picks`、`location`、`opinion`、`features`、`life-arts`、`techasia`。`/[^/?#]+/.+` 要求至少 3 段路径。\n- **excludeUrlPattern** 剔除 `/podcasts/`、`/static/`、`/infographics/`、`/member/` 等非文章路径。\n- **validateArticle: true** 在 fetchPage 阶段用 og:type + JSON-LD + 正文字符数剔除分类页(卡片 tag 链接如 `/business/markets/commodities` 会被判 isArticle=false 并跳过)。\n- `scrollSteps: 3` 触发懒加载;Nikkei 用 `?page=N` 分页,query strip 自动排除分页链接。\n\n## 详情页正文验证\n\n`validateArticle: true` 让 `fetchPage` 采集判别信号,返回 `isArticle` 布尔;harvest 对 `isArticle === false` 直接 skip:\n\n```\nisArticle = (og:type === 'article' || JSON-LD @type ∈ {NewsArticle, Article})\n && bodyChars >= 500\n && bodyParagraphs >= 3\n```\n\n## 其他\n\n- 正文优先 `[class*=\"ArticleBody\"]`(Nikkei CSS Modules 正文容器)。\n- 部分文章有付费墙,bodyMinChars=500 + bodyMinParagraphs=3 过滤被截断到极短的页面。\n"
}
}
\ No newline at end of file
......@@ -626,7 +626,11 @@ Examples:
nextStep: `Read this manifest, write Chinese summaries to ${path.join(dayDir, '.summaries.json')}, then run: node scripts/finalize.js "${dayDir}"`,
summariesContract: {
path: path.join(dayDir, '.summaries.json'),
format: 'array of { url, titleZh, category, tags[], summaryZh } — one per article url; summaryZh = the 4 sections markdown (事件概述/背景/关键引语/影响分析)'
format: 'array of { url, titleZh, category, tags[], summaryZh } — one per article url; summaryZh = the 4 sections markdown (事件概述/背景/关键引语/影响分析)',
batchHint: `If ${mergedArticles.length} articles is more than ~8, do NOT write all summaries in one response — ` +
`it will hit the output-token ceiling and get truncated. Work in batches of ≤4: write each batch to ` +
`${path.join(dayDir, '.summaries-batch.json')} and merge with "node scripts/summary-add.js \\"${dayDir}\\"" ` +
`(repeat per batch), then run finalize.js once. Check gaps with "node scripts/summary-add.js \\"${dayDir}\\" --status".`
},
articles: mergedArticles
};
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment