Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
M
macos-runtime
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
郭阔
macos-runtime
Commits
ab0f8df6
Commit
ab0f8df6
authored
Sep 02, 2026
by
谢宇轩
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add provider support
parent
c2d5873b
Changes
22
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
792 additions
and
47 deletions
+792
-47
.gitignore
.gitignore
+3
-0
README.md
README.md
+151
-16
add-source.sh
add-source.sh
+7
-2
continue.sh
bin/continue.sh
+16
-0
daily-digest.sh
bin/daily-digest.sh
+7
-0
harvest.sh
bin/harvest.sh
+8
-0
provider.sh
bin/provider.sh
+91
-0
status.sh
bin/status.sh
+11
-4
default.conf.example
config/default.conf.example
+17
-0
nikkei-world.conf.example
config/nikkei-world.conf.example
+14
-0
easyrouter.json.example
config/providers/easyrouter.json.example
+14
-0
deploy.sh
deploy.sh
+48
-9
modify-source.sh
modify-source.sh
+19
-7
app.py
web/app.py
+149
-7
add.html
web/templates/add.html
+11
-0
base.html
web/templates/base.html
+1
-0
detail.html
web/templates/detail.html
+43
-2
edit.html
web/templates/edit.html
+22
-0
index.html
web/templates/index.html
+11
-0
provider_add.html
web/templates/provider_add.html
+48
-0
provider_detail.html
web/templates/provider_detail.html
+45
-0
providers.html
web/templates/providers.html
+56
-0
No files found.
.gitignore
View file @
ab0f8df6
...
...
@@ -34,3 +34,6 @@ __pycache__/
# macOS dir files
.DS_Store
# Claude Provider 配置(含敏感 token,不提交;只保留 .example.json)
config/providers/*.json
!config/providers/*.example.json
README.md
View file @
ab0f8df6
This diff is collapsed.
Click to expand it.
add-source.sh
View file @
ab0f8df6
...
...
@@ -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
...
...
bin/continue.sh
View file @
ab0f8df6
...
...
@@ -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
}
"
...
...
bin/daily-digest.sh
View file @
ab0f8df6
...
...
@@ -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
}
"
...
...
bin/harvest.sh
View file @
ab0f8df6
...
...
@@ -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/provider.sh
0 → 100644
View file @
ab0f8df6
#!/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
"
}
bin/status.sh
View file @
ab0f8df6
...
...
@@ -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 %-2
2s %-16s %s | %s | sid:%s
\n
"
"[
$icon
]"
"
$src
"
"
$mtime_fm
t
"
"
$status_text
"
"
$cost_fmt
"
"
$session_id
"
printf
"%-6s %-2
0s %-16s %-14s %s | %s | sid:%s
\n
"
"[
$icon
]"
"
$src
"
"
$mtime_fmt
"
"
$provider_tex
t
"
"
$status_text
"
"
$cost_fmt
"
"
$session_id
"
else
printf
"%-6s %-2
2s %-16s %s | %s
\n
"
"[
$icon
]"
"
$src
"
"
$mtime_fm
t
"
"
$status_text
"
"
$cost_fmt
"
printf
"%-6s %-2
0s %-16s %-14s %s | %s
\n
"
"[
$icon
]"
"
$src
"
"
$mtime_fmt
"
"
$provider_tex
t
"
"
$status_text
"
"
$cost_fmt
"
fi
done
shopt
-u
nullglob
...
...
@@ -182,7 +188,7 @@ show_overview() {
echo
"最近运行"
hr
printf
"%-6s %-2
2s %-16s %-20s %-10s %s
\n
"
""
"来源"
"修改时间
"
"状态"
"成本"
"session"
printf
"%-6s %-2
0s %-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
#成本
:
}
"
;;
...
...
config/default.conf.example
View file @
ab0f8df6
...
...
@@ -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
...
...
config/nikkei-world.conf.example
View file @
ab0f8df6
# /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
config/providers/easyrouter.json.example
0 → 100644
View file @
ab0f8df6
{
"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"
}
deploy.sh
View file @
ab0f8df6
...
...
@@ -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
"部署完成!"
...
...
modify-source.sh
View file @
ab0f8df6
...
...
@@ -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,13 +128,17 @@ 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
==="
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
"
$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
{
...
...
@@ -169,9 +179,11 @@ if [ -n "$COUNT" ] || [ -n "$MAIL_TO" ] || [ -n "$MAIL_ON_SUCCESS" ]; then
echo
""
}
>
"
$CONF_PATH
"
fi
[
-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
"
$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
...
...
web/app.py
View file @
ab0f8df6
This diff is collapsed.
Click to expand it.
web/templates/add.html
View file @
ab0f8df6
...
...
@@ -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>
...
...
web/templates/base.html
View file @
ab0f8df6
...
...
@@ -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) %}
...
...
web/templates/detail.html
View file @
ab0f8df6
...
...
@@ -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 confirm
Continue('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 confirm
Continue('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>
...
...
web/templates/edit.html
View file @
ab0f8df6
...
...
@@ -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>
...
...
web/templates/index.html
View file @
ab0f8df6
...
...
@@ -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>
...
...
web/templates/provider_add.html
0 → 100644
View file @
ab0f8df6
{% 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/
<
name
>
.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 %}
web/templates/provider_detail.html
0 → 100644
View file @
ab0f8df6
{% 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 %}
web/templates/providers.html
0 → 100644
View file @
ab0f8df6
{% 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 %}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment