Commit 155faf6a authored by 郭阔's avatar 郭阔

feat(arch): site-shared browser profiles, auto-recycle and run watchdog

- platform.sh: nh_ensure_instance/nh_entry_write now use per-SITE instance
  (port 9300+hash(site), profiles/<site>/) so one account logs in once per
  site; prevents same-account multi-login session kicking (digitimes/nikkei
  both enforce device limits)
- harvest.sh: quit shared instance on EXIT with port-keyed reference counting
  ($WORK_DIR/runlocks, stale-PID cleanup) — last same-site run recycles
- harvest.sh: hard watchdog kills a hung claude run after MAX_RUNTIME_MIN
  (default 60, per-source conf overridable) so launchd next-day trigger is
  never blocked
parent 12434efb
......@@ -63,11 +63,39 @@ prepare_result_contract || exit 1
# 登录预检:配置了凭据的源自动检查/登录其浏览器实例(失败不中断)
nh_login_preflight "$SOURCE"
# 任务结束(任何退出路径)回收浏览器实例:登录态在 profile 目录,关闭不丢;
# 实例按站点共享 → 按端口引用计数,同站(同端口)最后一个任务退出才回收
_nh_lock_dir="$WORK_DIR/runlocks"
mkdir -p "$_nh_lock_dir" 2>/dev/null || true
_nh_port="${CDP_PORT_OVERRIDE:-$(nh_cdp_port "$(nh_login_site "$SOURCE")" 2>/dev/null)}"
[ -n "$_nh_port" ] && touch "$_nh_lock_dir/port-${_nh_port}.$$.lock"
_nh_quit_instance() {
local skill_dir f pid
rm -f "$_nh_lock_dir/port-${_nh_port}.$$.lock" 2>/dev/null
# 清理已被 kill -9 的任务留下的陈旧锁(锁名含 PID)
for f in "$_nh_lock_dir"/port-"${_nh_port}".*.lock; do
[ -e "$f" ] || continue
pid="${f##*.}"; pid="${pid%.lock}"
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null || rm -f "$f"
done
# 同端口仍有其他采集任务在跑:不回收共享实例
ls "$_nh_lock_dir"/port-"${_nh_port}".*.lock >/dev/null 2>&1 && return 0
skill_dir="$(nh_skill_dir 2>/dev/null)" || return 0
[ -f "$skill_dir/scripts/instance-quit.js" ] || return 0
[ -n "$_nh_port" ] || return 0
CDP_PORT_OVERRIDE="$_nh_port" node "$skill_dir/scripts/instance-quit.js" 2>&1 | sed 's/^/ [instance] /'
return 0
}
trap _nh_quit_instance EXIT
START_EPOCH=$(date +%s)
# --- 运行 claude 无头模式,输出 JSON ---
# 不用 set -e,因为要捕获 claude 的退出码
# --- 运行 claude 无头模式,输出 JSON,带硬超时看门狗 ---
# 无人在场时防挂死:claude 卡住(网络/服务无响应)最迟 MAX_RUNTIME_MIN 分钟被终止,
# 走正常失败路径 → trap 回收浏览器实例,不阻塞次日 launchd 触发。
# 可在栏目 conf 覆盖(如 MAX_RUNTIME_MIN=90),默认 60。
set +e
MAX_RUNTIME_MIN="${MAX_RUNTIME_MIN:-60}"
claude -p \
"${CLAUDE_SETTINGS_ARGS[@]}" \
--permission-mode bypassPermissions \
......@@ -83,8 +111,25 @@ claude -p \
- 所有文章抓取完毕后再依次生成摘要并写入 Obsidian,全部完成后再返回结果。
- 如果某篇文章抓取失败,跳过它继续下一篇,不要因此中断整个任务。
${RESULT_REQUIREMENTS}" \
> "$JSON_OUT" 2> "$RAW_OUT"
> "$JSON_OUT" 2> "$RAW_OUT" &
_claude_pid=$!
(
_deadline=$(( SECONDS + MAX_RUNTIME_MIN * 60 ))
while kill -0 "$_claude_pid" 2>/dev/null; do
if [ "$SECONDS" -ge "$_deadline" ]; then
echo "[watchdog] claude 运行超过 ${MAX_RUNTIME_MIN} 分钟,强制终止(防挂死;浏览器实例由 EXIT trap 回收)" >> "$RAW_OUT"
kill -TERM "$_claude_pid" 2>/dev/null
sleep 5
kill -9 "$_claude_pid" 2>/dev/null
exit 0
fi
sleep 15
done
) &
_watchdog_pid=$!
wait "$_claude_pid"
RC=$?
kill "$_watchdog_pid" 2>/dev/null
set -e
# 如果 claude 完全没输出(可能 PATH 未生效),写入诊断信息到 RAW_OUT
......
......@@ -180,15 +180,16 @@ nh_cdp_port() { # <name> → 按源名稳定的端口(9300-9499),不同源不
}
nh_entry_path() { echo "$WORK_DIR/agents/news-harvester-${1}.sh"; }
nh_entry_write() { # <name> 生成入口脚本(每次重写,保持幂等;注入该源专属浏览器实例)
local name="$1" entry
nh_entry_write() { # <name> 生成入口脚本(每次重写,保持幂等;注入该源所属站点的共享浏览器实例)
local name="$1" entry site
entry="$(nh_entry_path "$name")"
site="$(nh_login_site "$name")"
mkdir -p "$(dirname "$entry")"
if [ "$name" = "digest" ]; then
printf '#!/bin/bash\n# LaunchAgent 入口:%s 每日汇总(文件名显示在 系统设置-登录项)\nexec "%s/daily-digest.sh"\n' "$name" "$NH_BIN_DIR" > "$entry"
else
printf '#!/bin/bash\n# LaunchAgent 入口:%s 定时采集(文件名显示在 系统设置-登录项)\n# 该源专属浏览器实例:独立 CDP 端口 + 独立 profile,并行采集互不干扰\nexport CDP_PORT_OVERRIDE=%s\nexport CDP_PROFILE_OVERRIDE="%s/.news-harvester/profiles/%s"\nexec "%s/harvest.sh" "%s"\n' \
"$name" "$(nh_cdp_port "$name")" "$HOME" "$name" "$NH_BIN_DIR" "$name" > "$entry"
printf '#!/bin/bash\n# LaunchAgent 入口:%s 定时采集(文件名显示在 系统设置-登录项)\n# 站点共享浏览器实例(站点 %s):同站多频道共用一份登录与 profile,避免同账号多登互踢\nexport CDP_PORT_OVERRIDE=%s\nexport CDP_PROFILE_OVERRIDE="%s/.news-harvester/profiles/%s"\nexec "%s/harvest.sh" "%s"\n' \
"$name" "$site" "$(nh_cdp_port "$site")" "$HOME" "$site" "$NH_BIN_DIR" "$name" > "$entry"
fi
chmod +x "$entry"
}
......@@ -395,11 +396,14 @@ nh_login_creds_file() { # <source> → 输出凭据文件路径(站点级优先
done
return 1
}
nh_ensure_instance() { # <source> 确保该源专属浏览器实例在线(已在线为无操作)
local source="$1"
nh_ensure_instance() { # <source> 确保该源所属站点共享浏览器实例在线(已在线为无操作)
# 实例与 profile 按站点(源名第一段)共享:同站同账号只登一次,
# 避免多频道各自登录互相踢会话(digitimes/nikkei 均有设备数上限)。
local source="$1" site
site="$(nh_login_site "$source")"
if nh_is_darwin && [ -z "${CDP_PORT_OVERRIDE:-}" ]; then
export CDP_PORT_OVERRIDE="$(nh_cdp_port "$source")"
export CDP_PROFILE_OVERRIDE="$HOME/.news-harvester/profiles/${source}"
export CDP_PORT_OVERRIDE="$(nh_cdp_port "$site")"
export CDP_PROFILE_OVERRIDE="$HOME/.news-harvester/profiles/${site}"
fi
[ -f "$(nh_skill_dir)/scripts/ensure-chromium.js" ] || return 0
(cd "$(nh_skill_dir)" && node scripts/ensure-chromium.js >/dev/null 2>&1 || true)
......
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