Commit dc2f2cd3 authored by 郭阔's avatar 郭阔

feat: 调度中心新增「跳过本次采集」——只跳下一次定时场,排期不变

- 面板每源操作栏 ⏭ 按钮:写入 .skip-<source> 标记,下次触发列显示'⏭ 将跳过',
  再点可撤销;标记 26h 过期自清
- harvest.sh 开场闸:仅 NH_FROM_SCHEDULER=1(launchd 入口注入)消耗标记并秒退,
  last.log 记'⏭ 已跳过本次采集';手动触发不消耗
- 入口模板统一加 NH_FROM_SCHEDULER=1,现有 14 个入口已重生成
- 实测:设置→入口触发 1 秒跳过、标记消耗、last.log 正确、排期原样
parent de1c1f8d
...@@ -124,6 +124,32 @@ if [ -f "$_nh_wall_file" ]; then ...@@ -124,6 +124,32 @@ if [ -f "$_nh_wall_file" ]; then
fi fi
echo "排队中" > "$LOG_DIR/${SOURCE}.phase" echo "排队中" > "$LOG_DIR/${SOURCE}.phase"
# ── 按需跳过:面板"跳过本次采集"写入 .skip-<SOURCE>,只对定时触发的下一次生效
# (NH_FROM_SCHEDULER=1 由 LaunchAgent 入口注入;手动触发不消耗标记)。消耗后即删,
# 之后的排期不受任何影响。超过 26 小时的陈旧标记自动清理。
_nh_skip_file="$_nh_lock_dir/.skip-${SOURCE}"
if [ -f "$_nh_skip_file" ]; then
if [ $(( $(date +%s) - $(stat -f %m "$_nh_skip_file" 2>/dev/null || echo 0) )) -gt 93600 ]; then
rm -f "$_nh_skip_file" 2>/dev/null || true # 排期已变/遗留,过期清理
elif [ "${NH_FROM_SCHEDULER:-0}" = "1" ]; then
rm -f "$_nh_skip_file" 2>/dev/null || true
echo "$(date '+%F %T') [skip] 按用户要求跳过本次定时采集(标记已消耗,之后的排期不变)"
{
echo "===== $(date '+%Y-%m-%d %H:%M:%S') 抓取 [$SOURCE] ====="
echo "状态: ⏭ 已跳过本次采集"
echo "Provider: (未启动)"
echo ""
echo "----- result -----"
echo "⏭ 按用户要求跳过,未采集、无消耗"
echo ""
echo "说明:"
echo "- 你在面板设置了\"跳过本次采集\",本次定时触发被跳过。"
echo "- 标记已消耗,之后的排期完全不受影响。"
} > "$LOG_DIR/${SOURCE}.last.log"
exit 0
fi
fi
_queue_t0=$SECONDS _queue_t0=$SECONDS
until mkdir "$_nh_queue_dir" 2>/dev/null; do until mkdir "$_nh_queue_dir" 2>/dev/null; do
# 陈旧队列锁自愈:持锁进程已死则窃取 # 陈旧队列锁自愈:持锁进程已死则窃取
......
...@@ -188,7 +188,7 @@ nh_entry_write() { # <name> 生成入口脚本(每次重写,保持幂等;注入 ...@@ -188,7 +188,7 @@ nh_entry_write() { # <name> 生成入口脚本(每次重写,保持幂等;注入
if [ "$name" = "digest" ]; then if [ "$name" = "digest" ]; then
printf '#!/bin/bash\n# LaunchAgent 入口:%s 每日汇总(文件名显示在 系统设置-登录项)\nexec "%s/daily-digest.sh"\n' "$name" "$NH_BIN_DIR" > "$entry" printf '#!/bin/bash\n# LaunchAgent 入口:%s 每日汇总(文件名显示在 系统设置-登录项)\nexec "%s/daily-digest.sh"\n' "$name" "$NH_BIN_DIR" > "$entry"
else else
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' \ printf '#!/bin/bash\n# LaunchAgent 入口:%s 定时采集(文件名显示在 系统设置-登录项)\n# 站点共享浏览器实例(站点 %s):同站多频道共用一份登录与 profile,避免同账号多登互踢\nexport CDP_PORT_OVERRIDE=%s\nexport CDP_PROFILE_OVERRIDE="%s/.news-harvester/profiles/%s"\nexport NH_FROM_SCHEDULER=1\nexec "%s/harvest.sh" "%s"\n' \
"$name" "$site" "$(nh_cdp_port "$site")" "$HOME" "$site" "$NH_BIN_DIR" "$name" > "$entry" "$name" "$site" "$(nh_cdp_port "$site")" "$HOME" "$site" "$NH_BIN_DIR" "$name" > "$entry"
fi fi
chmod +x "$entry" chmod +x "$entry"
......
...@@ -1534,9 +1534,10 @@ def schedule_board(): ...@@ -1534,9 +1534,10 @@ def schedule_board():
buckets[w].pop(0) if buckets.get(w) else "" buckets[w].pop(0) if buckets.get(w) else ""
for w in r["intent"]["runs"] for w in r["intent"]["runs"]
] ]
# 上次运行文章明细(点击徽标展开的详情抽屉用) # 上次运行文章明细(点击徽标展开的详情抽屉用) + 跳过标记状态
for r in rows: for r in rows:
r["last_articles"] = _latest_run_articles(r["name"]) r["last_articles"] = _latest_run_articles(r["name"])
r["skip"] = (Path(pl.default_work_dir()) / "runlocks" / f".skip-{r['name']}").exists()
return render_template("schedules.html", rows=rows, now=now, return render_template("schedules.html", rows=rows, now=now,
digest_next=_next_fire(digest_times, now), digest_next=_next_fire(digest_times, now),
digest_times=digest_times, digest_times=digest_times,
...@@ -1706,6 +1707,26 @@ def schedule_plan_apply(): ...@@ -1706,6 +1707,26 @@ def schedule_plan_apply():
return redirect(url_for("schedule_board")) return redirect(url_for("schedule_board"))
@app.route("/schedule-skip/<name>", methods=["POST"])
@require_auth
def schedule_skip(name):
"""跳过/撤销跳过该源的下一次定时采集(只影响一次,手动触发与后续排期不变)。"""
if not re.match(r"^[a-zA-Z0-9_-]+$", name) or name in ("digest", "web"):
abort(404)
marker = Path(pl.default_work_dir()) / "runlocks" / f".skip-{name}"
if marker.exists():
marker.unlink(missing_ok=True)
flash(f"已撤销 {name} 的跳过设置,后续采集恢复正常", "success")
return redirect(url_for("schedule_board"))
times = [t for t in pl.schedule_times(name).split(",") if t.strip()]
nxt = _next_fire(times, datetime.now())
marker.write_text(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), encoding="utf-8")
flash(f"已设置 {name} 跳过下一次定时采集"
+ (f"(将于 {nxt.strftime('%m-%d %H:%M')} 触发的那场)" if nxt else "")
+ ",手动触发和之后的排期不受影响", "success")
return redirect(url_for("schedule_board"))
@app.route("/schedule-delete/<name>", methods=["POST"]) @app.route("/schedule-delete/<name>", methods=["POST"])
@require_auth @require_auth
def schedule_delete(name): def schedule_delete(name):
......
...@@ -116,7 +116,8 @@ function toggleDrawer(id) { ...@@ -116,7 +116,8 @@ function toggleDrawer(id) {
<td><code>{{ r.times|join(', ') if r.times else '—' }}</code>{% if r.times|length > 1 %} <span class="badge ok">每日{{ r.times|length }}次</span>{% endif %}</td> <td><code>{{ r.times|join(', ') if r.times else '—' }}</code>{% if r.times|length > 1 %} <span class="badge ok">每日{{ r.times|length }}次</span>{% endif %}</td>
<td class="next-cell"> <td class="next-cell">
{% if r.next_in_min is not none %} {% if r.next_in_min is not none %}
{{ r.next }}<br><span class="in">{{ r.next_in_min }} 分钟后</span> {{ r.next }}{% if r.skip %} <span class="badge unknown">⏭ 将跳过</span>{% endif %}<br>
<span class="in">{{ r.next_in_min }} 分钟后{% if r.skip %}(该场将跳过){% endif %}</span>
{% else %}—{% endif %} {% else %}—{% endif %}
</td> </td>
<td> <td>
...@@ -138,6 +139,18 @@ function toggleDrawer(id) { ...@@ -138,6 +139,18 @@ function toggleDrawer(id) {
<button type="submit" class="btn btn-sm" title="立即采集一次">▶ 采集</button> <button type="submit" class="btn btn-sm" title="立即采集一次">▶ 采集</button>
</form> </form>
{% endif %} {% endif %}
{% if r.state == 'active' %}
{% if r.skip %}
<form method="POST" action="/schedule-skip/{{ r.name }}">
<button type="submit" class="btn btn-sm" title="撤销跳过,恢复后续采集" style="color:#e8c34a">⏭ 撤销跳过</button>
</form>
{% else %}
<form method="POST" action="/schedule-skip/{{ r.name }}"
onsubmit="return confirm('跳过 {{ r.name }} 最近一次定时采集?\n只跳过下一场,之后的排期不受影响;手动触发不受限。')">
<button type="submit" class="btn btn-sm" title="只跳过下一次定时采集">⏭ 跳过下次</button>
</form>
{% endif %}
{% endif %}
{% if r.state != 'none' %} {% if r.state != 'none' %}
{% if r.state == 'active' %} {% if r.state == 'active' %}
<form method="POST" action="/toggle/{{ r.name }}" onsubmit="return confirm('停用 {{ r.name }}?(调度与技能一并停用,可随时再开启)')"> <form method="POST" action="/toggle/{{ r.name }}" onsubmit="return confirm('停用 {{ r.name }}?(调度与技能一并停用,可随时再开启)')">
......
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