Commit c95a5227 authored by 谢宇轩's avatar 谢宇轩

feat: robust cross-platform Chromium launcher + skill flow cleanup

Chromium launch was fragile: the documented `chromium ... & sleep 5` assumed
`chromium` was on PATH, but on macOS the binary lives inside an .app bundle
and the command silently failed (&>/dev/null swallowed the error). sleep 5
was not a readiness check, getWsUrl() had zero retries, and the health check
depended on python3.

Changes:
- Add scripts/ensure-chromium.js: cross-platform binary discovery (macOS
  .app bundles, Linux PATH, Windows install dirs), stale-SingletonLock
  cleanup, spawn with CDP, poll /json/version until ready (30s), and a
  diagnostic stderr-log tail on timeout. Pure node http, no python3.
  Supports config.chromium.{cdpPort,headless,executablePath,userDataDir}.
- harvest.js: auto-call ensureChromium before browser-source fetch (no-op
  if already running). Sources needing login can set chromium.userDataDir
  to a logged-in profile; auto-launches then carry the cookies.
- fetcher-browser.js: getWsUrl() now retries (6x500ms) and reads the CDP
  port from config (env > config.chromium.cdpPort > 9222).
- config-template.json: add optional chromium block.
- SKILL.md / README.md: replace the manual launch command with the
  auto-launch explanation; add --help docs and dedicated-profile guidance.

Also clean up the skill flow to cut a 16-step run down to ~4:
- All commands now use the `scripts/` prefix (bare `node harvest.js` failed
  with MODULE_NOT_FOUND — 5x in the trace).
- First-Run Setup becomes a single pre-flight gate: print OK -> skip to
  Workflow, don't re-check vault/source/config mid-task.
- Workflow adds a "stay lean" note: if the user named a source, don't run
  source-manage.js list; the archive flow is exactly 3 script calls.
parent eb5973f6
......@@ -64,6 +64,7 @@ news-harvester/
│ ├── harvest.js # 主入口:node harvest.js <source_id> <count> [--preview] [--full]
│ ├── finalize.js # 补全中文摘要:node finalize.js <dayDir>
│ ├── source-manage.js # 数据源增删改查
│ ├── ensure-chromium.js # 跨平台自动发现并启动 Chromium(browser 源用)
│ ├── fetcher-direct.js # 纯 HTTP 抓取(开放站点)
│ └── fetcher-browser.js # Chromium CDP 抓取(JS 渲染站点)
└── data/ # ⚠️ 运行时去重数据,不入库
......@@ -78,7 +79,14 @@ news-harvester/
- [Node.js](https://nodejs.org/) ≥ 18
- 一个可写的 Obsidian vault 目录
- (可选)Chromium —— 仅当使用 `browser` 方法的源(如 reuters)时需要
- (可选)Chromium / Google Chrome / Edge —— 仅当使用 `browser` 方法的源(如 reuters)时需要。脚本会跨平台自动发现已安装的浏览器(macOS `.app`、Linux PATH、Windows 安装目录),推荐手动启动,指定profile 目录和端口
```sh
"/Applications/Chromium.app/Contents/MacOS/Chromium" \
--remote-debugging-port=9222 \
--user-data-dir="$HOME/.news-harvester/chromium-profile" \
--no-first-run --no-default-browser-check &
```
### 1. 克隆并安装依赖
......@@ -240,11 +248,7 @@ YYYYMMDD/
- **正文提取依赖正则**:网站改版可能导致提取失败(正文 < 100 字会被跳过)
- **中文标题/摘要由 Agent 生成**`harvest.js` 在原文/registry/index 中用哨兵 `__SUMMARY_<slug>__` 占位,`finalize.js` 读取 Agent 写的 `.summaries.json` 后替换为真实标题并生成摘要文档
- **browser 方法依赖 Chromium CDP**(端口 9222),需预先启动:
```bash
chromium --no-sandbox --remote-debugging-port=9222 \
--user-data-dir=/tmp/chromium-harvest --disable-gpu "about:blank" &>/dev/null &
```
- **browser 方法自动启动 Chromium**`harvest.js` 在抓取前调用 `ensure-chromium.js`,自动发现并启动浏览器(跨平台:macOS `.app` bundle、Linux PATH、Windows 安装目录),默认 `headless=new`,轮询 `/json/version` 直到就绪。无需手动 `chromium ... &`。若发现失败可在 `config.json``chromium.executablePath` 指定路径;调试用 `node scripts/ensure-chromium.js`(失败时打印二进制路径/端口/stderr 日志尾)。需要登录态的源:设 `chromium.userDataDir` 指向一个登录过的固定 profile,后续自动启动即带 cookie/环境。配置项见 SKILL.md 的 Configuration
- **图片下载无重试**:失败仅跳过
- **Token 卫生**:归档任务为「harvest → 写 .summaries.json → finalize」三步;不要为同一源/日重跑 harvest 仅为"多取几篇"(registry 已去重,二次跑只取真正的新文章并安全合并进待处理 manifest);不要用 Read 反复读原文大文件——manifest 里的 `bodyPreview`(~4000 字)足够写摘要
......
This diff is collapsed.
{
"vaultPath": "/path/to/your/obsidian/vault",
"registryWindowDays": 7,
"chromium": {
"cdpPort": 9222,
"headless": true,
"executablePath": null,
"userDataDir": null
},
"sources": [
{
"id": "the-conversation",
......
This diff is collapsed.
......@@ -5,26 +5,59 @@
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
const WebSocket = require('ws');
// Chromium CDP debug port — must match the --remote-debugging-port used to start
// Chromium (9222 per SKILL.md / README). Override via env if you run elsewhere.
const CDP_PORT = parseInt(process.env.CDP_PORT_OVERRIDE || '9222');
const SKILL_DIR = path.dirname(__dirname);
const CONFIG_PATH = path.join(SKILL_DIR, 'config.json');
const TEMPLATE_PATH = path.join(SKILL_DIR, 'references', 'config-template.json');
// Resolve the CDP port lazily so config.json changes are picked up per run.
// Precedence: env override > config.chromium.cdpPort > default 9222.
function resolveCdpPort() {
if (process.env.CDP_PORT_OVERRIDE) return parseInt(process.env.CDP_PORT_OVERRIDE, 10);
try {
const cfgPath = fs.existsSync(CONFIG_PATH) ? CONFIG_PATH : TEMPLATE_PATH;
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
if (cfg.chromium && cfg.chromium.cdpPort) return parseInt(cfg.chromium.cdpPort, 10);
} catch (e) { /* fall through to default */ }
return 9222;
}
// Retry the /json endpoint a few times: right after ensure-chromium spawns the
// browser, the port may accept connections a beat before /json responds.
function getWsUrl() {
const port = resolveCdpPort();
const MAX_ATTEMPTS = 6;
const STEP_MS = 500;
return new Promise((resolve, reject) => {
http.get(`http://localhost:${CDP_PORT}/json`, (res) => {
let data = '';
res.on('data', d => data += d);
res.on('end', () => {
try {
const tabs = JSON.parse(data);
// 优先找 page 类型的 tab,避免拿到 iframe/service_worker
const tab = tabs.find(t => t.type === 'page') || tabs[0];
resolve(tab.webSocketDebuggerUrl);
} catch (e) { reject(e); }
let attempt = 0;
const tryOnce = () => {
attempt++;
http.get(`http://localhost:${port}/json`, (res) => {
let data = '';
res.on('data', d => data += d);
res.on('end', () => {
try {
const tabs = JSON.parse(data);
// 优先找 page 类型的 tab,避免拿到 iframe/service_worker
const tab = tabs.find(t => t.type === 'page') || tabs[0];
if (!tab || !tab.webSocketDebuggerUrl) {
return reject(new Error(`CDP /json returned no page tab on :${port}`));
}
resolve(tab.webSocketDebuggerUrl);
} catch (e) {
if (attempt < MAX_ATTEMPTS) return setTimeout(tryOnce, STEP_MS);
reject(e);
}
});
}).on('error', (e) => {
if (attempt < MAX_ATTEMPTS) return setTimeout(tryOnce, STEP_MS);
reject(new Error(`Cannot reach CDP on :${port} after ${MAX_ATTEMPTS} tries (${e.message}). Run: node scripts/ensure-chromium.js`));
});
}).on('error', reject);
};
tryOnce();
});
}
......
......@@ -364,6 +364,20 @@ Examples:
? require('./fetcher-browser')
: require('./fetcher-direct');
// For browser sources, ensure a Chromium CDP endpoint is up before fetching.
// ensure-chromium.js is a no-op if already running, so this is safe every run.
if (source.method === 'browser') {
const { ensureChromium } = require('./ensure-chromium');
console.error(' Ensuring Chromium (CDP)...');
try {
const r = await ensureChromium(config);
console.error(` CDP ready on :${r.port}${r.alreadyRunning ? ' (already running)' : ` (pid ${r.pid})`}`);
} catch (e) {
console.error(` Chromium startup failed:\n${e.message}`);
process.exit(1);
}
}
// Build dedup set (skip in preview — preview shows latest regardless)
const seen = preview ? new Set() : buildDedupSet(config, sourceId);
console.error(` Registry: ${seen.size} URLs in last ${config.registryWindowDays || 7} days`);
......@@ -381,7 +395,7 @@ Examples:
// Normalize URLs (strip query params for dedup)
const normalizeUrl = (u) => { try { const p = new URL(u); p.search = ''; return p.toString(); } catch(e) { return u; } };
const normalizedLinks = links.map(u => normalizeUrl(u));
const normalizedLinks = [...new Set(links.map(u => normalizeUrl(u)))];
const newLinks = normalizedLinks.filter(u => !seen.has(u)).slice(0, count);
const skipped = normalizedLinks.filter(u => seen.has(u)).length;
console.error(` New: ${newLinks.length} | Already seen: ${skipped}`);
......
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