<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Posts on Two Tigers Engineering</title>
    <link>https://blog.twotigers.xyz/posts/</link>
    <description>Recent content in Posts on Two Tigers Engineering</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 12 May 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://blog.twotigers.xyz/posts/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>OpenSpec 与 Superpowers 结合使用：从需求脑暴到规格归档的 AI Coding 工作流</title>
      <link>https://blog.twotigers.xyz/posts/openspec-superpowers-workflow/</link>
      <pubDate>Tue, 12 May 2026 00:00:00 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/openspec-superpowers-workflow/</guid>
      <description>最近我尝试把 OpenSpec 和 Superpowers 结合起来，用 Claude Code 做一个“英语自然拼读学习网站”。这篇文章整理这次探索过程：为什么要同时使用这两个工具、每个阶段应该产出什么文件、提示词应该怎么写，以及实现完成后如何归档 spec。
核心结论很简单：
Superpowers 负责过程纪律。 OpenSpec 负责长期规格事实。 实现阶段只以 OpenSpec change 为需求来源。 如果只用 Superpowers，AI agent 的执行过程会更稳，但需求容易停留在聊天或临时设计文档里。
如果只用 OpenSpec，需求和规格更清楚，但 agent 执行时仍可能跳步骤、少测试或扩范围。
两者结合后，比较理想的分工是：
Superpowers：帮助想清楚、拆清楚、按 TDD 做清楚。 OpenSpec：把确认后的需求变成仓库里的正式规格。 整体流程 我最终采用的流程是：
Superpowers brainstorming/spec -&amp;gt; OpenSpec change -&amp;gt; OpenSpec validate/review -&amp;gt; Superpowers implementation plan -&amp;gt; Superpowers TDD execution -&amp;gt; Review/verify -&amp;gt; OpenSpec archive 每一步都有明确边界。最关键的一点是：Superpowers 先帮助形成设计共识，但一旦 OpenSpec change 被确认，OpenSpec 就成为唯一需求基线。
这样做是为了避免需求源冲突：
Superpowers 草稿说 A OpenSpec change 说 B 代码到底按哪个做？ 所以进入实现阶段后，规则应该是：</description>
    </item>
    
    <item>
      <title>在 ARM VPS 上用 llama.cpp 部署 Gemma 4 E2B 本地模型</title>
      <link>https://blog.twotigers.xyz/posts/llama-cpp/</link>
      <pubDate>Sat, 04 Apr 2026 14:00:00 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/llama-cpp/</guid>
      <description>在 ARM VPS 上，使用 llama.cpp 的 Docker 镜像部署本地 LLM，记录从 Gemma 3 到 Gemma 4 的部署过程、性能测试和资源监控。
背景：Gemma 4 发布 2026 年 3 月，Google DeepMind 发布了 Gemma 4 系列开源模型。Gemma 4 带来了多项重大升级：
推理能力：全系模型支持可配置的思维链（Chain of Thought）推理模式 多模态：支持文本、图像输入，小模型（E2B/E4B）额外支持音频 MoE + Dense 双架构：提供 Dense 和混合专家（Mixture-of-Experts）两种架构 超长上下文：小模型 128K tokens，大模型 256K tokens 原生函数调用：支持 function calling，适用于 Agent 场景 原生系统提示：首次原生支持 system role Gemma 4 共 4 个规格：
模型 架构 有效参数 上下文长度 特点 E2B Dense 2.3B 128K 轻量高效，支持音频，适合手机/边缘设备 E4B Dense 4.5B 128K 平衡性能，支持音频 26B A4B MoE 3.</description>
    </item>
    
    <item>
      <title>Claude Code 完整使用手册</title>
      <link>https://blog.twotigers.xyz/posts/claude-code/</link>
      <pubDate>Wed, 07 Jan 2026 23:26:00 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/claude-code/</guid>
      <description>引言 Claude Code 是由 Anthropic 公司开发的 AI 驱动的代码编辑器和开发助手。它将强大的 Claude AI 模型深度集成到开发环境中，为开发者提供前所未有的智能编程体验。
与传统 IDE 的区别 传统 IDE（如 VS Code、IntelliJ IDEA）主要提供代码编辑、语法高亮、调试等基础功能，而 Claude Code 通过以下方式重新定义了开发体验：
自然语言交互：可以用中文或英文直接描述任务，无需记忆复杂的命令 智能理解：能够理解整个代码库的上下文，跨文件进行推理 自动化执行：从任务规划到代码实现，全自动完成 多工具集成：集成文件操作、命令执行、浏览器测试等全套开发工具 学习适应：随着使用，AI 会逐渐理解你的编码风格和项目需求 核心优势 效率提升：减少 70% 以上的重复编码工作 质量保证：AI 帮助发现潜在 bug 和优化机会 学习加速：实时解释代码逻辑，快速掌握新技术 跨领域能力：前端、后端、DevOps 全栈支持 安全可靠：本地化执行，代码不离开你的机器 适用场景 新项目快速原型开发 代码重构和优化 调试和问题排查 学习新技术栈 自动化脚本编写 文档生成和维护 测试用例编写 安装和配置 系统要求 操作系统：macOS、Linux、Windows（WSL2） Node.js：v16 或更高版本（如使用 CLI 版本） IDE：Visual Studio Code（推荐）或支持的编辑器 网络：稳定的互联网连接（访问 Anthropic API） 安装方式 方式一：VS Code 扩展（推荐） 打开 VS Code 在扩展商店搜索 &amp;ldquo;Claude Code&amp;rdquo; 点击安装 安装完成后，会在侧边栏看到 Claude Code 图标 方式二：CLI 版本 # 使用 npm 安装 npm install -g @anthropic-ai/claude-code # 或使用 yarn yarn global add @anthropic-ai/claude-code # 验证安装 claude-code --version 初始化配置 首次使用需要配置 API 密钥：</description>
    </item>
    
    <item>
      <title>wg 使用</title>
      <link>https://blog.twotigers.xyz/posts/witeguard/</link>
      <pubDate>Mon, 01 Sep 2025 00:30:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/witeguard/</guid>
      <description>wg-go 使用说明 项目地址: https://github.com/marlin2024/wg-go
介绍 wg-go 提供了一套工具，可以在 非 root 用户 下运行 WireGuard，并且支持通过 Cloudflare Warp 落地到指定国家或地区。 它的核心思路是把 WireGuard 封装成 SOCKS5 代理，避免直接使用 tun，这样既安全又方便，也非常适合容器环境。
特性 非 root 用户即可运行 WireGuard 提供 SOCKS5 代理接口 支持 Warp 与其他代理叠加 容器环境友好 可以搭建 wg-over-wg 这样的实验场景 使用方法 1. socks-wg 直接将 WireGuard 转换成本地 SOCKS5 代理。
go run ./socks_over_wg \ -config socks_over_wg/wg.conf \ -listen 127.0.0.1:9999 效果对比：
# 直连（本机 IP） curl https://ipinfo.io/json # 通过 socks-wg 代理（Warp IP） curl -x socks5://127.0.0.1:9999 https://ipinfo.io/json 2. socks-wg-upstream_socks 在 socks-wg 的基础上，可以指定上游 SOCKS 代理。</description>
    </item>
    
    <item>
      <title>如何优雅地重复读取HTTP响应体</title>
      <link>https://blog.twotigers.xyz/posts/go/http_body/</link>
      <pubDate>Mon, 26 May 2025 14:00:00 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/go/http_body/</guid>
      <description>背景介绍 在Go语言中处理HTTP请求时，我们经常需要多次读取响应体(Response Body)的内容。然而，默认情况下Response Body只能被读取一次，这是因为它实现了io.ReadCloser接口，读取完毕后数据就会被消费掉。本文将介绍一个优雅的解决方案。
问题描述 假设我们有以下场景：
需要在日志中记录API的响应内容 需要对响应内容进行多次处理 需要在中间件中查看响应内容，同时不影响后续处理 解决方案 我们可以使用io.TeeReader来实现响应体的复制，这样就能多次读取相同的内容。以下是具体实现：
package main import ( &amp;#34;bytes&amp;#34; &amp;#34;fmt&amp;#34; &amp;#34;io&amp;#34; &amp;#34;net/http&amp;#34; ) func DupReadCloser(reader io.ReadCloser) (io.ReadCloser, io.ReadCloser) { var buf bytes.Buffer tee := io.TeeReader(reader, &amp;amp;buf) return io.NopCloser(tee), io.NopCloser(&amp;amp;buf) } func DupResponseBody(resp *http.Response) ([]byte, error) { var buf io.ReadCloser resp.Body, buf = DupReadCloser(resp.Body) data, err := io.ReadAll(resp.Body) if err != nil { return nil, err } resp.Body = buf return data, nil } 代码解析 DupReadCloser函数：接收一个io.</description>
    </item>
    
    <item>
      <title>Jetbrains 非预期 format 代码过滤</title>
      <link>https://blog.twotigers.xyz/posts/ide/</link>
      <pubDate>Mon, 26 May 2025 13:00:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/ide/</guid>
      <description>在升级到 2025.1 系列版本后，我们项目中的 .proto 文件出现了一个令人头疼的问题：仅仅修改几行内容，却引发了整个文件的大面积 format 变化。
由于我们使用的是公共 repo，我们一直非常注重 diff 的最小化，避免格式化带来的冗余变更。但这次 .proto 文件却被“悄悄”改了样子。
❓ 发生了什么？ 我们原本以为是 protobuf 插件导致的自动格式化，于是尝试关闭 .proto 文件的相关插件支持，退回纯文本模式。然而，问题依旧存在。
在多次对比 format 前后的差异后，我们终于发现——
原来是 所有注释行最后的空格被自动移除了。
也就是说，哪怕我们只改了一行逻辑代码，整个文件中尾部有空格的注释行也被一并“清理”，导致 Git diff 看起来像是大动干戈，实际只是视觉污染。
✅ 解决方案：添加 .editorconfig 为了彻底杜绝这类问题，我们最终采用了 .editorconfig 文件配置来控制编辑器行为。
在项目根目录下创建 .editorconfig 文件，内容如下：
[*] trim_trailing_whitespace = false
这条配置的意思是：对所有文件类型，不自动清除行尾空格。
配置完成后，记得重启编辑器，确保设置生效。</description>
    </item>
    
    <item>
      <title>解决 SELinux 拦截导致 systemctl 启动服务失败的问题</title>
      <link>https://blog.twotigers.xyz/posts/linux/selinux/</link>
      <pubDate>Mon, 10 Jun 2024 12:00:00 +0000</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/linux/selinux/</guid>
      <description>SELinux 与 systemctl 执行问题的解决方案 在系统管理中，SELinux（Security-Enhanced Linux）是一种强制访问控制（MAC）安全机制。尽管它提供了更高的安全性，但有时也会引发一些问题。本文将介绍一个常见的问题，并详细说明如何通过日志确认问题并修正它。
问题描述 我们遇到了一个问题，即可以直接使用命令执行某个服务，但当尝试使用 systemctl start caddy 命令启动时，却会出现以下错误：
(code=exited, status=203/EXEC) 日志确认问题 要确认是否是SELinux导致的问题，可以通过查看系统的审计日志。在终端中使用以下命令：
grep &amp;#39;avc: &amp;#39; /var/log/audit/audit.log 你会看到类似下面的日志条目：
type=AVC msg=******: avc: denied { execute } for pid=**** comm=&amp;#34;(caddy)&amp;#34; name=&amp;#34;caddy&amp;#34; dev=&amp;#34;dm-0&amp;#34; ino=**** scontext=********* tclass=file permissive=0 这条日志中的信息显示，SELinux 拒绝了 caddy 服务执行的请求。
解决方案 为了解决这个问题，我们可以采取临时方案，即将需要执行的文件放置在 /usr/bin 目录下。以下是具体步骤：
将 caddy 文件移动到 /usr/bin 目录下：
sudo mv /path/to/caddy /usr/bin/ 执行 restorecon 命令，恢复文件的SELinux安全上下文：
sudo restorecon -Rv /usr/bin restorecon 命令会根据SELinux策略恢复指定目录下文件的默认安全上下文。在执行完这个命令之后，你放在 /usr/bin 下的 caddy 文件就可以正常使用了。
为什么放在 /usr/bin 下才能用？ SELinux 对系统中的每个文件和进程都施加了安全上下文。当你将文件放置在 /usr/bin 目录下时，系统会自动为这些文件分配合适的安全上下文，使得它们可以被系统服务执行。而自定义目录中的文件可能没有正确的安全上下文，因此被SELinux拒绝执行。</description>
    </item>
    
    <item>
      <title>adguard home 安装与配置</title>
      <link>https://blog.twotigers.xyz/posts/adguard/</link>
      <pubDate>Wed, 29 May 2024 21:46:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/adguard/</guid>
      <description>引言 AdGuard Home 是一款网络级广告和跟踪器拦截器。它可以帮助你在家中或办公室的网络上拦截广告，保护隐私，并且提升上网速度。选择 AdGuard Home 的原因包括它的开源性质、易于配置和强大的功能。
安装 部署我们选择 docker + caddy 的方式
使用 docker 部署 adguard adguard 提供了 docker 部署的方式: github地址 建议使用
docker run --name adguardhome --restart unless-stopped -v /root/adguardhome/work:/opt/adguardhome/work -v /root/adguardhome/confdir:/opt/adguardhome/conf -p 53:53/tcp -p 53:53/udp -p 8080:80/tcp -p 4443:443/tcp -p 4434:443/udp -p 3000:3000/tcp -d adguard/adguardhome 配置 caddy yourdomain.com { reverse_proxy 127.0.0.1:3000 配置 adguard 访问 http://yourdomain.com:3000 进行配置
访问 admin 配置页面 修改 CaddyFile 文件，修改为配置
yourdomain.com { reverse_proxy 127.0.0.1:8080 配置 adguard 配置 DOH 需要配置上, 证书和密钥, 证书可以使用 acme.</description>
    </item>
    
    <item>
      <title>golang json.Marshal(error) 返回 `{}`, 问题分析与解决方案</title>
      <link>https://blog.twotigers.xyz/posts/go/json/</link>
      <pubDate>Wed, 29 May 2024 21:00:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/go/json/</guid>
      <description>序列化 Go 语言中的 error 接口问题：问题发现、原因与解决方案 在开发 Go 语言项目时，我们常常需要将结构体序列化为 JSON 格式。然而，当结构体中包含 error 接口时，序列化结果往往会不如预期。在这篇博客中，我们将讨论这个问题的原因，并提供两种解决方案。
问题描述 我们有一个结构体 CustomError，其中包含一个 error 类型的字段。如下所示：
package main import ( &amp;#34;encoding/json&amp;#34; &amp;#34;fmt&amp;#34; ) type CustomError struct { UserID int64 Err error } func (e *CustomError) Error() string { return fmt.Sprintf(&amp;#34;user %d: %s&amp;#34;, e.UserID, e.Err.Error()) } func main() { var errs []CustomError errs = append(errs, CustomError{UserID: 1, Err: fmt.Errorf(&amp;#34;error 1&amp;#34;)}) errs = append(errs, CustomError{UserID: 2, Err: fmt.Errorf(&amp;#34;error 2&amp;#34;)}) errs = append(errs, CustomError{UserID: 3, Err: fmt.</description>
    </item>
    
    <item>
      <title>使用 docker 搭建 filebrowser</title>
      <link>https://blog.twotigers.xyz/posts/filebrowser/</link>
      <pubDate>Sun, 26 May 2024 23:39:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/filebrowser/</guid>
      <description>filebrowser 是一个文件管理器，可以通过 web 界面管理文件，支持文件上传、下载、预览等功能。
详情可以参考github：github
使用 docker 搭建 filebrowser 先初始化配置文件和数据库文件 这个在 https://filebrowser.org/installation 上没有说明, 如果直接启动将直接失败 mkdir -p /root/filebrowser touch /root/filebrowser/filebrowser.db touch /root/filebrowser/config.json echo &amp;#39;{ &amp;#34;port&amp;#34;: 80, &amp;#34;baseURL&amp;#34;: &amp;#34;&amp;#34;, &amp;#34;address&amp;#34;: &amp;#34;&amp;#34;, &amp;#34;log&amp;#34;: &amp;#34;stdout&amp;#34;, &amp;#34;database&amp;#34;: &amp;#34;/database.db&amp;#34;, &amp;#34;root&amp;#34;: &amp;#34;/srv&amp;#34; }&amp;#39; &amp;gt; /root/filebrowser/config.json 使用 docker 启动 filebrowser docker run \ -d --name=filebrowser \ -v /root/filebrowser/file:/srv \ -v /root/filebrowser/filebrowser.db:/database.db \ -v /root/filebrowser/config.json:/.filebrowser.json \ -u $(id -u):$(id -g) \ -p 8081:80 \ --restart=always \ filebrowser/filebrowser 查看日志, 看到如下日志就证明已经启动成功了 2024/05/26 15:38:15 Warning: filebrowser.</description>
    </item>
    
    <item>
      <title>博客迁移到 cloudflare pages</title>
      <link>https://blog.twotigers.xyz/posts/blog/cf/</link>
      <pubDate>Sun, 26 May 2024 01:17:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/blog/cf/</guid>
      <description>最近把博客迁移到了cloudflare pages，感觉还不错，速度也挺快的。 顺便记录下这个过程。
需要先在 github 上创建好仓库, 可以参开这里。 在 cloudflare pages 上创建一个应用, 连接到 github 上的仓库。 选择使用 hugo 静态网站生成器。 </description>
    </item>
    
    <item>
      <title>搭建了一个 chat-next-web 服务, 支持 GPT-4</title>
      <link>https://blog.twotigers.xyz/posts/chat-next-web/</link>
      <pubDate>Sat, 09 Mar 2024 22:00:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/chat-next-web/</guid>
      <description>最近, 我搭建了一个 chat-next-web 服务, 支持 GPT-4, 你可以在这里https://chat.twotigers.xyz体验一下
服务是基于 ChatGPT-Next-Web 当然你可以用你喜欢的客户端来进行链接
进入后进行一次设置 , Endpoint &amp;amp;&amp;amp; Key 可以关注微信公众号 代码日记 发送 gpt 关键词获取
然后就可以开始聊天了</description>
    </item>
    
    <item>
      <title>使用 cf 来实现无限多个私人定制邮箱</title>
      <link>https://blog.twotigers.xyz/posts/cf/mail/</link>
      <pubDate>Sun, 07 Jan 2024 22:00:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/cf/mail/</guid>
      <description>拥有个私人定制的邮箱一直是一件很酷的事情, 除了能彰显个性, 还能让你的邮箱更加安全, 例如我们在注册一些网站的时候, 我们想用一些临时邮箱来代替
临时邮箱固然能带来一些便利, 但是也有一些缺点, 例如: 持久化和安全性, 临时邮箱的安全性是很低的, 你的邮件可能会被别人看到, 有些临时邮箱也不会持久化, 你的邮件可能会在几天后就消失
那么有没有一种方法能够让我们拥有无限多个私人定制的邮箱呢? 答案是肯定的, 本文将会介绍如何使用 cf 来实现无限多个私人定制邮箱
那么我们首先需要一个域名, 例如: example.com, 购买域名的方法很多, 这里就不再赘述了, 本文将会以 example.com 为例
第二步, 使用 cf 来托管你的域名 这里选择免费的账户就可以了 按照提示下一步就可以了, 需要在你注册域名的网站更换 DNS 服务器 注意: 这个过程时间肯能会比较久一些, 请耐心等待
第三步, 在 cf 开启邮箱服务, 需要注意首次使用时会提示你缺少相应的 DNS 记录, 直接点击添加就可以了, 会自动添加相关记录 进入 routing rules 点击 crate address 在上面的配置都完成之后, 会提示让你去校验邮箱地址, 至此, 你的邮箱就已经开通了, 可以使用你刚刚配置的邮箱地址来收取邮件了, 邮件会发送到你刚刚绑定的邮箱里面
等等, 我们标题上说的无限多个私人定制邮箱呢? 难道要手动添加吗? 当然不是 cf 提供了获取所有前缀的功能, 开启后 任意前缀@example.com 都可以收到邮件了, 现在我们来开启下
在这里配置好之后, 我们就可以使用任意前缀@example.</description>
    </item>
    
    <item>
      <title>使用 docker 部署 kafka</title>
      <link>https://blog.twotigers.xyz/posts/kafka/</link>
      <pubDate>Mon, 01 Jan 2024 15:23:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/kafka/</guid>
      <description>在很多时候我们需要使用到消息队列, 其中 kafka 是一个非常优秀的消息队列, 在我们平时开发中也经常会用到, 但是在开发环境中部署 kafka 是一个非常麻烦的事情
在 kafka 官网上, 有一个详细的部署文档, 需要的小伙伴可以参考这个文档 https://kafka.apache.org/quickstart, 但是随之带来的问题是, 我们需要 java 环境, 并且需要安装 zookerper,
那么如果我仅仅是想本地开发环境中使用 kafka, 有没有更简单的方法呢? 为了解决这个问题, 我们可以使用 docker 来部署 kafka
使用 docker 来部署 kafka 的测试节点, 我们需要先安装 docker 和 docker-compose, 这里就不再赘述了, 请自行安装
首先我们在已经安装好 docker 和 docker-compose 的机器上, 创建一个目录, 并且在这个目录下创建一个 docker-compose.yml 文件, 文件内容如下
version: &amp;#34;2&amp;#34; services: kafka: image: docker.io/bitnami/kafka:3.6 ports: - &amp;#39;9094:9094&amp;#39; volumes: - &amp;#34;kafka_data:/bitnami&amp;#34; environment: # KRaft settings - KAFKA_CFG_NODE_ID=0 - KAFKA_CFG_PROCESS_ROLES=controller,broker - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@kafka:9093 # Listeners - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094 - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,EXTERNAL://localhost:9094 - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=PLAINTEXT volumes: kafka_data: driver: local 然后我们就可以在这个目录下执行 docker-compose up -d 来启动 kafka 了, 启动完成后, 我们可以使用 docker ps 命令来查看 kafka 是否启动成功</description>
    </item>
    
    <item>
      <title>使用 cloudflare 实现自己的自定义邮箱</title>
      <link>https://blog.twotigers.xyz/posts/email/</link>
      <pubDate>Thu, 02 Nov 2023 23:30:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/email/</guid>
      <description>拥有自己域名的邮箱在很多程序员心目中是一件非常 cool 的事情, 但是搭建服务非常的麻烦, 需要公网服务器, 以及开放 53 端口
在网上寻找搭建私有邮箱服务教程很多也不详尽, 有些需要用到很重的服务, 这里先挖个坑, 有空了可以开一篇教程来用介绍如何轻量级服务接收邮件
如果我们仅仅是想用自定义域名接收邮件, 其实我们根本不需要拥有一台服务器
拥有一个域名, 这个就不多解释了, 网上很多介绍便宜域名的文章 买好域名后, 去注册 cloudflare 绑定站点, 绑定站点按照 cf 的教程即可, 会提示每一步的操作 绑定完成后, 在站点的页面有一个 email tab, 点击进入后, 会提示你邮箱前缀以及要转发的邮箱地址 输入邮箱地址, 会提示去邮箱验证, 验证完之后继续下一步的配置 等到配置都完成之后, 你就拥有了一个自定义邮箱, 如果你想接收你的域名的任意前缀邮件, 也可以配置
PS 当前 CF dash 页面服务异常了, 等恢复添加一些截图</description>
    </item>
    
    <item>
      <title>caddy</title>
      <link>https://blog.twotigers.xyz/posts/caddy/</link>
      <pubDate>Sun, 12 Feb 2023 22:00:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/caddy/</guid>
      <description>caddy 是什么 golang 编写的反向代理服务器, 并提供了很多的插件, 可以自动注册 https 证书, 对懒人极度友好
版本选择 目前分为 v1, v2 两个版本, 本人迁移发现 v1,v2 版本并不兼容(Caddyfile), 建议使用 V2 版本, 但是目前中文教程里面使用 v1 版本的还是有很多 v1 版本密码认证是明文, v2 是一个 hash 后的密文
下载 &amp;amp;&amp;amp; 安装 下载 在很早的时候 caddy 跟 cloudflare 不太兼容, 会导致无法注册 https 证书, 虽然概率不高, 但是遇见了就比较蛋疼, 建议选择上 caddy-dns/cloudflare 插件 安装 下载完二进制文件后, 建议配置 systemd, 参考 官方文档 以下是我自己使用的配置文件, 大家可以参考下
# caddy.service # # For using Caddy with a config file. # # Make sure the ExecStart and ExecReload commands are correct # for your installation.</description>
    </item>
    
    <item>
      <title>golang 使用反射处理 string, 用于 request body 中的 string 字段进行 trim</title>
      <link>https://blog.twotigers.xyz/posts/go/parser_string/</link>
      <pubDate>Thu, 01 Dec 2022 18:21:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/go/parser_string/</guid>
      <description>将 struct 中的string 字段进行 trim
func trimSpace(any interface{}) { ov := reflect.ValueOf(any) if ov.Kind() == reflect.Ptr &amp;amp;&amp;amp; !ov.IsNil() { ov = ov.Elem() } ot := reflect.TypeOf(any) if ot.Kind() == reflect.Ptr { ot = ot.Elem() } for i := 0; i &amp;lt; ot.NumField(); i++ { field := ov.Field(i) if field.Kind() == reflect.Ptr { if field.Elem().Kind() == reflect.Struct { trimSpace(field.Interface()) continue } field = field.Elem() } if field.CanInterface() &amp;amp;&amp;amp; field.IsValid() { if field.</description>
    </item>
    
    <item>
      <title>chrome 清理当前页面缓存</title>
      <link>https://blog.twotigers.xyz/posts/chrome/</link>
      <pubDate>Fri, 25 Nov 2022 14:23:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/chrome/</guid>
      <description>chrome 清理当前页面的缓存</description>
    </item>
    
    <item>
      <title>vim 中文乱码</title>
      <link>https://blog.twotigers.xyz/posts/linux/vim/</link>
      <pubDate>Fri, 18 Nov 2022 09:10:25 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/linux/vim/</guid>
      <description>原因 字符集配置不匹配 mac 上是 UTF-8 解决方法 更新配置 vim ~/.zshrc 在最后加上 export LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 配置生效 source ~/.zshrc </description>
    </item>
    
    <item>
      <title>ufw 无法禁用 docker端口问题</title>
      <link>https://blog.twotigers.xyz/posts/docker/</link>
      <pubDate>Sun, 06 Nov 2022 13:00:00 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/docker/</guid>
      <description>装好了docker,以及 ufw 然后禁用了 3000 端口, ufw deny 3000
并且启动了一个容器 启动命令是这样的 docker run -it -p 3000:3000 python 但之后发现一个问题, IP:3000 居然可以访问到
查询资料得知, 淦, 发现这是个陈年老问题了 查到一个好用的 repo, https://github.com/chaifeng/ufw-docker 并且按照他的教程进行了设置
但是紧接着发现了问题 这样设置完之后, 开放端口3000(ufw allow 3000), 居然不生效了 看了下 readme 显示 现在外部就已经无法访问 Docker 发布出来的任何端口了，但是容器内部以及私有网络地址上可以正常互相访问，而且容器也可以正常访问外部的网络。 显然这不是我想要的结果
最终在stackoverflow找到了符合我需求的解决方案
cat &amp;lt;&amp;lt; EOF &amp;gt;&amp;gt; /etc/docker/daemon.json { &amp;#34;iptables&amp;#34;: false } EOF echo &amp;#34;DOCKER_OPTS=\&amp;#34;--iptables=false\&amp;#34;&amp;#34; &amp;gt;&amp;gt; /etc/default/docker service docker restart -参考资料: https://stackoverflow.com/a/67641547</description>
    </item>
    
    <item>
      <title>golang 处理图片</title>
      <link>https://blog.twotigers.xyz/posts/go/image/</link>
      <pubDate>Mon, 28 Feb 2022 11:23:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/go/image/</guid>
      <description>支持不同格式之间的转换 支持图片像素的压缩以及拉长 package main import ( &amp;#34;bytes&amp;#34; &amp;#34;errors&amp;#34; &amp;#34;fmt&amp;#34; &amp;#34;image&amp;#34; &amp;#34;image/jpeg&amp;#34; &amp;#34;image/png&amp;#34; &amp;#34;io/ioutil&amp;#34; &amp;#34;os&amp;#34; &amp;#34;golang.org/x/image/draw&amp;#34; _ &amp;#34;golang.org/x/image/webp&amp;#34; // for RegisterFormat ) type imageHelper struct { img image.Image fileName string format string scale draw.Scaler } func NewImageHelper(fileName string, scale ...draw.Scaler) *imageHelper { var s draw.Scaler if scale == nil { s = draw.ApproxBiLinear } else { s = scale[0] } return &amp;amp;imageHelper{ fileName: fileName, scale: s, } } func (r *imageHelper) GuessFileFormat() (string, error) { if r.</description>
    </item>
    
    <item>
      <title>golang 合并多个文件</title>
      <link>https://blog.twotigers.xyz/posts/go/combine/</link>
      <pubDate>Wed, 15 Dec 2021 23:40:25 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/go/combine/</guid>
      <description>package main import ( &amp;#34;fmt&amp;#34; &amp;#34;github.com/coreos/pkg/progressutil&amp;#34; &amp;#34;io&amp;#34; &amp;#34;os&amp;#34; &amp;#34;path/filepath&amp;#34; &amp;#34;time&amp;#34; ) func combineMultipleFiles(outPutFile string, files ...string) { abs, err := filepath.Abs(outPutFile) if err != nil { panic(&amp;#34;can not get outPutFile absolute path&amp;#34;) } dir := filepath.Dir(abs) if _, err := os.Stat(dir); err != nil { if os.IsNotExist(err) { err := os.MkdirAll(dir, os.ModePerm) if err != nil { panic(fmt.Sprintf(&amp;#34;can not mkdirs, path: %s, err: %s&amp;#34;, dir, err)) } } } file, err := os.</description>
    </item>
    
    <item>
      <title>一个简单的快排算法</title>
      <link>https://blog.twotigers.xyz/posts/algorithm/sort/quicksort/</link>
      <pubDate>Thu, 09 Dec 2021 09:12:25 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/algorithm/sort/quicksort/</guid>
      <description> 使用快排的思想 空间复杂度很垃圾, 只是为了更好的理解快排算法 TODO 修改成不需要额外空间的算法 def quicksort(data: list): if len(data) &amp;lt;= 1: return data less = [] more = [] pivot = data[0] for item in data: if item &amp;lt; pivot: less.append(item) elif item &amp;gt; pivot: more.append(item) return quicksort(less) + [pivot] + quicksort(more) data = [1, 4, 2, 6, 3, 10, 12, 11] print(quicksort(data)) </description>
    </item>
    
    <item>
      <title>二叉树生成与遍历</title>
      <link>https://blog.twotigers.xyz/posts/algorithm/tree/</link>
      <pubDate>Wed, 08 Dec 2021 14:12:25 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/algorithm/tree/</guid>
      <description>class Node: def __init__(self, val, left=None, right=None) -&amp;gt; None: self.val = val self.left = left self.right = right def create_tree(data: list): if not data: return None first_node = Node(data[0]) tmp_list = [first_node] tmp_count = 0 for item in data[1:]: node = tmp_list[0] new_node = Node(item) if item is not None else None if tmp_count == 0: node.left = new_node # add to tmp_list if item is not None: tmp_list.append(new_node) tmp_count += 1 continue if tmp_count == 1: node.</description>
    </item>
    
    <item>
      <title>Python 枚举使用</title>
      <link>https://blog.twotigers.xyz/posts/python/enum/</link>
      <pubDate>Wed, 24 Nov 2021 14:10:25 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/python/enum/</guid>
      <description>from enum import Enum class Gender(Enum): male = 0 female = 1 print(Gender(0)) # Gender.male print(Gender(0).name) # female print(Gender(0).value) # 0 class EnumWithLabel(Enum): &amp;#34;&amp;#34;&amp;#34; 允许带 label 的枚举类型, 参考: https://docs.python.org/zh-cn/3/library/enum.html#when-to-use-new-vs-init &amp;#34;&amp;#34;&amp;#34; def __new__(cls, value, label): obj = object.__new__(cls) obj._value_ = value obj.label = label return obj class GenderWithLabel(EnumWithLabel): male = (0, &amp;#34;MAN&amp;#34;) female = (1, &amp;#34;WOMAN&amp;#34;) print(GenderWithLabel(0)) # GenderWithLabel.male print(GenderWithLabel(0).value) # 0 print(GenderWithLabel(0).label) # MAN class EnumWithDefault(Enum): &amp;#34;&amp;#34;&amp;#34; 允许返回默认值的枚举类型, 不存在枚举值时不会报错 如果需要修改默认值则需要覆盖 new_default_obj 函数 &amp;#34;&amp;#34;&amp;#34; @classmethod def _missing_(cls, value): new_member = cls.</description>
    </item>
    
    <item>
      <title>golang validator 支持翻译以及使用 json tag</title>
      <link>https://blog.twotigers.xyz/posts/go/validator/</link>
      <pubDate>Fri, 19 Nov 2021 23:40:25 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/go/validator/</guid>
      <description>package main import ( &amp;#34;encoding/json&amp;#34; &amp;#34;fmt&amp;#34; &amp;#34;github.com/go-playground/locales/en&amp;#34; &amp;#34;github.com/go-playground/locales/zh&amp;#34; &amp;#34;github.com/go-playground/locales/zh_Hant_TW&amp;#34; ut &amp;#34;github.com/go-playground/universal-translator&amp;#34; &amp;#34;github.com/go-playground/validator/v10&amp;#34; entranslations &amp;#34;github.com/go-playground/validator/v10/translations/en&amp;#34; zhtranslations &amp;#34;github.com/go-playground/validator/v10/translations/zh&amp;#34; zhtwtranslations &amp;#34;github.com/go-playground/validator/v10/translations/zh_tw&amp;#34; &amp;#34;reflect&amp;#34; &amp;#34;strings&amp;#34; ) type InternalAccountTransferRequest struct { Name string `json:&amp;#34;name&amp;#34; binding:&amp;#34;required&amp;#34;` Age string `json:&amp;#34;age_test&amp;#34; binding:&amp;#34;required&amp;#34;` Class string `binding:&amp;#34;required&amp;#34;` } func main() { s := `{&amp;#34;name&amp;#34;: &amp;#34;123&amp;#34;}` var requset InternalAccountTransferRequest json.Unmarshal([]byte(s), &amp;amp;requset) //requset.Class = &amp;#34;123&amp;#34; v := validator.New() var err error uni := ut.New(en.New(), en.New(), zh_Hant_TW.New(), zh.New()) zh_, _ := uni.GetTranslator(&amp;#34;zh&amp;#34;) zhtranslations.RegisterDefaultTranslations(v, zh_) tw_, _ := uni.</description>
    </item>
    
    <item>
      <title>Mac 安装 mysqlclient</title>
      <link>https://blog.twotigers.xyz/posts/python/mysql/</link>
      <pubDate>Wed, 17 Nov 2021 11:17:25 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/python/mysql/</guid>
      <description>Python 版本 (venv) ➜ python --version Python 3.8.2 安装 brew install mysql export LDFLAGS=&amp;#34;-L/usr/local/opt/openssl/lib&amp;#34; export CPPFLAGS=&amp;#34;-I/usr/local/opt/openssl/include&amp;#34; pip install mysqlclient brew uninstall mysql </description>
    </item>
    
    <item>
      <title>常用 git 命令</title>
      <link>https://blog.twotigers.xyz/posts/git/command/</link>
      <pubDate>Tue, 19 Oct 2021 10:40:25 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/git/command/</guid>
      <description>git log -p | grep &amp;#39;xxx&amp;#39; -B 300 // 查询修改历史 git push origin branch_name // 推送 git commit -m &amp;#34;commit_message&amp;#34; // 提交 commit git add -A . // add 文件 </description>
    </item>
    
    <item>
      <title>Python 中的else</title>
      <link>https://blog.twotigers.xyz/posts/python/else/</link>
      <pubDate>Fri, 15 Oct 2021 14:00:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/python/else/</guid>
      <description>if&amp;hellip;else 最常见的 else if 1 &amp;gt; 0: pass else: pass for else for 循环中 只有 for 循环结束了才执行, 注意空循环也会执行 for i in range(3): print(i) else: print(&amp;#34;end&amp;#34;) # 0 # 1 # 2 # end for i in range(3): print(i) if i == 1: break else: print(&amp;#34;end&amp;#34;) # 0 # 1 try&amp;hellip; else 这个就很好理解了, else 只会在 try 未发生任何异常的时候执行 finally 在所有状态下都会执行 try: 1 except Exception as e: print(e) else: print(&amp;#34;else&amp;#34;) finally: print(&amp;#34;finally&amp;#34;) # else # finally try: 1/0 except Exception as e: print(e) else: print(&amp;#34;else&amp;#34;) finally: print(&amp;#34;finally&amp;#34;) # division by zero # finally </description>
    </item>
    
    <item>
      <title>二叉树</title>
      <link>https://blog.twotigers.xyz/posts/structure/tree/</link>
      <pubDate>Thu, 14 Oct 2021 20:00:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/structure/tree/</guid>
      <description>二叉树 本文摘自 代码随想录
定义 满二叉树 除最后一层无任何子节点外，每一层上的所有结点都有两个子结点的二叉树。
国内教程定义：一个二叉树，如果每一个层的结点数都达到最大值，则这个二叉树就是满二叉树。也就是说，如果一个二叉树的层数为K，且结点总数是(2^k) -1 ，则它就是满二叉树。
满二叉树的结点要么是叶子结点，度为0，要么是度为2的结点，不存在度为1的结点。
但是, 下图根据国内定义就不是一个满二叉树 满二叉树 百度百科
完全二叉树 一棵深度为k的有n个结点的二叉树，对树中的结点按从上至下、从左到右的顺序进行编号，如果编号为i（1≤i≤n）的结点与满二叉树中编号为i的结点在二叉树中的位置相同，则这棵二叉树称为完全二叉树
完全二叉树
二叉搜索树 一个有序树 若它的左子树不空，则左子树上所有结点的值均小于它的根结点的值；
若它的右子树不空，则右子树上所有结点的值均大于它的根结点的值；
它的左、右子树也分别为二叉排序树
平衡二叉搜索树 AVL (Adelson-Velsky and Landis) 空树 左右两个子树高度绝对值不超过1 (&amp;lt;=1), 并且两个子树都是 AVL 存储方式 顺序, 数组 链式, 指针方式 链式: 顺序: 遍历 如果父节点的数组下表是i，那么它的左孩子就是i * 2 + 1，右孩子就是 i * 2 + 2。 注意: 常用的还是链式</description>
    </item>
    
    <item>
      <title>数据库跟 null 比较会得到非预期结果</title>
      <link>https://blog.twotigers.xyz/posts/database/operator/</link>
      <pubDate>Wed, 13 Oct 2021 16:00:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/database/operator/</guid>
      <description>现象 在执行 sql 中得到了非预期结果
select * from user where age &amp;gt; 20; 其中, 数据库中存在大量的 age 为 null 的记录, 在执行完上述 sql 后, 没有将 age is null 的数据查出
测试 SELECT 1&amp;gt;=1, NULL = NULL, Null &amp;gt; 1; mysql 5.7.21-log sqlite 3.34.0 postgresql PostgreSQL 14.0 (Debian 14.0-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit 原因 stackoverflow
在 The Three-Valued Logic of SQL 中规定, 与 null 比较返回值为 null
The SQL null value basically means “could be anything”.</description>
    </item>
    
    <item>
      <title>Vaultwarden 搭建</title>
      <link>https://blog.twotigers.xyz/posts/blog/vaultwarden/</link>
      <pubDate>Mon, 11 Oct 2021 14:00:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/blog/vaultwarden/</guid>
      <description>准备 docker nginx/caddy 启动 docker run -d --name vaultwarden -v `pwd`:/data -p 8088:80 vaultwarden/server:latest 此时打开 http://yourhost:8088, 就可以看到页面
配置 在工作目录(即上述执行的目录, 如果 -v 指定了目录, 则为其指定目录)中, 增加一个 config.json 其中 signups_allowed 为是否允许注册, 注册完第一个账号之后建议关闭 admin_token 为 /admin 管理页面的 token, 建议配置完之后将其置空(即不开启 /admin) { &amp;#34;domain&amp;#34;: &amp;#34;http://localhost&amp;#34;, &amp;#34;sends_allowed&amp;#34;: false, &amp;#34;disable_icon_download&amp;#34;: false, &amp;#34;signups_allowed&amp;#34;: true, &amp;#34;signups_verify&amp;#34;: false, &amp;#34;signups_verify_resend_time&amp;#34;: 3600, &amp;#34;signups_verify_resend_limit&amp;#34;: 6, &amp;#34;invitations_allowed&amp;#34;: false, &amp;#34;password_iterations&amp;#34;: 100000, &amp;#34;show_password_hint&amp;#34;: false, &amp;#34;admin_token&amp;#34;: &amp;#34;token&amp;#34;, &amp;#34;invitation_org_name&amp;#34;: &amp;#34;Vaultwarden&amp;#34;, &amp;#34;ip_header&amp;#34;: &amp;#34;X-Real-IP&amp;#34;, &amp;#34;icon_cache_ttl&amp;#34;: 2592000, &amp;#34;icon_cache_negttl&amp;#34;: 259200, &amp;#34;icon_download_timeout&amp;#34;: 10, &amp;#34;icon_blacklist_non_global_ips&amp;#34;: true, &amp;#34;disable_2fa_remember&amp;#34;: false, &amp;#34;authenticator_disable_time_drift&amp;#34;: false, &amp;#34;require_device_email&amp;#34;: false, &amp;#34;reload_templates&amp;#34;: false, &amp;#34;log_timestamp_format&amp;#34;: &amp;#34;%Y-%m-%d %H:%M:%S.</description>
    </item>
    
    <item>
      <title>博客搭建</title>
      <link>https://blog.twotigers.xyz/posts/blog/set_up/</link>
      <pubDate>Sat, 09 Oct 2021 22:00:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/blog/set_up/</guid>
      <description>准备 hugo github nginx/caddy hugo 注意: 安装 extended 版本, 否则使用第三方主题时会有问题
hugo new site quickstart cd quickstart git clone https://github.com/flysnow-org/maupassant-hugo themes/maupassant echo theme = \&amp;#34;maupassant\&amp;#34; &amp;gt;&amp;gt; config.toml hugo new posts/my-first-post.md // 测试 hugo server -D // 生成静态文件 hugo -D github 创建 repo
设置 webhook
将上面的 quickstart 文件夹整体进行版本管理(其实只管理 content 也行, 要是为了方便换主题, 可以直接搞整个)
在 server 上启动 webhook server eg.
package main import ( &amp;#34;crypto/hmac&amp;#34; &amp;#34;crypto/sha1&amp;#34; &amp;#34;encoding/hex&amp;#34; &amp;#34;fmt&amp;#34; &amp;#34;io/ioutil&amp;#34; &amp;#34;net/http&amp;#34; &amp;#34;os/exec&amp;#34; ) var ( key []byte ) func sha1Data(data []byte) string { h := hmac.</description>
    </item>
    
    <item>
      <title>python 异常处理</title>
      <link>https://blog.twotigers.xyz/posts/python/exception/</link>
      <pubDate>Sat, 09 Oct 2021 20:00:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/python/exception/</guid>
      <description>异常处理 def demo(): try: 1 / 0 except Exception as e: print(type(e)) #&amp;lt;class &amp;#39;ZeroDivisionError&amp;#39;&amp;gt; 自定义异常 class CustomError(Exception): pass 异常链 class CustomError(Exception): pass def demo(): try: 1 / 0 except Exception as e: print(type(e)) #&amp;lt;class &amp;#39;ZeroDivisionError&amp;#39;&amp;gt; raise CustomError() from e &amp;lt;class &amp;#39;ZeroDivisionError&amp;#39;&amp;gt; Traceback (most recent call last): File &amp;#34;/Users/tiger/work/customer/dd.py&amp;#34;, line 23, in demo 1 / 0 ZeroDivisionError: division by zero The above exception was the direct cause of the following exception: Traceback (most recent call last): File &amp;#34;/Users/tiger/work/customer/dd.</description>
    </item>
    
    <item>
      <title>常用 Linux 命令</title>
      <link>https://blog.twotigers.xyz/posts/linux/command/</link>
      <pubDate>Sat, 09 Oct 2021 17:10:25 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/linux/command/</guid>
      <description>文件夹大小 du -h --max-depth=1 </description>
    </item>
    
    <item>
      <title>Hello World</title>
      <link>https://blog.twotigers.xyz/posts/blog/hellowrold/</link>
      <pubDate>Fri, 01 Oct 2021 18:23:08 +0800</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/blog/hellowrold/</guid>
      <description>the first blog</description>
    </item>
    
    <item>
      <title>alpine docker container 中安装 mysql client</title>
      <link>https://blog.twotigers.xyz/posts/database/docker/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://blog.twotigers.xyz/posts/database/docker/</guid>
      <description>apk add mysql mysql-client </description>
    </item>
    
  </channel>
</rss>
