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

feat: add provider support

parent c2d5873b
......@@ -34,3 +34,6 @@ __pycache__/
# macOS dir files
.DS_Store
# Claude Provider 配置(含敏感 token,不提交;只保留 .example.json)
config/providers/*.json
!config/providers/*.example.json
This diff is collapsed.
......@@ -9,6 +9,7 @@
# --count N 抓取条数(写入 <source>.conf)
# --mail-to ADDR 收件人(写入 <source>.conf)
# --mail-on-success 0|1 成功是否发邮件(写入 <source>.conf)
# --provider NAME Claude provider 名称(写入 <source>.conf 的 PROVIDER=)
# --yes, -y 跳过确认
# --no-enable 只创建文件,不 enable
# --force 同名 timer 已存在时覆盖
......@@ -32,6 +33,7 @@ TIME=""
COUNT=""
MAIL_TO=""
MAIL_ON_SUCCESS=""
PROVIDER=""
ASSUME_NO=0
NO_ENABLE=0
FORCE=0
......@@ -47,6 +49,7 @@ while [ $# -gt 0 ]; do
--count) COUNT="${2:?--count 需要值}"; shift 2;;
--mail-to) MAIL_TO="${2:?--mail-to 需要值}"; shift 2;;
--mail-on-success) MAIL_ON_SUCCESS="${2:?--mail-on-success 需要值}"; shift 2;;
--provider) PROVIDER="${2:?--provider 需要值}"; shift 2;;
--yes|-y) ASSUME_NO=1; shift;;
--no-enable) NO_ENABLE=1; shift;;
--force) FORCE=1; shift;;
......@@ -149,11 +152,12 @@ echo "=== 即将创建栏目: $SOURCE ==="
echo " 运行时间: $TIME"
[ "$AUTO_TIME" = "1" ] && echo " (自动推算:现有最晚栏目 +15 分钟)"
echo " timer 文件: $TIMER_PATH"
if [ -n "$COUNT" ] || [ -n "$MAIL_TO" ] || [ -n "$MAIL_ON_SUCCESS" ]; then
if [ -n "$COUNT" ] || [ -n "$MAIL_TO" ] || [ -n "$MAIL_ON_SUCCESS" ] || [ -n "$PROVIDER" ]; then
echo " conf 文件: $CONF_PATH"
[ -n "$COUNT" ] && echo " COUNT=$COUNT"
[ -n "$MAIL_TO" ] && echo " MAIL_TO=$MAIL_TO"
[ -n "$MAIL_ON_SUCCESS" ] && echo " MAIL_ON_SUCCESS=$MAIL_ON_SUCCESS"
[ -n "$PROVIDER" ] && echo " PROVIDER=$PROVIDER"
else
echo " conf 文件: (不创建,继承 default.conf)"
fi
......@@ -179,7 +183,7 @@ EOF
ok "已创建 $TIMER_PATH"
# --- 写 conf(仅当有覆盖项) ---
if [ -n "$COUNT" ] || [ -n "$MAIL_TO" ] || [ -n "$MAIL_ON_SUCCESS" ]; then
if [ -n "$COUNT" ] || [ -n "$MAIL_TO" ] || [ -n "$MAIL_ON_SUCCESS" ] || [ -n "$PROVIDER" ]; then
mkdir -p "$CONF_DIR"
{
echo "# /etc/news-harvester/${SOURCE}.conf — 由 add-source.sh 生成 $(date '+%Y-%m-%d %H:%M:%S')"
......@@ -188,6 +192,7 @@ if [ -n "$COUNT" ] || [ -n "$MAIL_TO" ] || [ -n "$MAIL_ON_SUCCESS" ]; then
[ -n "$COUNT" ] && echo "COUNT=$COUNT"
[ -n "$MAIL_TO" ] && echo "MAIL_TO=$MAIL_TO"
[ -n "$MAIL_ON_SUCCESS" ] && echo "MAIL_ON_SUCCESS=$MAIL_ON_SUCCESS"
[ -n "$PROVIDER" ] && echo "PROVIDER=$PROVIDER"
} > "$CONF_PATH"
ok "已创建 $CONF_PATH"
fi
......
......@@ -7,7 +7,9 @@
# 选项:
# --mode resume|repair resume=中断续采(默认),repair=清理重采
# --budget N 单次预算上限美元(默认 15)
# --provider NAME 指定 claude provider(优先级最高;默认用 FALLBACK_PROVIDER)
# --yes, -y 跳过确认
# --debug 详细诊断输出
# --help, -h 显示帮助
#
# 模式说明:
......@@ -34,6 +36,7 @@ MODE="resume"
BUDGET=15
ASSUME_YES=0
DEBUG=0
CLI_PROVIDER=""
usage() {
sed -n '3,28p' "$0"
......@@ -44,6 +47,7 @@ while [ $# -gt 0 ]; do
case "$1" in
--mode) MODE="${2:?--mode 需要值}"; shift 2;;
--budget) BUDGET="${2:?--budget 需要值}"; shift 2;;
--provider) CLI_PROVIDER="${2:?--provider 需要值}"; shift 2;;
--yes|-y) ASSUME_YES=1; shift;;
--debug) DEBUG=1; shift;;
--help|-h) usage; exit 0;;
......@@ -89,10 +93,18 @@ done
MAIL_TO="${MAIL_TO:-}"
MAIL_ON_SUCCESS="${MAIL_ON_SUCCESS:-1}"
ALERT_COST_OVER="${ALERT_COST_OVER:-}"
PROVIDER="${PROVIDER:-}"
FALLBACK_PROVIDER="${FALLBACK_PROVIDER:-}"
WORK_DIR="${WORK_DIR:-/home/yichu/Documents/News-Harvester}"
LOG_DIR="${LOG_DIR:-$WORK_DIR/logs}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# --- 加载 provider 配置,确定使用哪个 provider ---
# continue.sh 默认用 FALLBACK_PROVIDER(因为 continue 本身就是失败重试)
# --provider 参数优先级最高
source "$SCRIPT_DIR/provider.sh"
resolve_continue_provider "$CLI_PROVIDER"
mkdir -p "$LOG_DIR"
# --- 从最新 JSON 日志提取 session_id ---
......@@ -125,8 +137,10 @@ esac
info "找到上次运行日志: $LATEST_JSON"
info "Session ID: $SESSION_ID"
info "模式: ${MODE_TAG} (${MODE})"
info "Provider: ${USED_PROVIDER}"
debug "当前用户: $(whoami)"
debug "claude 路径: $(command -v claude 2>/dev/null || echo 未找到)"
debug "PROVIDER=$PROVIDER FALLBACK_PROVIDER=$FALLBACK_PROVIDER CLI_PROVIDER=$CLI_PROVIDER"
debug "WORK_DIR=$WORK_DIR LOG_DIR=$LOG_DIR"
echo ""
......@@ -187,6 +201,7 @@ debug "PROMPT 长度: ${#PROMPT} 字符"
set +e
claude -p \
"${CLAUDE_SETTINGS_ARGS[@]}" \
--resume "$SESSION_ID" \
--permission-mode bypassPermissions \
--allowed-tools Bash Write \
......@@ -290,6 +305,7 @@ API_SECS=$(awk -v ms="$API_MS" 'BEGIN{printf "%.1f", ms/1000}')
{
echo "===== $(date '+%Y-%m-%d %H:%M:%S') ${MODE_TAG} [${SOURCE}] (mode=${MODE}) ====="
echo "状态: ${STATUS_EMOJI} ${STATUS_TEXT}"
echo "Provider: ${USED_PROVIDER}"
echo "退出码: ${RC}"
echo "耗时: ${MINS}m${SECS}s (wall) / ${API_SECS}s (api)"
echo "成本: \$${COST}"
......
......@@ -35,6 +35,7 @@ CONF_DIR="${CONF_DIR:-/etc/news-harvester}"
MAIL_TO="${MAIL_TO:-}"
DIGEST_MAIL_ALWAYS="${DIGEST_MAIL_ALWAYS:-1}"
PROVIDER="${PROVIDER:-}"
WORK_DIR="${WORK_DIR:-/home/yichu/Documents/News-Harvester}"
LOG_DIR="${LOG_DIR:-$WORK_DIR/logs}"
DIGEST_RETAIN_DAYS="${DIGEST_RETAIN_DAYS:-30}"
......@@ -43,6 +44,10 @@ COST_ALERT_THRESHOLD="${COST_ALERT_THRESHOLD:-1}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# --- 加载 provider 配置,构造 --settings 参数 ---
source "$SCRIPT_DIR/provider.sh"
build_claude_settings_args "$PROVIDER"
mkdir -p "$LOG_DIR"
TODAY="$(date +%Y%m%d)"
TODAY_HUMAN="$(date '+%Y-%m-%d')"
......@@ -149,6 +154,7 @@ SUMMARY_CONTENT="$(cat "$SUMMARY_JSON")"
set +e
claude -p \
"${CLAUDE_SETTINGS_ARGS[@]}" \
--permission-mode bypassPermissions \
--allowed-tools Bash Read Write \
--output-format json \
......@@ -214,6 +220,7 @@ fi
echo ""
echo "数据源: ${UNIQUE_SOURCES} 个(共 ${SOURCE_COUNT} 次运行)"
echo "成功/失败: ${SUCCESS_COUNT}/${FAILED_COUNT}"
echo "Provider: ${USED_PROVIDER}"
echo "总成本: \$${TOTAL_COST}"
echo "总 Turns: ${TOTAL_TURNS} 总耗时: ${TOTAL_DURATION_S}s"
echo "总 Token: 输入 ${TOTAL_IN_TOK} / 输出 ${TOTAL_OUT_TOK}"
......
......@@ -29,11 +29,17 @@ COUNT="${2:-20}"
MAIL_TO="${MAIL_TO:-}"
MAIL_ON_SUCCESS="${MAIL_ON_SUCCESS:-1}"
ALERT_COST_OVER="${ALERT_COST_OVER:-}"
PROVIDER="${PROVIDER:-}"
WORK_DIR="${WORK_DIR:-/home/yichu/Documents/News-Harvester}"
LOG_DIR="${LOG_DIR:-$WORK_DIR/logs}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# --- 加载 provider 配置,构造 --settings 参数 ---
CONF_DIR="${CONF_DIR:-/etc/news-harvester}"
source "$SCRIPT_DIR/provider.sh"
build_claude_settings_args "$PROVIDER"
mkdir -p "$LOG_DIR"
DATE_TAG="$(date +%Y%m%d)"
TIME_TAG="$(date +%H%M%S)"
......@@ -49,6 +55,7 @@ START_EPOCH=$(date +%s)
# 不用 set -e,因为要捕获 claude 的退出码
set +e
claude -p \
"${CLAUDE_SETTINGS_ARGS[@]}" \
--permission-mode bypassPermissions \
--allowed-tools Bash Write \
--output-format json \
......@@ -150,6 +157,7 @@ API_SECS=$(awk -v ms="$API_MS" 'BEGIN{printf "%.1f", ms/1000}')
{
echo "===== $(date '+%Y-%m-%d %H:%M:%S') 抓取 [$SOURCE] ====="
echo "状态: ${STATUS_EMOJI} ${STATUS_TEXT}"
echo "Provider: ${USED_PROVIDER}"
echo "退出码: ${RC}"
echo "耗时: ${MINS}m${SECS}s (wall) / ${API_SECS}s (api)"
echo "成本: \$${COST}"
......
#!/bin/bash
# provider.sh — Claude Provider 配置管理(共享函数库,被 harvest/continue/digest source)
#
# Provider 配置文件存放在 $CONF_DIR/providers/<name>.json,
# 内容是 env 对象(key-value),如:
# {"ANTHROPIC_AUTH_TOKEN":"sk-xxx","ANTHROPIC_BASE_URL":"https://xxx.io",...}
#
# 数据源通过 conf 中的 PROVIDER=<name> 指定主 provider,
# FALLBACK_PROVIDER=<name> 指定备用 provider(continue.sh 默认用这个)。
#
# PROVIDER 为空或 "default" 时不加 --settings,使用系统全局配置。
# 依赖外部变量: CONF_DIR (默认 /etc/news-harvester)
# 设置外部变量: CLAUDE_SETTINGS_ARGS (数组,可能为空)
PROVIDER_DIR="${PROVIDER_DIR:-${CONF_DIR:-/etc/news-harvester}/providers}"
# build_claude_settings_args [provider_name]
# 读取指定 provider 的 JSON,构造 --settings '{"env":{...}}' 参数。
# 不传参数时用 $PROVIDER 变量。
# 结果写入全局数组 CLAUDE_SETTINGS_ARGS(空数组=不加 --settings)。
# 同时设置 USED_PROVIDER 变量记录实际使用的 provider 名。
build_claude_settings_args() {
local name="${1:-${PROVIDER:-}}"
CLAUDE_SETTINGS_ARGS=()
USED_PROVIDER="${name:-default}"
# default 或空 → 不加 --settings
if [ -z "$name" ] || [ "$name" = "default" ]; then
USED_PROVIDER="default"
return 0
fi
local json_file="$PROVIDER_DIR/${name}.json"
if [ ! -f "$json_file" ]; then
echo "[provider] ⚠️ Provider 配置文件不存在: $json_file,退回默认配置" >&2
USED_PROVIDER="default(missing:${name})"
return 0
fi
# 读取 JSON 文件内容,构造 --settings '{"env": <文件内容>}'
local json_content
json_content=$(cat "$json_file" 2>/dev/null)
if [ -z "$json_content" ]; then
echo "[provider] ⚠️ Provider 配置文件为空: $json_file,退回默认配置" >&2
USED_PROVIDER="default(empty:${name})"
return 0
fi
# 校验是否为合法 JSON(需要 jq)
if ! command -v jq >/dev/null 2>&1; then
echo "[provider] ⚠️ 未安装 jq,无法校验 provider JSON,退回默认配置" >&2
USED_PROVIDER="default(no-jq:${name})"
return 0
fi
if ! echo "$json_content" | jq empty 2>/dev/null; then
echo "[provider] ⚠️ Provider JSON 格式无效: $json_file,退回默认配置" >&2
USED_PROVIDER="default(invalid:${name})"
return 0
fi
# 构造 settings JSON: {"env": {...文件内容...}}
local settings_json
settings_json=$(echo "$json_content" | jq -c '{env: .}')
CLAUDE_SETTINGS_ARGS=(--settings "$settings_json")
}
# build_fallback_claude_settings_args
# 读取 FALLBACK_PROVIDER,构造参数。结果同上写入 CLAUDE_SETTINGS_ARGS。
build_fallback_claude_settings_args() {
local fb="${FALLBACK_PROVIDER:-}"
build_claude_settings_args "$fb"
}
# resolve_continue_provider
# continue.sh 专用:确定续采/修复使用哪个 provider。
# 优先级: --provider 参数 > FALLBACK_PROVIDER > PROVIDER > default
# 参数: $1 = CLI 传入的 --provider 值(可能为空)
# 设置: PROVIDER(覆盖为实际使用的值), USED_PROVIDER, CLAUDE_SETTINGS_ARGS
resolve_continue_provider() {
local cli_provider="${1:-}"
if [ -n "$cli_provider" ]; then
# 手动指定优先
PROVIDER="$cli_provider"
elif [ -n "${FALLBACK_PROVIDER:-}" ]; then
# continue.sh 默认用 fallback(因为 continue 本身就是失败重试)
PROVIDER="$FALLBACK_PROVIDER"
fi
# PROVIDER 为空或 default → 不加 --settings
build_claude_settings_args "$PROVIDER"
}
......@@ -44,11 +44,12 @@ show_all_last_logs() {
for f in "$LOG_DIR"/*.last.log; do
[ -f "$f" ] || continue
found=1
local src status_line cost_line session_line mtime_fmt
local src status_line cost_line session_line provider_line mtime_fmt
src=$(basename "$f" .last.log)
status_line=$(grep '^状态:' "$f" 2>/dev/null | head -1)
cost_line=$(grep '^成本:' "$f" 2>/dev/null | head -1)
session_line=$(grep '^Session:' "$f" 2>/dev/null | head -1)
provider_line=$(grep '^Provider:' "$f" 2>/dev/null | head -1)
# 取日志文件修改时间,格式 YYYY-MM-DD HH:MM
mtime_fmt=$(date -r "$f" '+%Y-%m-%d %H:%M' 2>/dev/null || stat -c '%y' "$f" 2>/dev/null | cut -d. -f1 || echo "?")
......@@ -69,15 +70,20 @@ show_all_last_logs() {
session_id="${session_line#Session: }"
[ -z "$session_id" ] && session_id="${session_line#Session:}"
local provider_text
provider_text="${provider_line#Provider: }"
[ -z "$provider_text" ] && provider_text="${provider_line#Provider:}"
[ -z "$provider_text" ] && provider_text="-"
local icon
if echo "$status_text" | grep -q "成功"; then icon="OK"
elif echo "$status_text" | grep -q "失败"; then icon="FAIL"
else icon="?"; fi
if [ -n "$session_id" ]; then
printf "%-6s %-22s %-16s %s | %s | sid:%s\n" "[$icon]" "$src" "$mtime_fmt" "$status_text" "$cost_fmt" "$session_id"
printf "%-6s %-20s %-16s %-14s %s | %s | sid:%s\n" "[$icon]" "$src" "$mtime_fmt" "$provider_text" "$status_text" "$cost_fmt" "$session_id"
else
printf "%-6s %-22s %-16s %s | %s\n" "[$icon]" "$src" "$mtime_fmt" "$status_text" "$cost_fmt"
printf "%-6s %-20s %-16s %-14s %s | %s\n" "[$icon]" "$src" "$mtime_fmt" "$provider_text" "$status_text" "$cost_fmt"
fi
done
shopt -u nullglob
......@@ -182,7 +188,7 @@ show_overview() {
echo "最近运行"
hr
printf "%-6s %-22s %-16s %-20s %-10s %s\n" "" "来源" "修改时间" "状态" "成本" "session"
printf "%-6s %-20s %-16s %-14s %-20s %-10s %s\n" "" "来源" "修改时间" "Provider" "状态" "成本" "session"
show_all_last_logs
}
......@@ -269,6 +275,7 @@ show_source_detail() {
case "$line" in
=====*) echo " $line" ;;
状态:*) printf " %-12s %s\n" "状态:" "${line#状态: }" ;;
Provider:*)printf " %-12s %s\n" "Provider:" "${line#Provider: }" ;;
退出码:*) printf " %-12s %s\n" "退出码:" "${line#退出码: }" ;;
耗时:*) printf " %-12s %s\n" "耗时:" "${line#耗时: }" ;;
成本:*) printf " %-12s %s\n" "成本:" "${line#成本: }" ;;
......
......@@ -19,6 +19,23 @@ MAIL_ON_SUCCESS=1
# 单次抓取成本(美元)超过此值则在邮件标题标记 [ALERT],留空不判断
ALERT_COST_OVER=1
# === Claude Provider(可选) ===
# 指定默认使用的 Claude provider 名称。provider 配置文件存放在:
# /etc/news-harvester/providers/<name>.json
# 每个 JSON 文件是 env 对象(ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL / 模型配置等)。
# 脚本启动 claude 时会加上 --settings '{"env": {...}}' 注入这些环境变量。
#
# 留空或设为 default → 不加 --settings,使用系统全局配置(claude CLI 自带的认证和模型)
# 设为具体名称(如 easyrouter)→ 从 /etc/news-harvester/providers/easyrouter.json 加载
#
# 此处设置的是全局默认,单栏目可在 <source>.conf 中用 PROVIDER= 覆盖。
# PROVIDER=
#
# 全局备用 provider(可选):continue.sh 续采/修复时默认使用此 provider。
# 如果某栏目没有单独设置 FALLBACK_PROVIDER,会继承这里的值。
# 留空 = 续采时退回 PROVIDER(或 default)。
# FALLBACK_PROVIDER=
# === 每日汇总(digest 相关) ===
# 无论是否有告警都发汇总邮件:1=每次发,0=仅告警时发
DIGEST_MAIL_ALWAYS=1
......
# /etc/news-harvester/nikkei-world.conf
# 单栏目覆盖配置示例。仅写需要覆盖的字段,其余继承 default.conf。
# 此例:nikkei-world 抓 30 条,成功不单独发邮件(只在 digest 里看),成本超 $2 告警。
# 同时指定了 Claude provider 和备用 provider。
COUNT=30
MAIL_ON_SUCCESS=0
ALERT_COST_OVER=2
# === Claude Provider(可选) ===
# 指定此栏目使用的 Claude provider,覆盖 default.conf 中的全局设置。
# provider 配置文件: /etc/news-harvester/providers/<name>.json
# 留空或 default → 使用系统全局配置(不加 --settings)
# 也可通过 Web 面板的"编辑栏目"页面或 modify-source.sh --provider 设置
PROVIDER=easyrouter
# 备用 provider(可选):continue.sh 续采/修复时默认使用此 provider。
# 当主 provider 出问题(限流、API 故障等)时,用此 provider 重试。
# 留空 = 续采时退回 PROVIDER(或 default)。
# 也可通过 modify-source.sh --fallback-provider 设置
FALLBACK_PROVIDER=default
{
"ANTHROPIC_AUTH_TOKEN": "sk-xxx",
"ANTHROPIC_BASE_URL": "https://easyrouter.io",
"ANTHROPIC_DEFAULT_FABLE_MODEL": "claude-sonnet-5",
"ANTHROPIC_DEFAULT_FABLE_MODEL_NAME": "claude-sonnet-5",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5",
"ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME": "claude-haiku-4-5",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-sonnet-5",
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME": "claude-sonnet-5",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-5",
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "claude-sonnet-5",
"ANTHROPIC_MODEL": "claude-sonnet-5",
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-5"
}
......@@ -6,6 +6,7 @@
# ./deploy.sh yichu@192.168.1.100 # 显式指定用户
# ./deploy.sh <host> --no-systemd # 只更新 bin 脚本,不动 systemd unit
# ./deploy.sh <host> --no-restart-web # 不重启 web 面板
# ./deploy.sh <host> --no-providers # 不同步 provider 配置
# ./deploy.sh <host> --test # 部署后自动跑一次 nikkei-world 测试
#
# 做的事:
......@@ -14,6 +15,8 @@
# 3. 同步 systemd 模板 service + digest unit 到 /etc/systemd/system/(需要 sudo)
# ⚠️ 栏目 timer(news-harvester@<source>.timer)不在同步范围,由服务器端维护
# 4. daemon-reload
# 5. 同步本地 provider 配置到 /etc/news-harvester/providers/(需要 sudo)
# 只同步 .json 文件(不含 .example),保留服务器端已有的额外 provider
#
# 不动的东西:
# - /etc/news-harvester/*.conf(用户配置)
......@@ -25,6 +28,7 @@ set -euo pipefail
SYNC_SYSTEMD=1
RESTART_WEB=1
SYNC_PROVIDERS=1
RUN_TEST=0
HOST=""
......@@ -33,6 +37,7 @@ while [ $# -gt 0 ]; do
case "$1" in
--no-systemd) SYNC_SYSTEMD=0; shift;;
--no-restart-web) RESTART_WEB=0; shift;;
--no-providers) SYNC_PROVIDERS=0; shift;;
--test) RUN_TEST=1; shift;;
--help|-h)
sed -n '3,24p' "$0"; exit 0;;
......@@ -44,7 +49,7 @@ while [ $# -gt 0 ]; do
esac
done
[ -n "$HOST" ] || { echo "用法: $0 <[user@]host> [--no-systemd] [--no-restart-web] [--test]" >&2; exit 1; }
[ -n "$HOST" ] || { echo "用法: $0 <[user@]host> [--no-systemd] [--no-restart-web] [--no-providers] [--test]" >&2; exit 1; }
# 默认加用户名(如果没写 user@)
case "$HOST" in
......@@ -67,12 +72,13 @@ echo "=== 部署到 $HOST ==="
echo " 项目: $SCRIPT_DIR"
echo " 同步 bin: 是"
[ "$SYNC_SYSTEMD" = "1" ] && echo " 同步 systemd: 是(需要远程 sudo)" || echo " 同步 systemd: 否(--no-systemd)"
[ "$SYNC_PROVIDERS" = "1" ] && echo " 同步 providers: 是(需要远程 sudo)" || echo " 同步 providers: 否(--no-providers)"
[ "$RESTART_WEB" = "1" ] && echo " 重启 web: 是" || echo " 重启 web: 否(--no-restart-web)"
[ "$RUN_TEST" = "1" ] && echo " 部署后测试: 是(--test)"
echo ""
# --- 1. rsync 项目目录 ---
info "1/4 同步项目目录 → ~/news-harvester-pro/"
info "1/6 同步项目目录 → ~/news-harvester-pro/"
rsync -avz --delete \
--exclude='.git' \
--exclude='*.log' \
......@@ -83,23 +89,56 @@ rsync -avz --delete \
ok "项目目录已同步"
# --- 2. 复制 bin + web 到工作目录 ---
info "2/5 复制 bin/*.sh + web/ → ~/Documents/News-Harvester/"
info "2/6 复制 bin/*.sh + web/ → ~/Documents/News-Harvester/"
ssh "$HOST" 'mkdir -p ~/Documents/News-Harvester/bin ~/Documents/News-Harvester/web/templates && cp ~/news-harvester-pro/bin/*.sh ~/Documents/News-Harvester/bin/ && chmod +x ~/Documents/News-Harvester/bin/*.sh && cp ~/news-harvester-pro/web/app.py ~/Documents/News-Harvester/web/ && cp ~/news-harvester-pro/web/templates/*.html ~/Documents/News-Harvester/web/templates/'
ok "bin 脚本 + web 应用已就位"
# --- 3. 同步 systemd unit ---
if [ "$SYNC_SYSTEMD" = "1" ]; then
info "3/5 同步 systemd 模板 + digest + web unit → /etc/systemd/system/"
info "3/6 同步 systemd 模板 + digest + web unit → /etc/systemd/system/"
# 用 -t 分配伪终端,让远程 sudo 能交互式提示输入密码
# 只同步模板 service / digest / web,栏目 timer 由服务器端维护
ssh -t "$HOST" 'sudo cp ~/news-harvester-pro/systemd/news-harvester@.service /etc/systemd/system/ && sudo cp ~/news-harvester-pro/systemd/news-harvester-digest.service /etc/systemd/system/ && sudo cp ~/news-harvester-pro/systemd/news-harvester-digest.timer /etc/systemd/system/ && sudo cp ~/news-harvester-pro/systemd/news-harvester-web.service /etc/systemd/system/ && sudo systemctl daemon-reload'
ok "systemd unit 已同步并 reload"
else
info "3/5 跳过 systemd(--no-systemd)"
info "3/6 跳过 systemd(--no-systemd)"
fi
# --- 4. 提示 Web 面板首次配置 + 重启 ---
info "4/5 Web 面板检查"
# --- 3.5 同步 provider 配置 ---
if [ "$SYNC_PROVIDERS" = "1" ]; then
info "4/6 同步 provider 配置 → /etc/news-harvester/providers/"
# 检查本地是否有 provider JSON(不含 .example)
local_providers=""
if [ -d "$SCRIPT_DIR/config/providers" ]; then
local_providers=$(ls "$SCRIPT_DIR/config/providers/"*.json 2>/dev/null | grep -v '\.example\.json$' || true)
fi
if [ -z "$local_providers" ]; then
info " 本地无 provider JSON(只有 .example),跳过"
else
# 先 rsync 到远程临时目录(普通用户权限),再用 sudo 移到目标目录
# 不用 --delete,保留服务器端已有的 provider
info " 本地 provider 文件:"
for f in $local_providers; do echo " $(basename "$f")"; done
rsync -avz \
--include='*/' \
--include='*.json' \
--exclude='*.example.json' \
--exclude='*' \
"$SCRIPT_DIR/config/providers/" "$HOST:~/news-harvester-pro/.providers-tmp/"
# 远程:创建目录(如不存在),复制 JSON,设置权限,清理临时目录
ssh -t "$HOST" 'sudo mkdir -p /etc/news-harvester/providers && sudo cp ~/news-harvester-pro/.providers-tmp/*.json /etc/news-harvester/providers/ 2>/dev/null && sudo chmod 644 /etc/news-harvester/providers/*.json && rm -rf ~/news-harvester-pro/.providers-tmp' 2>/dev/null \
&& ok "provider 配置已同步" \
|| warn "provider 同步失败(可能需要 sudo 密码),可手动:\n rsync -avz config/providers/*.json $HOST:~/providers-tmp/\n ssh -t $HOST 'sudo mkdir -p /etc/news-harvester/providers && sudo cp ~/providers-tmp/*.json /etc/news-harvester/providers/'"
fi
else
info "4/6 跳过 providers(--no-providers)"
fi
# --- 5. 提示 Web 面板首次配置 + 重启 ---
info "5/6 Web 面板检查"
ssh "$HOST" 'test -f /etc/news-harvester/web.conf && echo "EXISTS" || echo "MISSING"' 2>/dev/null | grep -q EXISTS \
&& ok "web.conf 已存在" \
|| warn "web.conf 不存在,首次部署需创建:\n ssh $HOST 'sudo cp ~/news-harvester-pro/config/web.conf.example /etc/news-harvester/web.conf && sudo chmod 600 /etc/news-harvester/web.conf && sudo nano /etc/news-harvester/web.conf'\n 然后:ssh $HOST 'sudo systemctl enable --now news-harvester-web.service'"
......@@ -116,8 +155,8 @@ else
info "跳过 web 重启(--no-restart-web)"
fi
# --- 5. 完成 ---
info "5/5 完成"
# --- 6. 完成 ---
info "6/6 完成"
echo ""
echo "========================================"
ok "部署完成!"
......
......@@ -9,6 +9,8 @@
# --count N 更新抓取条数(写入/覆盖 <source>.conf)
# --mail-to ADDR 更新收件人
# --mail-on-success 0|1 更新成功是否发邮件
# --provider NAME 更新 Claude provider(写入 PROVIDER=)
# --fallback-provider NAME 更新备用 provider(写入 FALLBACK_PROVIDER=)
# --yes, -y 跳过确认
# --help, -h 显示帮助
#
......@@ -30,6 +32,8 @@ TIME=""
COUNT=""
MAIL_TO=""
MAIL_ON_SUCCESS=""
PROVIDER=""
FALLBACK_PROVIDER=""
ASSUME_NO=0
usage() {
......@@ -43,6 +47,8 @@ while [ $# -gt 0 ]; do
--count) COUNT="${2:?--count 需要值}"; shift 2;;
--mail-to) MAIL_TO="${2:?--mail-to 需要值}"; shift 2;;
--mail-on-success) MAIL_ON_SUCCESS="${2:?--mail-on-success 需要值}"; shift 2;;
--provider) PROVIDER="${2:?--provider 需要值}"; shift 2;;
--fallback-provider) FALLBACK_PROVIDER="${2:?--fallback-provider 需要值}"; shift 2;;
--yes|-y) ASSUME_NO=1; shift;;
--help|-h) usage; exit 0;;
--) shift; break;;
......@@ -122,6 +128,8 @@ read_conf_val() {
CUR_COUNT=$(read_conf_val COUNT)
CUR_MAIL_TO=$(read_conf_val MAIL_TO)
CUR_MOS=$(read_conf_val MAIL_ON_SUCCESS)
CUR_PROVIDER=$(read_conf_val PROVIDER)
CUR_FALLBACK=$(read_conf_val FALLBACK_PROVIDER)
# --- 展示变更计划,确认 ---
echo "=== 修改栏目: $SOURCE ==="
......@@ -129,6 +137,8 @@ echo " 定时时间: $CURRENT_TIME → $TIME"
[ -n "$COUNT" ] && { echo " 抓取条数: ${CUR_COUNT:-(继承default)} → $COUNT"; }
[ -n "$MAIL_TO" ] && { echo " 收件人: ${CUR_MAIL_TO:-(继承default)} → $MAIL_TO"; }
[ -n "$MAIL_ON_SUCCESS" ] && { echo " 成功发信: ${CUR_MOS:-(继承default)} → $MAIL_ON_SUCCESS"; }
[ -n "$PROVIDER" ] && { echo " Provider: ${CUR_PROVIDER:-(继承default)} → $PROVIDER"; }
[ -n "$FALLBACK_PROVIDER" ] && { echo " Fallback: ${CUR_FALLBACK:-(未设置)} → $FALLBACK_PROVIDER"; }
echo " timer 文件: $TIMER_PATH"
if [ "$HAS_DROPIN" = "1" ]; then
warn "检测到 drop-in 覆盖目录: $DROPIN_DIR"
......@@ -160,7 +170,7 @@ update_conf() {
echo "${key}=${val}" >> "$CONF_PATH"
fi
}
if [ -n "$COUNT" ] || [ -n "$MAIL_TO" ] || [ -n "$MAIL_ON_SUCCESS" ]; then
if [ -n "$COUNT" ] || [ -n "$MAIL_TO" ] || [ -n "$MAIL_ON_SUCCESS" ] || [ -n "$PROVIDER" ] || [ -n "$FALLBACK_PROVIDER" ]; then
mkdir -p "$CONF_DIR"
if [ ! -f "$CONF_PATH" ]; then
{
......@@ -172,6 +182,8 @@ if [ -n "$COUNT" ] || [ -n "$MAIL_TO" ] || [ -n "$MAIL_ON_SUCCESS" ]; then
[ -n "$COUNT" ] && { update_conf COUNT "$COUNT"; }
[ -n "$MAIL_TO" ] && { update_conf MAIL_TO "$MAIL_TO"; }
[ -n "$MAIL_ON_SUCCESS" ] && { update_conf MAIL_ON_SUCCESS "$MAIL_ON_SUCCESS"; }
[ -n "$PROVIDER" ] && { update_conf PROVIDER "$PROVIDER"; }
[ -n "$FALLBACK_PROVIDER" ] && { update_conf FALLBACK_PROVIDER "$FALLBACK_PROVIDER"; }
ok "已更新 $CONF_PATH"
fi
......
This diff is collapsed.
......@@ -46,6 +46,17 @@
</select>
</div>
<div class="form-group">
<label for="provider">Claude Provider</label>
<select id="provider" name="provider">
<option value="default">默认 (系统全局配置)</option>
{% for p in providers %}
<option value="{{ p.name }}" {% if form and form.get('provider') == p.name %}selected{% endif %}>{{ p.name }}</option>
{% endfor %}
</select>
<div class="hint">选择该栏目使用的 Claude provider。默认 = 不加 --settings,用系统全局配置。</div>
</div>
<div class="actions">
<button type="submit" class="btn btn-primary">创建栏目</button>
<a class="btn" href="{{ url_for('index') }}">取消</a>
......
......@@ -204,6 +204,7 @@
<a class="logo" href="{{ url_for('index') }}">📰 News Harvester</a>
<a href="{{ url_for('index') }}">栏目列表</a>
<a href="{{ url_for('add_source') }}">添加栏目</a>
<a href="{{ url_for('provider_list') }}">Provider</a>
</nav>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
......
......@@ -24,19 +24,56 @@
<button type="submit" class="btn btn-primary" {% if continue_status and continue_status.running %}disabled{% endif %}>手动触发</button>
</form>
<form method="POST" action="{{ url_for('continue_source', name=name) }}" style="display:inline"
onsubmit="return confirm('确定对 {{ name }} 执行继续采集?\n将恢复上次 Claude 对话,在已有基础上继续完成中断的采集。')">
onsubmit="return confirmContinue('resume', '{{ name }}', '{{ conf.get("FALLBACK_PROVIDER", "") }}', '{{ conf.get("PROVIDER", "") }}')">
<input type="hidden" name="mode" value="resume">
<select name="provider" style="background:#1a1d27;border:1px solid #2a2d37;border-radius:4px;color:#d4d4d4;padding:6px 8px;font-size:13px" {% if continue_status and continue_status.running %}disabled{% endif %}>
<option value="">默认 (FALLBACK)</option>
{% for p in providers %}
<option value="{{ p.name }}">{{ p.name }}</option>
{% endfor %}
</select>
<button type="submit" class="btn" {% if continue_status and continue_status.running %}disabled{% endif %}>
{% if continue_status and continue_status.running and continue_status.mode == 'resume' %}续采中…{% else %}继续采集{% endif %}
</button>
</form>
<form method="POST" action="{{ url_for('continue_source', name=name) }}" style="display:inline"
onsubmit="return confirm('⚠️ 确定对 {{ name }} 执行采集修复?\n\n这将:\n1. 恢复上次 Claude 对话\n2. 清理之前归档的错误数据\n3. 重新完整采集并 force 推送\n\n此操作会删除之前本次采集的文章,无法撤销!')">
onsubmit="return confirmContinue('repair', '{{ name }}', '{{ conf.get("FALLBACK_PROVIDER", "") }}', '{{ conf.get("PROVIDER", "") }}')">
<input type="hidden" name="mode" value="repair">
<select name="provider" style="background:#1a1d27;border:1px solid #2a2d37;border-radius:4px;color:#d4d4d4;padding:6px 8px;font-size:13px" {% if continue_status and continue_status.running %}disabled{% endif %}>
<option value="">默认 (FALLBACK)</option>
{% for p in providers %}
<option value="{{ p.name }}">{{ p.name }}</option>
{% endfor %}
</select>
<button type="submit" class="btn btn-danger" {% if continue_status and continue_status.running %}disabled{% endif %}>
{% if continue_status and continue_status.running and continue_status.mode == 'repair' %}修复中…{% else %}采集修复{% endif %}
</button>
</form>
<script>
function confirmContinue(mode, source, fallbackProvider, currentProvider) {
var selects = document.querySelectorAll('select[name="provider"]');
var form = event.target.closest('form');
var sel = form.querySelector('select[name="provider"]');
var selectedProvider = sel ? sel.value : '';
var modeLabel = mode === 'repair' ? '采集修复' : '继续采集';
// 没有选 provider (用默认),且没有 fallback
if (!selectedProvider && !fallbackProvider) {
var msg = '确定对 ' + source + ' 执行' + modeLabel + '?\n' +
'将恢复上次 Claude 对话' +
(mode === 'repair' ? ',清理之前归档的错误数据,重新完整采集并 force 推送' : ',在已有基础上继续完成中断的采集') + '。\n\n' +
'⚠️ 该数据源没有配置备用 provider(FALLBACK_PROVIDER),\n' +
'将使用与上次相同的 provider 重试。确认继续?';
return confirm(msg);
}
if (mode === 'repair') {
return confirm('⚠️ 确定对 ' + source + ' 执行采集修复?\n\n' +
'这将:\n1. 恢复上次 Claude 对话\n2. 清理之前归档的错误数据\n' +
'3. 重新完整采集并 force 推送\n\n此操作会删除之前本次采集的文章,无法撤销!');
}
return confirm('确定对 ' + source + ' 执行继续采集?\n将恢复上次 Claude 对话,在已有基础上继续完成中断的采集。');
}
</script>
<a class="btn" href="{{ url_for('index') }}">返回列表</a>
<form method="POST" action="{{ url_for('disable_source', name=name) }}" style="display:inline"
onsubmit="return confirm('确定停用栏目 {{ name }}?\n\n这将:\n1. 停止并 disable 定时器\n2. 清空运行时间(不再触发)\n\n文件保留,可随时通过编辑重新设置时间来启用。')">
......@@ -107,6 +144,10 @@
<div class="label">Turns</div>
<div class="value">{{ log_info.get('turns', '-') }}</div>
</div>
<div class="card">
<div class="label">Provider</div>
<div class="value" style="font-size:14px">{{ log_info.get('provider', '-') }}</div>
</div>
<div class="card">
<div class="label">修改时间</div>
<div class="value" style="font-size:14px">{{ log_info.get('mtime', '?') }}</div>
......
......@@ -39,6 +39,28 @@
<div class="hint">当前: {{ conf.get('MAIL_ON_SUCCESS', '(继承 default.conf)') }}</div>
</div>
<div class="form-group">
<label for="provider">Claude Provider</label>
<select id="provider" name="provider">
<option value="default" {% if not conf.get('PROVIDER') or conf.get('PROVIDER') == 'default' %}selected{% endif %}>默认 (系统全局配置)</option>
{% for p in providers %}
<option value="{{ p.name }}" {% if conf.get('PROVIDER') == p.name %}selected{% endif %}>{{ p.name }}</option>
{% endfor %}
</select>
<div class="hint">当前: {{ conf.get('PROVIDER', '(继承 default.conf / 默认)') }}</div>
</div>
<div class="form-group">
<label for="fallback_provider">备用 Provider (FALLBACK)</label>
<select id="fallback_provider" name="fallback_provider">
<option value="default" {% if not conf.get('FALLBACK_PROVIDER') or conf.get('FALLBACK_PROVIDER') == 'default' %}selected{% endif %}>无 (不使用备用)</option>
{% for p in providers %}
<option value="{{ p.name }}" {% if conf.get('FALLBACK_PROVIDER') == p.name %}selected{% endif %}>{{ p.name }}</option>
{% endfor %}
</select>
<div class="hint">continue.sh 续采/修复时默认使用此 provider。当前: {{ conf.get('FALLBACK_PROVIDER', '(未设置)') }}</div>
</div>
<div class="actions">
<button type="submit" class="btn btn-primary">保存修改</button>
<a class="btn" href="{{ url_for('source_detail', name=name) }}">取消</a>
......
......@@ -32,6 +32,7 @@
<th>栏目</th>
<th>状态</th>
<th>运行时间</th>
<th>Provider</th>
<th>上次触发</th>
<th>下次触发</th>
<th>最近运行</th>
......@@ -56,6 +57,16 @@
{% endif %}
</td>
<td><code>{{ s.run_time }}</code></td>
<td>
{% if s.provider %}
<span class="badge ok">{{ s.provider }}</span>
{% else %}
<span style="color:#666">默认</span>
{% endif %}
{% if s.fallback_provider %}
<span style="font-size:11px;color:#888" title="备用 provider">→ {{ s.fallback_provider }}</span>
{% endif %}
</td>
<td>{{ s.last }}</td>
<td>{{ s.next }}</td>
<td>
......
{% extends "base.html" %}
{% block title %}添加 Provider — News Harvester{% endblock %}
{% block content %}
<h1>添加 Provider</h1>
<p style="color:#888;font-size:13px;margin-bottom:16px">
Provider 配置是一个 JSON 对象,包含 Claude 的环境变量。
数据源通过 <code>PROVIDER=name</code> 引用此配置,
脚本启动 Claude 时会加上 <code>--settings '{"env": {...}}'</code>。
</p>
<form method="POST" action="{{ url_for('provider_add') }}">
<div class="form-group">
<label for="name">Provider 名称 *</label>
<input type="text" id="name" name="name" required
pattern="[a-zA-Z0-9_-]+"
placeholder="如 easyrouter"
value="{{ form.name if form else '' }}">
<div class="hint">只能含字母、数字、下划线、连字符。不能用 default(reserved)。</div>
</div>
<div class="form-group">
<label for="json">JSON 配置 *</label>
<textarea id="json" name="json" required rows="16"
style="width:100%;max-width:700px;padding:12px;background:#1a1d27;border:1px solid #2a2d37;border-radius:4px;color:#d4d4d4;font-size:13px;font-family:'SF Mono','Fira Code',monospace;line-height:1.5"
placeholder='{
"ANTHROPIC_AUTH_TOKEN": "sk-xxx",
"ANTHROPIC_BASE_URL": "https://xxx.io",
"ANTHROPIC_MODEL": "claude-sonnet-5"
}'>{{ form.json if form else '{
"ANTHROPIC_AUTH_TOKEN": "sk-xxx",
"ANTHROPIC_BASE_URL": "https://xxx.io",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-5",
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "claude-sonnet-5",
"ANTHROPIC_MODEL": "claude-sonnet-5",
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-5"
}' }}</textarea>
<div class="hint">JSON 对象,key-value 格式。文件将保存到 <code>/etc/news-harvester/providers/&lt;name&gt;.json</code></div>
</div>
<div class="actions">
<button type="submit" class="btn btn-primary">创建 Provider</button>
<a class="btn" href="{{ url_for('provider_list') }}">取消</a>
</div>
</form>
{% endblock %}
{% extends "base.html" %}
{% block title %}Provider {{ provider.name }} — News Harvester{% endblock %}
{% block content %}
<h1>Provider: {{ provider.name }}</h1>
<div class="actions">
<a class="btn" href="{{ url_for('provider_list') }}">返回列表</a>
<form method="POST" action="{{ url_for('provider_delete', name=provider.name) }}" style="display:inline"
onsubmit="return confirm('确定删除 provider {{ provider.name }}?\n正在使用此 provider 的数据源将退回默认配置。')">
<button type="submit" class="btn btn-danger">删除 Provider</button>
</form>
</div>
{% if provider.get('error') %}
<div class="flash error">JSON 解析失败: {{ provider.error }}</div>
{% else %}
<h2>环境变量配置 <span style="font-size:12px;color:#666">(敏感值已脱敏)</span></h2>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for item in provider.env %}
<tr>
<td><code>{{ item.key }}</code></td>
<td style="font-size:13px">{{ item.value }}</td>
</tr>
{% endfor %}
{% if not provider.env %}
<tr><td colspan="2" style="color:#666;text-align:center">无配置项</td></tr>
{% endif %}
</tbody>
</table>
<p style="color:#666;font-size:12px;margin-top:12px">
此 provider 在 Claude 启动时通过 <code>--settings '{"env": {...}}'</code> 注入。
敏感 key(TOKEN/KEY/SECRET/PASSWORD/AUTH)的值已脱敏显示。
</p>
{% endif %}
{% endblock %}
{% extends "base.html" %}
{% block title %}Provider 列表 — News Harvester{% endblock %}
{% block content %}
<div class="actions">
<a class="btn btn-primary" href="{{ url_for('provider_add') }}">+ 添加 Provider</a>
</div>
<h1>Provider 列表 <span style="font-size:14px;color:#666">({{ providers|length }} 个)</span></h1>
<p style="color:#888;font-size:13px;margin-bottom:16px">
Provider 配置存放在 <code>/etc/news-harvester/providers/</code>,
每个 JSON 文件包含 Claude 的 env 配置(ANTHROPIC_AUTH_TOKEN、ANTHROPIC_BASE_URL 等)。
数据源通过 conf 中的 <code>PROVIDER=name</code> 引用;
<code>default</code> 或未设置 = 使用系统全局配置(不加 --settings)。
</p>
<table>
<thead>
<tr>
<th>名称</th>
<th>环境变量数</th>
<th>包含的 Key</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for p in providers %}
<tr>
<td>
<a class="source-link" href="{{ url_for('provider_detail', name=p.name) }}">{{ p.name }}</a>
</td>
<td>{{ p.key_count }}</td>
<td style="font-size:12px;color:#888">
{{ p.env_keys | join(', ') }}
</td>
<td>
<a class="btn btn-sm" href="{{ url_for('provider_detail', name=p.name) }}">详情</a>
<form method="POST" action="{{ url_for('provider_delete', name=p.name) }}" style="display:inline"
onsubmit="return confirm('确定删除 provider {{ p.name }}?\n正在使用此 provider 的数据源将退回默认配置。')">
<button type="submit" class="btn btn-sm btn-danger">删除</button>
</form>
</td>
</tr>
{% endfor %}
{% if not providers %}
<tr>
<td colspan="4" style="color:#666;text-align:center;padding:24px">
暂无 provider 配置。点击上方"添加 Provider"创建。
</td>
</tr>
{% endif %}
</tbody>
</table>
{% endblock %}
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