修改说明
This commit is contained in:
@@ -29,6 +29,9 @@ RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple \
|
|||||||
# ── 复制项目代码 ──────────────────────────────────────────────
|
# ── 复制项目代码 ──────────────────────────────────────────────
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# ── 备份默认订阅源(用于 docker-compose 未挂载 resources 时 fallback)──
|
||||||
|
RUN mkdir -p /app/resources_default && cp /app/resources/rss_feeds.opml /app/resources_default/
|
||||||
|
|
||||||
# ── 创建运行时目录 ────────────────────────────────────────────
|
# ── 创建运行时目录 ────────────────────────────────────────────
|
||||||
RUN mkdir -p /app/data/fetch /app/data/hotalert /app/data/cluster /app/data/digest /app/data/images /app/data/cache /app/data/output /app/logs /app/cache
|
RUN mkdir -p /app/data/fetch /app/data/hotalert /app/data/cluster /app/data/digest /app/data/images /app/data/cache /app/data/output /app/logs /app/cache
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
"max_workers": 10,
|
"max_workers": 10,
|
||||||
"timeout": 15
|
"timeout": 15
|
||||||
},
|
},
|
||||||
|
"content_fetch": {
|
||||||
|
"enabled": false,
|
||||||
|
"max_workers": 5,
|
||||||
|
"timeout": 15
|
||||||
|
},
|
||||||
"translate": {
|
"translate": {
|
||||||
"max_concurrent": 6
|
"max_concurrent": 6
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ services:
|
|||||||
- /volume2/webdav/code/my-daily/docker/config.json:/app/config.json:ro
|
- /volume2/webdav/code/my-daily/docker/config.json:/app/config.json:ro
|
||||||
# 提示词文件
|
# 提示词文件
|
||||||
- /volume2/webdav/code/my-daily/docker/prompts:/app/prompts:ro
|
- /volume2/webdav/code/my-daily/docker/prompts:/app/prompts:ro
|
||||||
|
# 订阅源文件(持久化,便于管理 OPML 文件)
|
||||||
|
- /volume2/webdav/code/my-daily/resources:/app/resources:ro
|
||||||
# 运行时数据
|
# 运行时数据
|
||||||
- /volume2/webdav/code/my-daily/docker/data:/app/data
|
- /volume2/webdav/code/my-daily/docker/data:/app/data
|
||||||
# 运行日志
|
# 运行日志
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ services:
|
|||||||
- ./docker/config.json:/app/config.json:ro
|
- ./docker/config.json:/app/config.json:ro
|
||||||
# 提示词文件(在 NAS 上直接修改,容器内只读)
|
# 提示词文件(在 NAS 上直接修改,容器内只读)
|
||||||
- ./docker/prompts:/app/prompts:ro
|
- ./docker/prompts:/app/prompts:ro
|
||||||
|
# 订阅源文件(持久化,便于管理 OPML 文件)
|
||||||
|
- ./resources:/app/resources:ro
|
||||||
# 运行时数据(持久化保存,含 fetch/hotalert/cluster/digest/images/cache 子目录)
|
# 运行时数据(持久化保存,含 fetch/hotalert/cluster/digest/images/cache 子目录)
|
||||||
- ./docker/data:/app/data
|
- ./docker/data:/app/data
|
||||||
# 运行日志(持久化,方便排查问题)
|
# 运行日志(持久化,方便排查问题)
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<opml version="2.0">
|
||||||
|
<head>
|
||||||
|
<title>军事科技RSS订阅源</title>
|
||||||
|
<dateCreated>Mon, 29 May 2026 12:00:00 +0800</dateCreated>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<outline text="英文军事源" title="英文军事源">
|
||||||
|
<outline text="Defense One" title="Defense One" type="rss" xmlUrl="https://www.defenseone.com/rss/all/"/>
|
||||||
|
<outline text="Seapower" title="Seapower" type="rss" xmlUrl="https://seapowermagazine.org/feed/"/>
|
||||||
|
<outline text="The War Zone" title="The War Zone" type="rss" xmlUrl="https://www.twz.com/feed"/>
|
||||||
|
</outline>
|
||||||
|
<outline text="中文微信公众号源" title="中文微信公众号源">
|
||||||
|
<outline text="90号茶室" title="90号茶室" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2391931960.rss"/>
|
||||||
|
<outline text="IPP评论" title="IPP评论" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3087967104.rss"/>
|
||||||
|
<outline text="NEWUAS" title="NEWUAS" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3246130840.rss"/>
|
||||||
|
<outline text="艾瑞咨询" title="艾瑞咨询" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2399237400.rss"/>
|
||||||
|
<outline text="安全内参" title="安全内参" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3284660303.rss"/>
|
||||||
|
<outline text="安全圈" title="安全圈" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3233184559.rss"/>
|
||||||
|
<outline text="白马 V 视角" title="白马 V 视角" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3073217929.rss"/>
|
||||||
|
<outline text="半导体行业观察" title="半导体行业观察" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3864835480.rss"/>
|
||||||
|
<outline text="北大纵横" title="北大纵横" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2393231480.rss"/>
|
||||||
|
<outline text="兵路文化" title="兵路文化" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3271103541.rss"/>
|
||||||
|
<outline text="补三刀" title="补三刀" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3567246386.rss"/>
|
||||||
|
<outline text="补壹刀" title="补壹刀" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3698108903.rss"/>
|
||||||
|
<outline text="参考消息" title="参考消息" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2393041860.rss"/>
|
||||||
|
<outline text="参考消息智库" title="参考消息智库" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2393278785.rss"/>
|
||||||
|
<outline text="纯科学" title="纯科学" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3098610162.rss"/>
|
||||||
|
<outline text="从心不逾矩的防务菌" title="从心不逾矩的防务菌" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3259361210.rss"/>
|
||||||
|
<outline text="大浪淘沙" title="大浪淘沙" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2397165646.rss"/>
|
||||||
|
<outline text="大柳树防务" title="大柳树防务" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3218623168.rss"/>
|
||||||
|
<outline text="大湾区评论" title="大湾区评论" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3881524286.rss"/>
|
||||||
|
<outline text="电波之矛" title="电波之矛" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3004998095.rss"/>
|
||||||
|
<outline text="东南亚问题研究" title="东南亚问题研究" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2391747267.rss"/>
|
||||||
|
<outline text="动态大参考" title="动态大参考" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2391979278.rss"/>
|
||||||
|
<outline text="防务快讯" title="防务快讯" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3511027745.rss"/>
|
||||||
|
<outline text="防务指南" title="防务指南" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2399475774.rss"/>
|
||||||
|
<outline text="凤凰网军事频道" title="凤凰网军事频道" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3545426725.rss"/>
|
||||||
|
<outline text="阜成门六号院" title="阜成门六号院" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3253307008.rss"/>
|
||||||
|
<outline text="复旦大学中国研究院" title="复旦大学中国研究院" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3284625196.rss"/>
|
||||||
|
<outline text="浮空飞行器" title="浮空飞行器" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3291643707.rss"/>
|
||||||
|
<outline text="观察者网" title="观察者网" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2392082080.rss"/>
|
||||||
|
<outline text="光明军事" title="光明军事" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3948650622.rss"/>
|
||||||
|
<outline text="国防工业出版社" title="国防工业出版社" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3083160855.rss"/>
|
||||||
|
<outline text="国防科技要闻" title="国防科技要闻" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3921202880.rss"/>
|
||||||
|
<outline text="国防时报排头兵" title="国防时报排头兵" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3015378336.rss"/>
|
||||||
|
<outline text="国观智库" title="国观智库" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2390460653.rss"/>
|
||||||
|
<outline text="国际法务" title="国际法务" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3586411852.rss"/>
|
||||||
|
<outline text="国科环宇" title="国科环宇" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3932598371.rss"/>
|
||||||
|
<outline text="海洋防务前沿" title="海洋防务前沿" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3920305066.rss"/>
|
||||||
|
<outline text="海鹰资讯" title="海鹰资讯" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3091381580.rss"/>
|
||||||
|
<outline text="航小宇" title="航小宇" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3934934363.rss"/>
|
||||||
|
<outline text="黑天鹅商业情报站" title="黑天鹅商业情报站" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3091701544.rss"/>
|
||||||
|
<outline text="环时枢密院十号" title="环时枢密院十号" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3889757200.rss"/>
|
||||||
|
<outline text="环太国际战略" title="环太国际战略" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3092636490.rss"/>
|
||||||
|
<outline text="华山穹剑" title="华山穹剑" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3096373438.rss"/>
|
||||||
|
<outline text="华语智库" title="华语智库" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3207743237.rss"/>
|
||||||
|
<outline text="混沌巡洋舰" title="混沌巡洋舰" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3073407278.rss"/>
|
||||||
|
<outline text="胡锡进观察" title="胡锡进观察" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3557544392.rss"/>
|
||||||
|
<outline text="解放军报" title="解放军报" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2391164500.rss"/>
|
||||||
|
<outline text="精确打击洞见" title="精确打击洞见" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3931193121.rss"/>
|
||||||
|
<outline text="今日台湾" title="今日台湾" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3295805547.rss"/>
|
||||||
|
<outline text="军民融合观察" title="军民融合观察" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3549249397.rss"/>
|
||||||
|
<outline text="军事高科技在线" title="军事高科技在线" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3562758605.rss"/>
|
||||||
|
<outline text="军事文摘" title="军事文摘" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3073744139.rss"/>
|
||||||
|
<outline text="军武次位面" title="军武次位面" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3261086876.rss"/>
|
||||||
|
<outline text="军鹰动态" title="军鹰动态" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3289924814.rss"/>
|
||||||
|
<outline text="科技导报" title="科技导报" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3070380920.rss"/>
|
||||||
|
<outline text="空天大视野" title="空天大视野" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3291909422.rss"/>
|
||||||
|
<outline text="空天界" title="空天界" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3223068327.rss"/>
|
||||||
|
<outline text="跨境前沿" title="跨境前沿" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3079503503.rss"/>
|
||||||
|
<outline text="蓝血研究" title="蓝血研究" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3225909356.rss"/>
|
||||||
|
<outline text="瞭望智库" title="瞭望智库" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3990649802.rss"/>
|
||||||
|
<outline text="李光满说" title="李光满说" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3089234433.rss"/>
|
||||||
|
<outline text="砺剑" title="砺剑" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3074198822.rss"/>
|
||||||
|
<outline text="六爷阿旦" title="六爷阿旦" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3554694732.rss"/>
|
||||||
|
<outline text="龙牙的一座山" title="龙牙的一座山" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3517933009.rss"/>
|
||||||
|
<outline text="美国驻华大使馆" title="美国驻华大使馆" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2390064940.rss"/>
|
||||||
|
<outline text="梅特涅的信徒" title="梅特涅的信徒" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3207441864.rss"/>
|
||||||
|
<outline text="迷彩虎观察" title="迷彩虎观察" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3297325221.rss"/>
|
||||||
|
<outline text="米尔观天下" title="米尔观天下" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3916786326.rss"/>
|
||||||
|
<outline text="明叔杂谈" title="明叔杂谈" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3005999417.rss"/>
|
||||||
|
<outline text="南方周末" title="南方周末" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_699115.rss"/>
|
||||||
|
<outline text="牛弹琴" title="牛弹琴" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3099984603.rss"/>
|
||||||
|
<outline text="欧亚新观察" title="欧亚新观察" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3210250850.rss"/>
|
||||||
|
<outline text="盘古智库" title="盘古智库" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3090238314.rss"/>
|
||||||
|
<outline text="澎湃新闻" title="澎湃新闻" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2393295571.rss"/>
|
||||||
|
<outline text="破圈了" title="破圈了" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3896642917.rss"/>
|
||||||
|
<outline text="奇安网情局" title="奇安网情局" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3288433970.rss"/>
|
||||||
|
<outline text="前沿深度解码" title="前沿深度解码" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3889836759.rss"/>
|
||||||
|
<outline text="秦安战略" title="秦安战略" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3090850520.rss"/>
|
||||||
|
<outline text="全球技术地图" title="全球技术地图" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3259114655.rss"/>
|
||||||
|
<outline text="人大重阳" title="人大重阳" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3092044019.rss"/>
|
||||||
|
<outline text="人民日报" title="人民日报" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2392014380.rss"/>
|
||||||
|
<outline text="戎评" title="戎评" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3207447547.rss"/>
|
||||||
|
<outline text="三剑客" title="三剑客" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3891893742.rss"/>
|
||||||
|
<outline text="上海国际问题研究院" title="上海国际问题研究院" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3008760667.rss"/>
|
||||||
|
<outline text="盛唐如松" title="盛唐如松" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3018607289.rss"/>
|
||||||
|
<outline text="施展世界" title="施展世界" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3574336774.rss"/>
|
||||||
|
<outline text="时政国关分析" title="时政国关分析" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3076746747.rss"/>
|
||||||
|
<outline text="书剑杂谈" title="书剑杂谈" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3543213462.rss"/>
|
||||||
|
<outline text="数字孪生战场" title="数字孪生战场" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3937277593.rss"/>
|
||||||
|
<outline text="搜狐科技" title="搜狐科技" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3569830872.rss"/>
|
||||||
|
<outline text="太空与网络" title="太空与网络" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3078315210.rss"/>
|
||||||
|
<outline text="外军防务研究前沿" title="外军防务研究前沿" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3273813815.rss"/>
|
||||||
|
<outline text="望穹科技" title="望穹科技" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3894541959.rss"/>
|
||||||
|
<outline text="网信前沿观察" title="网信前沿观察" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3884585502.rss"/>
|
||||||
|
<outline text="沃德舆情观察" title="沃德舆情观察" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2399567124.rss"/>
|
||||||
|
<outline text="无人机邦" title="无人机邦" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3957361285.rss"/>
|
||||||
|
<outline text="无人机反制" title="无人机反制" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3253242524.rss"/>
|
||||||
|
<outline text="无人争锋" title="无人争锋" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3283663941.rss"/>
|
||||||
|
<outline text="乌鸦校尉" title="乌鸦校尉" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3292984219.rss"/>
|
||||||
|
<outline text="侠客岛" title="侠客岛" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3070365200.rss"/>
|
||||||
|
<outline text="新华国际头条" title="新华国际头条" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2392135301.rss"/>
|
||||||
|
<outline text="喜乔智研" title="喜乔智研" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3924530650.rss"/>
|
||||||
|
<outline text="央广军事" title="央广军事" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2392718635.rss"/>
|
||||||
|
<outline text="亚太安全与海洋研究" title="亚太安全与海洋研究" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3003645862.rss"/>
|
||||||
|
<outline text="一个坏土豆" title="一个坏土豆" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3542082915.rss"/>
|
||||||
|
<outline text="一号哨位" title="一号哨位" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2393475036.rss"/>
|
||||||
|
<outline text="渊亭防务" title="渊亭防务" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3865628826.rss"/>
|
||||||
|
<outline text="远望智库" title="远望智库" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3081622154.rss"/>
|
||||||
|
<outline text="舆情文摘" title="舆情文摘" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3218815490.rss"/>
|
||||||
|
<outline text="泽平宏观" title="泽平宏观" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3877603551.rss"/>
|
||||||
|
<outline text="占豪" title="占豪" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3516511878.rss"/>
|
||||||
|
<outline text="战略前沿技术" title="战略前沿技术" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3271903347.rss"/>
|
||||||
|
<outline text="战略纵横家" title="战略纵横家" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3070598345.rss"/>
|
||||||
|
<outline text="占知智库" title="占知智库" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3296995449.rss"/>
|
||||||
|
<outline text="正和岛" title="正和岛" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2398018440.rss"/>
|
||||||
|
<outline text="职场老校尉" title="职场老校尉" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3260157145.rss"/>
|
||||||
|
<outline text="智强战略咨询" title="智强战略咨询" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3247764091.rss"/>
|
||||||
|
<outline text="智源社区" title="智源社区" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3598841003.rss"/>
|
||||||
|
<outline text="知远战略与防务研究所" title="知远战略与防务研究所" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3074319407.rss"/>
|
||||||
|
<outline text="中国国防报" title="中国国防报" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3518062346.rss"/>
|
||||||
|
<outline text="中国舰船" title="中国舰船" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3004044381.rss"/>
|
||||||
|
<outline text="中国南海研究院" title="中国南海研究院" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3215640033.rss"/>
|
||||||
|
<outline text="中国现代国际关系研究院" title="中国现代国际关系研究院" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3013473766.rss"/>
|
||||||
|
<outline text="中国舆论场" title="中国舆论场" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3211151294.rss"/>
|
||||||
|
<outline text="中国指挥与控制学会" title="中国指挥与控制学会" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3088709111.rss"/>
|
||||||
|
<outline text="中国周边安全研究中心" title="中国周边安全研究中心" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3095455037.rss"/>
|
||||||
|
<outline text="装备参考" title="装备参考" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3545019176.rss"/>
|
||||||
|
<outline text="最黑科技" title="最黑科技" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3094611859.rss"/>
|
||||||
|
</outline>
|
||||||
|
</body>
|
||||||
|
</opml>
|
||||||
@@ -10,6 +10,10 @@
|
|||||||
<outline text="Seapower" title="Seapower" type="rss" xmlUrl="https://seapowermagazine.org/feed/"/>
|
<outline text="Seapower" title="Seapower" type="rss" xmlUrl="https://seapowermagazine.org/feed/"/>
|
||||||
<outline text="The War Zone" title="The War Zone" type="rss" xmlUrl="https://www.twz.com/feed"/>
|
<outline text="The War Zone" title="The War Zone" type="rss" xmlUrl="https://www.twz.com/feed"/>
|
||||||
</outline>
|
</outline>
|
||||||
|
<outline text="中文收集源" title="中文收集源">
|
||||||
|
<outline text="纽约时报中文网" title="纽约时报中文网" type="rss" xmlUrl="https://plink.anyfeeder.com/nytimes/cn"/>
|
||||||
|
<outline text="联合早报-中国" title="联合早报-中国" type="rss" xmlUrl="https://rsshub.yynnice.top/zaobao/realtime/china"/>
|
||||||
|
</outline>
|
||||||
<outline text="中文微信公众号源" title="中文微信公众号源">
|
<outline text="中文微信公众号源" title="中文微信公众号源">
|
||||||
<outline text="90号茶室" title="90号茶室" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2391931960.rss"/>
|
<outline text="90号茶室" title="90号茶室" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_2391931960.rss"/>
|
||||||
<outline text="IPP评论" title="IPP评论" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3087967104.rss"/>
|
<outline text="IPP评论" title="IPP评论" type="rss" xmlUrl="https://werss.yynnice.top/feed/MP_WXS_3087967104.rss"/>
|
||||||
|
|||||||
+39
-11
@@ -78,21 +78,49 @@ def load_env_config() -> Dict:
|
|||||||
|
|
||||||
|
|
||||||
def parse_opml(opml_path: str) -> List[Dict]:
|
def parse_opml(opml_path: str) -> List[Dict]:
|
||||||
|
"""解析 OPML 文件,支持 fallback 到默认订阅源
|
||||||
|
|
||||||
|
优先级:
|
||||||
|
1. 配置的 opml_path(通常是挂载的 resources/rss_feeds.opml)
|
||||||
|
2. /app/resources_default/rss_feeds.opml(镜像内默认文件)
|
||||||
|
"""
|
||||||
path = Path(opml_path)
|
path = Path(opml_path)
|
||||||
if not path.exists():
|
|
||||||
return []
|
|
||||||
|
|
||||||
tree = ET.parse(path)
|
# 尝试原始路径
|
||||||
root = tree.getroot()
|
if path.exists() and path.stat().st_size > 0:
|
||||||
|
try:
|
||||||
|
tree = ET.parse(path)
|
||||||
|
root = tree.getroot()
|
||||||
|
feeds = []
|
||||||
|
for outline in root.findall(".//outline[@type='rss']"):
|
||||||
|
feeds.append({
|
||||||
|
"title": outline.get("title", ""),
|
||||||
|
"xmlUrl": outline.get("xmlUrl", ""),
|
||||||
|
})
|
||||||
|
if feeds:
|
||||||
|
return feeds
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 解析 {opml_path} 失败: {e}")
|
||||||
|
|
||||||
feeds = []
|
# Fallback 到镜像内默认文件
|
||||||
for outline in root.findall(".//outline[@type='rss']"):
|
default_path = Path("/app/resources_default/rss_feeds.opml")
|
||||||
feeds.append({
|
if default_path.exists() and default_path.stat().st_size > 0:
|
||||||
"title": outline.get("title", ""),
|
try:
|
||||||
"xmlUrl": outline.get("xmlUrl", ""),
|
tree = ET.parse(default_path)
|
||||||
})
|
root = tree.getroot()
|
||||||
|
feeds = []
|
||||||
|
for outline in root.findall(".//outline[@type='rss']"):
|
||||||
|
feeds.append({
|
||||||
|
"title": outline.get("title", ""),
|
||||||
|
"xmlUrl": outline.get("xmlUrl", ""),
|
||||||
|
})
|
||||||
|
if feeds:
|
||||||
|
print(f" 使用镜像默认订阅源: {default_path}")
|
||||||
|
return feeds
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 解析默认订阅源失败: {e}")
|
||||||
|
|
||||||
return feeds
|
return []
|
||||||
|
|
||||||
|
|
||||||
def merge_sources(sources_config: Dict) -> List[Dict]:
|
def merge_sources(sources_config: Dict) -> List[Dict]:
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
"""全文内容抓取模块
|
||||||
|
|
||||||
|
从RSS条目的链接地址异步获取完整文章内容。
|
||||||
|
使用 aiohttp 并发请求 + BeautifulSoup 提取正文。
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from src.fetcher import DEFAULT_HEADERS
|
||||||
|
|
||||||
|
# 文章正文常见容器选择器
|
||||||
|
CONTENT_SELECTORS = [
|
||||||
|
"article",
|
||||||
|
"[role='main']",
|
||||||
|
"main",
|
||||||
|
".article-body",
|
||||||
|
".article-content",
|
||||||
|
".post-content",
|
||||||
|
".entry-content",
|
||||||
|
".content",
|
||||||
|
".article",
|
||||||
|
"#article-body",
|
||||||
|
"#content",
|
||||||
|
".field-body",
|
||||||
|
".news-content",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 需要移除的元素选择器(导航、广告、侧边栏等)
|
||||||
|
# 注意:不包含 main/article 等可能作为正文容器的标签
|
||||||
|
# 注意:避免使用 [class*='ad'] 这样过于宽泛的选择器,会误伤正常内容
|
||||||
|
REMOVE_SELECTORS = [
|
||||||
|
"nav", "script", "style",
|
||||||
|
".sidebar", ".navigation", ".menu",
|
||||||
|
".ad-container", ".advertisement", ".ads", ".ad-banner", # 精确的广告类名
|
||||||
|
".share-buttons", ".related-posts", ".comments", ".tags",
|
||||||
|
"[class*='sidebar']", "[class*='navigation']", "[class*='menu']",
|
||||||
|
"[class*='share']", "[class*='related']", "[class*='comment']",
|
||||||
|
"[id*='sidebar']", "[id*='ad-container']", "[id*='advertisement']",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 常见的时间/作者元数据标记
|
||||||
|
META_INDICATORS = [
|
||||||
|
"published", "updated", "author", "byline", "dateline",
|
||||||
|
"read time", "min read", "阅读时间", "作者", "发布时间",
|
||||||
|
]
|
||||||
|
|
||||||
|
FETCH_TIMEOUT = 15 # 单篇文章抓取超时(秒)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_likely_body_text(text: str) -> bool:
|
||||||
|
"""判断文本段落是否可能是正文内容"""
|
||||||
|
text = text.strip()
|
||||||
|
if len(text) < 20:
|
||||||
|
return False
|
||||||
|
# 过滤掉明显的元数据
|
||||||
|
lower = text.lower()
|
||||||
|
if any(indicator in lower for indicator in META_INDICATORS):
|
||||||
|
return False
|
||||||
|
# 过滤掉太短的行(可能是标题或标签)
|
||||||
|
if len(text) < 50 and not text.endswith(('.', '!', '?', '。', '!', '?')):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_main_content(soup: BeautifulSoup, url: str) -> Optional[str]:
|
||||||
|
"""从HTML中提取正文内容
|
||||||
|
|
||||||
|
策略:
|
||||||
|
1. 先尝试常见文章容器选择器
|
||||||
|
2. 如果没有找到,尝试查找包含最多段落的 <p> 的父元素
|
||||||
|
3. 移除导航、广告等干扰元素
|
||||||
|
4. 过滤掉过短的段落(可能是元数据)
|
||||||
|
"""
|
||||||
|
# 策略1: 尝试常见选择器
|
||||||
|
for selector in CONTENT_SELECTORS:
|
||||||
|
element = soup.select_one(selector)
|
||||||
|
if element and len(element.get_text(strip=True)) > 100:
|
||||||
|
# 移除干扰元素
|
||||||
|
for remove_sel in REMOVE_SELECTORS:
|
||||||
|
for bad in element.select(remove_sel):
|
||||||
|
bad.decompose()
|
||||||
|
return element
|
||||||
|
|
||||||
|
# 策略2: 查找包含最多段落的容器
|
||||||
|
body = soup.find('body')
|
||||||
|
if not body:
|
||||||
|
body = soup
|
||||||
|
|
||||||
|
# 找到包含最多 <p> 标签的元素
|
||||||
|
best_element = None
|
||||||
|
max_p_count = 0
|
||||||
|
|
||||||
|
for elem in body.find_all(True):
|
||||||
|
p_count = len(elem.find_all('p'))
|
||||||
|
if p_count > max_p_count:
|
||||||
|
max_p_count = p_count
|
||||||
|
best_element = elem
|
||||||
|
|
||||||
|
if best_element and max_p_count >= 2:
|
||||||
|
# 移除干扰元素
|
||||||
|
for remove_sel in REMOVE_SELECTORS:
|
||||||
|
for bad in best_element.select(remove_sel):
|
||||||
|
bad.decompose()
|
||||||
|
return best_element
|
||||||
|
|
||||||
|
# 策略3: 直接使用 body
|
||||||
|
if body and len(body.get_text(strip=True)) > 100:
|
||||||
|
for remove_sel in REMOVE_SELECTORS:
|
||||||
|
for bad in body.select(remove_sel):
|
||||||
|
bad.decompose()
|
||||||
|
return body
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_content(element) -> str:
|
||||||
|
"""清理提取的正文内容"""
|
||||||
|
# 过滤掉过短的段落(可能是元数据)
|
||||||
|
paragraphs = element.find_all('p')
|
||||||
|
valid_paragraphs = []
|
||||||
|
|
||||||
|
for p in paragraphs:
|
||||||
|
text = p.get_text(strip=True)
|
||||||
|
if _is_likely_body_text(text):
|
||||||
|
valid_paragraphs.append(p)
|
||||||
|
|
||||||
|
# 如果过滤后还有有效段落,使用它们
|
||||||
|
if valid_paragraphs:
|
||||||
|
# 用有效段落替换原内容
|
||||||
|
new_div = element.find_parent().new_tag('div')
|
||||||
|
for p in valid_paragraphs:
|
||||||
|
new_div.append(p)
|
||||||
|
return str(new_div)
|
||||||
|
|
||||||
|
# 如果没有有效段落(可能段落都很短或者是其他结构),保留原始内容
|
||||||
|
return str(element)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_text_from_html(html: str) -> str:
|
||||||
|
"""从HTML中提取纯文本正文"""
|
||||||
|
if not html or len(html.strip()) < 50:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
soup = BeautifulSoup(html, 'html.parser')
|
||||||
|
|
||||||
|
# 移除所有链接的 href(避免干扰)
|
||||||
|
for a in soup.find_all('a'):
|
||||||
|
a.unwrap()
|
||||||
|
|
||||||
|
# 移除图片
|
||||||
|
for img in soup.find_all('img'):
|
||||||
|
img.decompose()
|
||||||
|
|
||||||
|
# 保留段落结构,转换为文本
|
||||||
|
text_parts = []
|
||||||
|
for elem in soup.find_all(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'blockquote']):
|
||||||
|
text = elem.get_text(strip=True)
|
||||||
|
if text and len(text) > 10:
|
||||||
|
text_parts.append(text)
|
||||||
|
|
||||||
|
if not text_parts:
|
||||||
|
# 如果没有找到结构化内容,尝试获取所有文本
|
||||||
|
text_parts = [soup.get_text(separator='\n', strip=True)]
|
||||||
|
|
||||||
|
return '\n\n'.join(text_parts)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ 内容提取失败: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_article_content(
|
||||||
|
session: aiohttp.ClientSession,
|
||||||
|
url: str,
|
||||||
|
timeout: int = FETCH_TIMEOUT
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""异步抓取单篇文章的完整内容
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
提取后的纯文本正文,失败返回 None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
client_timeout = aiohttp.ClientTimeout(total=timeout)
|
||||||
|
headers = {
|
||||||
|
**DEFAULT_HEADERS,
|
||||||
|
"Referer": url,
|
||||||
|
}
|
||||||
|
|
||||||
|
async with session.get(url, headers=headers, timeout=client_timeout) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
print(f" ⚠️ HTTP {resp.status}: {url}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
content_type = resp.headers.get('Content-Type', '')
|
||||||
|
if 'text/html' not in content_type and 'application/xhtml' not in content_type:
|
||||||
|
# 不是HTML内容(可能是PDF等)
|
||||||
|
return None
|
||||||
|
|
||||||
|
html = await resp.text()
|
||||||
|
|
||||||
|
# 提取正文
|
||||||
|
soup = BeautifulSoup(html, 'html.parser')
|
||||||
|
main_content = _extract_main_content(soup, url)
|
||||||
|
|
||||||
|
if not main_content:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 清理并提取文本
|
||||||
|
cleaned = _clean_content(main_content) if hasattr(main_content, 'find_all') else str(main_content)
|
||||||
|
text = extract_text_from_html(cleaned)
|
||||||
|
|
||||||
|
return text if text else None
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
print(f" ⚠️ 抓取超时: {url}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠️ 抓取异常: {url} - {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_contents_batch(
|
||||||
|
entries: List[Dict],
|
||||||
|
max_workers: int = 5,
|
||||||
|
timeout: int = FETCH_TIMEOUT
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""批量抓取文章完整内容
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entries: RSS条目列表,每个条目需包含 'link' 字段
|
||||||
|
max_workers: 最大并发数
|
||||||
|
timeout: 单篇文章超时时间
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
更新后的条目列表,新增 'full_content' 字段
|
||||||
|
"""
|
||||||
|
if not entries:
|
||||||
|
return entries
|
||||||
|
|
||||||
|
print(f" 📄 开始抓取全文内容: {len(entries)} 篇文章 (并发={max_workers})")
|
||||||
|
|
||||||
|
semaphore = asyncio.Semaphore(max_workers)
|
||||||
|
|
||||||
|
async def fetch_one(entry, session):
|
||||||
|
async with semaphore:
|
||||||
|
link = entry.get("link", "")
|
||||||
|
if not link:
|
||||||
|
return entry
|
||||||
|
|
||||||
|
content = await fetch_article_content(session, link, timeout)
|
||||||
|
if content:
|
||||||
|
entry["full_content"] = content
|
||||||
|
# 如果原文太短(摘要),用全文替换
|
||||||
|
original_content = entry.get("content", "")
|
||||||
|
if len(original_content) < 200 and len(content) > 500:
|
||||||
|
entry["content"] = content
|
||||||
|
print(f" ✅ {entry.get('title', '')[:40]}... (摘要→全文)")
|
||||||
|
else:
|
||||||
|
print(f" ✅ {entry.get('title', '')[:40]}...")
|
||||||
|
else:
|
||||||
|
print(f" ⚠️ {entry.get('title', '')[:40]}... (抓取失败)")
|
||||||
|
return entry
|
||||||
|
|
||||||
|
# 创建共享的 session
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
tasks = [asyncio.create_task(fetch_one(e, session)) for e in entries]
|
||||||
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
# 处理结果
|
||||||
|
success_count = sum(1 for e in entries if e.get("full_content"))
|
||||||
|
fail_count = len(entries) - success_count
|
||||||
|
print(f" 全文抓取完成: 成功 {success_count} 条 | 失败 {fail_count} 条")
|
||||||
|
|
||||||
|
return entries
|
||||||
@@ -8,6 +8,7 @@ from typing import Dict, List, Tuple
|
|||||||
|
|
||||||
from src.cache import get_cache
|
from src.cache import get_cache
|
||||||
from src.config import get_merged_sources, get_timezone
|
from src.config import get_merged_sources, get_timezone
|
||||||
|
from src.content_fetcher import fetch_contents_batch
|
||||||
from src.fetcher import fetch_all_feeds
|
from src.fetcher import fetch_all_feeds
|
||||||
from src.llm import (
|
from src.llm import (
|
||||||
generate_immediate_push,
|
generate_immediate_push,
|
||||||
@@ -127,6 +128,42 @@ async def fetch_rss_entries(
|
|||||||
return entries, lookback
|
return entries, lookback
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_full_contents(
|
||||||
|
entries: List[Dict],
|
||||||
|
config: Dict,
|
||||||
|
monitor,
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""阶段1.5: 全文内容抓取
|
||||||
|
|
||||||
|
从RSS条目的链接地址获取完整文章内容。
|
||||||
|
仅当配置启用且条目内容较短(摘要)时触发。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entries: RSS条目列表
|
||||||
|
config: 配置字典
|
||||||
|
monitor: 监控器实例
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
更新后的条目列表
|
||||||
|
"""
|
||||||
|
content_config = config.get("content_fetch", {})
|
||||||
|
if not content_config.get("enabled", False):
|
||||||
|
return entries
|
||||||
|
|
||||||
|
# 检查是否有需要抓取全文的条目(内容较短的视为摘要)
|
||||||
|
needs_fetch = [e for e in entries if len(e.get("content", "")) < 200 and e.get("link")]
|
||||||
|
if not needs_fetch:
|
||||||
|
print(f" ℹ️ 所有条目内容完整,跳过全文抓取")
|
||||||
|
return entries
|
||||||
|
|
||||||
|
with monitor.stage("content_fetch"):
|
||||||
|
max_workers = content_config.get("max_workers", 5)
|
||||||
|
timeout = content_config.get("timeout", 15)
|
||||||
|
entries = await fetch_contents_batch(entries, max_workers=max_workers, timeout=timeout)
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
def deduplicate_entries(
|
def deduplicate_entries(
|
||||||
entries: List[Dict],
|
entries: List[Dict],
|
||||||
config: Dict,
|
config: Dict,
|
||||||
@@ -542,6 +579,9 @@ async def run_fetch_pipeline(
|
|||||||
if not entries:
|
if not entries:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 阶段1.5: 全文抓取(如启用)
|
||||||
|
entries = await fetch_full_contents(entries, config, monitor)
|
||||||
|
|
||||||
# 阶段2: 去重
|
# 阶段2: 去重
|
||||||
new_entries, cache_hits = deduplicate_entries(entries, config, monitor, lookback)
|
new_entries, cache_hits = deduplicate_entries(entries, config, monitor, lookback)
|
||||||
if not new_entries:
|
if not new_entries:
|
||||||
|
|||||||
Reference in New Issue
Block a user