本地化WordPress 的新浪微博图片外链

作者:matrix 发布时间:2024-09-30 分类:Golang 零零星星

😀 免费的才是最贵的。新浪微博的图床早就挂了,目前的图片会限制请求头 referer

今天空了才把这部分图片迁移到本地。记录下这个临时脚本。

脚本下载 WordPress 文章中的新浪图片到本地,然后数据库中的图片链接会执行替换。
配置好信息之后正式执行记得放开 #94行的TODO。 自行测试~

package main

import (
    "database/sql"
    "fmt"
    "io"
    "net/http"
    "os"
    "path/filepath"
    "regexp"

    _ "github.com/go-sql-driver/mysql"
)

const (
    //TODO
    domain       = "www.hhtjim.com"                                             //WordPress 域名
    db_user      = "root"                                                       //db 信息
    db_password  = "root"                                                       //db 信息
    db_host_port = "127.0.0.1:33060"                                            //db 信息
    db_name      = "wordpress_db"                                               //db 信息
    savePathBase = "/home/www/htdoc/wordpress/wp-content/uploads/2024/sinaimg/" //图片下载保存位置
    newBase      = "https://" + domain + "/wp-content/uploads/2024/sinaimg/"    // 替换后的图片 url path
    referer      = "https://m.weibo.cn"
    dsn          = db_user + ":" + db_password + "@tcp(" + db_host_port + ")/" + db_name
    query        = "SELECT ID, post_content,post_content_filtered FROM wp_posts WHERE post_type = 'post' AND post_content LIKE '%sinaimg.cn%'  ORDER BY ID DESC;"
)

var (
    re = regexp.MustCompile(`//([a-zA-Z\d]+.sinaimg.cn)/([\w]+/[\w_-]+.(?:jpg|png|gif))`)
)

func main() {
    // 连接数据库
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        fmt.Println("Failed to connect to database:", err)
        return
    }
    defer db.Close()

    rows, err := db.Query(query)
    if err != nil {
        fmt.Println("Failed to query database:", err)
        return
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var postContent string
        var post_content_filtered string
        if err := rows.Scan(&id, &postContent, &post_content_filtered); err != nil {
            fmt.Println("Failed to scan row:", err)
            continue
        }

        updatedContent := re.ReplaceAllStringFunc(postContent, func(match string) string {
            matches := re.FindStringSubmatch(match)
            if len(matches) != 3 {
                return match
            }
            imgURL := "https:" + match
            savePath := matches[2]

            if _, err := os.Stat(savePath); os.IsNotExist(err) {
                if err := downloadImage(imgURL, savePath); err != nil {
                    fmt.Println("Failed to download image:", err)
                    return match
                }
            }
            return newBase + savePath
        })

        updatedContentFiltered := re.ReplaceAllStringFunc(post_content_filtered, func(match string) string {
            matches := re.FindStringSubmatch(match)
            if len(matches) != 3 {
                return match
            }
            imgURL := "https:" + match
            savePath := matches[2]

            if _, err := os.Stat(savePath); os.IsNotExist(err) {
                if err := downloadImage(imgURL, savePathBase+savePath); err != nil {
                    fmt.Println("Failed to download image:", err)
                    return match
                }
            }
            return newBase + savePath
        })

        fmt.Println(id, updatedContent, updatedContentFiltered)
        // fmt.Printf("https://%s?p=%d\n", domain, id)
        continue //TODO
        // break

        _, err := db.Exec("UPDATE wp_posts SET post_content = ?,post_content_filtered=? WHERE ID = ?", updatedContent, updatedContentFiltered, id)
        if err != nil {
            fmt.Println("Failed to update database:", err)
            continue
        }
    }
}

func downloadImage(imgURL, savePath string) error {
    resp, err := http.Get(imgURL)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("failed to download image: %s", resp.Status)
    }

    dir := filepath.Dir(savePath)
    if err := os.MkdirAll(dir, 0755); err != nil {
        return err
    }

    file, err := os.Create(savePath)
    if err != nil {
        return err
    }
    defer file.Close()

    _, err = io.Copy(file, resp.Body)
    return err
}

自动给 Google 搜索结果添加查看缓存功能

作者:matrix 发布时间:2024-07-12 分类:零零星星

R.I.P.

2024年09月25日 google快照已死?️

https://www.solidot.org/story?sid=79336

Google 搜索结果的查看缓存功能下线其实很久了,每次都得手动 `cache:https://www.hhtjim.com/` 就很麻烦。

有空搞了个油猴脚本能自动在Google搜索结果中添加 [Cache] 链接到该网页的缓存版本 ?? 这就方便多了

安装地址

https://greasyfork.org/zh-CN/scripts/500422-google-cache-viewer

脚本代码

// ==UserScript==
// @name         Google cache viewer
// @namespace    http://hhtjim.com/
// @version      1.0.1
// @description  Automatically adds a cache link to Google Search results. / Google搜索结果中添加缓存按钮
// @author       Hootrix
// @include      https://www.google.tld/search?*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    window.addEventListener('load', function() {
        // select containers `cite[role="text"]`
        const containers = document.querySelectorAll('.g.Ww4FFb.vt6azd.tF2Cxc.asEBEc');

        containers.forEach(container => {
            //const cite = container.querySelector('cite[role="text"]');
            let cites = container.querySelectorAll('cite[role="text"]');
            // last item
            let cite = cites[cites.length - 1];
            const link = container.querySelector('a[data-ved]');
            if (cite && cite.textContent.startsWith('http')) {
                //const url = cite.textContent;
                const url = link.href
                const cacheUrl = `https://webcache.googleusercontent.com/search?q=cache:${url}`;

                const cacheDiv = document.createElement('div');
                cacheDiv.className = '';  // class name  eFM0qc
                cacheDiv.innerHTML = `<a href="${cacheUrl}" target="_blank" style="visibility:visible;color: blue; margin-left: 10px;" rel="noopener">[Cache]</a>`;

                if (cite.parentElement) {
                    cite.parentElement.appendChild(cacheDiv);
                }
            }
        });
    });
})();

iStore软路由自定义DNS服务

作者:matrix 发布时间:2024-06-10 分类:零零星星

家庭网络使用iStoreOpenwrt软路由系统作为旁路网关可以更好的控制进出流量。比如拦截广告请求的域名,本地 server 域名...

局域网环境有一个常驻服务,这次给他配置一个专属域名homeserver.lan。只要设备接入家庭网络,不管客户端设置的什么DNS服务都可以使用。

静态IP配置

将常驻服务的IP固定,方便后续访问。我自己使用的 iKuai主路由配置。

iStore(Openwrt)的话可以参考下面形式自己设置:

打开iStore终端使用uci命令配置 dhcp 服务,或者自己在页面点击完成配置

命令行方式:

uci add dhcp host
uci set dhcp.@host[-1].name='homeserver.lan'
uci set dhcp.@host[-1].mac='AA:BB:CC:DD:EE:FF' # 绑定的 mac 
uci set dhcp.@host[-1].ip='192.168.11.11' # 需要访问的服务IP
uci commit dhcp

根据自己需要配置 mac 地址和 自己需要访问的IP

使用Dnsmasq配置DNS服务

目的:将软路由系统作为 DNS 服务器,自定义域名homeserver.lan映射到IP 192.168.11.11

iStore --> 网络 --> DHCP/DNS --> 主机名映射

命令行方式:

uci add dhcp domain 
uci set dhcp.@domain[-1].name='homeserver.lan'
uci set dhcp.@domain[-1].ip='192.168.11.11' #需要访问的服务IP
uci commit dhcp 

配置防火墙拦截 DNS 请求

目的是确保所有客户端的 DNS 请求都经过路由网关进行拦截,除非客户端手动配置 DNS 服务为路由网关 192.168.11.12。

iStore --> 网络 --> 防火墙 --> 端口转发

命令行方式:

uci add firewall redirect 
uci set firewall.@redirect[-1].target='DNAT'
uci set firewall.@redirect[-1].name='Redirect DNS' #自定义规则名称
uci add_list firewall.@redirect[-1].proto='udp'
uci set firewall.@redirect[-1].src='lan'
uci set firewall.@redirect[-1].src_dport='53'
uci set firewall.@redirect[-1].dest_ip='192.168.11.12' # 旁路由网关IP
uci set firewall.@redirect[-1].dest_port='53'
/etc/init.d/firewall restart

ping检测

使用ping命令检查连通性

% ping  homeserver.lan          
PING homeserver.lan (192.168.11.11): 56 data bytes
64 bytes from 192.168.11.11: icmp_seq=0 ttl=64 time=3.906 ms
64 bytes from 192.168.11.11: icmp_seq=1 ttl=64 time=4.432 ms
64 bytes from 192.168.11.11: icmp_seq=2 ttl=64 time=9.283 ms
64 bytes from 192.168.11.11: icmp_seq=3 ttl=64 time=4.778 ms

如果没通,可以清下DNS缓存

mac下清理DNS缓存信息

sudo killall -HUP mDNSResponder
sudo dscacheutil -flushcache

自定义域名注意

.local域名在 Mac、Linux、Windows系统下有特殊作用,就算DNS服务器配置了域名映射也无法正常使用。客户端可以nslookup命令查看DNS服务器配置的.local域名,但是无法完成网络请求。因为有mDNS协议的存在,.local会独立于其他域名进行处理。
如果你一定要使用.local,请遵循mDNS协议配置~ 见 利用mDNS协议使用局域网local域名服务

参考:
https://cloud.tencent.com/developer/article/1683190

GPG密钥基本使用

作者:matrix 发布时间:2024-05-31 分类:零零星星

创建密钥

$ gpg --full-gen-key

### 密钥的有效期限是? 0 永不过期

agpg: 密钥 AEBCD7019762DB73 被标记为绝对信任
gpg: 吊销证书已被存储为‘/Users/root/.gnupg/openpgp-revocs.d/18A7EFFD0146D38B207B30D0AEBCD7019762DB73.rev’
公钥和私钥已经生成并被签名。

pub   rsa3072 2024-04-07 [SC]
      18A7EFFD0146D38B207B30D0AEBCD7019762DB73
uid                      hhtjim <root@hhtjim.com>
sub   rsa3072 2024-04-07 [E]

这里 AEBCD7019762DB73 为简写密钥id,全写为18A7EFFD0146D38B207B30D0AEBCD7019762DB73

创建吊销凭证

生成一张"撤销证书",以后密钥作废时可以请求外部的公钥服务器撤销公钥。

 gpg --gen-revoke AEBCD7019762DB73 

已强行使用 ASCII 字符封装过的输出。

-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: This is a revocation certificate

iQG2BCABCAAgFiEEGKfv/QFG04sgezDQrrzXAZdi23MFAmYS+P0CHQIACgkQrrzX
AZdi23Pyswv/ePcki/yEsXLKRbiwaJkBVeuVzxdjSj/5WvnEimR+SZH+XH2M7WW+
1hvk9JkEl5tepBha6DcnJXSBeZdRs9I8zmTdh08KzbF3ujsz+tnwgkRVTykllAbw
G6ylqt8MFxYpjqLzNnmCviByAFcqSLS7a4sHAhMMYEJNlcgc6/5YXSIzYu8a5psd
AxLb44kzHTL4lVsQgSFw+hxJA7EfyltAX10T0IBtaVki3hIsyoWixK1NYtpHjemS
ycCaiRSVytK73F7n977Xu7vx20LXF9WMXnQogfbL4FQ+AtEXMmQcMJq2A4VXUSOg
zyKitqw7vu9pJfoTmwPepPa4TkNwhI1qLQDhrajCtC0Ga757+K3ffJIimY2H8L/9
qQqc9mh+mSE0cR1uanSi1kC9NVqBGUuu0g1nWpZxaRyHjPVxR9T8kNbaqm+d5OYB
9QFAV7tHItCMWIYWwdUS0dByqswRZyv3zJs40cimlpZwJxt7RVP3NiPp3Y9XzHv+
HgcIXK/1DpOg
=JXdU
-----END PGP PUBLIC KEY BLOCK-----

已创建吊销证书。

发布公钥到公网服务器

发布到公网后,其他人可以很方便的进行下载来使用(签名检查,解密....)

$ gpg --keyserver keys.openpgp.org --send-keys AEBCD7019762DB73

keyserver 是指定的公网服务器

keys.openpgp.org可以搜索密钥id,如果要邮箱搜索需要进行验证:

电子邮件地址验证:
当你首次上传密钥到 keys.openpgp.org,该服务器会发送一个验证链接到与该密钥关联的电子邮件地址。只有点击了这个链接并完成验证过程后,电子邮件地址才会与该密钥关联,并且在搜索中可见。

导入别人的公钥

 gpg --keyserver keys.openpgp.org --recv-keys B1F6E658B6A3DC2A2E30A09D29A7777777777777 

删除密钥

删除别人的公钥

 gpg --delete-key  B1F6E658B6A3DC2A2E30A09D29A7777777777777

导出私钥

gpg --armor --output private-key.gpg --export-secret-keys AEBCD7019762DB73

参考:

https://www.ruanyifeng.com/blog/2013/07/GPG.html

自动打码Twitter图片

作者:matrix 发布时间:2024-03-26 分类:零零星星

最近几年总是黄推泛滥,不管你有没有关注总会在评论区看到?
网页刷推时要是看到就很尴尬了,索性把所有图片全部打码,鼠标悬浮才显示。
完美解决???

图片5765-自动打码Twitter图片

安装地址

https://greasyfork.org/zh-CN/scripts/492051-twitter-safeview-auto-blur-with-hover-reveal

脚本代码

// ==UserScript==
// @name         Twitter SafeView: Auto-Blur with Hover Reveal
// @namespace    http://hhtjim.com/
// @version      1.0
// @description  自动模糊所有图片,悬停时才显示完整清晰图像。Automatically blurs all images and displays full clear images only when hovering.
// @author       You
// @match        https://twitter.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const debounceDelay = 100; // milliseconds

    // Debounce function to limit the rate of execution
    function debounce(func, delay) {
        let debounceTimer;
        return function() {
            const context = this;
            const args = arguments;
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(() => func.apply(context, args), delay);
        };
    }


    let mouseX = 0, mouseY = 0;

    document.addEventListener('mousemove', debounce(function(e) {
        mouseX = e.clientX;
        mouseY = e.clientY;
        updateImageBlur();
    }, debounceDelay));


    document.addEventListener('scroll', debounce(function(e) {
        updateImageBlur();
    }, debounceDelay));


    // Function to check if the mouse is over the element
    function isMouseOverElement(element) {
        const rect = element.getBoundingClientRect();
        return mouseX > rect.left && mouseX < rect.right && mouseY > rect.top && mouseY < rect.bottom;
    }

    // Function to update image blur
    function updateImageBlur() {
        // console.log('updateImageBlur')
        //列表
        document.querySelectorAll('article[data-testid="tweet"]').forEach(function(tweetDiv) {
            // Apply or remove blur based on mouse position
            if (isMouseOverElement(tweetDiv)) {
                closeBlur(tweetDiv)
            } else {
                applyBlur(tweetDiv)
            }

        });
    }

    // Apply blur to the div and nested img
    const applyBlur = (document) => {
        // 推文
        document.querySelectorAll('div[data-testid="tweetPhoto"], div[data-testid="card.layoutLarge.media"]').forEach(function(div) {
            div.style.filter = 'blur(8px)';
            const img = div.querySelector('img');
            if (img) img.style.filter = 'blur(8px)';
        });
    };
    const closeBlur = (document) => {
        document.querySelectorAll('div[data-testid="tweetPhoto"], div[data-testid="card.layoutLarge.media"]').forEach(function(div) {
            div.style.filter = '';
            const img = div.querySelector('img');
            if (img) img.style.filter = '';
        });
    };

    // Observe for changes in the document
    const observer = new MutationObserver(debounce(function() {
            updateImageBlur();
        },debounceDelay));


    // Configuration of the observer
    const config = { childList: true, subtree: true };

    // var target = document.querySelector('section[aria-labelledby="accessible-list-1"]')
    var target = document.body

    // Start observing the target node for configured mutations
    if(target){
        observer.observe(target, config);
    }

    // Initial application of blur to images
    updateImageBlur();
})();

搭建hysteria2 服务端

作者:matrix 发布时间:2024-01-31 分类:零零星星

服务器配置

配置文件的目录
/root/hysteria

自签证书key crt生成

openssl req -x509 -nodes -newkey ec:<(openssl ecparam -name prime256v1) -keyout /root/hysteria/server.key -out /root/hysteria/server.crt -subj "/CN=bing.com" -days 36500

# sudo chown hysteria /root/hysteria/server.key
# sudo chown hysteria /root/hysteria/server.crt

新建hysteria2.yaml文件


listen: :443 #监听端口 #使用CA证书 #acme: # domains: # - hhtjim.com #你的域名,需要先解析到服务器ip # email: test@hhtjim.com #使用自签证书 tls: cert: /app/server.crt key: /app/server.key auth: type: password password: 999999 #设置认证密码 masquerade: type: proxy proxy: url: https://www.baidu.com #伪装网址 rewriteHost: true

最终需要的三个文件

hysteria2.yaml
server.crt
server.key

启动服务

docker run -v /root/hysteria:/app/ --net=host --rm -it tobyxdd/hysteria server  -c /app/hysteria2.yaml

客户端使用

新建配置文件hysteria2-client.yaml


# 服务器信息及端口 server: server.hhtjim.com:443 auth: 999999 # 根据本地带宽设置上下行 bandwidth: up: 20 mbps down: 300 mbps tls: insecure: true #使用自签时需要改成true socks5: listen: 127.0.0.1:1084 http: listen: 127.0.0.1:8084

启动


docker run -v $(pwd)/hysteria2-client.yaml:/app/hysteria2.yaml --network=host --name=hysteria2-client -d tobyxdd/hysteria client -c /app/hysteria2.yaml

参考:
https://vpsxb.net/5008/

https://raw.githubusercontent.com/chika0801/hysteria-install/main/config_server.yaml

https://bulianglin.com/archives/hysteria2.html

部署Docker版Hysteria服务端

局域网游戏串流工具 parsec

作者:matrix 发布时间:2023-12-05 分类:零零星星

Parsec

https://Parsec.app/

Parsec是专门针对游戏玩家的远程桌面工具,有很好的低延迟体验。
我用Parsec的Mac端连接PC端来远程玩游戏,完全可替代微软的Microsoft Remote Desktop。MRD虽然可以稍微优化下网络延迟($ sysctl net.inet.tcp.delayed_ack=0),但是效果还是差强人意,MRD远程时拖拽窗口和游戏的高频操作依然能感受到明显延迟。不过MRD在作为办公远程方面还是不错的,毕竟什么粘贴复制那些还是方便。

注册

登录注册需要有外网访问能力,免费版本的个人使用完全够用。

https://dash.parsec.app/signup/

按照官网提示流程操作即可,这里不打算手把手截图了

安装

widnows: https://builds.parsec.app/package/parsec-windows.exe

mac: https://builds.parsec.app/package/parsec-macos.pkg

软件安装后需要重启,如果没接显示器内部会虚拟出显示器,然后GPU压缩视频流P2P传输数据

登录

mac控制端,Windows被控端 各自登录同一个账号。如果出现登录-800的错误码则是网络问题,你需要自己配置代理

Parsec代理配置文件路径:

#Windows
#Per User installation 方式: 
%appdata%\Parsec\config.txt

#Shared installation 方式: 
%programdata%\Parsec\config.txt

# macOS / Linux / Raspberry Pi
~/.parsec/config.txt

config.txt代理配置内容

# 代理地址
app_proxy_address = 127.0.0.1
# 代理协议 http / https
app_proxy_scheme = http
app_proxy = true
# 代理端口
app_proxy_port = 1087

官方代理配置参考:
https://support.parsec.app/hc/en-us/articles/5805484240269-Configure-App-Level-HTTP-Proxy

远程

打开被控端,本地主机Computers下面出现当前机器名称。点击分享Share后复制链接到 mac控制端浏览器,打开后点击连接就可以了

游戏体验很跟手~

参考:

https://zhuanlan.zhihu.com/p/557637085

https://www.bilibili.com/read/cv16988873/

https://www.pjkui.com/98/title/macOS使用windows远程桌面RDP反应速度很慢解决方案

GitHub Action自动release发布版本 + docker镜像打包

作者:matrix 发布时间:2023-06-23 分类:零零星星

https://github.com/Hootrix/keyword_alert_bot

针对keyword_alert_bot项目早就有添加docker镜像的想法,这次趁着有机会就完成这个feature,也算是使用下GitHubCI/CD

我的想法是利用GitHub action功能,推送代码更新后自动打包docker镜像且安装所有依赖。打包的时候把最新版本号写入到代码中(当前日期作为版本号)。使用者把docker镜像pull后可以轻松运行整个bot。

流水线配置

项目的 .github/workflows/main.yml路径下新建文件:

name: CI/CD Pipeline

on:
  push:

    # 指定分支push操作触发流水线
    branches:
      # - dev.20230419 # debug
      - master

jobs:

  # 自定义job流水线名字
  build-and-push:

    # 指定运行环境 ubuntu最新版本
    runs-on: ubuntu-latest

    # 设计流水线阶段
    steps:

    # 名称
    - name: Check out code
      # 使用预先定义好的action。actions/checkout@v2 是 GitHub 官方提供,目的是将代码检出(checkout)到运行器上
      uses: actions/checkout@v2
      with: # 配置参数
        fetch-depth: 2 # 表示只获取最新的两个commit提交记录 

    # 设置python3.7环境
    - name: Set up Python 3.7
      uses: actions/setup-python@v3
      with:
        python-version: 3.7

    # 安装pipenv依赖管理
    - name: Install pipenv
      run: |
        python -m pip install --upgrade pip
        python -m pip install pipenv

    # 标记是否存在py文件的更新,用于后续docker镜像打包的判断
    - name: Check for file changes
      id: file_check
      run: |
        if git diff --name-only HEAD^ | grep -q ".py$"; then
          echo "::set-output name=updated::true"
        else
          echo "::set-output name=updated::false"
        fi

    # 安装项目依赖
    - name: Install dependencies and lock
      if: steps.file_check.outputs.updated == 'true'
      run: |
        pipenv install --dev
        pipenv lock

    # 设置最新版本号 如 20230520.91a4ca1
    - name: Create version file
      run: |
        COMMIT_ID=$(git rev-parse --short HEAD)
        # 写入版本信息到指定文件 
        echo "__version__ = '$(TZ='Asia/Shanghai' date +'%Y%m%d').$COMMIT_ID'" > utils/__version__.py


    # 登录DockerHub
    - name: Login to DockerHub
      uses: docker/login-action@v1 
      with:
        username: ${{ secrets.DOCKER_HUB_USERNAME }}
        password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

    # 存在更新条件 则推送到指定docker镜像仓库 tag为latest
    - name: Build and push Docker image
      if: steps.file_check.outputs.updated == 'true'
      uses: docker/build-push-action@v2
      with:
        context: .
        push: true

        # 推送到指定仓库镜像地址
        tags: yha8897/keyword_alert_bot:latest

    # 使用gh命令创建github release 执行版本发布
    - name: Create release
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        # 读取指定文件的版本号。根据自己项目设置
        VERSION=$(python -c "from utils.__version__ import __version__; print(__version__)")
        echo $VERSION
        # 执行版本发布
        gh release create $VERSION

说明:

注意上面配置中的${{ secrets.DOCKER_HUB_USERNAME }},${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} 表示github action自动读取环境变量中的DOCKER_HUB_USERNAME,DOCKER_HUB_ACCESS_TOKEN 即 docker hub的登录名和密码

DOCKER_HUB_ACCESS_TOKEN 需要自己生成>>,权限设置Read & Write即可。

之后打开github项目页面的settings >> Secrets and variables >> Actions >> New repository secret按钮

TOKEN配置

New repository secret

New repository secret

填写你的docker hub的对应值。

secret list

${{ secrets.GITHUB_TOKEN }} 属于特殊环境变量(GITHUB_开头的都算),会自动读取你账户运行的权限,不需要自己单独设置。

触发流水线

配置完成后,提交或者合入代码到上面👆「branches」指定的master分支就能自动出发CI/CD

触发流水线

docker镜像会自动推送

参考:

https://raw.githubusercontent.com/Hootrix/keyword_alert_bot/master/.github/workflows/main.yml

https://docs.github.com/zh/actions/using-workflows/using-github-cli-in-workflows

https://zhuanlan.zhihu.com/p/526696611