MoreRSS

site iconXiaoZongLin | 肖宗林修改

网名:林林。学生,初二开始写博客,在维护(或曾经维护过)“开往-友链接力”和“中文博客列表导航”项目。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

XiaoZongLin | 肖宗林的 RSS 预览

小东西:用FreshRSS实现带AI摘要的订阅推送

2026-05-10 12:38:40

前几天在协会问了一下有没有什么开发任务,然后找了一个开发订阅推送的活。

聊天记录(昵称和头像已用白色遮罩)

工具需要实现的功能是:定时爬取一些安全newsletter和博客的订阅源,并将爬取到的文章推送到协会的QQ群,要有AI的摘要。

模块 任务
FreshRSS 爬取、存储内容
Napcat 部署QQ机器人
Python 脚本对接AI、FreshRSS和Napcat

FreshRSS我用Docker方式部署,在应用中开放接口登录并设置一下API密钥,原本打算自己看着接口文档搞的,结果一搜发现Python有对应的接口库freshrss-api,直接就拿来用了。

Python
from freshrss_api import FreshRSSAPI
client = FreshRSSAPI(
    host="xxx",
    username="xxx",
    password="xxx",
    verbose=False
)

unread_items = client.get_unreads()

passages = []
pass_text = ""


for i in unread_items:
    passages.append([i.author, i.title, i.url, i.html, str(trafilatura.extract(trafilatura.fetch_url(i.url), output_format='markdown', include_tables=True))])
    client.set_mark(as_="read", id=i.id)

思路大概是这样,每次推送的时候都从未读的文章里面取,取出来就把文章设置为已读。

在获取到还未推送的文章(未读文章)之后,接着需要爬取文章的内容,供后面AI推荐和生成摘要使用。此处使用的是trafilatura库(星火杯参赛小记 用过的),可以将网页内容清洗成Markdown。因为遇到反爬时可能会返回None,导致后面字符串拼接时可能报错,所以对清洗出的结果用str( )进行强制转换。

Python
if len(passages) > 5:
    pass_text += "本次抓取文章数大于5篇,根据AI推荐,推送五篇较有价值的文章。\n"
    push_index = getAIrecom(passage_list=passages)
    for i in range(5):
        pass_text += f"Title: {passages[int(push_index[i])][1]} \nURL: {passages[int(push_index[i])][2]} \nBrief: {aibrief(passages[int(push_index[i])][4], passages[int(push_index[i])][3])}\n\n"
elif len(passages) == 0:
    exit(0)
else:
    for i in passages:
        pass_text += f"Title: {i[1]} \nURL: {i[2]} \nBrief: {aibrief(i[4], i[3])}\n\n"

接下来对未读文章的数量进行判断,小于等于5篇就都推送,大于5篇就让AI判断哪些东西有价值再推送。getAIrecom(passages)的作用是将所有文章的内容发给AI让其判断,返回一个文章序号的列表。aibrief(content, rsscontent)的作用是根据爬取到的文章内容和rss里面的摘要生成一段AI摘要。

Python
def aibrief(content, rsscontent):
    client = OpenAI(
        api_key="sk-xxx",
        base_url="https://api.deepseek.com")

    response = client.chat.completions.create(
        model="deepseek-v4-flash",
        messages=[
            {"role": "system", "content": "你是一个专业的秘书,负责总结文章的内容,供网络安全协会的推送使用。请你根据给定的文章内容,生成一段不长于75字的摘要,概括文章的主要内容、思路、技术方法,供网络安全协会的成员快速判断是否对文章感兴趣。"},
            {"role": "user", "content": "trafilatura得到的文章内容,可能会因为反爬而为None或无意义字符" + str(content) + "\n 以下是订阅软件从 rss 中获取到的内容" + rsscontent}
        ],
        stream=False,
        reasoning_effort="high",
        extra_body={"thinking": {"type": "enabled"}}
    )

    return response.choices[0].message.content
Python
def getAIrecom(passage_list) -> list:
    client = OpenAI(
        api_key="sk-xxx",
        base_url="https://api.deepseek.com")
    
    toEvaluateContent = ""
    index = 0
    for i in passage_list:
        toEvaluateContent += f"第{index}篇文章:\n标题:{i[1]}\nRSS摘要:{i[3]}\n网页摘要:{i[4]}\n\n"
        index += 1

    response = client.chat.completions.create(
        model="deepseek-v4-flash",
        messages=[
            {"role": "system", "content": "你是一个专业的秘书,负责筛选有价值的文章,供网络安全协会的推送使用。请你根据给定的文章内容,回答出其中最有价值的五篇文章的序号,序号之间用空格分隔,不要有多余内容。文章的价值从重要性和影响力来评估。因为反爬的原因,有一些文章的网页内容可能为None或无意义内容,请忽视这一点,根据RSS摘要来做判断。"},
            {"role": "user", "content": toEvaluateContent}
        ],
        stream=False,
        reasoning_effort="xhigh",
        extra_body={"thinking": {"type": "enabled"}}
    )

    return str(response.choices[0].message.content).split()

我原本不太熟悉类型怎么限定的,但之前好像看过写这种限定的代码,在这里加-> list的原因是前面代码用这个函数返回值的地方静态判断会报错。

Napcat有HTTP接口可以发送群消息,弄好要推送的文章和摘要之后调用接口发群消息即可。

Python
token = "xxx"
url = "http://xxx/send_group_msg"

headers = {'User-Agent': 'Mozilla/5.0', 'Authorization': f"Bearer {token}"}

data = {"group_id": xxx, "message": f"最近几小时爬取到了{len(passages)}篇文章,信息如下:\n{pass_text}\n各位成员可以在 xxx 查看所有文章。"}

x = requests.post(url, headers=headers, data=data)

print(x.text)

小脚本的完整代码:XDSec Push Bot

小东西:用FreshRSS实现带AI摘要的订阅推送最先出现在林林杂语

五一假期:南京之行

2026-05-05 08:28:13

去年国庆节没出去玩,清明假期短,五一刚刚好。原本想着回一趟家,毕竟在学校待了两个月了,但没约到火车票。不回去了,改成旅游。去北京,但北京的朋友说那边挤,不推荐去。去成都吧,成都的同学不愿意出门。去长沙吧,没有什么时间点不错的车票。南京算是一个还行的选择。

临行前几天,我的母亲又告诉我机票价格降下来了,要不要回家,或者就近去山西太原看看也行,太原啊晋城啊也有好看的,但南京的计划已定,变更起来就麻烦多了。

西安北到南京南,从早上九点到下午三点,中午叫了一份郑州东的外卖,跨过长江。坐高铁的经历对我来说还算新奇,也是头两回坐G开头的车。

侵华日军南京遇难同胞纪念馆的票难约,尤其难约。它不像中山陵那样一下就告诉我票没了,它每天放两次票,一直放到参观前一天,我就这么掐着时间点抢着约,今天八点没约到下午五点再战。准点进去提交总是显示人太多了,接着是做滑块和算术题,做完之后接着做,直到这个时间段的票约完为止,也就知道自己白做题了。后面从抖音上看到这种抢票需要慢慢悠悠去预约,刚好避开刚开始的高峰期,就约成了。但我照着这个教程预约,还没出验证码呢,就告诉我没了。一鼓作气,再而衰,三而竭。到后面,我连那个预约小程序都不想打开了,约也是约不上的。南京大屠杀史实陈列的那个展览没约上,但三个必胜的展览倒是一开始就约成了。我将这个作为我到南京的第一站。南京的展览很多,而这算是我整个旅途中看得最认真的展览了。

参观完展览后,坐地铁到一家餐馆与高中同学晚餐。然后逛了逛德基广场,参观了一下豪华厕所。

晚上九点多回到民宿,玩会方舟,然后睡觉。第二天起来,附近找了一家店吃了个鸡蛋汉堡,然后坐地铁到钟山风景区,看看明孝陵,然后到梧桐大道看看,发现全是人。

到一个地方,参观参观同学的大学估计成了我们的一个传统……在Norcleeh的帮助下,我得以进入南邮参观参观。南邮的仙林校区感觉跟西电一样远离主城区,但因为南邮旁边还有其他几所大学所以附近有些广场和商铺,而西电就没有这样的待遇了。南邮一进去就是两侧栽着梧桐的大道,也算是一个梧桐大道了。高中同学告诉我,南邮又叫南京自行车大学,大道上面不是停满了车,就是骑满了车。

参观完南邮之后打了个车回民宿休息,一觉起来就是下午三点半,睡前头晕想吐,估计是中暑了,睡完之后好一些,洗一把脸起来逛一逛瞻园。晚上又到夫子庙看看,所谓不挤一次不算来南京,但感觉没什么好看的,没留什么照片。

次日早上参观总统府。

参观完总统府之后在南京的路上走走,不知不觉就走到浮桥的地铁站了。想着时间也不早了,附近找了一家兰州拉面吃炒刀削,就坐地铁到南京南站。从下午两点到晚上八点,又坐了六个小时的高铁。趴在小桌板上睡了一会,靠在座椅上又睡了几次。

钟山风雨起苍黄,百万雄师过大江。
虎踞龙盘今胜昔,天翻地覆慨而慷。
宜将剩勇追穷寇,不可沽名学霸王。
天若有情天亦老,人间正道是沧桑。

毛泽东《七律·人民解放军占领南京》

五一假期:南京之行最先出现在林林杂语

再见面板:Debian构建WordPress

2026-04-30 23:42:30

任务要求:使用Debian纯命令行构建自己的WordPress网站,并通过一些方法支持http://linlin.zzo访问。

Bash
linlinzzo@linlinzzo:~$ cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
NAME="Debian GNU/Linux"
VERSION_ID="13"
VERSION="13 (trixie)"
VERSION_CODENAME=trixie
DEBIAN_VERSION_FULL=13.4
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

安装所需要的应用

Bash
sudo apt install -y php-fpm # PHP
sudo apt install nginx

安装 nginx 的时候报错Not attempting to start NGINX, port 80 is already in use.,当我通过Linux 查看端口占用情况 | 菜鸟教程的方法去检查端口占用的时候发现lsof和netstat命令都不存在,用apt把lsof安装一下。发现是apache把80端口给占了。

Bash
linlinzzo@linlinzzo:~$ sudo lsof -i:80
COMMAND  PID     USER FD   TYPE DEVICE SIZE/OFF NODE NAME
apache2 9027     root 4u  IPv6  35905      0t0  TCP *:http (LISTEN)
apache2 9030 www-data 4u  IPv6  35905      0t0  TCP *:http (LISTEN)
apache2 9031 www-data 4u  IPv6  35905      0t0  TCP *:http (LISTEN)
apache2 9032 www-data 4u  IPv6  35905      0t0  TCP *:http (LISTEN)
apache2 9033 www-data 4u  IPv6  35905      0t0  TCP *:http (LISTEN)
apache2 9034 www-data 4u  IPv6  35905      0t0  TCP *:http (LISTEN)

把apache卸载之后就可以用systemctl启动nginx了。

正要打算从官网下载MySQL,结果发现wget没装,把wget装一下,然后安装MySQL。

SQL
CREATE USER '111'@'localhost' IDENTIFIED BY '111';
CREATE DATABASE lin;
GRANT ALL PRIVILEGES ON lin.* TO '111'@'localhost';

ip addr看虚拟环境的IP,通过设置hosts文件让linlin.zzo指向虚拟环境。因为Nginx的配置文件是照搬网上的,一开始看日志是root设置错了还没有设置好访问权限,改好后还是报错,发现是php-fpm的路径不对。用where命令找不到php-fpm。通过下面的方法找到.sock的地址。

Bash
linlinzzo@linlinzzo:/etc/nginx/conf.d$ sudo systemctl status php8.4-fpm
 php8.4-fpm.service - The PHP 8.4 FastCGI Process Manager
     Loaded: loaded (/usr/lib/systemd/system/php8.4-fpm.service; enabled; preset: enabled)
(...)
linlinzzo@linlinzzo:/etc/nginx/conf.d$ cat /usr/lib/systemd/system/php8.4-fpm.service
(...)
[Service]
Type=notify
ExecStart=/usr/sbin/php-fpm8.4 --nodaemonize --fpm-config /etc/php/8.4/fpm/php-fpm.conf
ExecStartPost=-/usr/lib/php/php-fpm-socket-helper install /run/php/php-fpm.sock /etc/php/8.4/fpm/pool.d/www.conf 84
ExecStopPost=-/usr/lib/php/php-fpm-socket-helper remove /run/php/php-fpm.sock /etc/php/8.4/fpm/pool.d/www.conf 84
(...)

这下可以显示出页面了,提示说需要安装一个PHP拓展,那我们安装一下。

Bash
linlinzzo@linlinzzo:/etc/nginx/conf.d$ sudo apt install php-mysqli
Note, selecting 'php8.4-mysql' instead of 'php-mysqli'
...

安装好之后回到刚才的安装界面设置管理账户的用户名和密码,接下来通过WordPress自带的迁移功能进行迁移。

WordPress无论是自己上传插件文件还是从插件市场上安装都需要配置FTP信息,奇怪,为什么我在我服务器上不用这些信息。我不太想配置FTP,所以直接在Windows把压缩包传到Debian然后解压。

当我启用导入插件并将我这个博客的导出文件传进去时,发生了413 Request Entity Too Large。我在Nginx的配置文件中加了一行放宽一下限制:client_max_body_size 100M;

这样大致就OK了。

未完待续

用VirtualBox安装虚拟机之后,使用普通用户登录发现没有sudo权限,改成root身份,后面配置就不用敲sudo了。

再见面板:Debian构建WordPress最先出现在林林杂语

再见面板:记zhblogs运维

2026-04-28 23:30:37

之前沐云问我愿不愿意回到 zhblogs 参加维护,我回答如果参加的话希望可以负责一些运维有关的任务,于是在 zhblogs 新版本上线前夕,沐云问我愿不愿意部署新版本。

与沐云的聊天记录

整个任务大概是这样的:我先登上服务器把数据库备份下来,然后重装服务器系统,拉取代码,部署。过程中需要给项目整一个维护页面。

沐云vibe了一个维护页面,先是部署在我的服务器上,配置CDN那边花了一点时间,然后发现因为项目域名没有在阿里云接入备案所以被拦截了,于是沐云改用EdgeOne部署维护页面。在他用EdgeOne的时候,我收到了来自EdgeOne的邮件和短信,里面说EdgeOne支持部署Python Flask和Go的进行时了,沐云也为EdgeOne的升级感到惊喜,如果真是这样的话就连后端都不用部署在服务器上了。(ToDo++:重写一下自己的博客,并部署到EdgeOne上)

接着是登录服务器,这个费老大劲了。我先是用ssh-keygen生成密钥对,然后把公钥上传到腾讯云的密钥管理里面,绑定lighthouse,但怎么登都登不进去。后面用腾讯云生成的密钥对,绑定,也是怎么登都登不进去。沐云来远程也找不到哪里出了问题,后面只能曲线救国:用他的私钥登上去。等我登上去发现腾讯云压根没把我的公钥加到~/.ssh/authorized_keys,我手动加上去之后就可以用自己的私钥登录了。

然后找一找postgresql装在哪里,发现是用Docker装的,执行sudo docker exec postgresql-17 pg_dump -U postgres -d zhblogs > /home/zhblogs/db_backup_$(date +%Y%m%d).sql将需要备份的数据库dump出来,再通过scp命令下载到电脑本地。

大佬们好像都对服务器面板不感冒,甚至有点排斥。沐云要求我用纯命令行进行操作,这对我来说是一个机会。(ToDo++:宝塔面板把我的服务器弄得乱七八糟,重装系统自己用命令行把博客部署在一个“干净”的服务器上)

接下来是在不使用面板的情况下重装系统,并完成这些任务:

  • 用户组配置
  • docker,pg,nginx,fail2ban的安装和配置

沐云给出的用户组配置的方案是这样的:

  • 创建一个www用户组
  • 创建一个zhblogs用户,不可登录,位于www和sudo用户组
  • 创建一个linlinzzo用户,位于www用户组
  • linlinzzo用户在执行命令时可以用zhblogs的身份

于是,如果linlinzzo用户要使用root用户的权限执行命令,就需要先sudo -u zhblogs,再sudo

提权之路

关于用户组的配置,我是这么设置的(使用root用户执行):

Bash
groupadd www
useradd -r -s /usr/sbin/nologin zhblogs
usermod -aG www zhblogs
usermod -aG sudo zhblogs

useradd linlinzzo
usermod -aG www linlinzzo

visudo
linlinzzo ALL=(zhblogs) NOPASSWD: ALL
zhblogs ALL=(ALL) NOPASSWD: ALL

给这两个命令加个别名,不加的话打多了挺烦的。后面需要用root权限把sudo改成zhb-sudo就可以了。

Bash
cat >> ~/.bashrc << 'EOF'
alias zhb='sudo -u zhblogs'
alias zhb-sudo='sudo -u zhblogs sudo'
EOF

source ~/.bashrc

接着就是按照官网的安装文档安装Docker,Postgresql和Valkey我是直接用apt安装的,其中Valkey被搜索到其实软件包叫valkey-server。

短暂的半天又不知道弄了什么。

4 月 30 日,因为我的能力实在堪忧,这些任务重新由沐云来做。于是就有 再见面板:Debian构建WordPress – 林林杂语 这一篇的故事了。

附录

再见面板:记zhblogs运维最先出现在林林杂语

半学期记:春日校园

2026-04-28 09:40:46

4月22日,期中考试结束了,半个学期就这么过去了。校园很美,翻了翻这半个学期来自己拍的照片,技术很烂,用手机记录下的照片并不清晰,有些照片我现在看过去自己都没看懂要表达什么。我将一些照片筛选出来,与各位分享。

半学期记:春日校园最先出现在林林杂语