Commit 5e5097d0 authored by 郭阔's avatar 郭阔

feat: setup.sh bootstrap and cross-platform web panel with harvest toggle

- bin/setup.sh: one-command machine bootstrap (auto platform detect) —
  directory layout, dependency check (brew install on consent), claude
  auth check, skills repo auto-clone from SKILLS_REPO_URL + npm install
  + full bundle import (all sources), dual skill symlink registration,
  default.conf/web.conf generation (random panel password), optional
  web panel service install, digest schedule, remaining-manual-steps
  summary; idempotent
- web/app.py: platform_layer integration — paths/timer reads/trigger/
  active-harvest checks unified; add/edit/disable actions now work on
  darwin; per-source harvest toggle route (schedule load/unload +
  skill enable/disable; auto staggered schedule creation on first enable)
- index.html: toggle switch column, unscheduled skill sources merged
  into list, platform-neutral labels
- bin/platform_layer.py: python facade reusing platform.sh via
  'platform.sh call' dispatcher for all mutations; file reads native
parent 3ac999b0
#!/bin/bash
# setup.sh — 一键自举(自动识别 macOS / Linux),新机器从此脚本开始。
#
# 用法:
# ./bin/setup.sh [--skills-repo <git-url>] [--with-web] [--yes]
#
# 做的事(幂等,可重复运行):
# 1. 平台检测 + 目录结构(WORK_DIR / conf / logs / agents)
# 2. 依赖检查:claude / node / jq / python3 / gnu-sed(darwin);brew 可装的征询后自动装
# 3. claude 配置检查(~/.claude/settings.json 的 auth;缺失提示从旧机拷贝)
# 4. 技能仓库:自动 clone(地址来自 --skills-repo 或 default.conf 的 SKILLS_REPO_URL)
# → 三技能 npm install → 全量导入 bundles → vault 配置 → symlink 注册
# 5. default.conf / web.conf 生成(面板密码随机,仅打印一次)
# 6. 可选 --with-web:安装 Web 面板常驻服务(launchd / systemd)
# 7. 收尾:打印剩余人工事项与首个 add-source 命令示例
#
# 环境变量:
# SKILLS_REPO_URL 技能仓库 git 地址(与 --skills-repo 等价)
# WORK_DIR / CONF_DIR / NH_SCHEDULE_DIR 覆盖默认路径
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/platform.sh"
nh_bootstrap_env
ASSUME_YES=0 WITH_WEB=0 SKILLS_REPO_ARG=""
while [ $# -gt 0 ]; do
case "$1" in
--skills-repo) SKILLS_REPO_ARG="${2:?--skills-repo 需要值}"; shift 2;;
--with-web) WITH_WEB=1; shift;;
--yes|-y) ASSUME_YES=1; shift;;
--help|-h) sed -n '2,20p' "$0"; exit 0;;
*) echo "未知参数: $1" >&2; exit 1;;
esac
done
ok() { echo "✅ $*"; }
warn() { echo "⚠️ $*"; }
miss() { echo "❌ $*"; }
confirm() {
[ "$ASSUME_YES" = "1" ] && return 0
local reply; read -rp "$1 [y/N] " reply
case "$reply" in y|Y|yes|YES) return 0 ;; *) return 1 ;; esac
}
echo "=========================================="
echo " News Harvester 自举($NH_OS)"
echo "=========================================="
# ── 1. 目录结构 ────────────────────────────────────────────
WORK_DIR="$(nh_default_work_dir)"
CONF_DIR="$(nh_default_conf_dir "$WORK_DIR")"
LOG_DIR="${LOG_DIR:-$WORK_DIR/logs}"
mkdir -p "$WORK_DIR" "$CONF_DIR/providers" "$LOG_DIR" "$WORK_DIR/agents"
ok "目录就绪: $WORK_DIR"
# ── 2. 依赖检查 ────────────────────────────────────────────
need_brew=()
command -v python3 >/dev/null 2>&1 || { miss "python3 缺失"; exit 1; }
command -v jq >/dev/null 2>&1 || need_brew+=(jq)
command -v node >/dev/null 2>&1 || need_brew+=(node)
command -v git >/dev/null 2>&1 || need_brew+=(git)
if nh_is_darwin; then
sed --version 2>/dev/null | head -1 | grep -q "GNU sed" || need_brew+=(gnu-sed)
fi
if [ ${#need_brew[@]} -gt 0 ]; then
warn "缺少依赖: ${need_brew[*]}"
if nh_is_darwin && command -v brew >/dev/null 2>&1; then
confirm "用 Homebrew 自动安装?" && brew install "${need_brew[@]}" \
&& ok "依赖已安装" || warn "跳过安装,请手动处理后再跑一次"
else
warn "请手动安装后重新运行(Linux: pacman/apt;macOS: 先装 Homebrew)"
fi
else
ok "依赖齐全(python3/jq/node/git$(nh_is_darwin && echo "/gnu-sed"))"
fi
if ! command -v claude >/dev/null 2>&1; then
warn "claude CLI 未安装 —— 采集核心依赖"
echo " 安装:npm install -g @anthropic-ai/claude-code"
echo " 装好后重跑本脚本"
fi
# ── 3. claude 配置检查 ─────────────────────────────────────
if [ -f "$HOME/.claude/settings.json" ] && jq -e '.env.ANTHROPIC_AUTH_TOKEN // empty' "$HOME/.claude/settings.json" >/dev/null 2>&1; then
ok "claude 认证已配置"
else
warn "claude 认证未配置(~/.claude/settings.json 无 ANTHROPIC_AUTH_TOKEN)"
echo " 含敏感信息不入仓库:从旧机拷贝该文件,或在文件中配置:"
echo ' {"env": {"ANTHROPIC_AUTH_TOKEN": "…", "ANTHROPIC_BASE_URL": "…", "ANTHROPIC_MODEL": "…"}}'
fi
# ── 4. 技能仓库(clone + 依赖 + 全量导入) ──────────────────
if [ -n "$SKILLS_REPO_ARG" ]; then
export SKILLS_REPO_URL="$SKILLS_REPO_ARG"
# 写入 default.conf 供后续 setup 增量使用
if [ -f "$CONF_DIR/default.conf" ] && ! grep -q "^SKILLS_REPO_URL=" "$CONF_DIR/default.conf"; then
printf '\n# 技能仓库(自动 clone 与全量导入)\nSKILLS_REPO_URL=%s\nSKILLS_DIR=%s\n' "$SKILLS_REPO_ARG" "$(nh_skills_root)" >> "$CONF_DIR/default.conf"
fi
fi
if [ -d "$(nh_skill_dir)" ]; then
ok "技能仓库已存在: $(nh_skills_root)"
confirm "重新全量导入 bundles?(新提交的源会补装)" && nh_skill_bootstrap
else
if [ -n "${SKILLS_REPO_URL:-}" ]; then
confirm "clone 技能仓库 ${SKILLS_REPO_URL} 到 $(nh_skills_root)?" && nh_skill_bootstrap \
&& ok "技能就绪(全部 bundle 已导入)" || miss "技能仓库初始化失败"
else
warn "技能仓库不存在,且未提供地址"
echo " 重跑:./bin/setup.sh --skills-repo <git-url>"
fi
fi
nh_skill_installed && echo " 已装源:$(nh_skill_list | awk '{printf "%s ", $1}')"
# symlink 注册(claude 技能发现:全局 + 项目级双保险)
if [ -d "$(nh_skill_dir)" ]; then
mkdir -p "$HOME/.agents/skills" "$WORK_DIR/.claude/skills"
for s in news-harvester news-source-analyzer news-hub-feed; do
[ -d "$(nh_skills_root)/skills/$s" ] || continue
ln -sfn "$(nh_skills_root)/skills/$s" "$HOME/.agents/skills/$s"
ln -sfn "$(nh_skills_root)/skills/$s" "$WORK_DIR/.claude/skills/$s"
done
ok "技能 symlink 已注册(~/.agents/skills + $WORK_DIR/.claude/skills)"
fi
# ── 5. 配置生成 ────────────────────────────────────────────
if [ ! -f "$CONF_DIR/default.conf" ]; then
cat > "$CONF_DIR/default.conf" <<EOF
# 全局默认配置(由 setup.sh 生成,可编辑)
COUNT=20
MAIL_TO=
MAIL_ON_SUCCESS=0
MAX_BUDGET_USD=15
ALERT_COST_OVER=
DIGEST_MAIL_ALWAYS=1
DIGEST_RETAIN_DAYS=30
DIGEST_BUDGET_USD=2
OBSIDIAN_VAULT_PATH=$HOME/NewsHub
EOF
ok "已生成 $CONF_DIR/default.conf"
else
ok "default.conf 已存在(保留)"
fi
mkdir -p "$HOME/NewsHub"
if [ ! -f "$CONF_DIR/web.conf" ]; then
WEB_PASS="$(openssl rand -hex 8 2>/dev/null || head -c8 /dev/urandom | xxd -p)"
printf 'WEB_USER=admin\nWEB_PASS=%s\n' "$WEB_PASS" > "$CONF_DIR/web.conf"
chmod 600 "$CONF_DIR/web.conf"
ok "面板认证已生成 —— 账号 admin 密码 $WEB_PASS(仅显示这一次,存于 $CONF_DIR/web.conf)"
fi
# ── 6. Web 面板服务(可选) ────────────────────────────────
if [ "$WITH_WEB" = "1" ]; then
if nh_is_darwin; then
if [ ! -f "$WORK_DIR/web-venv/bin/python" ]; then
python3 -m venv "$WORK_DIR/web-venv" && "$WORK_DIR/web-venv/bin/pip" install -q flask \
&& ok "面板 venv 就绪"
fi
if [ ! -f "$WORK_DIR/run-web.sh" ]; then
cat > "$WORK_DIR/run-web.sh" <<EOF
#!/bin/bash
# Web 面板启动入口(由 setup.sh 生成)
set -euo pipefail
export PATH="/opt/homebrew/bin:/usr/local/bin:\$PATH"
set +u; source "$CONF_DIR/web.conf"; set -u
export WEB_USER="\${WEB_USER:-admin}" WEB_PASS="\${WEB_PASS:?}"
export SYSTEMD_DIR="$HOME/Library/LaunchAgents" CONF_DIR="$CONF_DIR" WORK_DIR="$WORK_DIR" LOG_DIR="$LOG_DIR"
exec "$WORK_DIR/web-venv/bin/python" "$SCRIPT_DIR/../web/app.py"
EOF
chmod +x "$WORK_DIR/run-web.sh"
fi
mkdir -p "$WORK_DIR/agents"
printf '#!/bin/bash\n# LaunchAgent 入口:Web 管理面板(127.0.0.1:8080)\nexec "%s/run-web.sh"\n' "$WORK_DIR" > "$WORK_DIR/agents/news-harvester-web.sh"
chmod +x "$WORK_DIR/agents/news-harvester-web.sh"
cat > "$(nh_schedule_dir)/com.newsharvester.web.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.newsharvester.web</string>
<key>ProgramArguments</key><array><string>$WORK_DIR/agents/news-harvester-web.sh</string></array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>$LOG_DIR/launchd-web.log</string>
<key>StandardErrorPath</key><string>$LOG_DIR/launchd-web.log</string>
</dict>
</plist>
EOF
launchctl bootout "gui/$(id -u)/com.newsharvester.web" >/dev/null 2>&1 || true
launchctl bootstrap "gui/$(id -u)" "$(nh_schedule_dir)/com.newsharvester.web.plist" \
&& ok "面板常驻服务已启动(http://127.0.0.1:8080)"
else
warn "Linux 面板安装请沿用 systemd/news-harvester-web.service(见 README)"
fi
fi
# ── 7. digest 调度(默认 22:00,已存在则跳过) ──────────────
if ! nh_schedule_exists digest; then
confirm "创建每日汇总调度(每天 22:00)?" \
&& nh_schedule_write digest "22:00" && nh_schedule_enable digest \
&& ok "digest 调度已创建(22:00)"
else
ok "digest 调度已存在:$(nh_schedule_read digest)"
fi
echo ""
echo "=========================================="
ok "自举完成"
echo "=========================================="
echo ""
echo "剩余人工事项:"
echo " 1. 浏览器源登录(如 DigiTimes):cd $(nh_skill_dir) && node scripts/ensure-chromium.js --login --url <站点>"
echo " 已有登录脚本的可直接运行(见 ~/NewsHarvester/cdp-login.js 等)"
echo " 2. macOS 需在 系统设置→用户与群组 开启自动登录(launchd 与浏览器的前提)"
echo ""
echo "添加首个采集源(自动错峰):"
echo " $SCRIPT_DIR/add-source.sh <source-id> --time 09:00"
echo " 可用源见:$(nh_skill_dir)/bundles/ 或面板开关一键开启"
echo ""
echo "手动测试一次采集:"
echo " $SCRIPT_DIR/harvest.sh <source-id> 1 (只采 1 条验证)"
This diff is collapsed.
......@@ -4,6 +4,12 @@
{% if auto_refresh %}
<meta http-equiv="refresh" content="10">
{% endif %}
<style>
.switch{width:44px;height:24px;border-radius:12px;border:1px solid #2a2d37;background:#3a3d47;position:relative;cursor:pointer;padding:0;transition:background .15s}
.switch.on{background:#2ea043}
.switch .knob{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#d4d4d4;transition:left .15s;display:block}
.switch.on .knob{left:22px}
</style>
{% endblock %}
{% block content %}
......@@ -30,6 +36,7 @@
<table>
<thead>
<tr>
<th>采集</th>
<th>栏目</th>
<th>状态</th>
<th>运行时间</th>
......@@ -44,6 +51,16 @@
<tbody>
{% for s in sources %}
<tr class="{{ s.status_class }}">
<td>
<form method="POST" action="{{ url_for('toggle_source', name=s.source) }}"
{% if s.toggle_on %}onsubmit="return confirm('关闭 {{ s.source }} 的自动采集?(调度文件保留,可随时再开)')"{% endif %}>
<input type="hidden" name="on" value="{{ '0' if s.toggle_on else '1' }}">
<button type="submit" class="switch {{ 'on' if s.toggle_on else '' }}"
title="{{ '采集开启中,点击关闭' if s.toggle_on else '采集已关闭/未启用,点击开启(无调度时自动错峰创建)' }}">
<span class="knob"></span>
</button>
</form>
</td>
<td>
<a class="source-link" href="{{ url_for('source_detail', name=s.source) }}">{{ s.source }}</a>
</td>
......@@ -122,7 +139,7 @@
</thead>
<tbody>
<tr>
<td><code>news-harvester-digest.timer</code></td>
<td><code>每日汇总</code></td>
<td>
{% if digest_props.active == 'active' %}
<span class="badge active">active</span>
......
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