<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>z1HwanG</title>
    <link>https://z1hwang.cn/</link>
    <description>Welcome, this is Zeehow&#39;s blog.</description>
    <language>zh-cn</language>
    <generator>Gridea Pro</generator>
    <lastBuildDate>Tue, 22 Sep 2026 18:50:11 -0400</lastBuildDate>
    <atom:link href="https://z1hwang.cn/feed.xml" rel="self" type="application/rss+xml"></atom:link>
    <item>
      <title>在 Windows 上找不到顺手的 RSS 阅读器，于是我自己写了一个</title>
      <link>https://z1hwang.cn/post/rss-reader/</link>
      <guid isPermaLink="true">https://z1hwang.cn/post/rss-reader/</guid>
      <pubDate>Wed, 09 Sep 2026 17:31:16 -0400</pubDate>
      <description><![CDATA[<h2 id="heading">起因</h2>
<p>起因很简单：我想多看一些文章。</p>
<p>不是被信息流推着刷，而是自己挑一些值得读的源，攒起来慢慢看。RSS 恰好就是这个形态——没人给我排序，也没有推荐算法替我决定该看什么，读到什么完全取决于我订阅了谁。</p>
<p>于是我开始找客户端。我主要用 Windows，试过一圈之后发现选择相当尴尬：</p>
<ul>
<li><strong>网页版</strong>：打开慢，切个标签页就找不着了，还要看服务商什么时候关站。</li>
<li><strong>在线服务</strong>：数据在别人服务器上，隐私且不说，免费额度、导出限制、突然改版都是变量。</li>
<li><strong>老牌桌面客户端</strong>：功能其实够用，但界面停留在十年前，高分屏下字体发虚，装完先弹广告或者引导你买 Pro。</li>
<li><strong>Electron 系</strong>：装完先吃掉 300 MB 内存，冷启动能看见窗口一帧帧画出来。</li>
</ul>
<p>我不要求它有多花哨。我的需求清单其实很短：</p>
<ol>
<li>订阅源和阅读状态<strong>存在本机</strong>，不上传；</li>
<li>启动要快，常驻内存别太夸张；</li>
<li>中文排版能看，浅色 / 深色跟随系统；</li>
<li>支持 OPML 导入导出，别把我锁死；</li>
<li>图片能正常显示——这点后面成了最大的坑。</li>
</ol>
<p>找了几天没找到满意的，索性自己写。于是有了 <a href="https://github.com/z1HwanG/RSS-Reader">++RSS Reader++</a>。</p>
<h2 id="-tauri-2">技术选型：为什么是 Tauri 2</h2>
<p>选 Tauri 而不是 Electron，核心就一条：<strong>我不需要一个完整的 Chromium</strong>。</p>
<p>Windows 上 WebView2 是系统自带的（Win11 内置，Win10 装一次），Tauri 直接复用，安装包和内存占用都能压下来。代价是三个平台的 WebView 内核不同，遇到怪问题时得自己扛——但对我这种「自己用的工具」来说，这个交易划算。</p>
<p>分工也很清晰：</p>
<table>
<thead>
<tr>
<th align="left">层</th>
<th align="left">技术</th>
<th align="left">负责什么</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">桌面壳</td>
<td align="left">Tauri 2（Rust）</td>
<td align="left">窗口、持久化、网络、图片代理、更新</td>
</tr>
<tr>
<td align="left">前端</td>
<td align="left">React 18 + TypeScript strict + Vite 5</td>
<td align="left">只做展示与交互</td>
</tr>
<tr>
<td align="left">Feed 解析</td>
<td align="left">feed-rs 2</td>
<td align="left">RSS 2.0 / 1.0、Atom、JSON Feed</td>
</tr>
<tr>
<td align="left">HTTP</td>
<td align="left">reqwest 0.12</td>
<td align="left">rustls、gzip/brotli、代理</td>
</tr>
</tbody>
</table>
<p>有一条设计原则贯穿全程：<strong>状态的权威在 Rust 侧</strong>。订阅源、文章、分组都落在 <code>app_data_dir/state.json</code>，前端只是视图。主题、字号、抓取频率这些纯展示偏好才放 <code>localStorage</code>。这样窗口关掉再开，数据一定是对的；前端崩了也不会把数据写坏。</p>
<h2 id="heading-1">几个真正花时间的坑</h2>
<p>功能列表写起来一行，做起来是另一回事。下面这几件事占了我大部分调试时间，也最值得记下来。</p>
<h3 id="1-">1. 图片：防盗链、混合内容与一条重试阶梯</h3>
<p>RSS 里的图片看起来是 <code>&lt;img src=&quot;...&quot;&gt;</code> 这么简单，实际上一半显示不出来：</p>
<ul>
<li>图床校验 <code>Referer</code>，空 Referer 放行、带错 Referer 就 403；</li>
<li>正文里是 <code>http://</code> 图片，在 WebView 里被混合内容策略拦掉；</li>
<li>GitHub Pages 上的图片没跟着发布，原地址恒为 404；</li>
<li>图床按 IP 限流。</li>
</ul>
<p>最后的方案是本地注册一个 <code>rssimg://</code> 协议，所有文章图片都走它，由 Rust 侧带浏览器 User-Agent 去抓。抓取按阶梯来：</p>
<ol>
<li>先直连、不带 Referer（多数防盗链对空 Referer 是放行的）；</li>
<li>遇到 403 就补上文章页的 Referer 重试，<strong>并把这个图床记进一个集合</strong>——下次同一图床的第一个请求直接带 Referer，省掉一次注定失败的往返；</li>
<li>还失败就回退直连再试一次；</li>
<li>GitHub Pages 的图片额外生成 <code>cdn.jsdelivr.net</code> 镜像候选。这里有个细节：镜像优先走当前链路（国内直连 cdn.jsdelivr.net 经常不通），失败后才退直连。</li>
</ol>
<p>成功响应缓存 7 天，单张上限 50 MB，只放行 <code>http</code> / <code>https</code>。</p>
<p>顺带一个工程上的小坑：<code>rssimg://</code> 是自定义 URI scheme，不走 IPC，所以它拿不到命令参数上下文。应用里的代理配置得靠 <code>update_proxy_setting</code> 先同步到 Rust 的全局状态，图片代理才读得到。这种「看起来是一件事、实际跨了两条通道」的地方，最容易漏。</p>
<h3 id="2---">2. 写盘：临时文件 + 原子改名</h3>
<p>用户点一下「标记已读」，就可能要写盘。直接 <code>fs::write</code> 有风险：写到一半断电，<code>state.json</code> 就成了半截 JSON，下次启动数据全丢。</p>
<p>做法是写临时文件再 <code>rename</code>。而 Windows 上 <code>rename</code> 在目标被占用时会失败，所以失败后要小睡一下重试一次。这类平台差异，文档里通常只有一句话，踩到了才知道。</p>
<p>另一半是频率。标记已读、收藏、排序都是高频操作，每次都落盘会把磁盘打满。所以走 800 ms 防抖合并，窗口隐藏或关闭前强制 flush 一次——既省 IO，也不会丢最后一次操作。</p>
<h3 id="3-webview-">3. WebView 里的整页跳转和右键菜单</h3>
<p>这两个是我自己用的时候最先受不了的：</p>
<ul>
<li>文章里点个链接，WebView 直接整页跳转，自定义标题栏被外部网页盖住，<strong>回不来了</strong>；</li>
<li>右键弹出的是 WebView 自带的 Back / Refresh / Save as / Print 菜单，一股「这是个浏览器」的味道。</li>
</ul>
<p>修法是加全局守卫：<code>&lt;a&gt;</code> 的点击一律 <code>preventDefault</code>，<code>http(s)</code> 交给系统默认浏览器打开，只放行页内锚点；中键点击也拦掉，因为应用没有多窗口 UI。相对链接要靠正文容器上的 <code>data-link-base</code> 解析——文章原始地址才是正确的基准，不能拿应用自己的 origin 去凑。</p>
<p>右键菜单则整体屏蔽，但<strong>文本输入框要放行</strong>，否则搜索框里连粘贴都用不了。第一版就是一刀切，被自己骂了一顿才补上的。</p>
<h3 id="4-5-mb--37-kb">4. 图标字体：5 MB 变 37 KB</h3>
<p>界面用 Material Symbols Rounded。完整可变字体 5 MB 出头，而我实际只用了 40 多个图标，为了这几十个字形背 5 MB 显然不划算。</p>
<p>于是写了个子集化脚本：从源码里扫出用到的图标名 → 下载完整字体 → 按清单子集化 → 用 HarfBuzz 逐个校验连字可用性 → 写回 <code>woff2</code>。最终 37 KB，还不依赖 Google CDN。</p>
<p>代价是新增图标后必须重跑脚本，否则界面上会直接显示成文字（比如按钮上写着 <code>search</code>）。这个坑我在 README 的常见问题里专门写了一条。</p>
<h3 id="5-">5. 自动更新：签名、双源与回退</h3>
<p>0.2.0 加了应用内更新。几个我觉得不能省的点：</p>
<ul>
<li><strong>minisign 签名</strong>，公钥内置在应用里，签名校验不过就拒绝安装。私钥丢了就再也发不出老用户能装的更新，所以必须单独备份；</li>
<li>更新清单优先取 GitHub Releases，<strong>失败自动回退 Forgejo Releases</strong>——两边各托管一份 <code>latest.json</code>，各指向自己的资源；</li>
<li>应用内配的代理一并用于更新请求；</li>
<li>Windows 走 NSIS 安装器的 passive 模式，装完自动重启。</li>
</ul>
<h2 id="heading-2">现在它长什么样</h2>
<p>两栏布局：左边文章列表，右边阅读视图，中间分隔条可拖拽调宽（宽度会记住）。</p>
<ul>
<li><strong>订阅管理</strong>：RSS 2.0 / 1.0、Atom、JSON Feed；分组折叠与未读数；OPML 导入导出；单个源可选应用内阅读或外部浏览器打开</li>
<li><strong>抓取</strong>：<code>ETag</code> / <code>Last-Modified</code> 条件请求（304 直接跳过下载与解析），并发上限 6，自动刷新间隔 10 分钟到 1 小时可选</li>
<li><strong>阅读</strong>：全部 / 未读 / 收藏筛选，最新 / 最早 / 按源排序，紧凑 / 列表 / 卡片三种视图，标题 + 正文全文搜索，大列表首屏 300 篇、滚动加载更多</li>
<li><strong>全文</strong>：摘要过短时一键抓原文，正文容器启发式提取并清理广告 / 评论 / 侧栏，最近 20 篇缓存复用</li>
<li><strong>数据</strong>：JSON 备份 / 还原，按发布时间清理本地缓存（星标保留）</li>
<li><strong>网络</strong>：HTTP / SOCKS5 代理，一键连通性测试（多探测目标，避免单站误报），SOCKS5 走 <code>socks5h</code> 让代理解析域名</li>
<li><strong>安全</strong>：最小权限 capabilities、严格 CSP、正文渲染前移除 <code>script</code> / <code>iframe</code> / <code>form</code> / <code>base</code> 等节点</li>
</ul>
<p>当前版本 <code>0.2.1</code>，Apache-2.0 授权，Windows x64 有现成安装包（NSIS / MSI / 免安装单文件），macOS 和 Linux 需要自己编译。</p>
<h2 id="heading-3">不足的地方，我很清楚</h2>
<p>写着写着就明白为什么这类工具少有人做了——坑太多，而每个坑都要单独填。下面是目前已知的问题，我不打算藏：</p>
<ul>
<li><strong>正文提取只是启发式</strong>。没上 Readability 级别的评分算法，容器选择器 + 无关元素移除在不同站点效果差异明显，有些站抓出来还是会缺段落或者多塞东西。</li>
<li><strong>正文渲染不做完整 HTML 白名单净化</strong>。为了保住排版只做节点清理，因此只适合可信订阅源。这是明确的安全取舍，不是疏忽。</li>
<li><strong>没有云端同步</strong>。多设备迁移只能靠备份 / 还原或 OPML，计划里的 WebDAV 备份还没做。</li>
<li><strong>跨平台没验证</strong>。<code>bundle.targets = all</code> 也只打包当前平台，macOS / Linux 我没机器实测，只能保证代码层面没写死 Windows。</li>
<li><strong>翻译功能没做</strong>，外文源只能自己看原文。</li>
<li><strong>早期阶段</strong>。<code>0.2.1</code> 还在快速变动，<code>state.json</code> 的结构虽然带 <code>schema_version</code> 和迁移逻辑，但不保证百分百兼容。</li>
</ul>
<h2 id="-issue">所以，欢迎提 Issue</h2>
<p>我写它的直接原因就是「自己没找到好用的」，那就意味着<strong>你觉得别扭的地方，多半我也没考虑到</strong>。不管是 bug、界面建议、还是「这个源解析出来是乱的」，都欢迎开 issue：</p>
<ul>
<li>GitHub：<a href="https://github.com/z1HwanG/RSS-Reader/issues">++https://github.com/z1HwanG/RSS-Reader/issues++</a></li>
<li>Forgejo：<a href="https://git.z1hwang.cn/Zeehow/RSS-Reader/issues">++https://git.z1hwang.cn/Zeehow/RSS-Reader/issues++</a></li>
<li>应用内：设置 → 关于 也有直达入口</li>
</ul>
<p>想让 issue 好处理一点，附上这些会省很多来回：</p>
<ol>
<li><strong>复现步骤</strong>：点了什么、期望什么、实际什么；</li>
<li><strong>订阅源地址</strong>（如果和某个源有关）——解析问题九成要拿原始 XML 复现；</li>
<li><strong>界面问题直接截图</strong>，比文字描述快十倍；</li>
<li>出错的图 / 文章链接，图片问题基本都要看具体响应；</li>
<li>版本号（设置 → 关于里有）。</li>
</ol>
<p>Pull Request 同样欢迎。提交前跑一下 <code>npm run build</code>（TypeScript strict + Vite 构建），改了 Rust 建议再过一遍 <code>cargo fmt</code> 和 <code>cargo clippy --all-targets</code>。</p>
<h2 id="heading-4">最后</h2>
<p>做这个小工具最大的收获，是重新意识到「能用」和「好用」之间的距离有多远：抓取解析一两天就通了，但防盗链、原子写盘、WebView 跳转、图标子集化这些看起来边缘的细节，才真正决定你愿不愿意每天打开它。</p>
<p>它现在还不完美，但我自己就是它的第一个用户。如果你也想多看几篇文章、在 Windows 上找 RSS 阅读器，不妨试试；用得别扭，就来提 issue。</p>
]]></description>
      <category>开源</category>
    </item>
    <item>
      <title>一个让我告别「改 DNS 靠手打」的桌面小工具</title>
      <link>https://z1hwang.cn/post/dns-chooser/</link>
      <guid isPermaLink="true">https://z1hwang.cn/post/dns-chooser/</guid>
      <pubDate>Thu, 03 Sep 2026 17:35:13 -0400</pubDate>
      <description><![CDATA[<p>上网这么多年，总有些操作你明明做过无数次，但每次做的时候还是觉得「这都 2026 年了，怎么还要手动搞？」</p>
<p>对我来说，<strong>改 DNS</strong> 就是其中之一。</p>
<p>切个 114 要打开网络设置，找到适配器属性，找到 IPv4，把数字一个一个敲进去——万一哪天想换 Cloudflare 或者 Google DNS，还得再来一遍。更别提测速选最快的了，那可真是看缘分。</p>
<p>所以我就想，能不能做一个桌面小工具，把这些操作攒到一个界面里，点一下就行？</p>
<p>于是就捣鼓出了 <a href="https://github.com/z1HwanG/dns-chooser"><strong>++DNS Chooser++</strong></a>。</p>
<hr />
<h2 id="heading">它长什么样</h2>
<p>应用打开后分三个区域：</p>
<ul>
<li><strong>DNS 测速</strong> → 点一下，内置 12 个常用 DNS 挨个测延迟，按快慢排好，最快的那个标个「最快」。</li>
<li><strong>DNS 设置</strong> → 6 组预设一键选，或者手动敲 IP，应用就行。想恢复 DHCP 也一个按钮。</li>
<li><strong>工具</strong> → 清理 DNS 缓存、用指定 DNS 查域名解析，两个常用的诊断功能。</li>
</ul>
<p>顶部条实时显示当前在用的 DNS 和权限状态，一目了然。</p>
<hr />
<h2 id="heading-1">我比较喜欢的几个点</h2>
<h3 id="1--ping-icmp">1. 测速是「真 ping」，不是 ICMP</h3>
<p>很多测速工具用的是系统 ping 命令（ICMP），但 DNS 实际跑的是 UDP 53 端口，ICMP 快不代表 UDP 也快。</p>
<p>这个工具的做法是 Rust 后端直接发一个真实的 DNS 查询包到 53 端口，等回复，算延迟——<strong>更贴近实际体验</strong>。</p>
<pre><code class="language-rust">async fn ping_dns(ip: &amp;str, timeout_ms: u64) -&gt; Result&lt;f64, String&gt; {
    let addr = format!(&quot;{}:53&quot;, ip);
    let socket = UdpSocket::bind(&quot;0.0.0.0:0&quot;).await?;
    socket.send_to(&amp;build_dns_query(), &amp;addr).await?;
    let result = timeout(Duration::from_millis(timeout_ms), socket.recv_from(&amp;mut buf)).await;
    // ...
}

</code></pre>
<p>代码量不大，但效果实在。</p>
<h3 id="2-">2. 权限处理是个「小坑」</h3>
<p>改 DNS 和清缓存都需要管理员权限，这是绕不开的。</p>
<p>Windows 上我试过几种方案，最终还是回到了最稳的做法：<strong>检测到非管理员 → 界面提示 → 用户自己右键重新以管理员运行</strong>。同时也留了个 PowerShell 提权的路子给需要自动化的人。</p>
<p>macOS 那边则是靠 <code>osascript</code> 弹系统对话框来提权。</p>
<p>这个问题的本质是：<strong>桌面应用想改系统设置，就得跨过那道安全门槛</strong>。没有完美的方案，只有适合场景的取舍。</p>
<h3 id="3-tauri-2--electron-">3. Tauri 2 比 Electron 轻太多了</h3>
<p>之前也用 Electron 写过小工具，打包出来动辄一两百 MB。同样的功能换 Tauri 2，<strong>安装包不到 10MB</strong>，内存占用也低得多。</p>
<p>前端还是熟悉的 React + TypeScript，后端用 Rust 写系统命令的胶水层，再加上 tokio 做异步——写起来很顺手。</p>
<hr />
<h2 id="heading-2">技术栈一览</h2>
<table>
<thead>
<tr>
<th align="left">层</th>
<th align="left">技术</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">桌面框架</td>
<td align="left">Tauri 2</td>
</tr>
<tr>
<td align="left">前端</td>
<td align="left">React 18 + TypeScript + Vite 5</td>
</tr>
<tr>
<td align="left">后端</td>
<td align="left">Rust（tokio 异步）</td>
</tr>
<tr>
<td align="left">DNS 测速</td>
<td align="left">原生 UDP Socket 直连 53 端口</td>
</tr>
<tr>
<td align="left">系统命令</td>
<td align="left">netsh（Win）/ networksetup（macOS）</td>
</tr>
<tr>
<td align="left">样式</td>
<td align="left">纯 CSS（CSS 变量主题系统，跟随系统深色/浅色）</td>
</tr>
<tr>
<td align="left">通知</td>
<td align="left">react-hot-toast</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="heading-3">一些不那么技术的小收获</h2>
<p>写这个工具的过程中，最让我感慨的反而不是技术本身，而是：</p>
<p><strong>那些我们觉得「就该这么麻烦」的事情，很多时候只是没人做那个「不麻烦的版本」。</strong></p>
<p>改 DNS 本身不复杂，但每次打开五层菜单去改，加起来的时间成本远大于写一个工具的时间。类似的「微小但高频」的痛点，每个人身边应该都有一堆。</p>
<p>如果你也有类似的感受，不妨也试试写个小工具解决自己的痛点——选 Tauri 这样的框架，门槛真的不高。</p>
<hr />
<h2 id="--">使用 &amp; 获取</h2>
<p>项目已经开源在 GitHub：</p>
<p>:link: <strong><a href="https://github.com/z1HwanG/DNS-Chooser">++github.com/z1HwanG/DNS-Chooser++</a></strong></p>
<p>目前支持 Windows 和 macOS（Linux 没测，理论上可以但需要适配）。</p>
<pre><code class="language-bash"># 自己构建也很简单
git clone https://github.com/z1HwanG/DNS-Chooser.git
cd DNS-Chooser
npm install
npm run tauri:build

</code></pre>
<hr />
<p>如果你也有类似的“手动改 DNS 党”的经历，或者有什么其他小痛点的想法，欢迎聊聊 :grinning_face_with_closed_eyes:</p>
]]></description>
      <category>开源</category>
      <category>Rust</category>
    </item>
    <item>
      <title>Live Chat，直播我只播给朋友看</title>
      <link>https://z1hwang.cn/post/live-chat/</link>
      <guid isPermaLink="true">https://z1hwang.cn/post/live-chat/</guid>
      <pubDate>Sun, 30 Aug 2026 17:36:36 -0400</pubDate>
      <description><![CDATA[<h1 id="heading">我搭了一个给朋友看的游戏直播间</h1>
<p>平时玩游戏，偶尔想叫几个朋友一起看。用大直播平台当然可以，但广告多、延迟不稳定，而且路人也能进来，总觉得不够“私人”。</p>
<p>所以我干脆自己搭了一个小直播间：<strong>自己推流，朋友打开网页就能看，旁边还能实时聊天</strong>。</p>
<h2 id="heading-1">这套东西是什么</h2>
<p>整个方案由两部分组成：</p>
<ul>
<li><strong>直播流服务</strong>：负责接收推流、分发播放地址。我使用的是 SRS，这部分由我自己部署和维护。</li>
<li><a href="https://github.com/z1HwanG/Live-Chat"><strong>++LiveChat++</strong></a>：我写的一个网页聊天室，负责直播间的聊天、管理员管理，以及推流鉴权。</li>
</ul>
<p>简单说就是：</p>
<pre><code>OBS 推流
   │
   ▼
SRS 直播服务（自行部署）
   │
   ▼
朋友浏览器播放画面 + LiveChat 聊天

</code></pre>
<p>LiveChat 本身是一个独立的 Node.js 项目，不绑定具体的直播服务，通过接口和 SRS 对接。</p>
<h2 id="heading-2">为什么这样搭</h2>
<p>选择自己搭，主要是为了这几个体验：</p>
<ul>
<li><strong>给朋友看，不公开</strong>：推流地址带 token，只有知道地址和 token 的人能推流，陌生人顶不了流。</li>
<li><strong>低延迟</strong>：支持 WebRTC、FLV、HLS 自适应播放，朋友网络好的时候可以做到接近实时。</li>
<li><strong>有聊天</strong>：朋友边看边聊，比拉语音群更有“一起看”的感觉。</li>
<li><strong>没有广告</strong>：自己搭的页面干干净净，没有平台推流、没有贴片广告。</li>
<li><strong>管理自由</strong>：有人刷屏或乱说话，我可以禁言、删消息，甚至踢出去。</li>
</ul>
<h2 id="livechat-">LiveChat 的功能</h2>
<p>LiveChat 是我这个直播间里的“聊天 + 管理”部分：</p>
<ul>
<li>实时文字聊天，基于 WebSocket；</li>
<li>新朋友进来自动加载最近的历史消息；</li>
<li>显示当前在线人数和观看人数；</li>
<li>断线自动重连；</li>
<li>管理员口令登录后可以：
<ul>
<li>删除消息；</li>
<li>按分钟禁言；</li>
<li>解除禁言；</li>
<li>踢人下线；</li>
</ul>
</li>
<li>每个 IP 3 秒最多发 1 条消息，防止刷屏；</li>
<li>提供 SRS <code>on_publish</code> 推流鉴权回调，防止陌生人推流。</li>
</ul>
<h2 id="heading-3">技术架构</h2>
<pre><code>浏览器
  │
  │  HTTPS / WSS
  ▼
Nginx / Caddy 反向代理
  │
  ▼
LiveChat Web 服务（Node.js + Express + ws）
  │
  ▼
MariaDB / MySQL（历史消息、禁言列表）

</code></pre>
<ul>
<li>后端：Node.js + Express + ws；</li>
<li>数据库：MariaDB / MySQL；</li>
<li>前端：原生 JavaScript，没有重框架；</li>
<li>播放器：WebRTC / FLV / HLS 自适应，兼容电脑和手机浏览器。</li>
</ul>
<h2 id="heading-4">使用体验</h2>
<p>我自己用的时候大概是这样的：</p>
<ol>
<li>打开 OBS，填好推流地址和 token，开始推流；</li>
<li>把直播间网址发给朋友；</li>
<li>朋友打开网页，自动开始播放，旁边就是聊天区；</li>
<li>我在后台登录管理员，可以随时管理消息。</li>
</ol>
<p>整个过程没有复杂的客户端，朋友只需要一个浏览器。</p>
<h2 id="heading-5">部署</h2>
<p>LiveChat 面向通用 Linux 环境，使用 Docker 部署：</p>
<pre><code class="language-bash">docker compose up -d --build

</code></pre>
<p>然后用 Nginx 或 Caddy 反代到 <code>127.0.0.1:3010</code>。</p>
<p>SSL 证书推荐用 Certbot 自动申请（当然我用的1panel:beaming_face:）：</p>
<pre><code class="language-bash">certbot --nginx -d live.example.com

</code></pre>
<p>如果用 Caddy，连证书都不用管：</p>
<pre><code>live.example.com {
    reverse_proxy 127.0.0.1:3010
}

</code></pre>
<p>SRS 直播服务由用户自行部署，LiveChat 只负责聊天、管理和推流鉴权对接，两者互相独立。</p>
<h2 id="heading-6">最后</h2>
<p>这个项目不算大，但很实用。它让我拥有了一个只属于自己和朋友的游戏直播间：低延迟、有聊天、无广告、不公开。</p>
<p>如果你也喜欢自己折腾，想搭一个给朋友看的私人直播间，可以参考这个思路。</p>
]]></description>
      <category>开源</category>
      <category>Docker</category>
    </item>
    <item>
      <title>故地重游就像刻舟求剑</title>
      <link>https://z1hwang.cn/post/905/</link>
      <guid isPermaLink="true">https://z1hwang.cn/post/905/</guid>
      <pubDate>Wed, 27 Nov 2024 12:45:23 +0000</pubDate>
      <description><![CDATA[<p>拍摄于2021年8月4日19时11分，海南文昌。</p>
]]></description>
      <category>回忆</category>
      <category>晚霞</category>
      <enclosure url="https://file.z1hwang.cn/blog/%E8%90%BD%E6%97%A5.jpg" length="0" type="image/jpeg"></enclosure>
    </item>
    <item>
      <title>你也走了很远的路吧</title>
      <link>https://z1hwang.cn/post/nyzlhydlb/</link>
      <guid isPermaLink="true">https://z1hwang.cn/post/nyzlhydlb/</guid>
      <pubDate>Tue, 26 Nov 2024 21:38:04 +0000</pubDate>
      <description><![CDATA[<blockquote>
<p>作者介绍：</p>
<p>卢思浩，百万级畅销书作家。如今已出版《你要去相信，没有到不了的明天》、《愿有人陪你颠沛流离》、《离开前请叫醒我》、《我想和你在一起》等作品。一位细腻的生活家，一枚合格的铲屎官。</p>
<p align="right">微信公众号：卢思浩（KveinLu_）</p>
<p align="right">新浪微博：@卢思浩</p>
</blockquote>
<p> </p>
<p>这是一个奔波的时代。</p>
<p>我们不停告别，又不听重新出发，有些话还来不及讲，就已经分道扬镳。</p>
<p>心情越来越冷淡，很多话找不到人讲，于是什么都埋在心里。</p>
<p>你有千万句话想说，可点开那个对话框，你根本打不出一个字。</p>
<p>你才明白，原来你从一开始就怕被别人看穿，所以宁可孤独。</p>
<p>所以你宁可每天嘻嘻哈哈，也不要被别人看出你真的难受过。</p>
<p>只有在最深的夜里，你才能够允许自己难过。<br />
 <br />
 <br />
人们说，节哀顺变。</p>
<p>人们说，你还有自己的生活。</p>
<p>我们点头，然后把自己埋在工作里。</p>
<p>然后在某个夜里，想起曾经跟亲人在一起的日子。<br />
 <br />
 <br />
孤独是，某天突然下雨，你走在街上，只有你一个人没有带伞；</p>
<p>加班到深夜的你饥饿难耐，却发现周边所有便利店都关门；</p>
<p>期待很多年的电影今天上映，怎么也找不到人陪你去看；</p>
<p>深夜想要找一个人聊天，翻遍通讯录却找不到人说话。</p>
<p>走过了那么远的路，再也找不到人分享风景。<br />
 <br />
 <br />
我知道的，因为我也经历过。</p>
<p>我知道世事无常，反而想用力珍惜；我看到太多绝望，反而读懂了希望。</p>
<p>我经历过漫长的孤独，反而找到了真正重要的东西。</p>
<p>是那些陪在身边的朋友，是那些你不用说都能懂你的人，是那些你内心的渴望。</p>
<p>是那些藏在你心里某个角落，让你想起来就能嘴角上扬的事情。</p>
<p>我们成长，我们遗忘，我们弄丢回忆，我们无能为力。</p>
<p>可日升日落，潮退潮涨，永远有前路，等待前行的人。</p>
]]></description>
      <category>阅读</category>
      <enclosure url="https://file.z1hwang.cn/blog/%E4%BD%A0%E4%B9%9F%E8%B5%B0%E4%BA%86%E5%BE%88%E8%BF%9C%E7%9A%84%E8%B7%AF%E5%90%A7.png" length="0" type="image/png"></enclosure>
    </item>
    <item>
      <title>成长路上，我遇见了美好</title>
      <link>https://z1hwang.cn/post/cheng-chang-lu-shangwo-yu-jian-liao-mei-hao/</link>
      <guid isPermaLink="true">https://z1hwang.cn/post/cheng-chang-lu-shangwo-yu-jian-liao-mei-hao/</guid>
      <pubDate>Sun, 24 Oct 2021 13:48:18 -0400</pubDate>
      <description><![CDATA[<blockquote>
<p>  转自抖音</p>
</blockquote>
<center>“彼方尚有荣光在。”</center>
<p align="right">—— 题记</p>
<center>光・遇</center>
<p>盛夏，八月，聒噪蝉鸣。</p>
<p>我漫步于静谧的砖瓦小道上，鹅黄色暖阳倾泻在我的肩上，静坐，隔壁琴房悠扬的韵律令人听的出了神。梧桐树于地拔起，阳光静悄悄地穿过细隙落下，我有些迷茫的伸出手，抓住了带有余温的光。</p>
<p>那一刻，阳光是暖的。相隔时间线，着乍起的朝阳带着初生般蓬勃，向上，宁静，有力。</p>
<center>少・年</center>
<p>借我瞻前顾后，借我执拗如少年，借我一缕阳光让我清醒。</p>
<p>还忆起，那时的两小无猜，你我规规正正的坐于台阶上，一字一顿，字正腔圆的把诗词吐出，背对了，你给我鼓掌叫好；背错了，你神情严肃，给我分析品读。那是的心是甜蜜的，洋溢在空中的诗香将我带入那新奇而又神秘的别样世界。那个时候啊，阳光穿过梧桐树落在书卷上和你的肩头，我就笑你，笑你被簇拥，笑你是拯救人间的神明。“帘外的雨潺潺，春意阑珊；罗衾不耐五更寒，一晌贪欢。”</p>
<p>你教我识花认木读诗句，欢笑声中，我不再是那个小女孩了，你说，那叫成长。</p>
<p>依稀真切记得，那年元旦晚会，你有模有样的指挥舞台剧，江山在你的指点下激昂，我也装模作样地随着你的手势，你的每句话，笨拙又满怀开心地努力，在顽强中愈渐成长。</p>
<p>春叠的纸飞机被风吹散到梦的田野，它交织着时光的银河，让梦得以在浩瀚的星辰间盛开，零碎的星方坠入人间，那是少年对生活、理想的期许而热爱。</p>
<p>那叫成长。</p>
<center>成・长</center>
<p>风对云的朝思暮想是躇躇慌张迷茫无错，是无数次小心翼翼地尝试去靠近云端，与风低喃，与云相拥，与山河相见，与期望重逢。</p>
<p>在春花烂漫之中，与生活的甘甜，与成长的相遇。</p>
<p>周国平先生所言 “世上那些可不做的事是做不完的，永远要去做那些值得做的。” 风华正茂的少年，策马扬鞭，驰聘四海，访遍山河，光耀四方，那是成长。</p>
<p>“我的心里，有一簇迎着烈日而生的花，比一切美酒都要芬芳。” 那些我暗自成长的瞬间，那些阳光洒落台阶的瞬间，那些古诗词句的瞬间。</p>
<p>那些成长的瞬间，我抬头，望见你笑着朝我走来，那一瞬间，我才真切的明白，你是漫长成长路上仅存的美好。</p>
<p>他说：“彼方尚有荣光在。”</p>
]]></description>
      <category>转载</category>
    </item>
  </channel>
</rss>