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
Show 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__/
...
@@ -34,3 +34,6 @@ __pycache__/
# macOS dir files
# macOS dir files
.DS_Store
.DS_Store
# Claude Provider 配置(含敏感 token,不提交;只保留 .example.json)
config/providers/*.json
!config/providers/*.example.json
README.md
View file @
ab0f8df6
...
@@ -19,10 +19,11 @@
...
@@ -19,10 +19,11 @@
│ │ │
│ │ │
│ ▼ │
│ ▼ │
│ harvest.sh <source> <count> │
│ harvest.sh <source> <count> │
│ ├─ 加载 PROVIDER → --settings '{"env":{...}}' │
│ ├─ claude -p --output-format json │
│ ├─ claude -p --output-format json │
│ ├─ 解析元数据(成本/turns/token/session_id) │
│ ├─ 解析元数据(成本/turns/token/session_id) │
│ ├─ 落盘: logs/<source>.<date>.<time>.json │
│ ├─ 落盘: logs/<source>.<date>.<time>.json │
│ │ logs/<source>.last.log
│
│ │ logs/<source>.last.log
(含 Provider)
│
│ └─ 按状态发邮件(✅/❌/⚠️,含成本告警) │
│ └─ 按状态发邮件(✅/❌/⚠️,含成本告警) │
│ │
│ │
├─────────────────────────────────────────────────────────┤
├─────────────────────────────────────────────────────────┤
...
@@ -36,6 +37,7 @@
...
@@ -36,6 +37,7 @@
│ daily-digest.sh │
│ daily-digest.sh │
│ ├─ 扫描 logs/ 下今天的 *.json │
│ ├─ 扫描 logs/ 下今天的 *.json │
│ ├─ jq 提取每源元数据 → 紧凑 JSON 数组 │
│ ├─ jq 提取每源元数据 → 紧凑 JSON 数组 │
│ ├─ 加载 PROVIDER → --settings (可选) │
│ ├─ claude -p --json-schema(结构化分析) │
│ ├─ claude -p --json-schema(结构化分析) │
│ │ → {alert, alert_reasons, digest, total_cost} │
│ │ → {alert, alert_reasons, digest, total_cost} │
│ ├─ 写 logs/digest.<date>.md │
│ ├─ 写 logs/digest.<date>.md │
...
@@ -52,7 +54,8 @@
...
@@ -52,7 +54,8 @@
news-harvester-pro/
news-harvester-pro/
├── bin/
├── bin/
│ ├── harvest.sh # 单源抓取(被 @.service 调用)
│ ├── harvest.sh # 单源抓取(被 @.service 调用)
│ ├── continue.sh # 续采/修复(--resume 恢复上次对话)
│ ├── continue.sh # 续采/修复(--resume 恢复上次对话,默认用 FALLBACK_PROVIDER)
│ ├── provider.sh # Claude Provider 配置管理(共享函数库)
│ ├── notify-mail.sh # 通用邮件发送(msmtp)
│ ├── notify-mail.sh # 通用邮件发送(msmtp)
│ ├── daily-digest.sh # 每日汇总 + 告警(被 digest.service 调用)
│ ├── daily-digest.sh # 每日汇总 + 告警(被 digest.service 调用)
│ └── status.sh # 友好查看 timer 状态 / 栏目详情 / 汇总
│ └── status.sh # 友好查看 timer 状态 / 栏目详情 / 汇总
...
@@ -63,16 +66,18 @@ news-harvester-pro/
...
@@ -63,16 +66,18 @@ news-harvester-pro/
│ ├── news-harvester-digest.service # 每日汇总服务
│ ├── news-harvester-digest.service # 每日汇总服务
│ └── news-harvester-digest.timer # 每天 22:00
│ └── news-harvester-digest.timer # 每天 22:00
├── config/
├── config/
│ ├── default.conf.example # 全局默认配置(COUNT/MAIL_*/DIGEST_*)
│ ├── default.conf.example # 全局默认配置(COUNT/MAIL_*/
PROVIDER/
DIGEST_*)
│ ├── msmtprc.example # msmtp SMTP 配置示例
│ ├── msmtprc.example # msmtp SMTP 配置示例
│ └── nikkei-world.conf.example # 单栏目覆盖示例
│ ├── nikkei-world.conf.example # 单栏目覆盖示例(含 PROVIDER/FALLBACK_PROVIDER)
│ └── providers/ # Claude Provider 配置目录
│ └── easyrouter.json.example # Provider JSON 示例(脱敏)
├── migrate.sh # 智能迁移脚本(备份+安装+保留配置)
├── migrate.sh # 智能迁移脚本(备份+安装+保留配置)
├── add-source.sh # 新增栏目脚本(自动错开时间+生成timer/conf)
├── add-source.sh # 新增栏目脚本(自动错开时间+生成timer/conf)
├── modify-source.sh # 修改栏目定时时间及配置(改OnCalendar+conf+重启timer)
├── modify-source.sh # 修改栏目定时时间及配置(改OnCalendar+conf+重启timer)
├── deploy.sh # 一键部署到服务器(rsync + 同步 bin + systemd)
├── deploy.sh # 一键部署到服务器(rsync + 同步 bin + systemd
+ providers
)
├── web/ # Flask Web 管理面板
├── web/ # Flask Web 管理面板
│ ├── app.py # 单文件 Flask 应用(list/add/edit/detail/run)
│ ├── app.py # 单文件 Flask 应用(list/add/edit/detail/run
/provider CRUD
)
│ └── templates/ # Jinja2 模板(base/index/add/edit/detail)
│ └── templates/ # Jinja2 模板(base/index/add/edit/detail
/providers
)
└── README.md
└── README.md
```
```
...
@@ -88,6 +93,7 @@ news-harvester-pro/
...
@@ -88,6 +93,7 @@ news-harvester-pro/
|
`systemd/news-harvester-web.service`
|
`/etc/systemd/system/`
|
|
`systemd/news-harvester-web.service`
|
`/etc/systemd/system/`
|
|
`config/default.conf`
|
`/etc/news-harvester/default.conf`
|
|
`config/default.conf`
|
`/etc/news-harvester/default.conf`
|
|
`config/<source>.conf`
|
`/etc/news-harvester/<source>.conf`
|
|
`config/<source>.conf`
|
`/etc/news-harvester/<source>.conf`
|
|
`config/providers/*.json`
|
`/etc/news-harvester/providers/*.json`
(Claude Provider 配置) |
|
`config/web.conf`
|
`/etc/news-harvester/web.conf`
(Web 面板认证) |
|
`config/web.conf`
|
`/etc/news-harvester/web.conf`
(Web 面板认证) |
|
`.msmtprc`
|
`~/.msmtprc`
(用户家目录) |
|
`.msmtprc`
|
`~/.msmtprc`
(用户家目录) |
...
@@ -149,6 +155,12 @@ sudo ./migrate.sh
...
@@ -149,6 +155,12 @@ sudo ./migrate.sh
# 只更新 bin 脚本,不动 systemd(改脚本不需要 sudo 时用)
# 只更新 bin 脚本,不动 systemd(改脚本不需要 sudo 时用)
./deploy.sh 192.168.1.100
--no-systemd
./deploy.sh 192.168.1.100
--no-systemd
# 不同步 provider 配置(只改了脚本,不动 provider)
./deploy.sh 192.168.1.100
--no-providers
# 不重启 web 面板
./deploy.sh 192.168.1.100
--no-restart-web
# 部署后自动跑一次 nikkei-world 测试并实时看日志
# 部署后自动跑一次 nikkei-world 测试并实时看日志
./deploy.sh 192.168.1.100
--test
./deploy.sh 192.168.1.100
--test
```
```
...
@@ -157,8 +169,10 @@ deploy.sh 做的事:
...
@@ -157,8 +169,10 @@ deploy.sh 做的事:
1.
`rsync`
整个项目到
`~/news-harvester-pro/`
(
`--delete`
保持镜像,排除 logs/.git)
1.
`rsync`
整个项目到
`~/news-harvester-pro/`
(
`--delete`
保持镜像,排除 logs/.git)
2.
复制
`bin/*.sh`
→
`~/Documents/News-Harvester/bin/`
并赋可执行权限
2.
复制
`bin/*.sh`
→
`~/Documents/News-Harvester/bin/`
并赋可执行权限
3.
同步 systemd 模板 service + digest unit →
`/etc/systemd/system/`
并
`daemon-reload`
3.
同步 systemd 模板 service + digest unit →
`/etc/systemd/system/`
并
`daemon-reload`
4.
同步本地
`config/providers/*.json`
→
`/etc/systemd/system/providers/`
(需要 sudo,不含
`.example.json`
,不用
`--delete`
保留服务器端新增的 provider)
5.
检查 web.conf + 重启 web 面板(可选)
**不动的**
:栏目 timer(服务器端可能改过时间)、
`/etc/news-harvester/*.conf`
、
`~/.msmtprc`
、logs 目录。
**不动的**
:栏目 timer(服务器端可能改过时间)、
`/etc/news-harvester/*.conf`
(栏目配置)
、
`~/.msmtprc`
、logs 目录。
---
---
...
@@ -179,6 +193,8 @@ service 模板按顺序加载两个 `EnvironmentFile`,后者覆盖前者(systemd
...
@@ -179,6 +193,8 @@ service 模板按顺序加载两个 `EnvironmentFile`,后者覆盖前者(systemd
|
`MAIL_TO`
| 收件人;
**留空不发邮件**
;支持多个(空格或逗号分隔) | 空 | harvest + digest |
|
`MAIL_TO`
| 收件人;
**留空不发邮件**
;支持多个(空格或逗号分隔) | 空 | harvest + digest |
|
`MAIL_ON_SUCCESS`
| 成功是否发邮件:1=发,0=仅失败发 | 1 | harvest |
|
`MAIL_ON_SUCCESS`
| 成功是否发邮件:1=发,0=仅失败发 | 1 | harvest |
|
`ALERT_COST_OVER`
| 单次成本超此值(美元)则标题标
`[ALERT]`
| 空(不判断) | harvest |
|
`ALERT_COST_OVER`
| 单次成本超此值(美元)则标题标
`[ALERT]`
| 空(不判断) | harvest |
|
`PROVIDER`
| Claude provider 名称;空或
`default`
= 系统全局配置 | 空 | harvest + digest |
|
`FALLBACK_PROVIDER`
| 备用 provider;continue.sh 续采/修复时默认使用 | 空 | continue |
|
`DIGEST_MAIL_ALWAYS`
| 每日汇总:1=每次发,0=仅告警时发 | 1 | digest |
|
`DIGEST_MAIL_ALWAYS`
| 每日汇总:1=每次发,0=仅告警时发 | 1 | digest |
|
`DIGEST_RETAIN_DAYS`
| 旧日志保留天数 | 30 | digest |
|
`DIGEST_RETAIN_DAYS`
| 旧日志保留天数 | 30 | digest |
|
`DIGEST_BUDGET_USD`
| 汇总分析的 claude 预算上限 | 2 | digest |
|
`DIGEST_BUDGET_USD`
| 汇总分析的 claude 预算上限 | 2 | digest |
...
@@ -193,6 +209,8 @@ COUNT=20
...
@@ -193,6 +209,8 @@ COUNT=20
MAIL_TO
=
you@example.com
MAIL_TO
=
you@example.com
MAIL_ON_SUCCESS
=
1
MAIL_ON_SUCCESS
=
1
ALERT_COST_OVER
=
1
ALERT_COST_OVER
=
1
# PROVIDER= # 留空=系统全局配置;设为 easyrouter 等则用对应 provider
# FALLBACK_PROVIDER= # 全局备用 provider(可选)
DIGEST_MAIL_ALWAYS
=
1
DIGEST_MAIL_ALWAYS
=
1
DIGEST_RETAIN_DAYS
=
30
DIGEST_RETAIN_DAYS
=
30
DIGEST_BUDGET_USD
=
2
DIGEST_BUDGET_USD
=
2
...
@@ -203,10 +221,109 @@ DIGEST_BUDGET_USD=2
...
@@ -203,10 +221,109 @@ DIGEST_BUDGET_USD=2
COUNT
=
30
COUNT
=
30
MAIL_ON_SUCCESS
=
0 # 成功不单独发邮件,只在 digest 里看
MAIL_ON_SUCCESS
=
0 # 成功不单独发邮件,只在 digest 里看
ALERT_COST_OVER
=
2
ALERT_COST_OVER
=
2
PROVIDER
=
easyrouter # 此栏目用 easyrouter provider(覆盖全局默认)
FALLBACK_PROVIDER
=
default # 续采时退回系统默认配置
```
```
---
---
## Claude Provider 配置
Provider 机制允许不同数据源使用不同的 Claude API 端点和认证(第三方中转、多 key 轮换等),而无需修改脚本代码。
### 工作原理
1.
Provider 配置是 JSON 文件,存放在
`/etc/news-harvester/providers/<name>.json`
2.
每个 JSON 文件是一个 env 对象,包含 Claude 的环境变量:
```
json
{
"ANTHROPIC_AUTH_TOKEN"
:
"sk-xxx"
,
"ANTHROPIC_BASE_URL"
:
"https://easyrouter.io"
,
"ANTHROPIC_MODEL"
:
"claude-sonnet-5"
,
"ANTHROPIC_DEFAULT_SONNET_MODEL"
:
"claude-sonnet-5"
,
"CLAUDE_CODE_SUBAGENT_MODEL"
:
"claude-sonnet-5"
}
```
3.
数据源通过 conf 中的
`PROVIDER=<name>`
引用;脚本启动 Claude 时自动加上
`--settings '{"env": {...}}'`
注入这些环境变量
4.
`PROVIDER`
留空或设为
`default`
→ 不加
`--settings`
,使用系统全局配置(Claude CLI 自带的认证和模型)
5.
`FALLBACK_PROVIDER`
→ continue.sh 续采/修复时
**默认使用**
此 provider(因为 continue 本身就是失败重试)
6.
continue.sh 也支持
`--provider <name>`
参数手动指定,优先级最高
### Provider 优先级(continue.sh)
```
--provider 参数 > FALLBACK_PROVIDER(conf) > PROVIDER(conf/default.conf) > default
```
### 创建 Provider
#### 方式一:Web 面板(推荐)
1.
浏览器打开 Web 面板 → 导航栏 "Provider"
2.
点击 "添加 Provider"
3.
填入名称 + JSON 配置 → 创建
4.
在栏目编辑页面选择该栏目使用的 Provider 和 Fallback Provider
#### 方式二:手动创建
```
bash
# 1. 创建目录
sudo mkdir
-p
/etc/news-harvester/providers
# 2. 创建 provider JSON
sudo
nano /etc/news-harvester/providers/easyrouter.json
# 粘入 JSON 内容(参考 config/providers/easyrouter.json.example)
# 3. 设置权限
sudo chmod
644 /etc/news-harvester/providers/easyrouter.json
# 4. 在栏目 conf 中引用
echo
"PROVIDER=easyrouter"
|
sudo tee
-a
/etc/news-harvester/nikkei-world.conf
echo
"FALLBACK_PROVIDER=default"
|
sudo tee
-a
/etc/news-harvester/nikkei-world.conf
```
### 从本地部署 Provider 到服务器
将本地
`config/providers/*.json`
(不含
`.example.json`
)同步到服务器:
```
bash
# deploy.sh 默认会同步 providers(需要远程 sudo)
./deploy.sh 192.168.1.100
# 只同步 providers 不动其他(配合 --no-systemd --no-restart-web)
./deploy.sh 192.168.1.100
--no-systemd
--no-restart-web
# 跳过 provider 同步
./deploy.sh 192.168.1.100
--no-providers
```
deploy.sh 同步 provider 时
**不用 `--delete`**
,保留服务器端通过 Web 面板创建的 provider,只覆盖/新增同名文件。
### Provider 在日志中的记录
每次运行的
`.last.log`
会记录实际使用的 provider:
```
===== 2026-09-02 10:00:00 抓取 [nikkei-world] =====
状态: ✅ 成功
Provider: easyrouter
退出码: 0
...
```
status.sh 概览和详情模式都会显示 Provider 列/行。Web 面板的栏目列表和详情页也会显示。
### 注意事项
-
Provider JSON 中的
`ANTHROPIC_AUTH_TOKEN`
等敏感值在 Web 面板详情页会
**脱敏显示**
(只显示前 8 位)
-
`default`
是保留名称,不能用作 provider 文件名
-
Provider 文件不存在或 JSON 无效时,脚本会 warn 并退回默认配置(不加
`--settings`
),不会中断运行
-
digest(每日汇总)使用
`default.conf`
中的
`PROVIDER`
(通常不设或 default)
---
## 日志文件
## 日志文件
每次抓取运行会落盘:
每次抓取运行会落盘:
...
@@ -284,7 +401,7 @@ echo -e 'To: you@example.com\nSubject: test\n\nhello' | msmtp you@example.com
...
@@ -284,7 +401,7 @@ echo -e 'To: you@example.com\nSubject: test\n\nhello' | msmtp you@example.com
## Web 管理面板
## Web 管理面板
除了命令行操作,项目还提供一个 Flask Web 面板,可以在浏览器中查看所有栏目状态、添加/编辑/
删除栏目、手动触发抓取和汇总
。
除了命令行操作,项目还提供一个 Flask Web 面板,可以在浏览器中查看所有栏目状态、添加/编辑/
停用栏目、管理 Claude Provider、手动触发抓取和汇总、续采/修复
。
### 首次安装
### 首次安装
...
@@ -311,13 +428,18 @@ ssh -L 8080:127.0.0.1:8080 yichu@<server-ip>
...
@@ -311,13 +428,18 @@ ssh -L 8080:127.0.0.1:8080 yichu@<server-ip>
| 页面 | 路径 | 功能 |
| 页面 | 路径 | 功能 |
|------|------|------|
|------|------|------|
| 栏目列表 |
`/`
| 所有 source 的 timer 状态、运行时间、
上次/下次触发、最近运行摘要(状态/成本/session
) |
| 栏目列表 |
`/`
| 所有 source 的 timer 状态、运行时间、
Provider、上次/下次触发、最近运行摘要(状态/成本/session/provider
) |
| 栏目详情 |
`/source/<name>`
| timer 信息、配置、最近运行日志(.last.log 全文)、Claude JSON 元数据
(成本/turns/tokens/session_id/权限拒绝
) |
| 栏目详情 |
`/source/<name>`
| timer 信息、配置、最近运行日志(.last.log 全文)、Claude JSON 元数据
、续采/修复按钮(带 provider 选择
) |
| 添加栏目 |
`/add`
| 表单创建新栏目,调用 add-source.sh(自动错开时间) |
| 添加栏目 |
`/add`
| 表单创建新栏目,调用 add-source.sh(自动错开时间
,可选 provider
) |
| 编辑栏目 |
`/edit/<name>`
| 表单修改运行时间/抓取条数/收件人,调用 modify-source.sh |
| 编辑栏目 |
`/edit/<name>`
| 表单修改运行时间/抓取条数/收件人
/provider/fallback
,调用 modify-source.sh |
| 手动触发 |
`/run/<name>`
| 一键触发指定栏目的抓取 |
| 手动触发 |
`/run/<name>`
| 一键触发指定栏目的抓取 |
| 手动汇总 |
`/digest`
| 一键触发每日汇总 |
| 手动汇总 |
`/digest`
| 一键触发每日汇总(后台异步,页面轮询进度) |
| 删除栏目 |
`/delete/<name>`
| 停用并删除 timer/conf(需二次确认) |
| 续采/修复 |
`/continue/<name>`
| 后台异步执行 continue.sh,支持选择 provider,页面轮询进度 |
| 停用栏目 |
`/disable/<name>`
| 停用 timer + 清空 OnCalendar(不删文件,可重新编辑启用) |
| Provider 列表 |
`/providers`
| 所有 provider 配置(名称、env key 数量) |
| Provider 详情 |
`/provider/<name>`
| 查看 provider 配置(敏感值脱敏) |
| 添加 Provider |
`/provider/add`
| 创建新 provider(JSON 输入) |
| 删除 Provider |
`/provider/delete/<name>`
| 删除 provider(使用中的栏目退回默认) |
### 安全说明
### 安全说明
...
@@ -412,11 +534,15 @@ cat ~/Documents/News-Harvester/logs/digest.$(date +%Y%m%d).md
...
@@ -412,11 +534,15 @@ cat ~/Documents/News-Harvester/logs/digest.$(date +%Y%m%d).md
cd
~/Documents/News-Harvester/bin
cd
~/Documents/News-Harvester/bin
# 续采(默认):在已有基础上继续完成中断的采集
# 续采(默认):在已有基础上继续完成中断的采集
# 默认使用 FALLBACK_PROVIDER(如果配置了),否则用 PROVIDER
./continue.sh nikkei-world
--mode
resume
./continue.sh nikkei-world
--mode
resume
# 修复:清理上次错误数据后重新完整采集(force 推送)
# 修复:清理上次错误数据后重新完整采集(force 推送)
./continue.sh nikkei-world
--mode
repair
./continue.sh nikkei-world
--mode
repair
# 手动指定 provider(优先级最高,覆盖 FALLBACK_PROVIDER)
./continue.sh nikkei-world
--mode
resume
--provider
easyrouter
# 指定预算上限(默认 $15)
# 指定预算上限(默认 $15)
./continue.sh nikkei-world
--mode
resume
--budget
10
./continue.sh nikkei-world
--mode
resume
--budget
10
...
@@ -467,6 +593,8 @@ sudo ./modify-source.sh <source-name> --time HH:MM [选项]
...
@@ -467,6 +593,8 @@ sudo ./modify-source.sh <source-name> --time HH:MM [选项]
|
`--count N`
| 更新抓取条数,写入/覆盖
`<source>.conf`
|
|
`--count N`
| 更新抓取条数,写入/覆盖
`<source>.conf`
|
|
`--mail-to ADDR`
| 更新收件人 |
|
`--mail-to ADDR`
| 更新收件人 |
|
`--mail-on-success 0\|1`
| 更新成功是否发邮件 |
|
`--mail-on-success 0\|1`
| 更新成功是否发邮件 |
|
`--provider NAME`
| 更新 Claude provider,写入
`PROVIDER=`
|
|
`--fallback-provider NAME`
| 更新备用 provider,写入
`FALLBACK_PROVIDER=`
|
|
`--yes`
/
`-y`
| 跳过确认 |
|
`--yes`
/
`-y`
| 跳过确认 |
示例:
示例:
...
@@ -517,6 +645,7 @@ sudo ./add-source.sh <source-name> [选项]
...
@@ -517,6 +645,7 @@ sudo ./add-source.sh <source-name> [选项]
|
`--count N`
| 抓取条数,写入
`<source>.conf`
|
|
`--count N`
| 抓取条数,写入
`<source>.conf`
|
|
`--mail-to ADDR`
| 收件人,写入
`<source>.conf`
|
|
`--mail-to ADDR`
| 收件人,写入
`<source>.conf`
|
|
`--mail-on-success 0\|1`
| 成功是否发邮件,写入
`<source>.conf`
|
|
`--mail-on-success 0\|1`
| 成功是否发邮件,写入
`<source>.conf`
|
|
`--provider NAME`
| Claude provider 名称,写入
`PROVIDER=`
|
|
`--yes`
/
`-y`
| 跳过确认 |
|
`--yes`
/
`-y`
| 跳过确认 |
|
`--no-enable`
| 只创建文件不启用 |
|
`--no-enable`
| 只创建文件不启用 |
|
`--force`
| 同名 timer 已存在时覆盖 |
|
`--force`
| 同名 timer 已存在时覆盖 |
...
@@ -605,7 +734,13 @@ harvest.sh 用 `jq` 解析 JSON,提取元数据写入日志和邮件。digest.sh
...
@@ -605,7 +734,13 @@ harvest.sh 用 `jq` 解析 JSON,提取元数据写入日志和邮件。digest.sh
## 注意事项
## 注意事项
1.
**PATH 问题(最易踩坑)**
:systemd 不加载 shell profile,脚本循环 source 了
`.bashrc/.bash_profile/.profile/.zshrc`
。若手动测试仍报
`claude: command not found`
,先
`which claude`
拿绝对路径替换脚本里的
`claude`
,或取消脚本里
`export PATH=...`
注释。
1.
**PATH 问题(最易踩坑)**
:systemd 和
`sudo -u yichu`
都是非交互 shell,不加载
`.zshrc`
。所有脚本(harvest/continue/digest)在开头显式设置 PATH 和 nvm:
```bash
export NVM_DIR="/home/yichu/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
export PATH="/home/yichu/.local/bin:/home/yichu/.nvm/versions/node/v24.14.0/bin:$PATH"
```
若 Node 版本升级导致路径变化,需同步修改这三个脚本中的
`v24.14.0`
。
2.
**jq 必需**
:新版依赖
`jq`
解析 JSON 输出。未安装时 harvest 会降级为无法提取元数据,digest 直接报错退出。
2.
**jq 必需**
:新版依赖
`jq`
解析 JSON 输出。未安装时 harvest 会降级为无法提取元数据,digest 直接报错退出。
...
...
add-source.sh
View file @
ab0f8df6
...
@@ -9,6 +9,7 @@
...
@@ -9,6 +9,7 @@
# --count N 抓取条数(写入 <source>.conf)
# --count N 抓取条数(写入 <source>.conf)
# --mail-to ADDR 收件人(写入 <source>.conf)
# --mail-to ADDR 收件人(写入 <source>.conf)
# --mail-on-success 0|1 成功是否发邮件(写入 <source>.conf)
# --mail-on-success 0|1 成功是否发邮件(写入 <source>.conf)
# --provider NAME Claude provider 名称(写入 <source>.conf 的 PROVIDER=)
# --yes, -y 跳过确认
# --yes, -y 跳过确认
# --no-enable 只创建文件,不 enable
# --no-enable 只创建文件,不 enable
# --force 同名 timer 已存在时覆盖
# --force 同名 timer 已存在时覆盖
...
@@ -32,6 +33,7 @@ TIME=""
...
@@ -32,6 +33,7 @@ TIME=""
COUNT
=
""
COUNT
=
""
MAIL_TO
=
""
MAIL_TO
=
""
MAIL_ON_SUCCESS
=
""
MAIL_ON_SUCCESS
=
""
PROVIDER
=
""
ASSUME_NO
=
0
ASSUME_NO
=
0
NO_ENABLE
=
0
NO_ENABLE
=
0
FORCE
=
0
FORCE
=
0
...
@@ -47,6 +49,7 @@ while [ $# -gt 0 ]; do
...
@@ -47,6 +49,7 @@ while [ $# -gt 0 ]; do
--count
)
COUNT
=
"
${
2
:?--count
需要值
}
"
;
shift
2
;;
--count
)
COUNT
=
"
${
2
:?--count
需要值
}
"
;
shift
2
;;
--mail-to
)
MAIL_TO
=
"
${
2
:?--mail-to
需要值
}
"
;
shift
2
;;
--mail-to
)
MAIL_TO
=
"
${
2
:?--mail-to
需要值
}
"
;
shift
2
;;
--mail-on-success
)
MAIL_ON_SUCCESS
=
"
${
2
:?--mail-on-success
需要值
}
"
;
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
;;
--yes
|
-y
)
ASSUME_NO
=
1
;
shift
;;
--no-enable
)
NO_ENABLE
=
1
;
shift
;;
--no-enable
)
NO_ENABLE
=
1
;
shift
;;
--force
)
FORCE
=
1
;
shift
;;
--force
)
FORCE
=
1
;
shift
;;
...
@@ -149,11 +152,12 @@ echo "=== 即将创建栏目: $SOURCE ==="
...
@@ -149,11 +152,12 @@ echo "=== 即将创建栏目: $SOURCE ==="
echo
" 运行时间:
$TIME
"
echo
" 运行时间:
$TIME
"
[
"
$AUTO_TIME
"
=
"1"
]
&&
echo
" (自动推算:现有最晚栏目 +15 分钟)"
[
"
$AUTO_TIME
"
=
"1"
]
&&
echo
" (自动推算:现有最晚栏目 +15 分钟)"
echo
" timer 文件:
$TIMER_PATH
"
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
"
echo
" conf 文件:
$CONF_PATH
"
[
-
n
"
$COUNT
"
]
&&
echo
" COUNT=
$COUNT
"
[
-
n
"
$COUNT
"
]
&&
echo
" COUNT=
$COUNT
"
[
-
n
"
$MAIL_TO
"
]
&&
echo
" MAIL_TO=
$MAIL_TO
"
[
-
n
"
$MAIL_TO
"
]
&&
echo
" MAIL_TO=
$MAIL_TO
"
[
-
n
"
$MAIL_ON_SUCCESS
"
]
&&
echo
" MAIL_ON_SUCCESS=
$MAIL_ON_SUCCESS
"
[
-
n
"
$MAIL_ON_SUCCESS
"
]
&&
echo
" MAIL_ON_SUCCESS=
$MAIL_ON_SUCCESS
"
[
-
n
"
$PROVIDER
"
]
&&
echo
" PROVIDER=
$PROVIDER
"
else
else
echo
" conf 文件: (不创建,继承 default.conf)"
echo
" conf 文件: (不创建,继承 default.conf)"
fi
fi
...
@@ -179,7 +183,7 @@ EOF
...
@@ -179,7 +183,7 @@ EOF
ok
"已创建
$TIMER_PATH
"
ok
"已创建
$TIMER_PATH
"
# --- 写 conf(仅当有覆盖项) ---
# --- 写 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
"
mkdir
-
p
"
$CONF_DIR
"
{
{
echo
"# /etc/news-harvester/
${
SOURCE
}
.conf — 由 add-source.sh 生成
$(
date
'+%Y-%m-%d %H:%M:%S'
)
"
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
...
@@ -188,6 +192,7 @@ if [ -n "$COUNT" ] || [ -n "$MAIL_TO" ] || [ -n "$MAIL_ON_SUCCESS" ]; then
[
-
n
"
$COUNT
"
]
&&
echo
"COUNT=
$COUNT
"
[
-
n
"
$COUNT
"
]
&&
echo
"COUNT=
$COUNT
"
[
-
n
"
$MAIL_TO
"
]
&&
echo
"MAIL_TO=
$MAIL_TO
"
[
-
n
"
$MAIL_TO
"
]
&&
echo
"MAIL_TO=
$MAIL_TO
"
[
-
n
"
$MAIL_ON_SUCCESS
"
]
&&
echo
"MAIL_ON_SUCCESS=
$MAIL_ON_SUCCESS
"
[
-
n
"
$MAIL_ON_SUCCESS
"
]
&&
echo
"MAIL_ON_SUCCESS=
$MAIL_ON_SUCCESS
"
[
-
n
"
$PROVIDER
"
]
&&
echo
"PROVIDER=
$PROVIDER
"
}
>
"
$CONF_PATH
"
}
>
"
$CONF_PATH
"
ok
"已创建
$CONF_PATH
"
ok
"已创建
$CONF_PATH
"
fi
fi
...
...
bin/continue.sh
View file @
ab0f8df6
...
@@ -7,7 +7,9 @@
...
@@ -7,7 +7,9 @@
# 选项:
# 选项:
# --mode resume|repair resume=中断续采(默认),repair=清理重采
# --mode resume|repair resume=中断续采(默认),repair=清理重采
# --budget N 单次预算上限美元(默认 15)
# --budget N 单次预算上限美元(默认 15)
# --provider NAME 指定 claude provider(优先级最高;默认用 FALLBACK_PROVIDER)
# --yes, -y 跳过确认
# --yes, -y 跳过确认
# --debug 详细诊断输出
# --help, -h 显示帮助
# --help, -h 显示帮助
#
#
# 模式说明:
# 模式说明:
...
@@ -34,6 +36,7 @@ MODE="resume"
...
@@ -34,6 +36,7 @@ MODE="resume"
BUDGET
=
15
BUDGET
=
15
ASSUME_YES
=
0
ASSUME_YES
=
0
DEBUG
=
0
DEBUG
=
0
CLI_PROVIDER
=
""
usage
()
{
usage
()
{
sed
-n
'3,28p'
"
$0
"
sed
-n
'3,28p'
"
$0
"
...
@@ -44,6 +47,7 @@ while [ $# -gt 0 ]; do
...
@@ -44,6 +47,7 @@ while [ $# -gt 0 ]; do
case
"
$1
"
in
case
"
$1
"
in
--mode
)
MODE
=
"
${
2
:?--mode
需要值
}
"
;
shift
2
;;
--mode
)
MODE
=
"
${
2
:?--mode
需要值
}
"
;
shift
2
;;
--budget
)
BUDGET
=
"
${
2
:?--budget
需要值
}
"
;
shift
2
;;
--budget
)
BUDGET
=
"
${
2
:?--budget
需要值
}
"
;
shift
2
;;
--provider
)
CLI_PROVIDER
=
"
${
2
:?--provider
需要值
}
"
;
shift
2
;;
--yes
|
-y
)
ASSUME_YES
=
1
;
shift
;;
--yes
|
-y
)
ASSUME_YES
=
1
;
shift
;;
--debug
)
DEBUG
=
1
;
shift
;;
--debug
)
DEBUG
=
1
;
shift
;;
--help
|
-h
)
usage
;
exit
0
;;
--help
|
-h
)
usage
;
exit
0
;;
...
@@ -89,10 +93,18 @@ done
...
@@ -89,10 +93,18 @@ done
MAIL_TO
=
"
${
MAIL_TO
:-}
"
MAIL_TO
=
"
${
MAIL_TO
:-}
"
MAIL_ON_SUCCESS
=
"
${
MAIL_ON_SUCCESS
:-
1
}
"
MAIL_ON_SUCCESS
=
"
${
MAIL_ON_SUCCESS
:-
1
}
"
ALERT_COST_OVER
=
"
${
ALERT_COST_OVER
:-}
"
ALERT_COST_OVER
=
"
${
ALERT_COST_OVER
:-}
"
PROVIDER
=
"
${
PROVIDER
:-}
"
FALLBACK_PROVIDER
=
"
${
FALLBACK_PROVIDER
:-}
"
WORK_DIR
=
"
${
WORK_DIR
:-
/home/yichu/Documents/News-Harvester
}
"
WORK_DIR
=
"
${
WORK_DIR
:-
/home/yichu/Documents/News-Harvester
}
"
LOG_DIR
=
"
${
LOG_DIR
:-
$WORK_DIR
/logs
}
"
LOG_DIR
=
"
${
LOG_DIR
:-
$WORK_DIR
/logs
}
"
SCRIPT_DIR
=
"
$(
cd
"
$(
dirname
"
$0
"
)
"
&&
pwd
)
"
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
"
mkdir
-p
"
$LOG_DIR
"
# --- 从最新 JSON 日志提取 session_id ---
# --- 从最新 JSON 日志提取 session_id ---
...
@@ -125,8 +137,10 @@ esac
...
@@ -125,8 +137,10 @@ esac
info
"找到上次运行日志:
$LATEST_JSON
"
info
"找到上次运行日志:
$LATEST_JSON
"
info
"Session ID:
$SESSION_ID
"
info
"Session ID:
$SESSION_ID
"
info
"模式:
${
MODE_TAG
}
(
${
MODE
}
)"
info
"模式:
${
MODE_TAG
}
(
${
MODE
}
)"
info
"Provider:
${
USED_PROVIDER
}
"
debug
"当前用户:
$(
whoami
)
"
debug
"当前用户:
$(
whoami
)
"
debug
"claude 路径:
$(
command
-v
claude 2>/dev/null
||
echo
未找到
)
"
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
"
debug
"WORK_DIR=
$WORK_DIR
LOG_DIR=
$LOG_DIR
"
echo
""
echo
""
...
@@ -187,6 +201,7 @@ debug "PROMPT 长度: ${#PROMPT} 字符"
...
@@ -187,6 +201,7 @@ debug "PROMPT 长度: ${#PROMPT} 字符"
set
+e
set
+e
claude
-p
\
claude
-p
\
"
${
CLAUDE_SETTINGS_ARGS
[@]
}
"
\
--resume
"
$SESSION_ID
"
\
--resume
"
$SESSION_ID
"
\
--permission-mode
bypassPermissions
\
--permission-mode
bypassPermissions
\
--allowed-tools
Bash Write
\
--allowed-tools
Bash Write
\
...
@@ -290,6 +305,7 @@ API_SECS=$(awk -v ms="$API_MS" 'BEGIN{printf "%.1f", ms/1000}')
...
@@ -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
"=====
$(
date
'+%Y-%m-%d %H:%M:%S'
)
${
MODE_TAG
}
[
${
SOURCE
}
] (mode=
${
MODE
}
) ====="
echo
"状态:
${
STATUS_EMOJI
}
${
STATUS_TEXT
}
"
echo
"状态:
${
STATUS_EMOJI
}
${
STATUS_TEXT
}
"
echo
"Provider:
${
USED_PROVIDER
}
"
echo
"退出码:
${
RC
}
"
echo
"退出码:
${
RC
}
"
echo
"耗时:
${
MINS
}
m
${
SECS
}
s (wall) /
${
API_SECS
}
s (api)"
echo
"耗时:
${
MINS
}
m
${
SECS
}
s (wall) /
${
API_SECS
}
s (api)"
echo
"成本:
\$
${
COST
}
"
echo
"成本:
\$
${
COST
}
"
...
...
bin/daily-digest.sh
View file @
ab0f8df6
...
@@ -35,6 +35,7 @@ CONF_DIR="${CONF_DIR:-/etc/news-harvester}"
...
@@ -35,6 +35,7 @@ CONF_DIR="${CONF_DIR:-/etc/news-harvester}"
MAIL_TO
=
"
${
MAIL_TO
:-}
"
MAIL_TO
=
"
${
MAIL_TO
:-}
"
DIGEST_MAIL_ALWAYS
=
"
${
DIGEST_MAIL_ALWAYS
:-
1
}
"
DIGEST_MAIL_ALWAYS
=
"
${
DIGEST_MAIL_ALWAYS
:-
1
}
"
PROVIDER
=
"
${
PROVIDER
:-}
"
WORK_DIR
=
"
${
WORK_DIR
:-
/home/yichu/Documents/News-Harvester
}
"
WORK_DIR
=
"
${
WORK_DIR
:-
/home/yichu/Documents/News-Harvester
}
"
LOG_DIR
=
"
${
LOG_DIR
:-
$WORK_DIR
/logs
}
"
LOG_DIR
=
"
${
LOG_DIR
:-
$WORK_DIR
/logs
}
"
DIGEST_RETAIN_DAYS
=
"
${
DIGEST_RETAIN_DAYS
:-
30
}
"
DIGEST_RETAIN_DAYS
=
"
${
DIGEST_RETAIN_DAYS
:-
30
}
"
...
@@ -43,6 +44,10 @@ COST_ALERT_THRESHOLD="${COST_ALERT_THRESHOLD:-1}"
...
@@ -43,6 +44,10 @@ COST_ALERT_THRESHOLD="${COST_ALERT_THRESHOLD:-1}"
SCRIPT_DIR
=
"
$(
cd
"
$(
dirname
"
$0
"
)
"
&&
pwd
)
"
SCRIPT_DIR
=
"
$(
cd
"
$(
dirname
"
$0
"
)
"
&&
pwd
)
"
# --- 加载 provider 配置,构造 --settings 参数 ---
source
"
$SCRIPT_DIR
/provider.sh"
build_claude_settings_args
"
$PROVIDER
"
mkdir
-p
"
$LOG_DIR
"
mkdir
-p
"
$LOG_DIR
"
TODAY
=
"
$(
date
+%Y%m%d
)
"
TODAY
=
"
$(
date
+%Y%m%d
)
"
TODAY_HUMAN
=
"
$(
date
'+%Y-%m-%d'
)
"
TODAY_HUMAN
=
"
$(
date
'+%Y-%m-%d'
)
"
...
@@ -149,6 +154,7 @@ SUMMARY_CONTENT="$(cat "$SUMMARY_JSON")"
...
@@ -149,6 +154,7 @@ SUMMARY_CONTENT="$(cat "$SUMMARY_JSON")"
set
+e
set
+e
claude
-p
\
claude
-p
\
"
${
CLAUDE_SETTINGS_ARGS
[@]
}
"
\
--permission-mode
bypassPermissions
\
--permission-mode
bypassPermissions
\
--allowed-tools
Bash Read Write
\
--allowed-tools
Bash Read Write
\
--output-format
json
\
--output-format
json
\
...
@@ -214,6 +220,7 @@ fi
...
@@ -214,6 +220,7 @@ fi
echo
""
echo
""
echo
"数据源:
${
UNIQUE_SOURCES
}
个(共
${
SOURCE_COUNT
}
次运行)"
echo
"数据源:
${
UNIQUE_SOURCES
}
个(共
${
SOURCE_COUNT
}
次运行)"
echo
"成功/失败:
${
SUCCESS_COUNT
}
/
${
FAILED_COUNT
}
"
echo
"成功/失败:
${
SUCCESS_COUNT
}
/
${
FAILED_COUNT
}
"
echo
"Provider:
${
USED_PROVIDER
}
"
echo
"总成本:
\$
${
TOTAL_COST
}
"
echo
"总成本:
\$
${
TOTAL_COST
}
"
echo
"总 Turns:
${
TOTAL_TURNS
}
总耗时:
${
TOTAL_DURATION_S
}
s"
echo
"总 Turns:
${
TOTAL_TURNS
}
总耗时:
${
TOTAL_DURATION_S
}
s"
echo
"总 Token: 输入
${
TOTAL_IN_TOK
}
/ 输出
${
TOTAL_OUT_TOK
}
"
echo
"总 Token: 输入
${
TOTAL_IN_TOK
}
/ 输出
${
TOTAL_OUT_TOK
}
"
...
...
bin/harvest.sh
View file @
ab0f8df6
...
@@ -29,11 +29,17 @@ COUNT="${2:-20}"
...
@@ -29,11 +29,17 @@ COUNT="${2:-20}"
MAIL_TO
=
"
${
MAIL_TO
:-}
"
MAIL_TO
=
"
${
MAIL_TO
:-}
"
MAIL_ON_SUCCESS
=
"
${
MAIL_ON_SUCCESS
:-
1
}
"
MAIL_ON_SUCCESS
=
"
${
MAIL_ON_SUCCESS
:-
1
}
"
ALERT_COST_OVER
=
"
${
ALERT_COST_OVER
:-}
"
ALERT_COST_OVER
=
"
${
ALERT_COST_OVER
:-}
"
PROVIDER
=
"
${
PROVIDER
:-}
"
WORK_DIR
=
"
${
WORK_DIR
:-
/home/yichu/Documents/News-Harvester
}
"
WORK_DIR
=
"
${
WORK_DIR
:-
/home/yichu/Documents/News-Harvester
}
"
LOG_DIR
=
"
${
LOG_DIR
:-
$WORK_DIR
/logs
}
"
LOG_DIR
=
"
${
LOG_DIR
:-
$WORK_DIR
/logs
}
"
SCRIPT_DIR
=
"
$(
cd
"
$(
dirname
"
$0
"
)
"
&&
pwd
)
"
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
"
mkdir
-p
"
$LOG_DIR
"
DATE_TAG
=
"
$(
date
+%Y%m%d
)
"
DATE_TAG
=
"
$(
date
+%Y%m%d
)
"
TIME_TAG
=
"
$(
date
+%H%M%S
)
"
TIME_TAG
=
"
$(
date
+%H%M%S
)
"
...
@@ -49,6 +55,7 @@ START_EPOCH=$(date +%s)
...
@@ -49,6 +55,7 @@ START_EPOCH=$(date +%s)
# 不用 set -e,因为要捕获 claude 的退出码
# 不用 set -e,因为要捕获 claude 的退出码
set
+e
set
+e
claude
-p
\
claude
-p
\
"
${
CLAUDE_SETTINGS_ARGS
[@]
}
"
\
--permission-mode
bypassPermissions
\
--permission-mode
bypassPermissions
\
--allowed-tools
Bash Write
\
--allowed-tools
Bash Write
\
--output-format
json
\
--output-format
json
\
...
@@ -150,6 +157,7 @@ API_SECS=$(awk -v ms="$API_MS" 'BEGIN{printf "%.1f", ms/1000}')
...
@@ -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
"=====
$(
date
'+%Y-%m-%d %H:%M:%S'
)
抓取 [
$SOURCE
] ====="
echo
"状态:
${
STATUS_EMOJI
}
${
STATUS_TEXT
}
"
echo
"状态:
${
STATUS_EMOJI
}
${
STATUS_TEXT
}
"
echo
"Provider:
${
USED_PROVIDER
}
"
echo
"退出码:
${
RC
}
"
echo
"退出码:
${
RC
}
"
echo
"耗时:
${
MINS
}
m
${
SECS
}
s (wall) /
${
API_SECS
}
s (api)"
echo
"耗时:
${
MINS
}
m
${
SECS
}
s (wall) /
${
API_SECS
}
s (api)"
echo
"成本:
\$
${
COST
}
"
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() {
...
@@ -44,11 +44,12 @@ show_all_last_logs() {
for
f
in
"
$LOG_DIR
"
/
*
.last.log
;
do
for
f
in
"
$LOG_DIR
"
/
*
.last.log
;
do
[
-f
"
$f
"
]
||
continue
[
-f
"
$f
"
]
||
continue
found
=
1
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
)
src
=
$(
basename
"
$f
"
.last.log
)
status_line
=
$(
grep
'^状态:'
"
$f
"
2>/dev/null |
head
-1
)
status_line
=
$(
grep
'^状态:'
"
$f
"
2>/dev/null |
head
-1
)
cost_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
)
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
# 取日志文件修改时间,格式 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
"?"
)
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() {
...
@@ -69,15 +70,20 @@ show_all_last_logs() {
session_id
=
"
${
session_line
#Session
:
}
"
session_id
=
"
${
session_line
#Session
:
}
"
[
-z
"
$session_id
"
]
&&
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
local
icon
if
echo
"
$status_text
"
|
grep
-q
"成功"
;
then
icon
=
"OK"
if
echo
"
$status_text
"
|
grep
-q
"成功"
;
then
icon
=
"OK"
elif
echo
"
$status_text
"
|
grep
-q
"失败"
;
then
icon
=
"FAIL"
elif
echo
"
$status_text
"
|
grep
-q
"失败"
;
then
icon
=
"FAIL"
else
icon
=
"?"
;
fi
else
icon
=
"?"
;
fi
if
[
-n
"
$session_id
"
]
;
then
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
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
fi
done
done
shopt
-u
nullglob
shopt
-u
nullglob
...
@@ -182,7 +188,7 @@ show_overview() {
...
@@ -182,7 +188,7 @@ show_overview() {
echo
"最近运行"
echo
"最近运行"
hr
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
show_all_last_logs
}
}
...
@@ -269,6 +275,7 @@ show_source_detail() {
...
@@ -269,6 +275,7 @@ show_source_detail() {
case
"
$line
"
in
case
"
$line
"
in
=====
*
)
echo
"
$line
"
;;
=====
*
)
echo
"
$line
"
;;
状态:
*
)
printf
" %-12s %s
\n
"
"状态:"
"
${
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
#耗时
:
}
"
;;
耗时:
*
)
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
...
@@ -19,6 +19,23 @@ MAIL_ON_SUCCESS=1
# 单次抓取成本(美元)超过此值则在邮件标题标记 [ALERT],留空不判断
# 单次抓取成本(美元)超过此值则在邮件标题标记 [ALERT],留空不判断
ALERT_COST_OVER=1
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 相关) ===
# === 每日汇总(digest 相关) ===
# 无论是否有告警都发汇总邮件:1=每次发,0=仅告警时发
# 无论是否有告警都发汇总邮件:1=每次发,0=仅告警时发
DIGEST_MAIL_ALWAYS=1
DIGEST_MAIL_ALWAYS=1
...
...
config/nikkei-world.conf.example
View file @
ab0f8df6
# /etc/news-harvester/nikkei-world.conf
# /etc/news-harvester/nikkei-world.conf
# 单栏目覆盖配置示例。仅写需要覆盖的字段,其余继承 default.conf。
# 单栏目覆盖配置示例。仅写需要覆盖的字段,其余继承 default.conf。
# 此例:nikkei-world 抓 30 条,成功不单独发邮件(只在 digest 里看),成本超 $2 告警。
# 此例:nikkei-world 抓 30 条,成功不单独发邮件(只在 digest 里看),成本超 $2 告警。
# 同时指定了 Claude provider 和备用 provider。
COUNT=30
COUNT=30
MAIL_ON_SUCCESS=0
MAIL_ON_SUCCESS=0
ALERT_COST_OVER=2
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 @@
...
@@ -6,6 +6,7 @@
# ./deploy.sh yichu@192.168.1.100 # 显式指定用户
# ./deploy.sh yichu@192.168.1.100 # 显式指定用户
# ./deploy.sh <host> --no-systemd # 只更新 bin 脚本,不动 systemd unit
# ./deploy.sh <host> --no-systemd # 只更新 bin 脚本,不动 systemd unit
# ./deploy.sh <host> --no-restart-web # 不重启 web 面板
# ./deploy.sh <host> --no-restart-web # 不重启 web 面板
# ./deploy.sh <host> --no-providers # 不同步 provider 配置
# ./deploy.sh <host> --test # 部署后自动跑一次 nikkei-world 测试
# ./deploy.sh <host> --test # 部署后自动跑一次 nikkei-world 测试
#
#
# 做的事:
# 做的事:
...
@@ -14,6 +15,8 @@
...
@@ -14,6 +15,8 @@
# 3. 同步 systemd 模板 service + digest unit 到 /etc/systemd/system/(需要 sudo)
# 3. 同步 systemd 模板 service + digest unit 到 /etc/systemd/system/(需要 sudo)
# ⚠️ 栏目 timer(news-harvester@<source>.timer)不在同步范围,由服务器端维护
# ⚠️ 栏目 timer(news-harvester@<source>.timer)不在同步范围,由服务器端维护
# 4. daemon-reload
# 4. daemon-reload
# 5. 同步本地 provider 配置到 /etc/news-harvester/providers/(需要 sudo)
# 只同步 .json 文件(不含 .example),保留服务器端已有的额外 provider
#
#
# 不动的东西:
# 不动的东西:
# - /etc/news-harvester/*.conf(用户配置)
# - /etc/news-harvester/*.conf(用户配置)
...
@@ -25,6 +28,7 @@ set -euo pipefail
...
@@ -25,6 +28,7 @@ set -euo pipefail
SYNC_SYSTEMD
=
1
SYNC_SYSTEMD
=
1
RESTART_WEB
=
1
RESTART_WEB
=
1
SYNC_PROVIDERS
=
1
RUN_TEST
=
0
RUN_TEST
=
0
HOST
=
""
HOST
=
""
...
@@ -33,6 +37,7 @@ while [ $# -gt 0 ]; do
...
@@ -33,6 +37,7 @@ while [ $# -gt 0 ]; do
case
"
$1
"
in
case
"
$1
"
in
--no-systemd
)
SYNC_SYSTEMD
=
0
;
shift
;;
--no-systemd
)
SYNC_SYSTEMD
=
0
;
shift
;;
--no-restart-web
)
RESTART_WEB
=
0
;
shift
;;
--no-restart-web
)
RESTART_WEB
=
0
;
shift
;;
--no-providers
)
SYNC_PROVIDERS
=
0
;
shift
;;
--test
)
RUN_TEST
=
1
;
shift
;;
--test
)
RUN_TEST
=
1
;
shift
;;
--help
|
-h
)
--help
|
-h
)
sed
-n
'3,24p'
"
$0
"
;
exit
0
;;
sed
-n
'3,24p'
"
$0
"
;
exit
0
;;
...
@@ -44,7 +49,7 @@ while [ $# -gt 0 ]; do
...
@@ -44,7 +49,7 @@ while [ $# -gt 0 ]; do
esac
esac
done
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@)
# 默认加用户名(如果没写 user@)
case
"
$HOST
"
in
case
"
$HOST
"
in
...
@@ -67,12 +72,13 @@ echo "=== 部署到 $HOST ==="
...
@@ -67,12 +72,13 @@ echo "=== 部署到 $HOST ==="
echo
" 项目:
$SCRIPT_DIR
"
echo
" 项目:
$SCRIPT_DIR
"
echo
" 同步 bin: 是"
echo
" 同步 bin: 是"
[
"
$SYNC_SYSTEMD
"
=
"1"
]
&&
echo
" 同步 systemd: 是(需要远程 sudo)"
||
echo
" 同步 systemd: 否(--no-systemd)"
[
"
$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)"
[
"
$RESTART_WEB
"
=
"1"
]
&&
echo
" 重启 web: 是"
||
echo
" 重启 web: 否(--no-restart-web)"
[
"
$RUN_TEST
"
=
"1"
]
&&
echo
" 部署后测试: 是(--test)"
[
"
$RUN_TEST
"
=
"1"
]
&&
echo
" 部署后测试: 是(--test)"
echo
""
echo
""
# --- 1. rsync 项目目录 ---
# --- 1. rsync 项目目录 ---
info
"1/
4
同步项目目录 → ~/news-harvester-pro/"
info
"1/
6
同步项目目录 → ~/news-harvester-pro/"
rsync
-avz
--delete
\
rsync
-avz
--delete
\
--exclude
=
'.git'
\
--exclude
=
'.git'
\
--exclude
=
'*.log'
\
--exclude
=
'*.log'
\
...
@@ -83,23 +89,56 @@ rsync -avz --delete \
...
@@ -83,23 +89,56 @@ rsync -avz --delete \
ok
"项目目录已同步"
ok
"项目目录已同步"
# --- 2. 复制 bin + web 到工作目录 ---
# --- 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/'
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 应用已就位"
ok
"bin 脚本 + web 应用已就位"
# --- 3. 同步 systemd unit ---
# --- 3. 同步 systemd unit ---
if
[
"
$SYNC_SYSTEMD
"
=
"1"
]
;
then
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 能交互式提示输入密码
# 用 -t 分配伪终端,让远程 sudo 能交互式提示输入密码
# 只同步模板 service / digest / web,栏目 timer 由服务器端维护
# 只同步模板 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'
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"
ok
"systemd unit 已同步并 reload"
else
else
info
"3/
5
跳过 systemd(--no-systemd)"
info
"3/
6
跳过 systemd(--no-systemd)"
fi
fi
# --- 4. 提示 Web 面板首次配置 + 重启 ---
# --- 3.5 同步 provider 配置 ---
info
"4/5 Web 面板检查"
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
\
ssh
"
$HOST
"
'test -f /etc/news-harvester/web.conf && echo "EXISTS" || echo "MISSING"'
2>/dev/null |
grep
-q
EXISTS
\
&&
ok
"web.conf 已存在"
\
&&
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'"
||
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
...
@@ -116,8 +155,8 @@ else
info
"跳过 web 重启(--no-restart-web)"
info
"跳过 web 重启(--no-restart-web)"
fi
fi
# ---
5
. 完成 ---
# ---
6
. 完成 ---
info
"
5/5
完成"
info
"
6/6
完成"
echo
""
echo
""
echo
"========================================"
echo
"========================================"
ok
"部署完成!"
ok
"部署完成!"
...
...
modify-source.sh
View file @
ab0f8df6
...
@@ -9,6 +9,8 @@
...
@@ -9,6 +9,8 @@
# --count N 更新抓取条数(写入/覆盖 <source>.conf)
# --count N 更新抓取条数(写入/覆盖 <source>.conf)
# --mail-to ADDR 更新收件人
# --mail-to ADDR 更新收件人
# --mail-on-success 0|1 更新成功是否发邮件
# --mail-on-success 0|1 更新成功是否发邮件
# --provider NAME 更新 Claude provider(写入 PROVIDER=)
# --fallback-provider NAME 更新备用 provider(写入 FALLBACK_PROVIDER=)
# --yes, -y 跳过确认
# --yes, -y 跳过确认
# --help, -h 显示帮助
# --help, -h 显示帮助
#
#
...
@@ -30,6 +32,8 @@ TIME=""
...
@@ -30,6 +32,8 @@ TIME=""
COUNT
=
""
COUNT
=
""
MAIL_TO
=
""
MAIL_TO
=
""
MAIL_ON_SUCCESS
=
""
MAIL_ON_SUCCESS
=
""
PROVIDER
=
""
FALLBACK_PROVIDER
=
""
ASSUME_NO
=
0
ASSUME_NO
=
0
usage
()
{
usage
()
{
...
@@ -43,6 +47,8 @@ while [ $# -gt 0 ]; do
...
@@ -43,6 +47,8 @@ while [ $# -gt 0 ]; do
--count
)
COUNT
=
"
${
2
:?--count
需要值
}
"
;
shift
2
;;
--count
)
COUNT
=
"
${
2
:?--count
需要值
}
"
;
shift
2
;;
--mail-to
)
MAIL_TO
=
"
${
2
:?--mail-to
需要值
}
"
;
shift
2
;;
--mail-to
)
MAIL_TO
=
"
${
2
:?--mail-to
需要值
}
"
;
shift
2
;;
--mail-on-success
)
MAIL_ON_SUCCESS
=
"
${
2
:?--mail-on-success
需要值
}
"
;
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
;;
--yes
|
-y
)
ASSUME_NO
=
1
;
shift
;;
--help
|
-h
)
usage
;
exit
0
;;
--help
|
-h
)
usage
;
exit
0
;;
--
)
shift
;
break
;;
--
)
shift
;
break
;;
...
@@ -122,6 +128,8 @@ read_conf_val() {
...
@@ -122,6 +128,8 @@ read_conf_val() {
CUR_COUNT
=
$(
read_conf_val COUNT
)
CUR_COUNT
=
$(
read_conf_val COUNT
)
CUR_MAIL_TO
=
$(
read_conf_val MAIL_TO
)
CUR_MAIL_TO
=
$(
read_conf_val MAIL_TO
)
CUR_MOS
=
$(
read_conf_val MAIL_ON_SUCCESS
)
CUR_MOS
=
$(
read_conf_val MAIL_ON_SUCCESS
)
CUR_PROVIDER
=
$(
read_conf_val PROVIDER
)
CUR_FALLBACK
=
$(
read_conf_val FALLBACK_PROVIDER
)
# --- 展示变更计划,确认 ---
# --- 展示变更计划,确认 ---
echo
"=== 修改栏目:
$SOURCE
==="
echo
"=== 修改栏目:
$SOURCE
==="
...
@@ -129,6 +137,8 @@ echo " 定时时间: $CURRENT_TIME → $TIME"
...
@@ -129,6 +137,8 @@ echo " 定时时间: $CURRENT_TIME → $TIME"
[
-n
"
$COUNT
"
]
&&
{
echo
" 抓取条数:
${
CUR_COUNT
:-
(继承default)
}
→
$COUNT
"
;
}
[
-n
"
$COUNT
"
]
&&
{
echo
" 抓取条数:
${
CUR_COUNT
:-
(继承default)
}
→
$COUNT
"
;
}
[
-n
"
$MAIL_TO
"
]
&&
{
echo
" 收件人:
${
CUR_MAIL_TO
:-
(继承default)
}
→
$MAIL_TO
"
;
}
[
-n
"
$MAIL_TO
"
]
&&
{
echo
" 收件人:
${
CUR_MAIL_TO
:-
(继承default)
}
→
$MAIL_TO
"
;
}
[
-n
"
$MAIL_ON_SUCCESS
"
]
&&
{
echo
" 成功发信:
${
CUR_MOS
:-
(继承default)
}
→
$MAIL_ON_SUCCESS
"
;
}
[
-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
"
echo
" timer 文件:
$TIMER_PATH
"
if
[
"
$HAS_DROPIN
"
=
"1"
]
;
then
if
[
"
$HAS_DROPIN
"
=
"1"
]
;
then
warn
"检测到 drop-in 覆盖目录:
$DROPIN_DIR
"
warn
"检测到 drop-in 覆盖目录:
$DROPIN_DIR
"
...
@@ -160,7 +170,7 @@ update_conf() {
...
@@ -160,7 +170,7 @@ update_conf() {
echo
"
${
key
}
=
${
val
}
"
>>
"
$CONF_PATH
"
echo
"
${
key
}
=
${
val
}
"
>>
"
$CONF_PATH
"
fi
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
"
mkdir
-p
"
$CONF_DIR
"
if
[
!
-f
"
$CONF_PATH
"
]
;
then
if
[
!
-f
"
$CONF_PATH
"
]
;
then
{
{
...
@@ -172,6 +182,8 @@ if [ -n "$COUNT" ] || [ -n "$MAIL_TO" ] || [ -n "$MAIL_ON_SUCCESS" ]; then
...
@@ -172,6 +182,8 @@ if [ -n "$COUNT" ] || [ -n "$MAIL_TO" ] || [ -n "$MAIL_ON_SUCCESS" ]; then
[
-n
"
$COUNT
"
]
&&
{
update_conf COUNT
"
$COUNT
"
;
}
[
-n
"
$COUNT
"
]
&&
{
update_conf COUNT
"
$COUNT
"
;
}
[
-n
"
$MAIL_TO
"
]
&&
{
update_conf MAIL_TO
"
$MAIL_TO
"
;
}
[
-n
"
$MAIL_TO
"
]
&&
{
update_conf MAIL_TO
"
$MAIL_TO
"
;
}
[
-n
"
$MAIL_ON_SUCCESS
"
]
&&
{
update_conf MAIL_ON_SUCCESS
"
$MAIL_ON_SUCCESS
"
;
}
[
-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
"
ok
"已更新
$CONF_PATH
"
fi
fi
...
...
web/app.py
View file @
ab0f8df6
...
@@ -30,6 +30,7 @@ from flask import (
...
@@ -30,6 +30,7 @@ from flask import (
# ── 路径常量(与 bash 脚本一致) ──────────────────────────────
# ── 路径常量(与 bash 脚本一致) ──────────────────────────────
SYSTEMD_DIR
=
Path
(
os
.
environ
.
get
(
"SYSTEMD_DIR"
,
"/etc/systemd/system"
))
SYSTEMD_DIR
=
Path
(
os
.
environ
.
get
(
"SYSTEMD_DIR"
,
"/etc/systemd/system"
))
CONF_DIR
=
Path
(
os
.
environ
.
get
(
"CONF_DIR"
,
"/etc/news-harvester"
))
CONF_DIR
=
Path
(
os
.
environ
.
get
(
"CONF_DIR"
,
"/etc/news-harvester"
))
PROVIDER_DIR
=
CONF_DIR
/
"providers"
WORK_DIR
=
Path
(
os
.
environ
.
get
(
"WORK_DIR"
,
"/home/yichu/Documents/News-Harvester"
))
WORK_DIR
=
Path
(
os
.
environ
.
get
(
"WORK_DIR"
,
"/home/yichu/Documents/News-Harvester"
))
LOG_DIR
=
Path
(
os
.
environ
.
get
(
"LOG_DIR"
,
str
(
WORK_DIR
/
"logs"
)))
LOG_DIR
=
Path
(
os
.
environ
.
get
(
"LOG_DIR"
,
str
(
WORK_DIR
/
"logs"
)))
PROJECT_DIR
=
Path
(
__file__
)
.
resolve
()
.
parent
.
parent
PROJECT_DIR
=
Path
(
__file__
)
.
resolve
()
.
parent
.
parent
...
@@ -216,6 +217,7 @@ def parse_last_log(source):
...
@@ -216,6 +217,7 @@ def parse_last_log(source):
(
"duration"
,
"耗时:"
),
(
"exit_code"
,
"退出码:"
),
(
"duration"
,
"耗时:"
),
(
"exit_code"
,
"退出码:"
),
(
"turns"
,
"Turns:"
),
(
"token"
,
"Token:"
),
(
"turns"
,
"Turns:"
),
(
"token"
,
"Token:"
),
(
"denials"
,
"拒绝:"
),
(
"terminal"
,
"Terminal:"
),
(
"denials"
,
"拒绝:"
),
(
"terminal"
,
"Terminal:"
),
(
"provider"
,
"Provider:"
),
]:
]:
if
line
.
startswith
(
label
):
if
line
.
startswith
(
label
):
# 去掉 label 前缀(含可能的对齐空格)
# 去掉 label 前缀(含可能的对齐空格)
...
@@ -261,6 +263,52 @@ def read_conf(source):
...
@@ -261,6 +263,52 @@ def read_conf(source):
return
result
return
result
# ── Provider 辅助函数 ──────────────────────────────────────
SENSITIVE_KEY_HINTS
=
(
"TOKEN"
,
"KEY"
,
"SECRET"
,
"PASSWORD"
,
"AUTH"
)
def
mask_provider_value
(
key
,
value
):
"""脱敏 provider 配置值:敏感 key 只显示前 8 位"""
if
any
(
h
in
key
.
upper
()
for
h
in
SENSITIVE_KEY_HINTS
):
if
len
(
value
)
>
8
:
return
value
[:
8
]
+
"..."
return
value
[:
4
]
+
"***"
return
value
def
get_providers
():
"""扫描 providers 目录,返回 [{name, env_keys, key_count}]"""
if
not
PROVIDER_DIR
.
exists
():
return
[]
result
=
[]
for
f
in
sorted
(
PROVIDER_DIR
.
glob
(
"*.json"
)):
name
=
f
.
stem
try
:
data
=
json
.
loads
(
f
.
read_text
(
encoding
=
"utf-8"
))
keys
=
list
(
data
.
keys
())
if
isinstance
(
data
,
dict
)
else
[]
except
Exception
:
keys
=
[]
result
.
append
({
"name"
:
name
,
"env_keys"
:
keys
,
"key_count"
:
len
(
keys
)})
return
result
def
read_provider
(
name
):
"""读取单个 provider JSON,值脱敏。返回 {name, env: [{key, value}]} 或 None"""
path
=
PROVIDER_DIR
/
f
"{name}.json"
if
not
path
.
exists
():
return
None
try
:
data
=
json
.
loads
(
path
.
read_text
(
encoding
=
"utf-8"
))
except
Exception
as
e
:
return
{
"name"
:
name
,
"error"
:
str
(
e
),
"env"
:
[]}
env
=
[]
if
isinstance
(
data
,
dict
):
for
k
,
v
in
data
.
items
():
env
.
append
({
"key"
:
k
,
"value"
:
mask_provider_value
(
k
,
str
(
v
))})
return
{
"name"
:
name
,
"env"
:
env
}
def
get_source_list
():
def
get_source_list
():
"""获取所有 source 的完整信息列表"""
"""获取所有 source 的完整信息列表"""
sources
=
[]
sources
=
[]
...
@@ -279,6 +327,11 @@ def get_source_list():
...
@@ -279,6 +327,11 @@ def get_source_list():
# 最近运行日志
# 最近运行日志
log_info
=
parse_last_log
(
source
)
log_info
=
parse_last_log
(
source
)
# 从 conf 读 provider 配置
conf
=
read_conf
(
source
)
provider
=
conf
.
get
(
"PROVIDER"
,
""
)
fallback_provider
=
conf
.
get
(
"FALLBACK_PROVIDER"
,
""
)
# 状态判定
# 状态判定
status_text
=
log_info
.
get
(
"status"
,
""
)
if
log_info
else
""
status_text
=
log_info
.
get
(
"status"
,
""
)
if
log_info
else
""
if
"成功"
in
status_text
:
if
"成功"
in
status_text
:
...
@@ -297,6 +350,8 @@ def get_source_list():
...
@@ -297,6 +350,8 @@ def get_source_list():
"last"
:
props
[
"last"
],
"last"
:
props
[
"last"
],
"log"
:
log_info
,
"log"
:
log_info
,
"status_class"
:
status_class
,
"status_class"
:
status_class
,
"provider"
:
provider
,
"fallback_provider"
:
fallback_provider
,
})
})
# 按运行时间排序
# 按运行时间排序
sources
.
sort
(
key
=
lambda
s
:
s
[
"run_time"
])
sources
.
sort
(
key
=
lambda
s
:
s
[
"run_time"
])
...
@@ -393,14 +448,15 @@ def source_detail(name):
...
@@ -393,14 +448,15 @@ def source_detail(name):
return
render_template
(
"detail.html"
,
name
=
name
,
run_time
=
run_time
,
return
render_template
(
"detail.html"
,
name
=
name
,
run_time
=
run_time
,
props
=
props
,
log_info
=
log_info
,
json_info
=
json_info
,
props
=
props
,
log_info
=
log_info
,
json_info
=
json_info
,
json_meta
=
json_meta
,
conf
=
conf
,
json_meta
=
json_meta
,
conf
=
conf
,
continue_status
=
continue_status
,
auto_refresh
=
auto_refresh
)
continue_status
=
continue_status
,
auto_refresh
=
auto_refresh
,
providers
=
get_providers
())
@
app
.
route
(
"/add"
,
methods
=
[
"GET"
,
"POST"
])
@
app
.
route
(
"/add"
,
methods
=
[
"GET"
,
"POST"
])
@
require_auth
@
require_auth
def
add_source
():
def
add_source
():
if
request
.
method
==
"GET"
:
if
request
.
method
==
"GET"
:
return
render_template
(
"add.html"
)
return
render_template
(
"add.html"
,
providers
=
get_providers
()
)
# POST: 执行 add-source.sh
# POST: 执行 add-source.sh
source
=
request
.
form
.
get
(
"source"
,
""
)
.
strip
()
source
=
request
.
form
.
get
(
"source"
,
""
)
.
strip
()
...
@@ -408,16 +464,17 @@ def add_source():
...
@@ -408,16 +464,17 @@ def add_source():
count
=
request
.
form
.
get
(
"count"
,
""
)
.
strip
()
count
=
request
.
form
.
get
(
"count"
,
""
)
.
strip
()
mail_to
=
request
.
form
.
get
(
"mail_to"
,
""
)
.
strip
()
mail_to
=
request
.
form
.
get
(
"mail_to"
,
""
)
.
strip
()
mail_on_success
=
request
.
form
.
get
(
"mail_on_success"
,
""
)
.
strip
()
mail_on_success
=
request
.
form
.
get
(
"mail_on_success"
,
""
)
.
strip
()
provider
=
request
.
form
.
get
(
"provider"
,
""
)
.
strip
()
if
not
source
or
not
re
.
match
(
r"^[a-zA-Z0-9_-]+$"
,
source
):
if
not
source
or
not
re
.
match
(
r"^[a-zA-Z0-9_-]+$"
,
source
):
flash
(
"栏目名只能含字母、数字、下划线、连字符"
,
"error"
)
flash
(
"栏目名只能含字母、数字、下划线、连字符"
,
"error"
)
return
render_template
(
"add.html"
,
form
=
request
.
form
)
return
render_template
(
"add.html"
,
form
=
request
.
form
,
providers
=
get_providers
()
)
cmd
=
[
ADD_SOURCE
,
source
,
"--yes"
]
cmd
=
[
ADD_SOURCE
,
source
,
"--yes"
]
if
time
:
if
time
:
if
not
re
.
match
(
r"^([01]\d|2[0-3]):[0-5]\d$"
,
time
):
if
not
re
.
match
(
r"^([01]\d|2[0-3]):[0-5]\d$"
,
time
):
flash
(
"时间格式应为 HH:MM(24小时制)"
,
"error"
)
flash
(
"时间格式应为 HH:MM(24小时制)"
,
"error"
)
return
render_template
(
"add.html"
,
form
=
request
.
form
)
return
render_template
(
"add.html"
,
form
=
request
.
form
,
providers
=
get_providers
()
)
cmd
+=
[
"--time"
,
time
]
cmd
+=
[
"--time"
,
time
]
if
count
:
if
count
:
cmd
+=
[
"--count"
,
count
]
cmd
+=
[
"--count"
,
count
]
...
@@ -425,6 +482,8 @@ def add_source():
...
@@ -425,6 +482,8 @@ def add_source():
cmd
+=
[
"--mail-to"
,
mail_to
]
cmd
+=
[
"--mail-to"
,
mail_to
]
if
mail_on_success
:
if
mail_on_success
:
cmd
+=
[
"--mail-on-success"
,
mail_on_success
]
cmd
+=
[
"--mail-on-success"
,
mail_on_success
]
if
provider
and
provider
!=
"default"
:
cmd
+=
[
"--provider"
,
provider
]
rc
,
stdout
,
stderr
=
run_script
(
cmd
,
timeout
=
30
)
rc
,
stdout
,
stderr
=
run_script
(
cmd
,
timeout
=
30
)
if
rc
==
0
:
if
rc
==
0
:
...
@@ -432,7 +491,7 @@ def add_source():
...
@@ -432,7 +491,7 @@ def add_source():
return
redirect
(
url_for
(
"source_detail"
,
name
=
source
))
return
redirect
(
url_for
(
"source_detail"
,
name
=
source
))
else
:
else
:
flash
(
f
"添加失败 (exit {rc})
\n
{stdout}
\n
{stderr}"
,
"error"
)
flash
(
f
"添加失败 (exit {rc})
\n
{stdout}
\n
{stderr}"
,
"error"
)
return
render_template
(
"add.html"
,
form
=
request
.
form
)
return
render_template
(
"add.html"
,
form
=
request
.
form
,
providers
=
get_providers
()
)
@
app
.
route
(
"/edit/<name>"
,
methods
=
[
"GET"
,
"POST"
])
@
app
.
route
(
"/edit/<name>"
,
methods
=
[
"GET"
,
"POST"
])
...
@@ -448,20 +507,24 @@ def edit_source(name):
...
@@ -448,20 +507,24 @@ def edit_source(name):
timer_name
=
f
"news-harvester@{name}.timer"
timer_name
=
f
"news-harvester@{name}.timer"
current_time
=
read_oncalendar_from_systemd
(
timer_name
)
or
parse_timer_file
(
timer_path
)
current_time
=
read_oncalendar_from_systemd
(
timer_name
)
or
parse_timer_file
(
timer_path
)
conf
=
read_conf
(
name
)
conf
=
read_conf
(
name
)
return
render_template
(
"edit.html"
,
name
=
name
,
current_time
=
current_time
,
conf
=
conf
)
return
render_template
(
"edit.html"
,
name
=
name
,
current_time
=
current_time
,
conf
=
conf
,
providers
=
get_providers
())
# POST: 执行 modify-source.sh
# POST: 执行 modify-source.sh
time
=
request
.
form
.
get
(
"time"
,
""
)
.
strip
()
time
=
request
.
form
.
get
(
"time"
,
""
)
.
strip
()
count
=
request
.
form
.
get
(
"count"
,
""
)
.
strip
()
count
=
request
.
form
.
get
(
"count"
,
""
)
.
strip
()
mail_to
=
request
.
form
.
get
(
"mail_to"
,
""
)
.
strip
()
mail_to
=
request
.
form
.
get
(
"mail_to"
,
""
)
.
strip
()
mail_on_success
=
request
.
form
.
get
(
"mail_on_success"
,
""
)
.
strip
()
mail_on_success
=
request
.
form
.
get
(
"mail_on_success"
,
""
)
.
strip
()
provider
=
request
.
form
.
get
(
"provider"
,
""
)
.
strip
()
fallback_provider
=
request
.
form
.
get
(
"fallback_provider"
,
""
)
.
strip
()
if
not
time
or
not
re
.
match
(
r"^([01]\d|2[0-3]):[0-5]\d$"
,
time
):
if
not
time
or
not
re
.
match
(
r"^([01]\d|2[0-3]):[0-5]\d$"
,
time
):
flash
(
"时间格式应为 HH:MM(24小时制)"
,
"error"
)
flash
(
"时间格式应为 HH:MM(24小时制)"
,
"error"
)
timer_name
=
f
"news-harvester@{name}.timer"
timer_name
=
f
"news-harvester@{name}.timer"
current_time
=
read_oncalendar_from_systemd
(
timer_name
)
or
parse_timer_file
(
timer_path
)
current_time
=
read_oncalendar_from_systemd
(
timer_name
)
or
parse_timer_file
(
timer_path
)
conf
=
read_conf
(
name
)
conf
=
read_conf
(
name
)
return
render_template
(
"edit.html"
,
name
=
name
,
current_time
=
current_time
,
conf
=
conf
)
return
render_template
(
"edit.html"
,
name
=
name
,
current_time
=
current_time
,
conf
=
conf
,
providers
=
get_providers
())
cmd
=
[
MODIFY_SOURCE
,
name
,
"--time"
,
time
,
"--yes"
]
cmd
=
[
MODIFY_SOURCE
,
name
,
"--time"
,
time
,
"--yes"
]
if
count
:
if
count
:
...
@@ -470,6 +533,10 @@ def edit_source(name):
...
@@ -470,6 +533,10 @@ def edit_source(name):
cmd
+=
[
"--mail-to"
,
mail_to
]
cmd
+=
[
"--mail-to"
,
mail_to
]
if
mail_on_success
:
if
mail_on_success
:
cmd
+=
[
"--mail-on-success"
,
mail_on_success
]
cmd
+=
[
"--mail-on-success"
,
mail_on_success
]
if
provider
:
cmd
+=
[
"--provider"
,
provider
]
if
fallback_provider
:
cmd
+=
[
"--fallback-provider"
,
fallback_provider
]
rc
,
stdout
,
stderr
=
run_script
(
cmd
,
timeout
=
30
)
rc
,
stdout
,
stderr
=
run_script
(
cmd
,
timeout
=
30
)
if
rc
==
0
:
if
rc
==
0
:
...
@@ -542,12 +609,15 @@ def continue_source(name):
...
@@ -542,12 +609,15 @@ def continue_source(name):
if
mode
not
in
(
"resume"
,
"repair"
):
if
mode
not
in
(
"resume"
,
"repair"
):
mode
=
"resume"
mode
=
"resume"
debug_flag
=
request
.
form
.
get
(
"debug"
,
""
)
.
strip
()
debug_flag
=
request
.
form
.
get
(
"debug"
,
""
)
.
strip
()
provider
=
request
.
form
.
get
(
"provider"
,
""
)
.
strip
()
# 后台启动 continue.sh(非阻塞)
# 后台启动 continue.sh(非阻塞)
# 以 yichu 用户执行(通过 sudo -u),确保文件权限/邮件/PATH 全部正确
# 以 yichu 用户执行(通过 sudo -u),确保文件权限/邮件/PATH 全部正确
cmd
=
[
"sudo"
,
"-u"
,
"yichu"
,
CONTINUE_SCRIPT
,
name
,
"--mode"
,
mode
,
"--yes"
]
cmd
=
[
"sudo"
,
"-u"
,
"yichu"
,
CONTINUE_SCRIPT
,
name
,
"--mode"
,
mode
,
"--yes"
]
if
debug_flag
:
if
debug_flag
:
cmd
.
append
(
"--debug"
)
cmd
.
append
(
"--debug"
)
if
provider
:
cmd
+=
[
"--provider"
,
provider
]
proc
=
subprocess
.
Popen
(
proc
=
subprocess
.
Popen
(
cmd
,
cmd
,
stdout
=
subprocess
.
PIPE
,
stderr
=
subprocess
.
PIPE
,
text
=
True
,
stdout
=
subprocess
.
PIPE
,
stderr
=
subprocess
.
PIPE
,
text
=
True
,
...
@@ -605,5 +675,77 @@ def disable_source(name):
...
@@ -605,5 +675,77 @@ def disable_source(name):
return
redirect
(
url_for
(
"source_detail"
,
name
=
name
))
return
redirect
(
url_for
(
"source_detail"
,
name
=
name
))
# ── Provider 管理路由 ──────────────────────────────────────
@
app
.
route
(
"/providers"
)
@
require_auth
def
provider_list
():
providers
=
get_providers
()
return
render_template
(
"providers.html"
,
providers
=
providers
)
@
app
.
route
(
"/provider/<name>"
)
@
require_auth
def
provider_detail
(
name
):
if
not
re
.
match
(
r"^[a-zA-Z0-9_-]+$"
,
name
):
abort
(
404
)
provider
=
read_provider
(
name
)
if
not
provider
:
abort
(
404
)
return
render_template
(
"provider_detail.html"
,
provider
=
provider
)
@
app
.
route
(
"/provider/add"
,
methods
=
[
"GET"
,
"POST"
])
@
require_auth
def
provider_add
():
if
request
.
method
==
"GET"
:
return
render_template
(
"provider_add.html"
)
# POST
name
=
request
.
form
.
get
(
"name"
,
""
)
.
strip
()
json_text
=
request
.
form
.
get
(
"json"
,
""
)
.
strip
()
if
not
name
or
not
re
.
match
(
r"^[a-zA-Z0-9_-]+$"
,
name
):
flash
(
"Provider 名称只能含字母、数字、下划线、连字符"
,
"error"
)
return
render_template
(
"provider_add.html"
,
form
=
request
.
form
)
if
name
==
"default"
:
flash
(
"Provider 名称不能用 default(reserved)"
,
"error"
)
return
render_template
(
"provider_add.html"
,
form
=
request
.
form
)
# 验证 JSON
try
:
data
=
json
.
loads
(
json_text
)
if
not
isinstance
(
data
,
dict
):
raise
ValueError
(
"JSON 顶层必须是对象(key-value)"
)
except
Exception
as
e
:
flash
(
f
"JSON 格式无效: {e}"
,
"error"
)
return
render_template
(
"provider_add.html"
,
form
=
request
.
form
)
# 写入文件
PROVIDER_DIR
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
path
=
PROVIDER_DIR
/
f
"{name}.json"
if
path
.
exists
():
flash
(
f
"Provider {name} 已存在,请用其他名称或先删除"
,
"error"
)
return
render_template
(
"provider_add.html"
,
form
=
request
.
form
)
path
.
write_text
(
json
.
dumps
(
data
,
indent
=
2
,
ensure_ascii
=
False
),
encoding
=
"utf-8"
)
flash
(
f
"Provider {name} 创建成功({len(data)} 个环境变量)"
,
"success"
)
return
redirect
(
url_for
(
"provider_detail"
,
name
=
name
))
@
app
.
route
(
"/provider/delete/<name>"
,
methods
=
[
"POST"
])
@
require_auth
def
provider_delete
(
name
):
if
not
re
.
match
(
r"^[a-zA-Z0-9_-]+$"
,
name
):
abort
(
404
)
path
=
PROVIDER_DIR
/
f
"{name}.json"
if
not
path
.
exists
():
flash
(
f
"Provider {name} 不存在"
,
"error"
)
return
redirect
(
url_for
(
"provider_list"
))
path
.
unlink
()
flash
(
f
"Provider {name} 已删除"
,
"success"
)
return
redirect
(
url_for
(
"provider_list"
))
if
__name__
==
"__main__"
:
if
__name__
==
"__main__"
:
app
.
run
(
host
=
"127.0.0.1"
,
port
=
8080
,
debug
=
False
)
app
.
run
(
host
=
"127.0.0.1"
,
port
=
8080
,
debug
=
False
)
web/templates/add.html
View file @
ab0f8df6
...
@@ -46,6 +46,17 @@
...
@@ -46,6 +46,17 @@
</select>
</select>
</div>
</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"
>
<div
class=
"actions"
>
<button
type=
"submit"
class=
"btn btn-primary"
>
创建栏目
</button>
<button
type=
"submit"
class=
"btn btn-primary"
>
创建栏目
</button>
<a
class=
"btn"
href=
"{{ url_for('index') }}"
>
取消
</a>
<a
class=
"btn"
href=
"{{ url_for('index') }}"
>
取消
</a>
...
...
web/templates/base.html
View file @
ab0f8df6
...
@@ -204,6 +204,7 @@
...
@@ -204,6 +204,7 @@
<a
class=
"logo"
href=
"{{ url_for('index') }}"
>
📰 News Harvester
</a>
<a
class=
"logo"
href=
"{{ url_for('index') }}"
>
📰 News Harvester
</a>
<a
href=
"{{ url_for('index') }}"
>
栏目列表
</a>
<a
href=
"{{ url_for('index') }}"
>
栏目列表
</a>
<a
href=
"{{ url_for('add_source') }}"
>
添加栏目
</a>
<a
href=
"{{ url_for('add_source') }}"
>
添加栏目
</a>
<a
href=
"{{ url_for('provider_list') }}"
>
Provider
</a>
</nav>
</nav>
<div
class=
"container"
>
<div
class=
"container"
>
{% with messages = get_flashed_messages(with_categories=true) %}
{% with messages = get_flashed_messages(with_categories=true) %}
...
...
web/templates/detail.html
View file @
ab0f8df6
...
@@ -24,19 +24,56 @@
...
@@ -24,19 +24,56 @@
<button
type=
"submit"
class=
"btn btn-primary"
{%
if
continue_status
and
continue_status
.
running
%}
disabled
{%
endif
%}
>
手动触发
</button>
<button
type=
"submit"
class=
"btn btn-primary"
{%
if
continue_status
and
continue_status
.
running
%}
disabled
{%
endif
%}
>
手动触发
</button>
</form>
</form>
<form
method=
"POST"
action=
"{{ url_for('continue_source', name=name) }}"
style=
"display:inline"
<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"
>
<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
%}
>
<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 %}
{% if continue_status and continue_status.running and continue_status.mode == 'resume' %}续采中…{% else %}继续采集{% endif %}
</button>
</button>
</form>
</form>
<form
method=
"POST"
action=
"{{ url_for('continue_source', name=name) }}"
style=
"display:inline"
<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"
>
<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
%}
>
<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 %}
{% if continue_status and continue_status.running and continue_status.mode == 'repair' %}修复中…{% else %}采集修复{% endif %}
</button>
</button>
</form>
</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>
<a
class=
"btn"
href=
"{{ url_for('index') }}"
>
返回列表
</a>
<form
method=
"POST"
action=
"{{ url_for('disable_source', name=name) }}"
style=
"display:inline"
<form
method=
"POST"
action=
"{{ url_for('disable_source', name=name) }}"
style=
"display:inline"
onsubmit=
"return confirm('确定停用栏目 {{ name }}?\n\n这将:\n1. 停止并 disable 定时器\n2. 清空运行时间(不再触发)\n\n文件保留,可随时通过编辑重新设置时间来启用。')"
>
onsubmit=
"return confirm('确定停用栏目 {{ name }}?\n\n这将:\n1. 停止并 disable 定时器\n2. 清空运行时间(不再触发)\n\n文件保留,可随时通过编辑重新设置时间来启用。')"
>
...
@@ -107,6 +144,10 @@
...
@@ -107,6 +144,10 @@
<div
class=
"label"
>
Turns
</div>
<div
class=
"label"
>
Turns
</div>
<div
class=
"value"
>
{{ log_info.get('turns', '-') }}
</div>
<div
class=
"value"
>
{{ log_info.get('turns', '-') }}
</div>
</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=
"card"
>
<div
class=
"label"
>
修改时间
</div>
<div
class=
"label"
>
修改时间
</div>
<div
class=
"value"
style=
"font-size:14px"
>
{{ log_info.get('mtime', '?') }}
</div>
<div
class=
"value"
style=
"font-size:14px"
>
{{ log_info.get('mtime', '?') }}
</div>
...
...
web/templates/edit.html
View file @
ab0f8df6
...
@@ -39,6 +39,28 @@
...
@@ -39,6 +39,28 @@
<div
class=
"hint"
>
当前: {{ conf.get('MAIL_ON_SUCCESS', '(继承 default.conf)') }}
</div>
<div
class=
"hint"
>
当前: {{ conf.get('MAIL_ON_SUCCESS', '(继承 default.conf)') }}
</div>
</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"
>
<div
class=
"actions"
>
<button
type=
"submit"
class=
"btn btn-primary"
>
保存修改
</button>
<button
type=
"submit"
class=
"btn btn-primary"
>
保存修改
</button>
<a
class=
"btn"
href=
"{{ url_for('source_detail', name=name) }}"
>
取消
</a>
<a
class=
"btn"
href=
"{{ url_for('source_detail', name=name) }}"
>
取消
</a>
...
...
web/templates/index.html
View file @
ab0f8df6
...
@@ -32,6 +32,7 @@
...
@@ -32,6 +32,7 @@
<th>
栏目
</th>
<th>
栏目
</th>
<th>
状态
</th>
<th>
状态
</th>
<th>
运行时间
</th>
<th>
运行时间
</th>
<th>
Provider
</th>
<th>
上次触发
</th>
<th>
上次触发
</th>
<th>
下次触发
</th>
<th>
下次触发
</th>
<th>
最近运行
</th>
<th>
最近运行
</th>
...
@@ -56,6 +57,16 @@
...
@@ -56,6 +57,16 @@
{% endif %}
{% endif %}
</td>
</td>
<td><code>
{{ s.run_time }}
</code></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.last }}
</td>
<td>
{{ s.next }}
</td>
<td>
{{ s.next }}
</td>
<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