MoreRSS

site iconLawtee | 法律小茶馆修改

原海若博客。89年,湖南人,法律工作者,可以提供法律帮助。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Lawtee | 法律小茶馆的 RSS 预览

为哪吒探针增加访问控制和简单防御

2025-10-29 10:35:00

Featured image of post 为哪吒探针增加访问控制和简单防御

最近老T因为此前使用哪吒探针V0版时所需要的通信域名将要到期,于是选择对探针进行升级。探针这种东西,虽然看起来是“个人畜无害”的小工具,但总体来说安全风险还是不小,万一被攻破,那探针所连接的服务器毫无疑问都会被“一锅端”。为此,老T在升级探针之余,也对这个探针增加了一点小防护。

哪吒探针

哪吒探针基本情况

相比V0版的哪吒探针,当时服务器之间所用的通信域名实际上是处于“裸奔”状态,无法使用 CDN 进行源站防护,只能尽可能错开域名使用,而V1版探针默认可以支持 Cloudflare CDN,较大提升了安全性。

在账号防护方面,V0版的探针,默认可以启用 Github 账号授权登录,相比较纯账号登录,显然多了一重保障。但V1版探针,默认只有账号登录方式,相对来说后台更容易被攻击。

虽然后来 V1 版也添加了 Oauth2 绑定功能,但启用也麻烦,我在论坛上看了很多人的探针页面,发现启用率不高。并且 Oauth2 实际只是一种补充登录方式,并不能限制仅用这种方式登录。

示例

为哪吒探针添加登陆页面防护

老T此前已经对自己常用的 VPS 管理面板、NAS 管理面板(外网访问)都添加了访问控制。主要就是使用 Cloudflare Zero Trust 功能,添加 Access 访问控制策略,确保仅限自己访问。

alt text

登录页面

但哪吒面板稍有区别,因为面板前端并不需要设置“仅限自己可见”,只用对探针的后台页面限制访问即可。

使用方法也比较简单。

  1. 登录Cloudflare Zero Trust仪表板

  2. 添加或编辑自托管应用

    • 导航到“Access” > “Applications”。
    • 如果已有应用,编辑它;否则点击“Add an application”,选择“Self-hosted”。
    • 在“Application domain”字段中,输入你的域名和路径,例如nezha.sample.com/dashboard/*
  3. 配置Access策略

    • 在应用设置中,添加策略(Policy)。例如,选择“Allow”动作,并定义访问条件(如基于邮箱、IP、身份提供商或设备姿态)。
    • 如果有特定子路径需要不同规则(如Bypass某些公开部分),可以添加额外的策略,并使用路径匹配来优先应用。

此前我在另一篇文章提到 Access 策略设置,这里就不重复了: 如何添加-access-访问控制

添加访问策略

不同运营商之间打电话能畅通无阻,聊天软件之间为何“老死不相往来”?

2025-10-28 23:35:00

Featured image of post 不同运营商之间打电话能畅通无阻,聊天软件之间为何“老死不相往来”?

最近老T翻旧文章,回忆起当年折腾QQ登录的日子,感慨万千。电话这东西,全球随便打,移动的号拨给联通,秒接通,毫无障碍。可聊天软件呢?微信、抖音、钉钉、微博、QQ等,个个都是 “围墙花园”,互相不搭理。那凭啥电话行,聊天软件就不行?

电话为啥能随便打?

电话系统能全球互通,表面上看是因为 标准化协议,比如国际电信联盟(ITU)定下的各种规矩,能够在技术上无缝衔接。但老T觉得,这背后应该还有更深层次的原因。

首先,电信运营商,尤其是国内几大运营商,基本都是 国企,比如中国移动、中国联通、中国电信。这些 “国家队”背负社会责任,不是单纯的商业公司。电话作为 基础设施,关系国计民生,必然要求互联互通。老T查了下电话历史,发现早年电话网络刚建时,也有运营商各自为战的情况,比如美国 AT&T 公司就曾禁止过其他电话公司接入其长途网络,但到后来发现 电话不互通,社会成本太高,用户怨声载道,监管层一出手,也就解决了。加上电信行业高度垄断,玩家少,协调起来相对容易,协议一签,网络一连,全球电话网就织成。

再者,电话是典型的 基础设施服务,类似水电煤,服务本质是“连接”,不以内容为核心竞争力。运营商赚的是通话费、流量费,客观上也没什么动力在内容上搞封闭。反过来,互联互通还能扩大用户群,增加收入。所以,技术标准+社会责任+垄断协调+基础设施属性,共同造就了电话的畅通。

老T想起当年2000年左右,家里刚装电话那会,在偏远农村用座机就可以打电话给远方的亲友,移动联通随便拨,完全没想过背后有多复杂。这就是基础设施的魅力:你感觉不到它的存在,但它就是好用

聊天软件为啥不互通?

反观网络聊天软件,情况完全不同。微信、抖音、钉钉、微博、QQ等,个个都像独立王国,互不往来。表面上是 技术问题,协议不统一,加密方式各异,强行互通可能出安全漏洞。但老T觉得,根源还是 商业利益和生态封闭

聊天软件公司靠 用户数据和生态赚钱。微信有朋友圈、公众号、小程序,抖音背靠字节跳动的广告帝国,微博搞粉丝文化。这些平台的 核心不是“连接”,而是“黏住用户”。开放接口的化,等于把用户往外推,谁干?老T当年写过一篇关于QQ登录的文章,感触颇深。比如Gaim(后来的Pidgin),可以在U盘系统里装过,功能强悍,当年能同时登录QQ、MSN、雅虎通,简直是神器!还有Instango,能同时登上各种聊天软件,还巨省流量。另外还有大量webim工具,像meebo、imtata、imhaha,可以直接在网页上登录多个聊天软件,方便得很。可惜,这些工具都在日后全部歇菜,核心原因就是各家聊天软件纷纷改接口、搞封闭。这些经历也告诉老T,聊天软件的封闭并不是技术难题,而是故意为之

这几年,各种平台之间在封闭道路上走得更远了。像在抖音上发淘宝、京东链接,轻则限流,重则封号,明明是个很正常的链接,但它动则说这是在“诱导使用第三方平台交易”,属于诈骗。微信这边稍微好点,2021年工信部要求解除外部链接屏蔽后,现在能直接打开淘宝链接了,但分享体验还是别扭,常要复制淘口令。讲白了,还是利益主导,各个网络巨头,护着自家流量池,谁也不想给对方导流

老T觉得,聊天软件和网络平台现在的状态,跟电信运营商有点像:都有垄断或半垄断属性,也有国资背景(比如腾讯、阿里都有国有资本参与)。但跟运营商不同,聊天软件的核心是内容和生态,数据就是命根子,开放互通等于割肉。所以,技术上能解决,商业上不想解决。

未来能打破围墙吗?

自从老T在一个技术微信群里发出“为什么不同运营商之间可以相互打电话,聊天软件之间却无法相互对话”这个看似可笑的问题后,就知道很难有积极正面的反馈。不过老T对未来依然持乐观态度,觉得互联网继续发展,必然还是要 “以人为中心”!技术上,开放标准协议像 XMPP、Matrix 早就有了,支持去中心化聊天。如果大公司用这些,互通分分钟。监管层也在发力,比如,上边提到工信部要求各家解除外部链接屏蔽等等,国外的话,欧盟的《数字市场法案》也逼着脸书和苹果开放接口。

但这里,老T特别想聊聊 AI 的潜力。AI 在未来互联网互联互通里,可能是一个大杀器般的存在!首先,AI 能当 “协议翻译官”,实现不同平台的内容格式的转换,比如把抖音的消息转成微信能识别的。其次,AI 能优化跨平台体验,预测用户需求,处理加密和隐私问题。更为正面的是,AI 能重塑商业模式。这些大公司完全可以 不靠锁定用户数来赚钱,而是靠提供 AI 服务,比如智能推荐、隐私保护、跨平台协作等等,收入照样来。

老T构想一个场景,假设未来有个 “超级聊天管家” AI 助手,它实时收集你所有平台的信息,比如各种微信的朋友圈更新、QQ的群聊、微博的关注订阅,全汇聚到一个统一界面。你想跟QQ上的老朋友聊天?直接在 AI 里输入消息,它自动转发过去,还处理格式兼容和隐私加密。如果对方用抖音消息,AI 就桥接过去,确保消息准确传递。甚至,你在 AI 里搜索“上周老板发的那个视频”,它能跨平台拉取,秒出结果。这不光省事儿,还能打破围墙,让沟通回归本质:人与人连接,而不是平台绑架。事实上,现在一些手机厂商已经在这方面走的更远了。比如,老T前阵子曾刷到个小米澎湃OS的演示视频:用小米手机内置 AI 语音控制,打开抖音并找到雷军的账号下边某个视频点赞并评论。

当然,老T也只到这种事情挑战不少。商业利益冲突最大,巨头们护数据如命。技术上,协议统一、隐私安全、用户体验都得平衡。监管也得跟上,防止大公司店大欺客。但老T相信,互联互通这种明显的正当需求,在未来应该更加会得到更多的重视,比如微信和QQ就完全可以打通,毕竟都是同一家公司,还分什么你和我呢

一点感想

写到这儿,老T不由得回想2000年代的互联网,回望那个 “互联网精神”和“开源精神”的黄金时代!合作、共享、创新,支持着整个人类走入信息化时代。但毫无疑问,如今这种共享、开源精神,遭遇了一定挫折。老T回想起年初看到一篇技术博客,标题叫《人工智能、国家行为者和供应链》,文章提到,全球99%的软件以开源软件为基础,97%的应用程序使用开源代码,90%以上的公司日常依赖开源软件运转。而近年来,整个开源世界,都面临着非常大的挑战,不确定性因素大幅增长,一些网络巨头也借开源之名,行控制之实,互联网从开放走向碎片化,围墙也越建越高

流浪地球剧照

老T觉得,就像《流浪地球》中表现的那样,人类本应该团结,在人工智能时代,更需要重拾开源精神,构建一个 “互联网命运共同体”。就像电话系统那样,大家放下私利,合作共赢。用户、开发者、公司、政府一起推动标准化和开放,AI 也能帮忙桥接。否则,围墙越厚,用户越累,创新越少。希望未来能回归初心,让互联网真正互联互通。

怎样让静态博客发布跟发微信朋友圈一样简单

2025-10-27 08:00:00

Featured image of post 怎样让静态博客发布跟发微信朋友圈一样简单

最近几年来,老T在静态博客发布这件事情上也算是走了无数弯路。随便搜下老T博客,关于Hugo的数篇文章,无一不是聚焦这个问题。但细数以前的经历,不管是哪种方法,很少有能让老T坚持一个星期以上还主动愿意持续用下去的。大多数时候用它们都是处于一种迫不得已的”窘境“。如今,终极解决方案来了。

使用 Github issue 作为静态博客的发布端

国内网络环境下,Github Mobile 是一个远比 GIthub 网站更稳定的应用。老T最开始萌生用 Github issue 当成博客发布端,是在今年初看到 Hackernews 上一则帖子,提到 Github issue 由于几乎无数量、容量限制,可能是最佳的博客写作平台。可由于长期以来 Github 在国内网络环境下一直处于抽风状态,所以老T并没有去试验这种方法。

直到今年 7 月份,老T在为博客添加一个”说说“功能时,才再度想起 Github issue 的便利性,当时也写了个简单教程:只需 2 步简单操作,一劳永逸解决静态博客添加朋友圈、说说之类的功能

不过那一次主要解决的是博客端添加”说说“功能,而发布过程的便捷性方面并没探索,直到最近用上 Github APP + Shortcut Marker ,在手机上只需点2次就能开始添加图片发”说说“,这过程,比微信发朋友圈的4次点击还要简单(打开微信—发现—朋友圈—发布)。

在持续高强度用了几天后,我开始真心喜欢上这种更新博客方式,仿佛又回到从前用 WordPress 的时代。也就是在这过程中,我开始思考,能不能把日常一些短平快的文章,也通过这种方式发布呢?

老T在周五晚上睡觉前,简单构思了一下这个事情。觉得不外乎就是三个问题:1.提取 issue 内容并转换为 markdown文件;2.转存图片;3.触发逻辑设置。于是周末开干,找了个缩小版的老T博客仓库进行测试。最终,只需要添加两个工作流文件即可实现这一目标。

详细教程:如何使用 GitHub Issue 发布 Hugo 博客

如何使用 GitHub Issue 发布 Hugo 博客

2025-10-27 08:00:00

Featured image of post 如何使用 GitHub Issue 发布 Hugo 博客

近年来,静态博客的发布方式层出不穷,但许多方法要么复杂,要么难以长期坚持。本文介绍一种简单高效的方式:通过 GitHub Issue 作为 Hugo 博客的发布端,利用 GitHub Actions 自动将 Issue 转换为 Hugo 内容并部署到 GitHub Pages。这种方法尤其适合喜欢用 GitHub 移动端 App 随时随地发布博客的用户。本教程基于老 T 的实践经验,解决了私有仓库图片下载、标签提取等问题,适合公开和私有仓库。

为什么选择 GitHub Issue?

  • 便捷性:GitHub 移动端 App 在国内网络环境下比网页版更稳定,发布只需几次点击,比微信朋友圈还简单。
  • 无限制:Issue 几乎没有数量或容量限制,适合博客内容存储。
  • 自动化:通过 GitHub Actions,可以将 Issue 自动转换为 Hugo 内容,并触发站点构建。
  • 灵活性:支持图片、标签、分类,适合短篇“说说”或长篇文章。

前提条件

  • 一个 Hugo 博客项目,已配置好 GitHub Pages(公开或私有仓库)。
  • GitHub 个人访问令牌(PAT),具有 repo 权限(私有仓库需要)和 workflow 权限(触发工作流)。在 GitHub SettingsDeveloper settingsPersonal access tokens 创建。
  • 基本的 GitHub Actions 和 Hugo 知识。
  • 仓库结构包含 content/posts/ 目录,用于存放生成的文章。

实现步骤

1. 配置 GitHub Actions 工作流

我们需要两个工作流文件:

  • deploy.yml:将 Issue 转换为 Hugo 内容并提交到仓库。
  • hugo.yml:构建 Hugo 站点并部署到 GitHub Pages。

1.1 创建 deploy.yml

.github/workflows/deploy.yml 中添加以下内容,用于处理 Issue 转换为 Hugo 内容,并触发 Hugo 构建:

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
name: Sync Issues to Hugo Content

on:
 issues:
 types: [opened]
 workflow_dispatch:

permissions:
 contents: write

concurrency:
 group: "issues-to-hugo"
 cancel-in-progress: false

defaults:
 run:
 shell: bash

jobs:
 convert_issues:
 name: Convert GitHub Issues
 runs-on: ubuntu-latest
 if: github.event_name == 'workflow_dispatch' || github.event_name == 'issues' && contains(github.event.issue.labels.*.name, '发布') && github.actor != 'IssueBot'
 steps:
 - name: Checkout repository
 uses: actions/checkout@v4
 with:
 fetch-depth: 0
 clean: true

 - name: Setup Python
 uses: actions/setup-python@v4
 with:
 python-version: '3.11'

 - name: Install dependencies
 run: |
 pip install requests PyGithub==2.4.0 beautifulsoup4

 - name: Create content/posts directory
 run: |
 mkdir -p content/posts
 echo "Created content/posts directory"

 - name: Convert issues to Hugo content
 id: convert
 env:
 GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
 run: |
 find content/posts -type f 2>/dev/null | sort > /tmp/original_files.txt || touch /tmp/original_files.txt
 if [ -s /tmp/original_files.txt ]; then
 xargs sha1sum < /tmp/original_files.txt > /tmp/original_hashes.txt
 else
 touch /tmp/original_hashes.txt
 fi

 python .github/workflows/issue_to_hugo.py \
 --repo "${{ github.repository }}" \
 --output "content/posts" \
 --debug

 find content/posts -type f 2>/dev/null | sort > /tmp/new_files.txt || touch /tmp/new_files.txt
 if [ -s /tmp/new_files.txt ]; then
 xargs sha1sum < /tmp/new_files.txt > /tmp/new_hashes.txt
 else
 touch /tmp/new_hashes.txt
 fi

 if cmp -s /tmp/original_hashes.txt /tmp/new_hashes.txt; then
 echo "needs_build=false" >> $GITHUB_OUTPUT
 echo "🔄 没有内容变更,将跳过提交"
 else
 echo "needs_build=true" >> $GITHUB_OUTPUT
 echo "✅ 检测到内容变更,将执行提交"
 fi
 
 - name: Commit changes (if any)
 if: steps.convert.outputs.needs_build == 'true'
 env:
 GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
 run: |
 git config --global user.name "IssueBot"
 git config --global user.email "[email protected]"
 git remote set-url origin https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/${{ github.repository }}.git

 if [ -n "${{ github.base_ref }}" ]; then
 DEFAULT_BRANCH="${{ github.base_ref }}"
 else
 DEFAULT_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
 fi

 if [ -z "$DEFAULT_BRANCH" ]; then
 DEFAULT_BRANCH="master"
 fi

 git checkout $DEFAULT_BRANCH

 git add content/posts

 if ! git diff-index --quiet HEAD --; then
 git commit -m "Automated: Sync GitHub Issues as content"
 echo "正在推送变更到分支: $DEFAULT_BRANCH"
 git push origin $DEFAULT_BRANCH
 else
 echo "没有需要提交的变更"
 fi

 - name: Trigger Hugo build
 if: steps.convert.outputs.needs_build == 'true'
 env:
 GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
 run: |
 curl -X POST \
 -H "Accept: application/vnd.github.v3+json" \
 -H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \
 https://api.github.com/repos/${{ github.repository }}/dispatches \
 -d '{"event_type": "trigger-hugo-build"}'

关键点

  • 触发条件:Issue 打开时带有“发布”标签,或手动触发。
  • 防止递归:github.actor != 'IssueBot' 确保 IssueBot 的提交不会再次触发工作流。
  • 提交检测:仅在内容变更时提交(通过文件哈希比较)。
  • 触发 Hugo 构建:通过 repository_dispatch 事件 (trigger-hugo-build) 调用 Hugo 工作流。

1.2 创建 hugo.yml

.github/workflows/hugo.yml 中添加以下内容,用于构建和部署 Hugo 站点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
name: Deploy Hugo site to Pages

on:
 push:
 branches: ["master"]
 workflow_dispatch:
 repository_dispatch:
 types: [trigger-hugo-build]

permissions:
 contents: read
 pages: write
 id-token: write

concurrency:
 group: "pages"
 cancel-in-progress: false

defaults:
 run:
 shell: bash

jobs:
 build:
 runs-on: ubuntu-latest
 env:
 HUGO_VERSION: 0.128.0
 steps:
 - name: Install Hugo CLI
 run: |
 wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
 && sudo dpkg -i ${{ runner.temp }}/hugo.deb
 - name: Install Dart Sass
 run: sudo snap install dart-sass
 - name: Checkout
 uses: actions/checkout@v4
 with:
 submodules: recursive
 - name: Setup Pages
 id: pages
 uses: actions/configure-pages@v5
 - name: Install Node.js dependencies
 run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
 - name: Build with Hugo
 env:
 HUGO_CACHEDIR: ${{ runner.temp }}/hugo_cache
 HUGO_ENVIRONMENT: production
 run: |
 hugo \
 --minify \
 --baseURL "${{ steps.pages.outputs.base_url }}/"
 - name: Upload artifact
 uses: actions/upload-pages-artifact@v3
 with:
 path: ./public
 deploy:
 environment:
 name: github-pages
 url: ${{ steps.deployment.outputs.page_url }}
 runs-on: ubuntu-latest
 needs: build
 steps:
 - name: Deploy to GitHub Pages
 id: deployment
 uses: actions/deploy-pages@v4

关键点

  • 触发条件:pushmaster 分支、手动触发,或 repository_dispatch 事件 (trigger-hugo-build)。
  • 权限:确保 pages: writeid-token: write 用于 GitHub Pages 部署。

1.3 添加转换脚本 issue_to_hugo.py

.github/workflows/issue_to_hugo.py 中添加以下 Python 脚本,用于将 Issue 转换为 Hugo 内容,支持私有仓库图片下载和标签提取:

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
import os
import argparse
import re
import requests
import json
import logging
from urllib.parse import unquote
from datetime import datetime
from github import Github, Auth
from bs4 import BeautifulSoup

CATEGORY_MAP = ["生活", "技术", "法律", "瞬间", "社会"]
PUBLISH_LABEL = "发布"

def setup_logger(debug=False):
 logger = logging.getLogger('issue-to-hugo')
 level = logging.DEBUG if debug else logging.INFO
 logger.setLevel(level)

 handler = logging.StreamHandler()
 formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 handler.setFormatter(formatter)
 logger.addHandler(handler)

 return logger

def extract_cover_image(body):
 """提取并删除封面图(正文第一张图片)"""
 img_pattern = r"!\[.*?\]\((https?:\/\/[^\)]+)\)"
 match = re.search(img_pattern, body)

 if match:
 img_url = match.group(1)
 body = body.replace(match.group(0), "")
 return img_url, body
 return None, body

def safe_filename(filename):
 """生成安全的文件名,保留或推断文件扩展名"""
 clean_url = re.sub(r"\?.*$", "", filename)
 basename = os.path.basename(clean_url)
 decoded_name = unquote(basename)

 name, ext = os.path.splitext(decoded_name)
 safe_name = re.sub(r"[^a-zA-Z0-9\-_]", "_", name)
 if not ext.lower() in [".jpg", ".jpeg", ".png", ".gif", ".webp"]:
 ext = ""
 if len(safe_name) > 100 - len(ext):
 safe_name = safe_name[:100 - len(ext)]

 return safe_name + ext

def download_image(url, output_path, token=None):
 """下载图片到指定路径,基于内容类型确定扩展名,并添加 GitHub 认证头"""
 try:
 headers = {}
 if token:
 headers['Authorization'] = f'token {token}'

 response = requests.get(url, stream=True, headers=headers)
 if response.status_code == 200:
 content_type = response.headers.get("content-type", "").lower()
 ext = ".jpg"
 if "image/png" in content_type:
 ext = ".png"
 elif "image/jpeg" in content_type or "image/jpg" in content_type:
 ext = ".jpg"
 elif "image/gif" in content_type:
 ext = ".gif"
 elif "image/webp" in content_type:
 ext = ".webp"

 base, current_ext = os.path.splitext(output_path)
 if current_ext.lower() not in [".jpg", ".jpeg", ".png", ".gif", ".webp"]:
 output_path = base + ext
 else:
 output_path = base + current_ext

 with open(output_path, 'wb') as f:
 f.write(response.content)
 logging.info(f"图片下载成功: {url} -> {output_path}")
 return output_path
 else:
 logging.error(f"图片下载失败,状态码: {response.status_code}, URL: {url}")
 except Exception as e:
 logging.error(f"下载图片失败: {url} - {e}")
 return None

def replace_image_urls(body, issue_number, output_dir, token=None):
 """替换正文中的远程图片为本地图片"""
 img_pattern = r"!\[(.*?)\]\((https?:\/\/[^\)]+)\)"

 def replacer(match):
 alt_text = match.group(1)
 img_url = match.group(2)
 filename = f"{issue_number}_{safe_filename(img_url)}"
 output_path = os.path.join(output_dir, filename)
 final_path = download_image(img_url, output_path, token)
 if final_path:
 final_filename = os.path.basename(final_path)
 return f"![{alt_text}]({final_filename})"
 return match.group(0)

 return re.sub(img_pattern, replacer, body, flags=re.IGNORECASE)

def sanitize_markdown(content):
 """清理Markdown中的不安全内容"""
 if not content:
 return ""

 soup = BeautifulSoup(content, "html.parser")
 allowed_tags = ["p", "a", "code", "pre", "blockquote", "ul", "ol", "li", "strong", "em", "img", "h1", "h2", "h3", "h4", "h5", "h6"]
 for tag in soup.find_all(True):
 if tag.name not in allowed_tags:
 tag.unwrap()

 return str(soup)

def extract_tags_from_body(body, logger):
 """从正文最后一行提取标签,使用 $tag$ 格式,并返回清理后的正文"""
 if not body:
 logger.debug("Body is empty, no tags to extract")
 return [], body

 body = body.replace('\r\n', '\n').rstrip()
 lines = body.split('\n')
 if not lines:
 logger.debug("No lines in body, no tags to extract")
 return [], body

 last_line = lines[-1].strip()
 logger.debug(f"Last line for tag extraction: '{last_line}'")

 tags = re.findall(r'\$(.+?)\$', last_line, re.UNICODE)
 tags = [tag.strip() for tag in tags if tag.strip()]

 if tags:
 logger.debug(f"Extracted tags: {tags}")
 body = '\n'.join(lines[:-1]).rstrip()
 else:
 logger.debug("No tags found in last line")

 return tags, body

def convert_issue(issue, output_dir, token, logger):
 """转换单个issue为Hugo内容"""
 try:
 labels = [label.name for label in issue.labels]
 if PUBLISH_LABEL not in labels or issue.state != "open":
 logger.debug(f"跳过 issue #{issue.number} - 未标记为发布")
 return False

 pub_date = issue.created_at.strftime("%Y%m%d")
 slug = f"{pub_date}_{issue.number}"
 post_dir = os.path.join(output_dir, slug)

 if os.path.exists(post_dir):
 logger.info(f"跳过 issue #{issue.number} - 目录 {post_dir} 已存在")
 return False

 os.makedirs(post_dir, exist_ok=True)

 body = issue.body or ""
 logger.debug(f"Raw issue body: '{body}'")
 cover_url, body = extract_cover_image(body)
 tags, body = extract_tags_from_body(body, logger)
 body = sanitize_markdown(body)
 body = replace_image_urls(body, issue.number, post_dir, token)
 logger.info(f"图片处理完成,{issue.number} 号 issue")

 categories = [tag for tag in labels if tag in CATEGORY_MAP]
 category = categories[0] if categories else "生活"

 cover_name = None
 if cover_url:
 try:
 cover_filename = f"cover_{safe_filename(cover_url)}"
 cover_path = os.path.join(post_dir, cover_filename)
 final_cover_path = download_image(cover_url, cover_path, token)
 if final_cover_path:
 cover_name = os.path.basename(final_cover_path)
 logger.info(f"封面图下载成功:{cover_url} > {cover_name}")
 else:
 logger.error(f"封面图下载失败:{cover_url}")
 except Exception as e:
 logger.error(f"封面图下载失败:{cover_url} - {e}")

 title_escaped = issue.title.replace('"', '\\"')
 category_escaped = category.replace('"', '\\"')
 frontmatter_lines = [
 "---",
 f'title: "{title_escaped}"',
 f"date: \"{issue.created_at.strftime('%Y-%m-%d')}\"",
 f"slug: \"{slug}\"",
 f"categories: [\"{category_escaped}\"]",
 f"tags: {json.dumps(tags, ensure_ascii=False)}"
 ]

 if cover_name:
 frontmatter_lines.append(f"image: \"{cover_name}\"")

 frontmatter_lines.append("---\n")
 frontmatter = "\n".join(frontmatter_lines)

 md_file = os.path.join(post_dir, "index.md")
 with open(md_file, "w", encoding="utf-8") as f:
 f.write(frontmatter + body)

 logger.info(f"成功转换 issue #{issue.number}{md_file}")
 return True
 except Exception as e:
 logger.exception(f"转换 issue #{issue.number} 时发生严重错误")
 error_file = os.path.join(output_dir, f"ERROR_{issue.number}.tmp")
 with open(error_file, "w") as f:
 f.write(f"Conversion failed: {str(e)}")
 return False

def main():
 args = parse_arguments()
 logger = setup_logger(args.debug)

 token = args.token or os.getenv("GITHUB_TOKEN")
 if not token:
 logger.error("Missing GitHub token")
 return

 try:
 auth = Auth.Token(token)
 g = Github(auth=auth)
 repo = g.get_repo(args.repo)
 logger.info(f"已连接至 GitHub 仓库:{args.repo}")
 except Exception as e:
 logger.error(f"连接GitHub失败: {str(e)}")
 return

 os.makedirs(args.output, exist_ok=True)
 logger.info(f"输出目录: {os.path.abspath(args.output)}")

 processed_count = 0
 error_count = 0

 try:
 issues = repo.get_issues(state="open")
 total_issues = issues.totalCount
 logger.info(f"开始处理 {total_issues} 个打开状态的 issue")

 for issue in issues:
 if issue.pull_request:
 continue
 try:
 if convert_issue(issue, args.output, token, logger):
 processed_count += 1
 except Exception as e:
 error_count += 1
 logger.error(f"处理 issue #{issue.number} 时出错: {str(e)}")
 try:
 error_comment = f"⚠️ 转换为Hugo内容失败,请检查格式错误:\n\n```\n{str(e)}\n```"
 if len(error_comment) > 65536:
 error_comment = error_comment[:65000] + "\n```\n...(内容过长,部分已省略)"

 issue.create_comment(error_comment)
 try:
 error_label = repo.get_label("conversion-error")
 except:
 error_label = repo.create_label("conversion-error", "ff0000")
 issue.add_to_labels(error_label)
 except Exception as inner_e:
 logger.error(f"创建评论或添加标签时出错: {inner_e}")
 except Exception as e:
 logger.exception(f"获取issues时出错: {e}")

 summary = f"处理完成!成功转换 {processed_count} 个issues,{error_count} 个错误"
 if processed_count == 0:
 logger.info(summary + " - 没有需要处理的内容变更")
 else:
 logger.info(summary)

 if args.debug:
 logger.debug("内容目录状态:")
 logger.debug(os.listdir(args.output))

def parse_arguments():
 parser = argparse.ArgumentParser(description='Convert GitHub issues to Hugo content')
 parser.add_argument('--token', type=str, default=None, help='GitHub access token')
 parser.add_argument('--repo', type=str, required=True, help='GitHub repository in format owner/repo')
 parser.add_argument('--output', type=str, default='content/posts', help='Output directory')
 parser.add_argument('--debug', action='store_true', help='Enable debug logging')
 return parser.parse_args()

if __name__ == "__main__":
 main()

关键点

  • 图片下载download_image 使用 PAT 认证头(Authorization: token {token}),支持私有仓库附件下载。
  • 标签提取:从 Issue 正文最后一行提取 $tag$ 格式标签(支持空格,如 $搞啥 呢$)。
  • 重复检查:跳过已存在的文章目录(YYYYMMDD_X)。
  • Markdown 生成:生成标准 Hugo frontmatter 和内容,图片转为本地路径。

2. 设置 PAT

  1. 创建 PAT

    • 访问 GitHub SettingsDeveloper settingsPersonal access tokensTokens (classic)
    • 创建新 PAT,勾选 repo(包括私有仓库访问)和 workflow(触发工作流)。
    • 复制生成的 token。
  2. 添加到仓库

    • 转到仓库(e.g., h2dcc/lawtee.github.io)→ SettingsSecrets and variablesActionsSecrets
    • 添加新 secret,命名为 PAT_TOKEN,粘贴 PAT 值。

3. 编写 Issue 发布博客

在 GitHub Issue 中按以下格式编写博客文章:

  1. 添加“发布”标签
    • 创建新 Issue,添加标签 发布 和分类标签(如 技术生活)。
  2. 正文格式
    • 标题:Issue 标题作为文章标题。
    • 封面图:正文第一张图片作为封面(Markdown 格式:![Image](URL))。
    • 正文:Markdown 格式,支持图片、标题、链接等。
    • 标签:最后一行以 $tag$ 格式添加标签(支持空格,如 $Hugo Post$)。
  3. 示例 Issue
    1
    2
    3
    4
    5
    6
    7
    
    ![Image](https://github.com/user-attachments/assets/5cc86d74-ff70-401f-820d-520a99a504b9)
    ## 我的第一篇 Hugo 博客
    这是一篇通过 GitHub Issue 发布的博客。
    支持 **Markdown** 格式,图片会自动下载到本地。
    <!--more-->
    ![Another Image](https://github.com/user-attachments/assets/926e7e9b-d279-4db9-bbb9-60bdcedd1804)
    $Hugo$ $博客$ $Hugo Post$
    
  4. 提交:保存 Issue,触发 deploy.yml 工作流。

4. 工作流运行流程

  1. 触发 deploy.yml

    • Issue 打开(带有“发布”标签)或手动触发。
    • 脚本 issue_to_hugo.py 运行:
      • 提取标题、分类(从标签)、正文、封面图、标签。
      • 下载图片(使用 PAT 认证,适用于私有仓库)。
      • 生成 Markdown 文件到 content/posts/YYYYMMDD_X/index.md
      • 提交到 master 分支。
    • 触发 hugo.yml 通过 repository_dispatch 事件。
  2. 触发 hugo.yml

    • 构建 Hugo 站点(hugo --minify)。
    • 部署到 GitHub Pages。

5. 验证发布

  1. 检查 Actions 日志

    • 打开 GitHub Actions 选项卡。
    • 确认 Sync Issues to Hugo Content 运行:
      • 日志显示 图片下载成功成功转换 issue #X
      • 提交到 master(e.g., Automated: Sync GitHub Issues as content)。
    • 确认 Deploy Hugo site to Pages 运行:
      • 检查 Build with HugoDeploy to GitHub Pages 步骤。
  2. 检查生成文件

    • 打开 content/posts/YYYYMMDD_X/
      • index.md 包含 frontmatter(title, date, slug, categories, tags, image)和正文。
      • 本地图片文件(e.g., X_uuid.jpg)存在。
    • 示例 index.md
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      
      ---
      title: "我的第一篇 Hugo 博客"
      date: "2025-10-27"
      slug: "20251027_2"
      categories: ["技术"]
      tags: ["Hugo", "博客", "Hugo Post"]
      image: "cover_5cc86d74-ff70-401f-820d-520a99a504b9.jpg"
      ---
      ## 我的第一篇 Hugo 博客
      这是一篇通过 GitHub Issue 发布的博客。
      支持 **Markdown** 格式,图片会自动下载到本地。
      <!--more-->
      ![Another Image](2_926e7e9b-d279-4db9-bbb9-60bdcedd1804.jpg)
      
  3. 访问站点

    • 访问 GitHub Pages 站点(e.g., https://h2dcc.github.io 或私有仓库的 Pages URL)。
    • 确认新文章显示,图片加载正常,标签和分类正确。

6. 常见问题及解决

  • 图片下载失败(404)
    • 原因:私有仓库图片需要 PAT 认证。
    • 解决:确保 PAT_TOKENrepo 权限,脚本已添加认证头。
  • Hugo 构建未触发
    • 原因repository_dispatch 事件失败。
    • 解决:检查 deploy.ymlTrigger Hugo build 步骤日志,确认 curl 请求返回 204。
  • 标签未提取
    • 原因:标签格式错误或不在最后一行。
    • 解决:确保标签以 $tag$ 格式在最后一行,空格需包含在 $ 内(如 $搞啥 呢$)。
  • 重复文章
    • 原因:脚本会跳过已有目录(YYYYMMDD_X)。
    • 解决:删除 content/posts/YYYYMMDD_X/ 后重新运行。

7. 优化建议

  • 单 Issue 处理:修改 deploy.ymlissue_to_hugo.py 支持仅处理触发 Issue,减少运行时间。
  • 错误通知:在 Issue 中添加评论,通知转换失败原因。
  • 移动端优化:使用 GitHub 移动端 App + Shortcut Maker,简化发布流程。
  • 日志查看:启用 --debug 模式,检查详细日志。

总结

通过 GitHub Issue 发布 Hugo 博客,只需几个步骤即可实现从移动端快速发布到自动部署的全流程。相比传统方式,这种方法简单、高效,尤其适合需要随时记录灵感的博主。无论是公开还是私有仓库,配合 PAT 认证和 GitHub Actions,图片、标签、分类都能完美处理。快去试试吧!

家用路由器地址为什么是192.168.1.1而不是其他更好记的数字?

2025-10-24 10:35:00

Featured image of post 家用路由器地址为什么是192.168.1.1而不是其他更好记的数字?

前几天老T在微信群看到有人吐槽:“为啥路由器地址是192.168.1.1这么一串数字,记着费劲,不能弄个简单点的,比如1.2.3.4?” 这问题乍一听挺有意思,老T自己也用过不少路由器,但从来没想过 192.168.1.1 这个数字来源是什么,于是在网上查了查资料,顺便也问了下 AI。不过,在多方查找比对下,发现目前中文互联网上对这个问题的解答,似乎都缺点什么,感觉不是很准确。这里,老T将自己考证的结论总结出来,争取还原出历史的真相。


网上关于路由器地址的常见解读

老T在网上搜了一圈,发现关于“为什么路由器用192.168.1.1”这事,中文互联网上的解释五花八门,AI的回答也大多似是而非。以下是几种常见的说法:

  1. 程序员随便选的
    不少论坛和文章说,192.168.1.1是程序员“拍脑袋”定的,觉得这串数字“顺眼”或者“简单”。还有人开玩笑说,192.168的二进制(10101000)看着“有规律”,所以被挑中了。老T觉得这说法听起来挺随意,但总觉得没这么简单。

  2. 走的人多了就变成了路
    老T在知名技术论坛 HN 上发现有一个热门讨论,提到曾参与过多个互联网 DNS 技术标准起草的技术大佬 Stéphane Bortzmeyer 贴出的一封 2009 年的老邮件。该邮件提到,一家公司在一些早期文档中使用了 192.168.x.x 作为示例地址,于是其他人都按照这个来操作,由于用了人多了,后来在制定技术标准规范时,就采用了该方案。颇有“世上本无路,走的人多了也就变成了路”的感觉。由于这个回答权威性很高,也被很多后续文章所采用。

  1. 协议规定只能用这个
    还有种说法是,192.168.1.1是“协议强制要求”的私有地址,路由器只能用这个范围。AI的回答也常提到 RFC 1918 技术标准,讲到私有地址有三个范围(10.x、172.16-31、192.168),但没解释为啥偏偏是192.168。有人甚至说“1.1.1.1是公网地址,不能用”,但老T觉得这只答了“为啥不用1.2.3.4”,没讲清楚192.168的来源。

  2. 历史遗留问题
    少数技术博客提到,192.168是1990年代互联网地址分配时“剩下的”,所以被用来做私有地址。但具体咋“剩下”的,语焉不详。AI的回答也常提到 IANA(互联网数字分配机构),但多半是泛泛而谈,细节不够。

老T翻了这些说法后,觉得都不够透彻。网上很多解释要么太笼统,要么带点“想当然”。于是,老T用搜索引擎,指定搜索90年代的英文资料,还查了点IANA的历史记录,试着把这事的来龙去脉掰扯清楚。


IP地址和私有地址的基本概念

简单说,IP 地址就是给网络设备分配的“门牌号”,让它们在网上找到彼此。家用路由器的 192.168.1.1 就是局域网(LAN)的网关地址,手机、电脑通过它连上外网。

老T查了网上讨论最多的 RFC 1918 技术标准《私有互联网的地址分配》,发现互联网早期 IP 地址就担心不够用,在 1996 年圈定了三个“私有地址”范围,专门给内网用,不会跟公网冲突:其中第一个 10.0.0.0 到 10.255.255.255,用于特大型的内网,大概有 1600 多万个地址;第二个 172.16.0.0 到 172.31.255.255 算是中型内网网,大概有 100 多万个地址;第三个 192.168.0.0 到 192.168.255.255,算小型内网,大概有 6.5 万个地址。

私有IP地址范围

家用路由器基本都用 192.168 这段,因为它地址数量足够家庭用,还不跟大公司的内网(10.x.x.x)撞车。


为什么偏偏是192.168?

老T又翻了点历史资料,找到的是美国知乎上关于 RFC 1918 的讨论。才发现,原来 192.168 这串数字不是随便挑的,而是 IANA(互联网数字分配机构)在1990年代分配时的一个“随手”选择:

  • 当时IP地址按“类”分配(Class A、B、C),192开头是 Class C,适合小网络。
  • 在 1990 年 IANA 分配 IP 地址时,192.0 到 192.167 已被其他机构抢先占用,而 192.168 是当时数据库里下一个可用的地址,于是 Jon Postel(IANA大佬)就拍板用了这个,没啥特别理由,纯属“下一个就是它”。
  • RFC 1918 的作者之一 Daniel Karrenberg 也提出:“没啥深意,就是按顺序来的,改了反而麻烦。”

至于为啥是192.168.1.1或0.1,厂商觉得子网第一个地址(.1)当网关最直观,配置简单。Linksys、Netgear这些早期路由器厂商用了192.168.1.1后,其他厂商跟风,慢慢就成了行规。


为什么不用更好记的数字?

本来老T也觉得 1.1.1.1 多好记,但也知道,这地址早就有人使用了,毕竟像 1.1.1.1 8.8.8.8 9.9.9.9 101.101.101.101 114.114.114.114 等带有“魔性”的常见好记的 DNS 服务器地址,早就刻进了老T的 DNA,而这些好记的地址,无一不是被互联网大厂或垄断机构所拥有。

现实中,这些好记的 IP 地址所蕴含的经济价值都不可估量,参考域名交易,很多都能卖出几千万甚至上亿美元天价。这种情况下,每家每户自行使用的无法产生任何经济价值的 IP 地址,也就没必要用那么好记的数字了。

另外,由于历史惯性,192.168 已经用了 30 多年,用户和厂商都习惯了。改成别的,哪怕更简单,用户得重新学,厂商得改文档,成本高收益低。比如,早年苹果 Airport 路由器就用 10.0.1.1,但市场反馈不好,后来也改用 192.168.x.x。可见,192.168 这串数字虽不完美,但确实最稳。


现代路由器的“解法”

虽然 192.168.1.1 不好记,但现在也用不着老盯着这串数字,比如现在多数新买的路由器,会在说明书上注明,可以用miwifi.comrouter.asus.com 这样的网址连接,直接输网址就能进设置页面,或者很多路由器也都会配有专门的 APP,点几下就能改设置,IP 地址都不用管。

如果想自己换成其他地址,也是挺麻烦的,虽然路由器设置里基本都可以更改网关地址,但其实改了后也很难记住。老T就经常出现改完就忘掉的情况,不得不在每次访问前,先找个设备联网状态去查看,特别是家里有多个光猫、路由器的情况下。


总结

老T折腾一番后,觉得这 192.168.1.1 还真不是程序员故意刁难人。想想 1990 年代确定这个地址的时候是啥条件,可能人家也压根没想到,日后路由器会进入千家万户。毕竟在当年 ADSL 拨号的时代,一个家庭能有一台电脑接入互联网已经很不错了,谁能想到日后每个家庭都会有一大堆需要联网的设备。好在,192.168 倒也不是特别难记,多搞几次,总能记住的。

悬索桥的主缆损坏后,就真的无法维修更换,只能整座桥重建吗?

2025-10-23 17:40:00

Featured image of post 悬索桥的主缆损坏后,就真的无法维修更换,只能整座桥重建吗?

前些天老T在知乎简单回答了个关于悬索桥的问题,虽然赞同的不少,但评论中质疑者也很多,主要争论的就是悬索桥主缆损坏是否可维修更换。由于老T本身对桥梁建设一窍不通,只能以最基本的文字阅读能力,通过查找严肃的文献资料,争取将这个事情说清。


某大桥火灾事件评论

这个争论的起因是知乎上一个问题:“怎么看待某大桥在清明假期关闭最右侧车道?” 老T此前写了个简单回答:

桥梁建设又被上一课了。谁能想到悬索桥有一天会面临火灾风险。估计以后主缆高度起码得调到10米以上,否则真是无解。目前这个高度,一辆3米高的大车如果在主缆旁边着火,整个主缆搞不好都得换,需要的时间就说不清了。

虽然是个“抖机灵”回答,但也获得了 300 多个点赞,不过评论区里就争得激烈了。有人说“主缆烧坏了强度降低,有暗伤”,有人直接断言“维修成本约等于修不了”,还有人脑洞大开建议“铺水管加喷淋”或“加气凝胶隔热”,甚至有人阴谋论“车是故意停在那烧的”。

最让老T感到惊讶的是,评论里好几个人都说“主缆没法换,封死在锚碇里了,得先拆梁”,或者“换一根得上亿,换不了就重建”。老T一开始也承认自己不懂这个,后来补充了几个吊索更换的例子,比如江阴大桥换了50多根吊索、润扬大桥两个月换了2根索等。但评论又有人质疑“那些是吊索不是主缆”,没办法,老T确实没意识到还有这种区别,于是又找了英国塞文桥换了主缆中间6米索股的资料补充上,终于算是止住了这波争论。

讲白了,这就是典型的网上“外行看热闹”?桥梁这种专业事,包括老T在内大多数人都不懂,但也都想一吐为快,过程中很容易把事情带偏。于是这次老T决定再继续挖挖一下严肃资料,看看这悬索桥主缆到底怎么回事。


悬索桥悬索桥主缆的基本结构

先说说悬索桥主缆长啥样。老T查了交通运输部的桥梁设计规范和一些工程报告,主缆是悬索桥的“脊梁”,由几千根高强度镀锌钢丝平行排列捆绑而成,每根钢丝直径5-7毫米,一根主缆通常有127到271股索股(每股又是一捆钢丝),总直径能到1米左右。外层缠保护丝、涂防腐层,一般设计寿命长达 100 年以上。一般悬索桥就是左右各一根主缆,两端锚固在巨大锚碇里,中间挂吊索吊住桥面。

大桥两侧主缆


主缆是否可维修更换?

老T在知网查了十几篇论文,对这个问题主要有三种看法:无法维修更换、维修更换难度极大、可维修更换。

  1. 主缆无法维修更换

比如,在《大跨悬索桥主缆抗火性能及其防护》这篇文章中,作者多次提到悬索桥主缆无法维修更换,该文章最后结论也是基于主缆无法维修更换,而提出最严格的主缆火灾防护目标:耐火极限 45 分钟、临界温度 300 度。

李雪红, 雷语璇, 赵军, 郭志明, 于俊杰, 徐秀丽. 大跨悬索桥主缆抗火性能及其防护. 浙江大学学报(工学版)

  1. 主缆维修更换难度极大

老T查询的多数论文都显示,由于主缆是悬索桥最核心的组成部分,从设计之初就是伴随桥梁寿命终身的,想要更换维修难度极大、成本极高。

陈胜.大跨悬索桥主缆防护的分析研究.大连理工大学

  1. 主缆可维修更换

在老T查询的一些较新的论文中,明显出现了与前边不同的声音。具体来说,就是最近这些年,由于新的主缆锚固技术引入,一些新的悬索桥已经可以实现高效更换主缆索股。

例如,在2025年10月23日(刚好是今天)《中外公路》中的一篇《洞庭溪沅水特大桥缆索及锚固系统设计》文章中,作者提到,目前湖南怀化在建的洞庭溪沅水特大桥,其主缆索股采用了防腐性好、更换效率高的多股成品索锚固系统,可以实现对主缆索股的高效更换。

徐自然,程丽娟,刘榕,等. 洞庭溪沅水特大桥缆索及锚固系统设计. 中外公路

老T进一步查询发现,早在2022年就有一篇东南大学的《超大跨悬索桥重力式锚碇结构及可更换主缆锚固体系研究》,系统分析了原有悬索桥主缆锚固系统的特点,并提出三种新的主缆锚固设计,目标就是实现悬索桥主缆索股的高效更换。

林夏. 超大跨悬索桥重力式锚碇结构及可更换主缆锚固体系研究. 东南大学

并且,在这篇文章所提出多个新方案之前,作者也对以往悬索桥主缆锚固技术进行总结,指出常见的五类锚固系统中,只有的“预埋钢构件锚固系统、有粘结预应力锚固系统”两种不可更换,而“无粘结灌油式预应力锚固系统、蜂窝式无粘结预应力锚固系统、多股成品索锚固系统”都是可以更换的。只是各种技术下,更换成本和效率有区别。

林夏. 超大跨悬索桥重力式锚碇结构及可更换主缆锚固体系研究. 东南大学

事情到这里也就告一段落。从老T目前查找的文献来看,悬索桥主缆是否可维修更换,其实已经比较清晰。对于较新的桥梁,很可能会使用新型锚固设计方案,便于更换。而对于较老的桥梁,可能因为当时条件限制,更换起来有极大难度,甚至无法更换。

但这也不意味着老的悬索桥主缆就无法维修。

2016年英国赛文桥维修

起码,老T此前在知乎评论中提到的英国赛文桥的案例就比较典型。2016年,英国人对这条已有 50 年寿命的现代悬索桥进行维修,通过打入一个楔子撑开一个工作面,然后将其中一根破损索股切掉了中间 6 米,并重新用螺丝扣接上。整个维修过程,也只是占用了单向 1.5 个车道,并未造成交通中断。


总结

最后,老T觉得,我们外行对桥梁这种专业事,还是应该保持敬畏,但也不要轻易放弃“求知欲”,多翻文献,多找案例,总能拨云见日。懂不懂不丢人,肯查肯对比才靠谱。