<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yupanzi</title>
        <link>https://yupanzi.com/</link>
        <description>个人博客 / 技术笔记</description>
        <lastBuildDate>Mon, 22 Dec 2025 21:18:37 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>agenote (Astro 6 + feed)</generator>
        <language>zh-Hans</language>
        <image>
            <title>yupanzi</title>
            <url>https://yupanzi.com/og/welcome.png</url>
            <link>https://yupanzi.com/</link>
        </image>
        <copyright>© 2026 yupanzi</copyright>
        <atom:link href="https://yupanzi.com/rss.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Codex 配置指南]]></title>
            <link>https://yupanzi.com/posts/codex-config/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/codex-config/</guid>
            <pubDate>Mon, 22 Dec 2025 21:18:37 GMT</pubDate>
            <description><![CDATA[Codex（OpenAI CLI）工具的配置教程，覆盖 Windows、macOS、Linux 三平台]]></description>
            <content:encoded><![CDATA[如果你使用支持 OpenAI API 的工具（如 Codex），需要设置以下配置来连接到中转服务。

---

## Codex 配置文件

在 `~/.codex/config.toml` 文件开头添加以下配置：

```toml
model_provider = "myai"
model = "gpt-5-codex"
model_reasoning_effort = "high"
disable_response_storage = true
preferred_auth_method = "apikey"

[model_providers.myai]
name = "myai"
base_url = "你的服务地址/openai"
wire_api = "responses"
requires_openai_auth = true
env_key = "MY_API_KEY"
```

在 `~/.codex/auth.json` 文件中配置API密钥：

```json
{
  "OPENAI_API_KEY": null
}
```

💡 将 OPENAI_API_KEY 设置为 null，然后设置环境变量 MY_API_KEY 为你的 API 密钥（格式如 sk-xxxxxxxxxx）。

---

## Windows 教程

### 环境变量设置方法

**CMD 临时设置：**

```cmd
set MY_API_KEY=sk-xxxxxxxxxx
```

**PowerShell 临时设置：**

```powershell
$env:MY_API_KEY = "sk-xxxxxxxxxx"
```

**PowerShell 永久设置（用户级）：**

```powershell
[System.Environment]::SetEnvironmentVariable("MY_API_KEY", "sk-xxxxxxxxxx", [System.EnvironmentVariableTarget]::User)
```

💡 设置后需要重新打开 PowerShell 窗口才能生效。

### 验证环境变量

在 PowerShell 中验证：

```powershell
echo $env:MY_API_KEY
```

在 CMD 中验证：

```cmd
echo %MY_API_KEY%
```

---

## macOS 教程

### 环境变量设置方法

**临时设置：**

```bash
export MY_API_KEY=sk-xxxxxxxxxx
```

**Shell 配置文件（持久保存）：**

添加到你的 shell 配置文件中：

```bash
# 对于 zsh (默认)
echo "export MY_API_KEY=sk-xxxxxxxxxx" >> ~/.zshrc
source ~/.zshrc

# 对于 bash
echo "export MY_API_KEY=sk-xxxxxxxxxx" >> ~/.bash_profile
source ~/.bash_profile
```

### 验证环境变量

在 Terminal 中验证：

```bash
echo $MY_API_KEY
```

---

## Linux / WSL2 教程

### 环境变量设置方法

**临时设置：**

```bash
export MY_API_KEY=sk-xxxxxxxxxx
```

**Shell 配置文件（持久保存）：**

添加到你的 shell 配置文件中：

```bash
# 对于 bash (默认)
echo "export MY_API_KEY=sk-xxxxxxxxxx" >> ~/.bashrc
source ~/.bashrc

# 对于 zsh
echo "export MY_API_KEY=sk-xxxxxxxxxx" >> ~/.zshrc
source ~/.zshrc
```

### 验证环境变量

在终端中验证：

```bash
echo $MY_API_KEY
```]]></content:encoded>
            <author>yupanzi</author>
            <category>AI</category>
            <category>CLI</category>
            <category>OpenAI</category>
        </item>
        <item>
            <title><![CDATA[Claude Code 配置指南]]></title>
            <link>https://yupanzi.com/posts/claude-code-config/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/claude-code-config/</guid>
            <pubDate>Mon, 22 Dec 2025 21:18:36 GMT</pubDate>
            <description><![CDATA[Claude Code 命令行工具的安装与配置教程，覆盖 Windows、macOS、Linux 三平台]]></description>
            <content:encoded><![CDATA[跟着这个教程，你可以轻松在自己的电脑上安装并使用 Claude Code。

---

## Windows 教程

### 1. 安装 Node.js 环境

Claude Code 需要 Node.js 环境才能运行。

#### Windows 安装方法

**方法一：官网下载（推荐）**

1. 打开浏览器访问 `https://nodejs.org/`
2. 点击 "LTS" 版本进行下载（推荐长期支持版本）
3. 下载完成后双击 `.msi` 文件
4. 按照安装向导完成安装，保持默认设置即可

**方法二：使用包管理器**

如果你安装了 Chocolatey 或 Scoop，可以使用命令行安装：

```bash
# 使用 Chocolatey
choco install nodejs

# 或使用 Scoop
scoop install nodejs
```

> **Windows 注意事项**
> - 建议使用 PowerShell 而不是 CMD
> - 如果遇到权限问题，尝试以管理员身份运行
> - 某些杀毒软件可能会误报，需要添加白名单

#### 验证安装是否成功

安装完成后，打开 PowerShell 或 CMD，输入以下命令：

```bash
node --version
npm --version
```

如果显示版本号，说明安装成功了！

---

### 2. 安装 Claude Code

打开 PowerShell 或 CMD，运行以下命令：

```bash
# 全局安装 Claude Code
npm install -g @anthropic-ai/claude-code
```

这个命令会从 npm 官方仓库下载并安装最新版本的 Claude Code。

> **提示**
> - 建议使用 PowerShell 而不是 CMD，功能更强大
> - 如果遇到权限问题，以管理员身份运行 PowerShell

#### 验证 Claude Code 安装

安装完成后，输入以下命令检查是否安装成功：

```bash
claude --version
```

如果显示版本号，恭喜你！Claude Code 已经成功安装了。

---

### 3. 设置环境变量

为了让 Claude Code 连接到你的中转服务，需要设置两个环境变量：

#### 方法一：PowerShell 临时设置（当前会话）

在 PowerShell 中运行以下命令：

```powershell
$env:ANTHROPIC_BASE_URL = "你的服务地址/api"
$env:ANTHROPIC_AUTH_TOKEN = "你的API密钥"
```

💡 记得将 "你的API密钥" 替换为在上方 "API Keys" 标签页中创建的实际密钥。

#### 方法二：PowerShell 永久设置（用户级）

在 PowerShell 中运行以下命令设置用户级环境变量：

```powershell
# 设置用户级环境变量（永久生效）
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", "你的服务地址/api", [System.EnvironmentVariableTarget]::User)
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_AUTH_TOKEN", "你的API密钥", [System.EnvironmentVariableTarget]::User)
```

查看已设置的环境变量：

```powershell
# 查看用户级环境变量
[System.Environment]::GetEnvironmentVariable("ANTHROPIC_BASE_URL", [System.EnvironmentVariableTarget]::User)
[System.Environment]::GetEnvironmentVariable("ANTHROPIC_AUTH_TOKEN", [System.EnvironmentVariableTarget]::User)
```

💡 设置后需要重新打开 PowerShell 窗口才能生效。

#### VSCode Claude 插件配置

如果使用 VSCode 的 Claude 插件，需要在配置文件中进行设置：

**配置文件位置：** `C:\Users\你的用户名\.claude\config.json`

💡 如果该文件不存在，请手动创建。

```json
{
  "primaryApiKey": "myai"
}
```

#### 验证环境变量设置

设置完环境变量后，可以通过以下命令验证是否设置成功：

**在 PowerShell 中验证：**

```powershell
echo $env:ANTHROPIC_BASE_URL
echo $env:ANTHROPIC_AUTH_TOKEN
```

**在 CMD 中验证：**

```cmd
echo %ANTHROPIC_BASE_URL%
echo %ANTHROPIC_AUTH_TOKEN%
```

**预期输出示例：**

```
你的服务地址/api
sk-xxxxxxxxxxxxxxxxxx
```

💡 如果输出为空或显示变量名本身，说明环境变量设置失败，请重新设置。

---

### 4. 开始使用 Claude Code

现在你可以开始使用 Claude Code 了！

**启动 Claude Code**

```bash
claude
```

**在特定项目中使用**

```bash
# 进入你的项目目录
cd C:\path\to\your\project

# 启动 Claude Code
claude
```

---

### Windows 常见问题解决

<details>
<summary>安装时提示 "permission denied" 错误</summary>

这通常是权限问题，尝试以下解决方法：
- 以管理员身份运行 PowerShell
- 或者配置 npm 使用用户目录：`npm config set prefix %APPDATA%\npm`

</details>

<details>
<summary>PowerShell 执行策略错误</summary>

如果遇到执行策略限制，运行：

```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```

</details>

<details>
<summary>环境变量设置后不生效</summary>

设置永久环境变量后需要：
- 重新启动 PowerShell 或 CMD
- 或者注销并重新登录 Windows
- 验证设置：`echo $env:ANTHROPIC_BASE_URL`

</details>

---

## macOS 教程

### 1. 安装 Node.js 环境

Claude Code 需要 Node.js 环境才能运行。

#### macOS 安装方法

**方法一：使用 Homebrew（推荐）**

如果你已经安装了 Homebrew，使用它安装 Node.js 会更方便：

```bash
# 更新 Homebrew
brew update

# 安装 Node.js
brew install node
```

**方法二：官网下载**

1. 访问 `https://nodejs.org/`
2. 下载适合 macOS 的 LTS 版本
3. 打开下载的 `.pkg` 文件
4. 按照安装程序指引完成安装

> **macOS 注意事项**
> - 如果遇到权限问题，可能需要使用 `sudo`
> - 首次运行可能需要在系统偏好设置中允许
> - 建议使用 Terminal 或 iTerm2

#### 验证安装是否成功

安装完成后，打开 Terminal，输入以下命令：

```bash
node --version
npm --version
```

如果显示版本号，说明安装成功了！

---

### 2. 安装 Claude Code

打开 Terminal，运行以下命令：

```bash
# 全局安装 Claude Code
npm install -g @anthropic-ai/claude-code
```

如果遇到权限问题，可以使用 sudo：

```bash
sudo npm install -g @anthropic-ai/claude-code
```

#### 验证 Claude Code 安装

安装完成后，输入以下命令检查是否安装成功：

```bash
claude --version
```

如果显示版本号，恭喜你！Claude Code 已经成功安装了。

---

### 3. 设置环境变量

为了让 Claude Code 连接到你的中转服务，需要设置两个环境变量：

#### 方法一：临时设置（当前会话）

在 Terminal 中运行以下命令：

```bash
export ANTHROPIC_BASE_URL="你的服务地址/api"
export ANTHROPIC_AUTH_TOKEN="你的API密钥"
```

💡 记得将 "你的API密钥" 替换为在上方 "API Keys" 标签页中创建的实际密钥。

#### 方法二：永久设置

编辑你的 shell 配置文件（根据你使用的 shell）：

```bash
# 对于 zsh (默认)
echo 'export ANTHROPIC_BASE_URL="你的服务地址/api"' >> ~/.zshrc
echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.zshrc
source ~/.zshrc
```

```bash
# 对于 bash
echo 'export ANTHROPIC_BASE_URL="你的服务地址/api"' >> ~/.bash_profile
echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.bash_profile
source ~/.bash_profile
```

#### VSCode Claude 插件配置

如果使用 VSCode 的 Claude 插件，需要在配置文件中进行设置：

**配置文件位置：** `~/.claude/config.json`

💡 如果该文件不存在，请手动创建。

```json
{
  "primaryApiKey": "myai"
}
```

---

### 4. 开始使用 Claude Code

现在你可以开始使用 Claude Code 了！

**启动 Claude Code**

```bash
claude
```

**在特定项目中使用**

```bash
# 进入你的项目目录
cd /path/to/your/project

# 启动 Claude Code
claude
```

---

### macOS 常见问题解决

<details>
<summary>安装时提示权限错误</summary>

尝试以下解决方法：
- 使用 sudo 安装：`sudo npm install -g @anthropic-ai/claude-code`
- 或者配置 npm 使用用户目录：`npm config set prefix ~/.npm-global`

</details>

<details>
<summary>macOS 安全设置阻止运行</summary>

如果系统阻止运行 Claude Code：
- 打开"系统偏好设置" → "安全性与隐私"
- 点击"仍要打开"或"允许"
- 或者在 Terminal 中运行：`sudo spctl --master-disable`

</details>

<details>
<summary>环境变量不生效</summary>

检查以下几点：
- 确认修改了正确的配置文件（.zshrc 或 .bash_profile）
- 重新启动 Terminal
- 验证设置：`echo $ANTHROPIC_BASE_URL`

</details>

---

## Linux / WSL2 教程

### 1. 安装 Node.js 环境

Claude Code 需要 Node.js 环境才能运行。

#### Linux 安装方法

**方法一：使用官方仓库（推荐）**

```bash
# 添加 NodeSource 仓库
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -

# 安装 Node.js
sudo apt-get install -y nodejs
```

**方法二：使用系统包管理器**

虽然版本可能不是最新的，但对于基本使用已经足够：

```bash
# Ubuntu/Debian
sudo apt update
sudo apt install nodejs npm

# CentOS/RHEL/Fedora
sudo dnf install nodejs npm
```

> **Linux 注意事项**
> - 某些发行版可能需要安装额外的依赖
> - 如果遇到权限问题，使用 `sudo`
> - 确保你的用户在 npm 的全局目录有写权限

#### 验证安装是否成功

安装完成后，打开终端，输入以下命令：

```bash
node --version
npm --version
```

如果显示版本号，说明安装成功了！

---

### 2. 安装 Claude Code

打开终端，运行以下命令：

```bash
# 全局安装 Claude Code
npm install -g @anthropic-ai/claude-code
```

如果遇到权限问题，可以使用 sudo：

```bash
sudo npm install -g @anthropic-ai/claude-code
```

#### 验证 Claude Code 安装

安装完成后，输入以下命令检查是否安装成功：

```bash
claude --version
```

如果显示版本号，恭喜你！Claude Code 已经成功安装了。

---

### 3. 设置环境变量

为了让 Claude Code 连接到你的中转服务，需要设置两个环境变量：

#### 方法一：临时设置（当前会话）

在终端中运行以下命令：

```bash
export ANTHROPIC_BASE_URL="你的服务地址/api"
export ANTHROPIC_AUTH_TOKEN="你的API密钥"
```

💡 记得将 "你的API密钥" 替换为在上方 "API Keys" 标签页中创建的实际密钥。

#### 方法二：永久设置

编辑你的 shell 配置文件：

```bash
# 对于 bash (默认)
echo 'export ANTHROPIC_BASE_URL="你的服务地址/api"' >> ~/.bashrc
echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.bashrc
source ~/.bashrc
```

```bash
# 对于 zsh
echo 'export ANTHROPIC_BASE_URL="你的服务地址/api"' >> ~/.zshrc
echo 'export ANTHROPIC_AUTH_TOKEN="你的API密钥"' >> ~/.zshrc
source ~/.zshrc
```

#### VSCode Claude 插件配置

如果使用 VSCode 的 Claude 插件，需要在配置文件中进行设置：

**配置文件位置：** `~/.claude/config.json`

💡 如果该文件不存在，请手动创建。

```json
{
  "primaryApiKey": "myai"
}
```

---

### 4. 开始使用 Claude Code

现在你可以开始使用 Claude Code 了！

**启动 Claude Code**

```bash
claude
```

**在特定项目中使用**

```bash
# 进入你的项目目录
cd /path/to/your/project

# 启动 Claude Code
claude
```

---

### Linux 常见问题解决

<details>
<summary>安装时提示权限错误</summary>

尝试以下解决方法：
- 使用 sudo 安装：`sudo npm install -g @anthropic-ai/claude-code`
- 或者配置 npm 使用用户目录：`npm config set prefix ~/.npm-global`
- 然后添加到 PATH：`export PATH=~/.npm-global/bin:$PATH`

</details>

<details>
<summary>缺少依赖库</summary>

某些 Linux 发行版需要安装额外依赖：

```bash
# Ubuntu/Debian
sudo apt install build-essential

# CentOS/RHEL
sudo dnf groupinstall "Development Tools"
```

</details>

<details>
<summary>环境变量不生效</summary>

检查以下几点：
- 确认修改了正确的配置文件（.bashrc 或 .zshrc）
- 重新启动终端或运行 `source ~/.bashrc`
- 验证设置：`echo $ANTHROPIC_BASE_URL`

</details>]]></content:encoded>
            <author>yupanzi</author>
            <category>AI</category>
            <category>CLI</category>
            <category>Claude</category>
        </item>
        <item>
            <title><![CDATA[Gemini CLI 配置指南]]></title>
            <link>https://yupanzi.com/posts/gemini-cli-config/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/gemini-cli-config/</guid>
            <pubDate>Mon, 22 Dec 2025 21:18:36 GMT</pubDate>
            <description><![CDATA[Gemini CLI 命令行工具的环境变量配置教程，覆盖 Windows、macOS、Linux 三平台]]></description>
            <content:encoded><![CDATA[如果你使用 Gemini CLI，需要设置以下环境变量来连接到中转服务。

---

## Windows 教程

### PowerShell 临时设置（当前会话）

在 PowerShell 中运行以下命令：

```powershell
$env:GOOGLE_GEMINI_BASE_URL = "你的服务地址/gemini"
$env:GEMINI_API_KEY = "你的API密钥"
$env:GEMINI_MODEL = "gemini-2.5-pro"
```

💡 使用与 Claude Code 相同的 API 密钥即可。

### PowerShell 永久设置（用户级）

在 PowerShell 中运行以下命令：

```powershell
# 设置用户级环境变量（永久生效）
[System.Environment]::SetEnvironmentVariable("GOOGLE_GEMINI_BASE_URL", "你的服务地址/gemini", [System.EnvironmentVariableTarget]::User)
[System.Environment]::SetEnvironmentVariable("GEMINI_API_KEY", "你的API密钥", [System.EnvironmentVariableTarget]::User)
[System.Environment]::SetEnvironmentVariable("GEMINI_MODEL", "gemini-2.5-pro", [System.EnvironmentVariableTarget]::User)
```

💡 设置后需要重新打开 PowerShell 窗口才能生效。

### 验证 Gemini CLI 环境变量

在 PowerShell 中验证：

```powershell
echo $env:GOOGLE_GEMINI_BASE_URL
echo $env:GEMINI_API_KEY
echo $env:GEMINI_MODEL
```

---

## macOS 教程

### 临时设置（当前会话）

在 Terminal 中运行以下命令：

```bash
export GOOGLE_GEMINI_BASE_URL="你的服务地址/gemini"
export GEMINI_API_KEY="你的API密钥"
export GEMINI_MODEL="gemini-2.5-pro"
```

💡 使用与 Claude Code 相同的 API 密钥即可。

### 永久设置

添加到你的 shell 配置文件：

```bash
# 对于 zsh (默认)
echo 'export GOOGLE_GEMINI_BASE_URL="你的服务地址/gemini"' >> ~/.zshrc
echo 'export GEMINI_API_KEY="你的API密钥"' >> ~/.zshrc
echo 'export GEMINI_MODEL="gemini-2.5-pro"' >> ~/.zshrc
source ~/.zshrc
```

```bash
# 对于 bash
echo 'export GOOGLE_GEMINI_BASE_URL="你的服务地址/gemini"' >> ~/.bash_profile
echo 'export GEMINI_API_KEY="你的API密钥"' >> ~/.bash_profile
echo 'export GEMINI_MODEL="gemini-2.5-pro"' >> ~/.bash_profile
source ~/.bash_profile
```

### 验证 Gemini CLI 环境变量

在 Terminal 中验证：

```bash
echo $GOOGLE_GEMINI_BASE_URL
echo $GEMINI_API_KEY
echo $GEMINI_MODEL
```

---

## Linux / WSL2 教程

### 临时设置（当前会话）

在终端中运行以下命令：

```bash
export GOOGLE_GEMINI_BASE_URL="你的服务地址/gemini"
export GEMINI_API_KEY="你的API密钥"
export GEMINI_MODEL="gemini-2.5-pro"
```

💡 使用与 Claude Code 相同的 API 密钥即可。

### 永久设置

添加到你的 shell 配置文件：

```bash
# 对于 bash (默认)
echo 'export GOOGLE_GEMINI_BASE_URL="你的服务地址/gemini"' >> ~/.bashrc
echo 'export GEMINI_API_KEY="你的API密钥"' >> ~/.bashrc
echo 'export GEMINI_MODEL="gemini-2.5-pro"' >> ~/.bashrc
source ~/.bashrc
```

```bash
# 对于 zsh
echo 'export GOOGLE_GEMINI_BASE_URL="你的服务地址/gemini"' >> ~/.zshrc
echo 'export GEMINI_API_KEY="你的API密钥"' >> ~/.zshrc
echo 'export GEMINI_MODEL="gemini-2.5-pro"' >> ~/.zshrc
source ~/.zshrc
```

### 验证 Gemini CLI 环境变量

在终端中验证：

```bash
echo $GOOGLE_GEMINI_BASE_URL
echo $GEMINI_API_KEY
echo $GEMINI_MODEL
```]]></content:encoded>
            <author>yupanzi</author>
            <category>AI</category>
            <category>CLI</category>
            <category>Gemini</category>
        </item>
        <item>
            <title><![CDATA[为 OpenSpec 添加多语言支持]]></title>
            <link>https://yupanzi.com/posts/openspec-translation-extension/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/openspec-translation-extension/</guid>
            <pubDate>Sat, 25 Oct 2025 14:11:38 GMT</pubDate>
            <description><![CDATA[通过 Claude Code 自定义命令实现 OpenSpec 文档多语言翻译]]></description>
            <content:encoded><![CDATA[OpenSpec 是个很棒的规范驱动开发框架，但只支持英文文档。对非英语团队来说，这挺麻烦的。好在 Claude Code 支持自定义命令，我们可以自己动手补上这个功能。

## 快速上手

一行命令搞定翻译：

```bash
# 翻译成中文
/openspec:translate chinese

# 也可以用中文输入
/openspec:translate 中文

# 支持多种语言
/openspec:translate japanese   # 日文
/openspec:translate español    # 西班牙语
```

## 核心功能

### 支持的语言

| 语言 | 英文名称 | 本地化名称 |
|------|---------|-----------|
| 中文 | chinese | 中文 |
| 日文 | japanese | 日本語 |
| 西班牙语 | spanish | español |
| 韩语 | korean | 한국어 |
| 法语 | french | français |

### 智能保护机制

翻译时自动保护技术内容：

- ✅ 代码块原样保留
- ✅ 文件路径不翻译
- ✅ 命令示例保持英文
- ✅ Markdown 结构不破坏
- ✅ 特殊标记（如 `<!-- OPENSPEC:START -->`）保持原样

### 自动更新配置

翻译完成后，命令会自动更新项目配置文件 `CLAUDE.md` 和 `AGENTS.md`，添加或更新语言指令。

## 翻译范围

命令会处理这些文件：

**核心文档**：
- `openspec/project.md`
- `openspec/AGENTS.md`

**命令文档**：
- `.claude/commands/openspec/proposal.md`
- `.claude/commands/openspec/apply.md`
- `.claude/commands/openspec/archive.md`

**全局配置**（仅 OpenSpec 相关部分）：
- `AGENTS.md`
- `CLAUDE.md`

**排除**：`.claude/commands/openspec/translate.md`（命令本身保持英文）

## 实现方式

### 命令文件结构

在项目中创建 `.claude/commands/openspec/translate.md`：

```markdown
---
name: OpenSpec: Translate
description: Translate OpenSpec documentation to any target language
category: OpenSpec
tags: [openspec, translation, i18n]
---

<!-- 这里写命令的详细执行指令 -->
```

### 工作原理

```mermaid
graph LR
    A[用户输入命令] --> B[Claude 读取命令文件]
    B --> C[解析语言参数]
    C --> D[读取目标文件]
    D --> E[执行翻译]
    E --> F[写回文件]
    F --> G[更新配置]
```

执行流程：

1. 提取目标语言（如 `chinese` → `中文`）
2. 读取每个目标文件
3. 翻译文本内容，保护技术部分
4. 写回原文件
5. 更新语言指令到配置文件

### 语言映射实现

```javascript
// 映射表（伪代码）
const languageMap = {
  'chinese': '中文',
  '中文': '中文',
  'japanese': '日本語',
  '日本語': '日本語'
}

// 智能识别
const input = 'chinese'  // 或 '中文'
const target = languageMap[input.toLowerCase()]  // '中文'
```

## 完整命令代码

```markdown
---
name: OpenSpec: Translate
description: Translate OpenSpec documentation to any target language
category: OpenSpec
tags: [openspec, translation, i18n]
---
<!-- OPENSPEC:START -->
**Overview**
This command translates OpenSpec-related documentation from English to a specified target language while preserving technical accuracy and document structure.

**Usage**
`/openspec:translate <language>`


**Examples**
- `/openspec:translate chinese` or `/openspec:translate 中文`
- `/openspec:translate japanese` or `/openspec:translate 日本語`
- `/openspec:translate spanish` or `/openspec:translate español`
- `/openspec:translate korean` or `/openspec:translate 한국어`
- `/openspec:translate french` or `/openspec:translate français`

**Target Files**
The following files will be translated:
- `openspec/project.md`
- `openspec/AGENTS.md`
- `.claude/commands/openspec/proposal.md`
- `.claude/commands/openspec/apply.md`
- `.claude/commands/openspec/archive.md`
- `AGENTS.md` (OpenSpec section only)
- `CLAUDE.md` (OpenSpec section only)

**Excluded Files**
The following files will NOT be translated and always remain in English:
- `.claude/commands/openspec/translate.md` (this command file itself)

**Language Name Mapping**
The system maps English language names to localized names for the language instruction:
- `chinese` or `中文` → "中文"
- `japanese` or `日本語` → "日本語"
- `spanish` or `español` → "español"
- `korean` or `한국어` → "한국어"
- `french` or `français` → "français"

**Process**
1. Extract target language from command arguments
2. Determine the localized language name for the language instruction
3. Read each target file using the Read tool (skip `.claude/commands/openspec/translate.md`)
4. Translate text content to the specified language while preserving:
   - Frontmatter (YAML headers)
   - Code blocks and command examples
   - Markdown structure (headings, lists, links, tables)
   - Special markers (e.g., `<!-- OPENSPEC:START -->`)
   - English commands, file paths, and technical identifiers
5. Write translated content back to original files using the Write tool
6. After translation, append or update language instruction in `CLAUDE.md` and `AGENTS.md`:
   - Check if file ends with "**Always respond in [language]**"
   - If exists, replace with new target language
   - If not exists, append new instruction: "Always respond in [localized language name]"
7. Provide translation progress and completion feedback

**Translation Principles**
- Maintain technical terminology accuracy
- Use natural, idiomatic expressions in the target language
- Preserve all English commands, code, and file paths
- Keep the original document's tone and style
- Support both English language names (e.g., "chinese") and native names (e.g., "中文")

**WARNING**
- Do not translate the command file itself `.claude/commands/openspec/translate.md`
<!-- OPENSPEC:END -->
```

## 扩展应用

### 更多自定义命令示例

**代码审查**：

```markdown
---
name: Code Review
description: Generate a comprehensive code review checklist
---
Review the code with focus on:
1. Security vulnerabilities
2. Performance issues
3. Code style consistency
4. Test coverage
```

**文档生成**：

```markdown
---
name: Generate API Docs
description: Auto-generate API documentation from code comments
---
Scan all files in `src/api/` and generate:
- Endpoint descriptions
- Request/response schemas
- Example usage
```

### 命令设计原则

| 原则 | 好的做法 | 不好的做法 |
|------|---------|-----------|
| **清晰性** | `/openspec:translate chinese` | `/t cn` |
| **健壮性** | 支持大小写、多种格式 | 只认固定格式 |
| **文档性** | 详细的用法示例和参数说明 | 缺少说明文档 |

### 项目组织

建议的目录结构：

```
.claude/
└── commands/
    ├── openspec/        # OpenSpec 相关
    │   ├── translate.md
    │   ├── proposal.md
    │   └── apply.md
    ├── git/             # Git 相关
    │   ├── commit-template.md
    │   └── pr-template.md
    └── docs/            # 文档相关
        ├── api-docs.md
        └── changelog.md
```

### 团队协作

把自定义命令加入版本控制：

```bash
git add .claude/commands/
git commit -m "feat: add OpenSpec translation command"
```

团队成员克隆代码后自动获得所有命令。

## 总结

通过自定义斜杠命令，为 OpenSpec 补上了多语言支持。

**简单易用**：
- 一个 Markdown 文件就能定义命令
- 用自然语言描述指令，无需编程
- 可以调用所有 Claude Code 工具

**实用价值**：
- 补充工具缺失的功能
- 打造团队专属工作流
- 沉淀最佳实践

别被工具限制住——通过自定义命令，让 AI 助手真正为你所用。

---

**相关资源**：
- [OpenSpec 官方文档](https://github.com/Fission-AI/OpenSpec)
- [Claude Code 命令文档](https://docs.claude.com/en/docs/claude-code)]]></content:encoded>
            <author>yupanzi</author>
            <category>AI</category>
            <category>OpenSpec</category>
            <category>Claude</category>
        </item>
        <item>
            <title><![CDATA[AI 编程工具使用指南]]></title>
            <link>https://yupanzi.com/posts/claude-code-usage-guide/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/claude-code-usage-guide/</guid>
            <pubDate>Sat, 25 Oct 2025 11:25:05 GMT</pubDate>
            <description><![CDATA[AI 编程工具对比选择、Claude Code 进阶技巧、MCP 多模型协作完全指南]]></description>
            <content:encoded><![CDATA[本文整合了 AI 编程工具的选择对比和 Claude Code 的进阶使用技巧。

## 第一梯队：Cursor vs Claude Code

| 特性 | Cursor | Claude Code |
|------|--------|-------------|
| **类型** | AI 编辑器（基于 VSCode） | CLI 命令行工具 |
| **安装** | 下载安装包 | `npm install -g @anthropic-ai/claude-code` |
| **核心优势** | IDE 体验、多模型、智能补全 | 终端原生、深度代码理解、Git 集成 |
| **适用场景** | 日常编码、大型项目 | 快速调试、代码审查、自动化 |

---

## 编辑器方案对比

### 工具层级

```mermaid
graph LR
    A[Cursor<br/>旗舰版] -->|廉价版| B[VSCode + Copilot]
    A -->|替代方案| C[VSCode + KiloCode]
    C -->|简化版| D[VSCode + Cline]
```

### 详细对比

| 工具 | 类型 | 核心特性 | 推荐度 |
|------|------|----------|--------|
| **Cursor** | AI 编辑器 | Plan Mode、多模型切换、Agent CLI | ⭐⭐⭐⭐⭐ |
| **VSCode + Copilot** | 编辑器 + 官方插件 | GitHub 官方、Agent 模式 | ⭐⭐⭐⭐ |
| **VSCode + KiloCode** | 编辑器 + 开源插件 | 自动补全、Memory Bank、团队协作 | ⭐⭐⭐⭐⭐ |
| **VSCode + Cline** | 编辑器 + 开源插件 | 完全开源、多 API、自主编辑 | ⭐⭐⭐⭐ |

### 选择建议

| 预算 | 推荐方案 |
|------|----------|
| 充足 | Cursor |
| 有限 | VSCode + Copilot Free 或 Cline |
| 团队 | VSCode + KiloCode |

---

## API 代理服务

| 服务 | 用途 | 特点 |
|------|------|------|
| **OpenRouter** | 统一 API 网关 | 400+ 模型、自动降级 |
| **Z.ai（智谱）** | Claude 替代 | Claude 协议兼容、价格 1/7 |

### 配置示例

**OpenRouter（Cline/KiloCode）：**

```json
{
  "apiProvider": "OpenRouter",
  "apiKey": "sk-or-v1-xxx",
  "baseURL": "https://openrouter.ai/api/v1"
}
```

**智谱（Claude Code 替代）：**

```bash
# 国际版
export ANTHROPIC_BASE_URL="https://api.z.ai/api/paas/v4"
export ANTHROPIC_API_KEY="your-key"

# 国内版
export ANTHROPIC_BASE_URL="https://open.bigmodel.cn/api/paas/v4"
export ANTHROPIC_API_KEY="your-key"
```

---

## Claude Code 进阶：MCP 生态

Model Context Protocol (MCP) 生态提供了强大的扩展能力。

### Zen MCP Server

多模型协作 MCP 服务器，支持 Gemini、OpenAI、OpenRouter 等。

**核心功能：**
- **多模型智能调度**：不同模型发挥各自优势
- **对话线程**：跨工具上下文延续
- **CLI 子代理**：任务分层处理

**配置示例：**

```json
{
  "mcpServers": {
    "zen": {
      "command": "npx",
      "args": ["-y", "@beehiveinnovations/zen-mcp-server"],
      "env": {
        "OPENAI_API_KEY": "your-key",
        "GEMINI_API_KEY": "your-key"
      }
    }
  }
}
```

**模型选择建议：**

| 场景 | 推荐模型 | 原因 |
|------|----------|------|
| 大型代码库 | Gemini 2.5 Pro | 100 万 token 上下文 |
| 算法优化 | GPT-5 | 强推理能力 |
| 日常编码 | Claude 4.5 | 平衡、快速响应 |

---

### Claude Code Router

智能代理工具，将 Claude Code 请求路由到其他 AI 提供商。

**功能：**
- 请求拦截与路由
- 多提供商支持（OpenRouter、DeepSeek、Ollama）
- 动态模型切换

```bash
# 切换模型
/model gpt-5
/model deepseek-coder
/model ollama:qwen2.5-coder
```

**替代方案：**

| 项目 | 特点 |
|------|------|
| ccproxy | 基于 LiteLLM，最大兼容性 |
| y-router | Cloudflare Worker，云端方案 |

---

### ZCF (Zero-Config Code Flow)

零配置 AI 编码工作流工具。

```bash
# 安装使用
npm install -g zcf
npx zcf
# 选择 "4. Configure Codex MCP services"
```

**特点：**
- 零配置启动
- Claude Code 和 Codex 共享 MCP 配置
- 内置搜索、Git、文件系统服务

---

### OpenSpec

规范驱动开发框架："先定义规格，再编写代码"。

```bash
# 工作流
openspec init feature-auth
openspec review feature-auth
openspec generate feature-auth
```

---

## 方案对比

| 维度 | 方案 A (Zen + OpenSpec) | 方案 B (Router + ZCF) |
|------|-------------------------|------------------------|
| 配置难度 | 需要多个 API Key | 零配置 |
| 灵活性 | 完全自定义 | 预设方案 |
| 成本 | 多个 API 订阅 | 可用免费模型 |
| 离线能力 | ❌ | ✅ Ollama |
| 适合人群 | 高级用户、团队 | 新手、个人 |

### 选择建议

**选择方案 A 如果：**
- 需要多模型协作
- 重视开发流程规范化
- 处理复杂大型项目

**选择方案 B 如果：**
- AI 工具新手
- 希望快速上手
- 预算有限或需离线

---

## 团队协作配置

以下工具支持配置文件提交到仓库：

### Cursor

```bash
.cursor/
  ├── settings.json
  ├── rules.json
  └── prompts/
```

### KiloCode

```bash
.kilocode/
  ├── config.json
  ├── models.json
  └── memory/
```

### Claude Code

```bash
.claude/
  ├── config.json
  ├── commands/
  └── hooks/
```

**注意**：⚠️ 不要将 API Key 提交到仓库。

---

## 快速上手

### 新手路径

```bash
# 1. ZCF 零配置（10 分钟）
npm install -g zcf
npx zcf

# 2. Claude Code Router（20 分钟）
npm install -g claude-code-router
export OPENROUTER_API_KEY=your-key
claude-code-router start
```

### 高级路径

```bash
# 1. 部署 Zen MCP Server
# 配置 ~/.config/claude/claude_desktop_config.json

# 2. 引入 OpenSpec
npm install -g @fission-ai/openspec
openspec init my-feature
```

---

## 总结

| 工具 | 核心价值 | 适合场景 |
|------|----------|----------|
| **Zen MCP Server** | 多模型协作 | 复杂项目、团队协作 |
| **OpenSpec** | 规范化开发 | 团队协作、知识沉淀 |
| **Claude Code Router** | 低成本/离线 | 预算有限、本地开发 |
| **ZCF** | 零配置启动 | 新手入门、快速上手 |

**建议：**
- 5 分钟：试试 ZCF
- 1 小时：部署 Router + ZCF
- 深度探索：学习 Zen MCP + OpenSpec

---

## 参考链接

- [Zen MCP Server](https://github.com/BeehiveInnovations/zen-mcp-server)
- [OpenSpec](https://github.com/Fission-AI/OpenSpec)
- [Claude Code Router](https://github.com/musistudio/claude-code-router)
- [ZCF](https://github.com/UfoMiao/zcf)
- [Model Context Protocol](https://github.com/modelcontextprotocol)]]></content:encoded>
            <author>yupanzi</author>
            <category>AI</category>
            <category>Claude</category>
            <category>MCP</category>
            <category>Cursor</category>
        </item>
        <item>
            <title><![CDATA[Notion 自动按钮生成每日 TODO]]></title>
            <link>https://yupanzi.com/posts/notion-auto-daily-todo-button/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/notion-auto-daily-todo-button/</guid>
            <pubDate>Mon, 13 Oct 2025 09:11:38 GMT</pubDate>
            <description><![CDATA[用 Notion 按钮一键生成每日待办清单]]></description>
            <content:encoded><![CDATA[每天手动创建 TODO 清单太麻烦？用 Notion 的按钮功能，点一下就能自动生成包含日期、待办和笔记的模板。

## 配置按钮

### 第 1 步：创建按钮

在 Notion 页面输入 `/button`，选择"Button"。

### 第 2 步：设置触发

- **按钮名称**："每日工作"（可自定义）
- **When**：`Button is clicked`

### 第 3 步：配置内容

**Do** 区域选择：`Insert blocks` → `below button`

在编辑框输入：

```
@Today

TODO
[ ] To-do

NOTE
```

![Notion 按钮配置界面](./Snipaste_2025-10-13_09-06-45.png)

**说明**：
- `@Today` - 自动显示当天日期
- `[ ] To-do` - 创建可勾选的待办项（Notion 中输入 `/todo` 创建）
- 空行分隔内容区域

点击 **Done** 保存。

## 自定义模板

### 多个待办项

```
TODO
[ ] 处理邮件
[ ] 代码审查
[ ] 团队会议
```

### 完整工作日模板

```
@Today

## 今日目标
[ ]

## 工作进展
[ ]

## 会议记录

## 备注
```

### 日期变量

| 变量 | 说明 |
|------|------|
| `@Today` | 当天日期 |
| `@Tomorrow` | 明天 |
| `@Yesterday` | 昨天 |
| `@Now` | 当前时间 |

## 进阶功能

### 结合数据库

如果 TODO 使用数据库管理，可以在按钮中：
- **Add pages to** - 向数据库添加新页面
- **Edit pages** - 批量更新页面属性

### 组合操作

**Do** 区域点击 `Add action` 可添加多个动作：
1. 插入今日模板
2. 发送提醒通知
3. 更新数据库字段

### 美化按钮

点击按钮右上角 `⋮` 可以：
- 修改颜色
- 添加图标
- 调整大小

## 注意事项

- 每次点击都会新增内容，建议每天只点击一次
- 已插入的内容可直接编辑，不影响按钮模板
- 在 Notion 中输入 `/todo` 可创建待办复选框]]></content:encoded>
            <author>yupanzi</author>
            <category>Notion</category>
            <category>Productivity</category>
            <category>Automation</category>
        </item>
        <item>
            <title><![CDATA[AI 编程与自动驾驶到排水渠过弯]]></title>
            <link>https://yupanzi.com/posts/ai-autonomous-driving-drainage-ditch/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/ai-autonomous-driving-drainage-ditch/</guid>
            <pubDate>Sun, 12 Oct 2025 00:45:06 GMT</pubDate>
            <description><![CDATA[AI 编程有些类似于自动驾驶，但目前自动驾驶还不知道排水渠过弯]]></description>
            <content:encoded><![CDATA[AI 编程很像自动驾驶，自动驾驶分为 L0 到 L5 六个级别，AI 编程也差不多：

## AI 编程的六个级别

| 级别 | 自动驾驶 | AI 编程 | 监控要求 | 特征 |
|------|---------|---------|---------|------|
| L0 | 无自动化 | 古法编程 | 人工全程 | 纯手写代码，啥都没有 |
| L1 | 驾驶辅助 | IDE 补全 | 人工全程 | 基础语法提示 |
| L2 | 部分自动化 | Tab 补全 | 人工全程 | Copilot 那种代码生成 |
| L3 | 有条件自动化 | Agent 使用 | 人需待命 | Claude Code 这类工具 |
| L4 | 高度自动化 | 规划编程 | 限定场景免监控 | AI 自己搞定大部分需求 |
| L5 | 完全自动化 | 完全自主 | 无需监控 | 任何场景都能编程 |

**重点来了**：
- **L0-L2**：辅助级别，你得全程盯着
- **L3-L5**：自动化级别，系统能自己干活

现在大概在 **L3 阶段**：特定场景下 AI 能搞定复杂任务，但碰到没见过的情况还得你接手。

## Claude Code 的真实表现

我最近让 Claude Code 提供算法进行实现，它上来就给了个 O(n⁴) 的暴力解：

```javascript
// Claude 最初的方案：四层循环暴力搜
for (i in positions)
  for (j in positions)
    for (k in positions)
      for (l in positions)
        检查是否对称矩形
```

能跑，但慢得要死。我跟它说了下我的优化算法方案，它才恍然大悟：

```javascript
// 我提示后的方案：分组优化
按数字值分组
for 每个数字 value1:
  for 该数字的每对位置:
    for 每个其他数字 value2:
      for 该数字的每对位置:
        检查是否对称矩形
```

看看差距：

| 指标 | Claude 最初方案 | 我提示后 | 提升 |
|------|----------------|---------|------|
| 算法复杂度 | O(n⁴) | O(k × m²) | - |
| 比较次数（30数字）| 81 万次 | 112 次 | 7200 倍 |
| 平均速度 | 75.87 种子/秒 | 273.49 种子/秒 | 3.6 倍 ⚡ |
| 总耗时 | 19 分钟 | 5.3 分钟 | 3.6 倍 |
| 准确性 | ✅ 通过 | ✅ 通过 | 一致 |

发现问题了吗？AI 能写出能跑的代码，但不一定是最优解。这就是 L3 的典型特征——常规场景没问题，碰到需要动脑筋的地方还得你来。

## AI 编程的"排水渠过弯"

还记得《头文字D》里的经典操作吗？在秋名山下坡弯道，利用排水渠的特殊地形，一侧轮胎压入排水渠，突破物理极限实现超车。

**自动驾驶的困境**：
- 系统知道最优行车路线的数据和参数
- 但它不知道"排水渠过弯"这种非常规技巧
- 这需要对真实场景的深刻理解和创造性思维

AI 编程也一样

### 啥时候你得接手？

**算法优化场景**（就是上面那个例子）
- AI 知道常规算法（标准路线）
- 但不知道"分组"这种巧妙优化（排水渠过弯）
- 你得告诉它这个思路
- 然后它能飞快实现细节

**没见过的领域**
- AI 训练数据里没这玩意儿
- 你得详细说清楚要干啥
- 来回沟通几轮才能搞定

**业务特殊情况**
- 行业规则、边界条件
- 性能要求、取舍权衡
- 这些你比 AI 清楚

**需要创新的地方**
- 得跳出常规思路
- 组合多个技术解决问题
- AI 能帮忙，但方向得你指

## 怎么更好地"驾驶" AI

这次优化给我几个感受：

**别满足于"能跑"**
- 代码能运行不代表写得好
- 多想想有没有更优解法
- AI 容易陷入训练数据的惯性

**提供关键思路就够了**
- 不用写完整代码
- 提供关键思路
- AI 会自己把细节补全

**用数据说话**
- 跑实际测试对比性能
- 确保优化没搞坏功能
- 数字比感觉靠谱

**来回迭代很正常**
- AI 给方案 → 你看看 → 提改进 → AI 优化
- 这就是 L3 的工作模式

## 离 L5 还有多远？

**L3（现在）**：AI 是副驾驶，你开车
- 大部分常规任务能搞定
- 关键决策得你拍板
- 碰到没见过的必须你接手

**L4（几年后？）**：特定领域自己搞定
- 像 CRUD 这种可能不用人管了
- 复杂系统还是得人盯着

**L5（遥远的未来）**：啥场景都能搞
- 理解任何需求
- 自己选最优方案
- 你就当乘客就行

现在的 AI 编程感觉就是 L3 自动驾驶：**大部分路段很稳，但得随时准备接手**。

碰到需要"排水渠过弯"的时候——那些需要突破常规的场景——你的经验和创造力还是不可替代的。AI 知道参数和数据，但**只有你知道真实场景里还藏着排水渠这条隐藏赛道**。]]></content:encoded>
            <author>yupanzi</author>
            <category>AI</category>
            <category>Programming</category>
            <category>Claude</category>
        </item>
        <item>
            <title><![CDATA[Hexo 添加 Mermaid 图表支持]]></title>
            <link>https://yupanzi.com/posts/hexo-mermaid-integration/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/hexo-mermaid-integration/</guid>
            <pubDate>Sat, 11 Oct 2025 20:03:05 GMT</pubDate>
            <description><![CDATA[用最小修改在 Hexo 中集成 Mermaid 图表渲染]]></description>
            <content:encoded><![CDATA[想在技术博客中画流程图、时序图？Mermaid 可以让我们用代码直接绘制各种图表，无需额外的绘图工具。

## 问题背景

Hexo 默认的 Markdown 渲染器会把所有代码块（包括 `mermaid`）渲染成带行号的 `<figure><table>` 结构，而 Mermaid.js 只能识别简单的 `<pre class="mermaid">` 标签。我们需要一个轻量的解决方案。

## 实现方案

本文基于 **Hexo Frame 主题**，但方案也适用于其他主题（需调整对应的配置文件路径）。

采用原生配置 + 脚本转换的方式，**无需安装任何 npm 插件**。

### 1. 添加主题配置

**Frame 主题**：编辑 `themes/frame/_config.yml`

**其他主题**：编辑对应主题的 `_config.yml`（如 `themes/next/_config.yml`）

添加 Mermaid 配置：

```yaml
# mermaid diagram setting
mermaid_enable: true
mermaid_version: "11"  # mermaid 版本
```

### 2. 加载 Mermaid.js

**Frame 主题**：编辑 `themes/frame/layout/partials/head.ejs`

**其他主题**：找到主题的 head 模板文件（通常在 `layout/_partial/head.ejs` 或 `layout/partials/head.ejs`）

在 `</head>` 前添加：

```ejs
<%# mermaid diagram support %>
<% if(theme.mermaid_enable){ %>
    <% var mermaidVersion = theme.mermaid_version || '11'; %>
    <script type="module">
        import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@<%= mermaidVersion %>/dist/mermaid.esm.min.mjs';
        mermaid.initialize({
            startOnLoad: true,
            theme: 'default'
        });
    </script>
<% } %>
```

### 3. 添加 HTML 转换脚本

创建 `scripts/mermaid-renderer.js`（**通用方案，适用于所有主题**）：

```javascript
/**
 * Mermaid renderer for Hexo
 * Converts mermaid code blocks from <figure> to <pre class="mermaid">
 */

hexo.extend.filter.register('after_post_render', function(data) {
  // Only process if mermaid is enabled
  if (!hexo.theme.config.mermaid_enable) {
    return data;
  }

  // Replace <figure class="highlight plaintext"> containing mermaid code
  data.content = data.content.replace(
    /<figure class="highlight plaintext"><table><tr><td class="gutter">[\s\S]*?<\/td><td class="code"><pre>([\s\S]*?)<\/pre><\/td><\/tr><\/table><\/figure>/g,
    function(match, code) {
      // Extract plain text from the code
      const plainCode = code
        .replace(/<span class="line">/g, '')
        .replace(/<\/span>/g, '')
        .replace(/<br>/g, '\n')
        .trim();

      // Check if it looks like mermaid code
      const mermaidKeywords = ['graph', 'sequenceDiagram', 'classDiagram', 'stateDiagram', 'gantt', 'pie', 'flowchart', 'erDiagram', 'journey'];
      const isMermaid = mermaidKeywords.some(keyword => plainCode.startsWith(keyword));

      if (isMermaid) {
        return '<pre class="mermaid">' + plainCode + '</pre>';
      }

      return match;
    }
  );

  return data;
});
```

## 使用示例

### 流程图

```mermaid
graph TD
    A[开始] --> B{需要图表?}
    B -->|是| C[使用 Mermaid]
    B -->|否| D[纯文本描述]
    C --> E[完成]
    D --> E
```

### 时序图

```mermaid
sequenceDiagram
    participant 客户端
    participant 服务器
    participant 数据库

    客户端->>服务器: 发起请求
    服务器->>数据库: 查询数据
    数据库-->>服务器: 返回结果
    服务器-->>客户端: 响应数据
```

### 甘特图

```mermaid
gantt
    title 项目计划
    dateFormat YYYY-MM-DD
    section 开发
    功能开发    :2025-01-01, 5d
    代码审查    :2025-01-06, 2d
    section 测试
    测试验证    :2025-01-08, 3d
```

## 配置说明

| 配置项 | 说明 | 默认值 |
|--------|------|--------|
| `mermaid_enable` | 是否启用 Mermaid | `false` |
| `mermaid_version` | Mermaid 版本号 | `"11"` |

**版本配置示例**：
- `"11"` - 使用 11.x 最新版
- `"10.9.0"` - 使用指定版本

## 主题适配说明

本方案由三部分组成：

1. **主题配置**：在主题的 `_config.yml` 中添加开关和版本配置
2. **脚本加载**：在主题的 `head` 模板中添加 CDN 脚本（**需要适配主题**）
3. **HTML 转换**：`scripts/mermaid-renderer.js` 脚本（**通用，无需修改**）

如果你使用的不是 Frame 主题，只需要调整步骤 1 和步骤 2 的文件路径，步骤 3 的脚本完全通用。

## 注意事项

- Hexo 脚本修改后需重启 `hexo server`
- Mermaid 代码块会自动识别关键字（`graph`、`sequenceDiagram` 等）
- 如需关闭渲染，设置 `mermaid_enable: false`

更多图表语法参考 [Mermaid 官方文档](https://mermaid.js.org/)。]]></content:encoded>
            <author>yupanzi</author>
            <category>Hexo</category>
            <category>Mermaid</category>
            <category>Visualization</category>
        </item>
        <item>
            <title><![CDATA[AI 编程工具配置指南]]></title>
            <link>https://yupanzi.com/posts/claude-code-configuration/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/claude-code-configuration/</guid>
            <pubDate>Fri, 10 Oct 2025 14:30:37 GMT</pubDate>
            <description><![CDATA[Claude Code、Codex、Gemini 命令行工具和 VS Code 插件的自定义 API 配置指南]]></description>
            <content:encoded><![CDATA[本文整合了 Claude Code、Codex、Gemini 等 AI 编程工具的配置方法，包括命令行工具和 VS Code 插件。

## Claude Code 命令行

### 安装

```bash
npm install -g @anthropic-ai/claude-code
```

### 配置文件

| 系统 | 路径 |
|------|------|
| Windows | `%USERPROFILE%\.claude\settings.json` |
| macOS/Linux | `~/.claude/settings.json` |

编辑配置文件：

```bash
# Windows
notepad $env:USERPROFILE\.claude\settings.json

# macOS/Linux
vim ~/.claude/settings.json
```

写入配置：

```json
{
  "env": {
    "ANTHROPIC_BASE_URL": "https://api.example.com/claude",
    "ANTHROPIC_AUTH_TOKEN": "sk-xxxx"
  }
}
```

配置完成后直接运行 `claude` 即可。

---

## Claude Code VS Code 插件

如果你更喜欢在 VS Code 中使用 Claude Code，可以安装插件并配置自定义 API。

### 安装插件

从 VS Code 扩展市场安装 [Claude Code 插件](https://marketplace.visualstudio.com/items?itemName=anthropic.claude-code)。

### 配置 API 地址

1. 打开 VS Code 设置（`Ctrl+,` 或 `Cmd+,`）
2. 搜索 `claude-code.environmentVariables`
3. 点击 **Edit in settings.json**
4. 添加配置：

```json
{
  "claude-code.environmentVariables": [
    {
      "name": "ANTHROPIC_BASE_URL",
      "value": "https://api.example.com"
    }
  ]
}
```

### 配置 API Key

插件从 `~/.claude/config.json` 读取 API 密钥：

**macOS / Linux:**

```bash
mkdir -p ~/.claude
echo '{"primaryApiKey": "your-api-key"}' > ~/.claude/config.json
```

**Windows:**

```powershell
New-Item -ItemType Directory -Force -Path "$HOME\.claude"
Set-Content -Path "$HOME\.claude\config.json" -Value '{"primaryApiKey": "your-api-key"}'
```

重启 VS Code 后，点击活动栏的 Claude 图标即可开始使用。

---

## Codex CLI

### 安装

```bash
npm install -g @openai/codex
```

### 配置文件

| 系统 | 路径 |
|------|------|
| Windows | `%USERPROFILE%\.codex\config.toml` |
| macOS/Linux | `~/.codex/config.toml` |

写入配置：

```toml
model = "gpt-5"
model_provider = "openai-chat-completions"

[model_providers.openai-chat-completions]
name = "PROXY"
base_url = "https://api.example.com/v1"
env_key = "PROXY_OPENAI_API_KEY"
wire_api = "chat"
```

### 设置环境变量

Codex 需要额外配置环境变量：

**Windows:**

```powershell
# 临时
$env:PROXY_OPENAI_API_KEY = "sk-xxxx"

# 永久
[System.Environment]::SetEnvironmentVariable('PROXY_OPENAI_API_KEY', 'sk-xxxx', 'User')
```

**macOS/Linux:**

```bash
# 临时
export PROXY_OPENAI_API_KEY="sk-xxxx"

# 永久
echo 'export PROXY_OPENAI_API_KEY="sk-xxxx"' >> ~/.bashrc  # 或 ~/.zshrc
source ~/.bashrc
```

---

## Gemini CLI

### 安装

```bash
npm install -g @google/gemini-cli
```

### 配置方式

Gemini 没有专门的配置文件，使用环境变量或 `.env` 文件。

**方法一：.env 文件**

```bash
# Windows: notepad $env:USERPROFILE\.env
# macOS/Linux: vim ~/.env

GOOGLE_GEMINI_BASE_URL=https://api.example.com/gemini
GEMINI_API_KEY=sk-xxxx
GEMINI_MODEL=gemini-2.5-pro
```

**方法二：环境变量**

Windows:

```powershell
[System.Environment]::SetEnvironmentVariable('GOOGLE_GEMINI_BASE_URL', 'https://api.example.com/gemini', 'User')
[System.Environment]::SetEnvironmentVariable('GEMINI_API_KEY', 'sk-xxxx', 'User')
[System.Environment]::SetEnvironmentVariable('GEMINI_MODEL', 'gemini-2.5-pro', 'User')
```

macOS/Linux:

```bash
cat >> ~/.bashrc << 'EOF'
export GOOGLE_GEMINI_BASE_URL=https://api.example.com/gemini
export GEMINI_API_KEY=sk-xxxx
export GEMINI_MODEL=gemini-2.5-pro
EOF

source ~/.bashrc
```

---

## 工具对比

| 工具 | 配置方式 | 环境变量 | 难度 |
|------|---------|---------|------|
| Claude Code CLI | JSON 配置文件 | 不需要 | 简单 |
| Claude Code 插件 | VS Code 设置 + JSON | 不需要 | 简单 |
| Codex | TOML 配置 + 环境变量 | 需要 | 中等 |
| Gemini CLI | 环境变量或 .env | 需要 | 简单 |

---

## 注意事项

### API Key 安全

把配置文件加到 `.gitignore`：

```
.env
settings.json
config.toml
```

### 文件权限

**macOS/Linux:**

```bash
chmod 600 ~/.claude/settings.json
chmod 600 ~/.codex/config.toml
chmod 600 ~/.env
```

**Windows:**

```powershell
icacls "$env:USERPROFILE\.claude\settings.json" /inheritance:r /grant:r "$env:USERNAME:F"
```

### 环境变量持久化

| 系统 | 配置文件 |
|------|----------|
| Windows | 系统环境变量或 `$PROFILE` |
| macOS | `~/.zshrc`（默认 Zsh） |
| Linux | `~/.bashrc` 或 `~/.zshrc` |

### 常见问题

| 问题 | 解决方案 |
|------|----------|
| 找不到配置目录 | 手动创建：`mkdir ~/.claude` |
| 环境变量不生效 | Windows 重启终端，macOS/Linux 执行 `source ~/.bashrc` |
| 切换不同 API | Codex 可配多个 `model_providers`，修改 `model_provider` 字段 |

---

## 使用建议

- **推荐**：命令行版 Claude Code 比 VS Code 插件更好用
- 如果使用官方服务，直接登录更方便，不需要这些自定义配置
- Codex 支持多 provider 切换，适合需要在不同 API 间切换的场景

## 参考资料

- [Claude Code 官方文档](https://docs.anthropic.com/en/docs/claude-code)
- [CSDN - Claude Code 配置教程](https://blog.csdn.net/weixin_49869937/article/details/152317666)]]></content:encoded>
            <author>yupanzi</author>
            <category>AI</category>
            <category>Claude</category>
            <category>VSCode</category>
            <category>Codex</category>
            <category>Gemini</category>
        </item>
        <item>
            <title><![CDATA[Nginx 反向代理 SSL 过期的网站并部署在 Kubernetes 上]]></title>
            <link>https://yupanzi.com/posts/nginx-reverse-proxy-ssl-expired-k8s/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/nginx-reverse-proxy-ssl-expired-k8s/</guid>
            <pubDate>Fri, 10 Oct 2025 10:18:12 GMT</pubDate>
            <description><![CDATA[使用 Nginx 反向代理 SSL 证书过期的网站，并通过 Helm 部署在 Kubernetes 集群上]]></description>
            <content:encoded><![CDATA[## 背景

遇到第三方 API 的 SSL 证书过期了，直接访问会报证书错误。可以用 Nginx 反向代理来绕过这个问题。

## Nginx 配置

在 `values.yaml` 里添加自定义的 server 配置：

```yaml
serverBlock: |
  server {
    listen 8080;

    location / {
      proxy_pass https://api.foobar.com/;
      proxy_ssl_verify off;  # 关键：忽略 SSL 证书验证

      proxy_set_header Host api.foobar.com;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
    }
  }
```

## 部署

```bash
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install my-nginx bitnami/nginx -f values.yaml
```

## 测试

直接访问 HTTPS（会失败）：

```bash
# 会报证书错误
curl https://api.foobar.com/endpoint
```

通过 Nginx 代理访问（成功）：

```bash
# 通过代理访问
curl http://my-nginx:8080/endpoint
```

如果想直接用 curl 忽略证书验证：

```bash
# -k 参数忽略 SSL 证书
curl -k https://api.foobar.com/endpoint

# 或者使用 --insecure
curl --insecure https://api.foobar.com/endpoint
```

## 注意

- 关闭 SSL 验证有安全风险，仅适合内网环境
- 生产环境还是建议让对方更新证书]]></content:encoded>
            <author>yupanzi</author>
            <category>Helm</category>
            <category>Kubernetes</category>
            <category>Network</category>
            <category>Nginx</category>
        </item>
        <item>
            <title><![CDATA[使用 Nginx Stream 代理数据库连接]]></title>
            <link>https://yupanzi.com/posts/how-to-start-database-proxy/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/how-to-start-database-proxy/</guid>
            <pubDate>Sun, 29 Sep 2024 15:34:32 GMT</pubDate>
            <description><![CDATA[Nginx Stream 模块实现 MySQL、PostgreSQL 等 TCP 服务代理]]></description>
            <content:encoded><![CDATA[使用 Nginx Stream 模块代理 TCP 连接，实现数据库端口转发和负载均衡。

## 应用场景

- **端口映射**：本地 33078 → 远程 MySQL 3306
- **负载均衡**：多个数据库实例分流
- **连接日志**：记录所有数据库连接信息
- **安全加固**：隐藏真实数据库地址

## Nginx Stream 配置

### 完整配置示例

`/etc/nginx/nginx.conf`：

```nginx
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

# 增加文件描述符限制
worker_rlimit_nofile 65535;

events {
    worker_connections 10240;
}

# HTTP 配置（Web 服务）
http {
    # ... HTTP 配置保留 ...
    include /etc/nginx/sites-enabled/*;
}

# Stream 配置（TCP/UDP 代理）
stream {
    # 日志格式
    log_format proxy '$remote_addr [$time_local] '
                     '$protocol $status $bytes_sent $bytes_received '
                     '$session_time "$upstream_addr" '
                     '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time"';

    access_log /var/log/nginx/tcp-access.log proxy;
    error_log /var/log/nginx/tcp-error.log;
    open_log_file_cache off;

    # MySQL 代理
    server {
        listen 33078;
        proxy_pass mysql.example.com:3306;
        proxy_timeout 300m;
        proxy_connect_timeout 10s;
    }

    # PostgreSQL 代理
    server {
        listen 54320;
        proxy_pass postgres.example.com:5432;
        proxy_timeout 300m;
    }

    # Redis 代理
    server {
        listen 63790;
        proxy_pass redis.example.com:6379;
        proxy_timeout 600s;
    }

    # MongoDB 代理
    server {
        listen 27018;
        proxy_pass mongodb.example.com:27017;
        proxy_timeout 300m;
    }
}
```

## 配置说明

### 日志字段

| 字段 | 说明 |
|------|------|
| `$remote_addr` | 客户端 IP |
| `$time_local` | 本地时间 |
| `$protocol` | 协议（TCP/UDP） |
| `$status` | 连接状态 |
| `$bytes_sent` | 发送字节数 |
| `$bytes_received` | 接收字节数 |
| `$session_time` | 会话时长 |
| `$upstream_addr` | 后端服务器地址 |
| `$upstream_connect_time` | 连接时间 |

### 超时设置

```nginx
proxy_timeout 300m;           # 会话超时（5 小时）
proxy_connect_timeout 10s;    # 连接超时（10 秒）
```

## 负载均衡配置

### 多个 MySQL 实例

```nginx
stream {
    # 定义后端服务器组
    upstream mysql_backend {
        # 负载均衡策略：least_conn（最少连接）
        least_conn;

        server mysql1.example.com:3306 weight=2 max_fails=3 fail_timeout=30s;
        server mysql2.example.com:3306 weight=1 max_fails=3 fail_timeout=30s;
        server mysql3.example.com:3306 backup;  # 备用服务器
    }

    server {
        listen 33078;
        proxy_pass mysql_backend;
        proxy_timeout 300m;
    }
}
```

### 负载均衡策略

| 策略 | 说明 |
|------|------|
| `round-robin` | 轮询（默认） |
| `least_conn` | 最少连接数 |
| `hash $remote_addr` | IP 哈希（会话保持） |
| `random` | 随机 |

### IP Hash（会话保持）

```nginx
upstream mysql_backend {
    hash $remote_addr consistent;
    server mysql1.example.com:3306;
    server mysql2.example.com:3306;
}
```

## SSL/TLS 加密

### 添加 SSL 层

```nginx
stream {
    server {
        listen 33078 ssl;
        proxy_pass mysql.example.com:3306;

        ssl_certificate /etc/nginx/ssl/server.crt;
        ssl_certificate_key /etc/nginx/ssl/server.key;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
    }
}
```

## 访问控制

### IP 白名单

```nginx
stream {
    # 定义允许的 IP
    geo $allowed_ip {
        default 0;
        192.168.1.0/24 1;
        10.0.0.0/8 1;
    }

    server {
        listen 33078;

        # 检查 IP
        if ($allowed_ip = 0) {
            return 403;
        }

        proxy_pass mysql.example.com:3306;
    }
}
```

## 使用示例

### 连接 MySQL

```bash
# 直接连接（原始）
mysql -h mysql.example.com -P 3306 -u user -p

# 通过代理连接
mysql -h proxy.example.com -P 33078 -u user -p
```

### 连接 PostgreSQL

```bash
psql -h proxy.example.com -p 54320 -U user -d database
```

### 连接 Redis

```bash
redis-cli -h proxy.example.com -p 63790
```

## 查看连接日志

```bash
# 实时查看
tail -f /var/log/nginx/tcp-access.log

# 示例输出：
# 192.168.1.100 [29/Sep/2024:15:34:32 +0800] TCP 200 1024 2048 300.123 "mysql.example.com:3306" "512" "1536" "0.003"
```

## 性能优化

### 连接池配置

```nginx
worker_rlimit_nofile 65535;  # 增加文件描述符

events {
    worker_connections 10240;  # 增加连接数
    use epoll;  # Linux 使用 epoll
}
```

### 缓冲区调优

```nginx
stream {
    server {
        listen 33078;
        proxy_pass mysql.example.com:3306;

        proxy_buffer_size 16k;
        proxy_upload_rate 0;
        proxy_download_rate 0;
    }
}
```

## 健康检查

需要 Nginx Plus 或第三方模块：

```nginx
upstream mysql_backend {
    server mysql1.example.com:3306;

    # Nginx Plus 健康检查
    health_check interval=5s fails=3 passes=2;
}
```

开源版替代方案：使用外部脚本定期检测。

## 注意事项

- Stream 模块默认编译，无需额外安装
- 与 HTTP 配置在同一个 `nginx.conf` 文件中
- 日志文件单独配置，避免与 HTTP 日志混淆
- 超时时间根据实际业务调整（长连接可设置更大值）
- 生产环境建议启用 SSL/TLS
- 定期清理日志文件，避免磁盘占满

## 参考资料

- [Nginx Stream 模块文档](https://nginx.org/en/docs/stream/ngx_stream_core_module.html)
- [Nginx TCP 负载均衡](https://nginx.org/en/docs/stream/ngx_stream_upstream_module.html)]]></content:encoded>
            <author>yupanzi</author>
            <category>Database</category>
            <category>Network</category>
            <category>Nginx</category>
        </item>
        <item>
            <title><![CDATA[Docker 私有服务部署指南]]></title>
            <link>https://yupanzi.com/posts/docker-private-services/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/docker-private-services/</guid>
            <pubDate>Sun, 29 Sep 2024 15:11:40 GMT</pubDate>
            <description><![CDATA[Nexus、Jenkins、LDAP、Vaultwarden 等私有服务的 Docker 部署指南]]></description>
            <content:encoded><![CDATA[本文整合了常用私有服务的 Docker 部署配置，包括制品仓库、CI/CD、身份认证和密码管理。

## Nexus 制品仓库

Nexus 支持 Docker、Helm、npm、PyPI、Maven 等多种格式的制品管理。

### Docker 部署

```yaml
version: '2.0'

services:
  nexus:
    image: sonatype/nexus3
    container_name: nexus
    restart: always
    ports:
      - "127.0.0.1:8081:8081"  # Web UI
      - "127.0.0.1:5000:5000"  # Docker Registry
    volumes:
      - ${HOME}/nexus-data:/nexus-data
```

```bash
# 获取初始密码
docker exec nexus cat /nexus-data/admin.password
```

### Nginx 反向代理

```nginx
server {
    listen 443 ssl;
    server_name repo.example.com;

    ssl_certificate /etc/letsencrypt/live/repo.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/repo.example.com/privkey.pem;

    client_max_body_size 10G;  # Docker 镜像可能很大

    location / {
        proxy_pass http://localhost:8081/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto "https";
    }
}
```

### 使用示例

```bash
# Docker Registry
docker login docker.example.com
docker tag myapp:latest docker.example.com/myapp:latest
docker push docker.example.com/myapp:latest

# Helm 仓库
helm repo add myrepo https://repo.example.com/repository/helm-hosted/ \
  --username admin --password password

# npm 仓库
npm config set registry https://repo.example.com/repository/npm-group/
```

---

## Jenkins CI/CD

### Docker 部署

```yaml
version: '3'
services:
  jenkins:
    image: jenkins/jenkins:lts
    user: root
    restart: always
    container_name: jenkins
    ports:
      - "127.0.0.1:8080:8080"
      - "127.0.0.1:50000:50000"  # Agent 连接
    volumes:
      - ${HOME}/jenkins/jenkins_home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock  # Docker in Docker
      - ${HOME}/.ssh:/root/.ssh
    environment:
      - JENKINS_OPTS=--sessionTimeout=43200
```

```bash
# 查看初始密码
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
```

### Nginx 反向代理（含 WebSocket）

```nginx
map $http_upgrade $connection_upgrade {
  default upgrade;
  '' close;
}

upstream jenkins {
  keepalive 32;
  server 127.0.0.1:8080;
}

server {
  listen 443 ssl;
  server_name jenkins.example.com;

  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  ignore_invalid_headers off;

  location / {
    sendfile off;
    proxy_pass http://jenkins;
    proxy_http_version 1.1;

    # WebSocket 支持
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Upgrade $http_upgrade;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_buffering off;
    proxy_request_buffering off;
  }
}
```

### Pipeline 示例

```groovy
pipeline {
  agent any
  environment {
    DOCKER_REGISTRY = 'docker.example.com'
    IMAGE_NAME = 'myapp'
  }
  stages {
    stage('Build') {
      steps {
        sh 'docker build -t ${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER} .'
      }
    }
    stage('Push') {
      steps {
        sh 'docker push ${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}'
      }
    }
  }
}
```

---

## OpenLDAP 身份认证

### Docker 部署

```yaml
version: '3.8'

services:
  openldap:
    image: osixia/openldap:1.5.0
    restart: always
    container_name: ldap
    hostname: ldap.example.com
    ports:
      - "389:389"
      - "636:636"
    environment:
      - LDAP_ORGANISATION=MyCompany
      - LDAP_DOMAIN=example.com
      - LDAP_ADMIN_PASSWORD=adminPassword
    volumes:
      - ${PWD}/ldap:/var/lib/ldap
      - ${PWD}/slapd.d:/etc/ldap/slapd.d

  phpldapadmin:
    image: osixia/phpldapadmin:latest
    container_name: phpldapadmin
    ports:
      - "8080:80"
    environment:
      - PHPLDAPADMIN_LDAP_HOSTS=ldap
      - PHPLDAPADMIN_HTTPS=false
    depends_on:
      - openldap
```

### 添加用户

`add-user.ldif`:

```ldif
dn: uid=john,ou=Users,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
uid: john
cn: John Doe
sn: Doe
mail: john@example.com
userPassword: {SSHA}xxxxx
uidNumber: 10000
gidNumber: 10000
homeDirectory: /home/john
```

```bash
# 生成密码
docker exec ldap slappasswd

# 添加用户
docker exec ldap ldapadd -x -D "cn=admin,dc=example,dc=com" -w adminPassword -f /tmp/add-user.ldif

# 搜索用户
ldapsearch -x -H ldap://localhost:389 -D "cn=admin,dc=example,dc=com" -w adminPassword \
  -b "dc=example,dc=com" "(uid=john)"
```

### 应用集成示例

**Grafana:**

```toml
[[servers]]
host = "ldap.example.com"
port = 389
bind_dn = "cn=admin,dc=example,dc=com"
bind_password = "adminPassword"
search_filter = "(uid=%s)"
search_base_dns = ["ou=Users,dc=example,dc=com"]
```

---

## Vaultwarden 密码管理

Vaultwarden 是 Bitwarden 的轻量级开源替代，资源占用极低。

### Docker 部署

```yaml
version: "3.9"

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: unless-stopped
    environment:
      - WEBSOCKET_ENABLED=true
      - SIGNUPS_ALLOWED=false
      - ADMIN_TOKEN=your-secure-random-token
      - DOMAIN=https://vault.example.com
    volumes:
      - ${HOME}/vaultwarden:/data
    ports:
      - "127.0.0.1:8080:80"
      - "127.0.0.1:3012:3012"  # WebSocket
```

```bash
# 生成管理员 Token
openssl rand -base64 48
```

### Nginx 反向代理

```nginx
server {
  listen 443 ssl http2;
  server_name vault.example.com;

  ssl_certificate /etc/letsencrypt/live/vault.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;

  client_max_body_size 128M;

  # Web UI
  location / {
    proxy_pass http://localhost:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }

  # WebSocket
  location /notifications/hub {
    proxy_pass http://localhost:3012;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }

  location /notifications/hub/negotiate {
    proxy_pass http://localhost:8080;
  }
}
```

### 客户端使用

1. 安装 Bitwarden 浏览器扩展/App
2. 设置 → 自托管环境
3. 服务器 URL：`https://vault.example.com`
4. 登录使用

---

## 服务对比

| 服务 | 用途 | 端口 | 资源占用 |
|------|------|------|----------|
| **Nexus** | 制品仓库 | 8081, 5000 | 中等 |
| **Jenkins** | CI/CD | 8080, 50000 | 较高 |
| **OpenLDAP** | 身份认证 | 389, 636 | 低 |
| **Vaultwarden** | 密码管理 | 80, 3012 | 极低 |

---

## 通用注意事项

### 安全配置

- **HTTPS**：所有服务通过 Nginx 反向代理启用 HTTPS
- **防火墙**：只开放必要端口，内部服务绑定 127.0.0.1
- **密码**：使用强密码，定期更换

### 备份策略

```bash
# 通用备份脚本
#!/bin/bash
BACKUP_DIR=~/backups
DATE=$(date +%Y%m%d)

# Nexus
tar -czf $BACKUP_DIR/nexus-$DATE.tar.gz ~/nexus-data

# Jenkins
tar -czf $BACKUP_DIR/jenkins-$DATE.tar.gz ~/jenkins/jenkins_home

# LDAP
docker exec ldap slapcat > $BACKUP_DIR/ldap-$DATE.ldif

# Vaultwarden
tar -czf $BACKUP_DIR/vault-$DATE.tar.gz ~/vaultwarden

# 清理 30 天前的备份
find $BACKUP_DIR -name "*.tar.gz" -mtime +30 -delete
```

### SSL 证书

使用 Let's Encrypt 自动证书：

```bash
certbot certonly --nginx -d repo.example.com -d jenkins.example.com -d vault.example.com
```

---

## 参考资料

- [Nexus 官方文档](https://help.sonatype.com/)
- [Jenkins 反向代理配置](https://www.jenkins.io/doc/book/system-administration/reverse-proxy-configuration-nginx/)
- [OpenLDAP Docker 镜像](https://github.com/osixia/docker-openldap)
- [Vaultwarden Wiki](https://github.com/dani-garcia/vaultwarden/wiki)]]></content:encoded>
            <author>yupanzi</author>
            <category>Docker</category>
            <category>CI/CD</category>
            <category>DevOps</category>
        </item>
        <item>
            <title><![CDATA[应用集成 Authentik OAuth 认证指南]]></title>
            <link>https://yupanzi.com/posts/authentik-oauth-integration/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/authentik-oauth-integration/</guid>
            <pubDate>Fri, 31 May 2024 14:22:19 GMT</pubDate>
            <description><![CDATA[Airflow、Grafana、JupyterHub 等应用集成 Authentik OAuth2 认证的完整配置指南]]></description>
            <content:encoded><![CDATA[本文整合了多个常用应用集成 Authentik OAuth 认证的配置方法，包括 Airflow、Grafana 和 JupyterHub。

## 通用 Authentik 配置

在 Authentik 中创建 OAuth2/OpenID 应用时，需要配置以下端点：

| 端点 | URL 格式 |
|------|----------|
| 授权 | `https://authentik.example.com/application/o/authorize/` |
| Token | `https://authentik.example.com/application/o/token/` |
| UserInfo | `https://authentik.example.com/application/o/userinfo/` |
| 元数据 | `https://authentik.example.com/application/o/{app}/.well-known/openid-configuration` |

**重要提示**：`userinfo` 端点的路径必须带斜杠 `/userinfo/`，否则会报错。

---

## Airflow 集成

### 版本信息
- App Version: 2.8.3
- Chart Version: 1.13.1

### OAuth 认证配置

`webserver_config.py`:

```python
from flask_appbuilder.security.manager import AUTH_OAUTH
from airflow.auth.managers.fab.security_manager.override import FabAirflowSecurityManagerOverride

AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "User"  # 默认角色

OAUTH_PROVIDERS = [
    {
        "name": "OAuth",
        "icon": "fa-circle-o",
        "token_key": "access_token",
        "remote_app": {
            "client_id": "your-client-id",
            "api_base_url": "https://authentik.example.com/application/o/",
            "client_secret": "your-client-secret",
            "client_kwargs": {"scope": "openid email profile"},
            "server_metadata_url": "https://authentik.example.com/application/o/oauth/.well-known/openid-configuration",
        },
    }
]

PERMANENT_SESSION_LIFETIME = 259200  # 3 天


class CustomSecurityManager(FabAirflowSecurityManagerOverride):
    def get_oauth_user_info(self, provider, resp):
        if provider != "OAuth":
            return {}

        # 关键：必须是 userinfo/ 而不是 userinfo
        me = self.appbuilder.sm.oauth_remotes[provider].get("userinfo/")
        data = me.json()

        return {
            "email": data["email"],
            "username": data.get("name", ""),
            "first_name": data.get("given_name", ""),
            "role_keys": data.get("groups", []),  # 支持组映射
        }


SECURITY_MANAGER_CLASS = CustomSecurityManager
```

### LDAP 认证配置（备选）

```python
from flask_appbuilder.security.manager import AUTH_LDAP

AUTH_TYPE = AUTH_LDAP
AUTH_LDAP_SERVER = "ldap://ldap.example.com"
AUTH_LDAP_USE_TLS = False

AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Admin"

AUTH_LDAP_LASTNAME_FIELD = "cn"
AUTH_LDAP_EMAIL_FIELD = "mail"
AUTH_LDAP_SEARCH = "ou=foo,ou=People,dc=example,dc=com"
AUTH_LDAP_UID_FIELD = "uid"
AUTH_LDAP_BIND_USER = "cn=admin,dc=example,dc=com"
AUTH_LDAP_BIND_PASSWORD = "adminPassword"

AUTH_ROLES_MAPPING = {
    "cn=g-admin,ou=Group,dc=example,dc=com": ["Admin"],
}

AUTH_LDAP_GROUP_FIELD = "memberOf"
AUTH_ROLES_SYNC_AT_LOGIN = True
PERMANENT_SESSION_LIFETIME = 86400
```

### API 认证

```yaml
config:
  api:
    auth_backends: "airflow.api.auth.backend.basic_auth"
```

---

## Grafana 集成

### OAuth 认证配置

`grafana.ini`:

```yaml
grafana:
  grafana.ini:
    auth:
      oauth_allow_insecure_email_lookup: true

    auth.generic_oauth:
      enabled: true
      name: "OAuth"
      client_id: "your-client-id"
      client_secret: "your-client-secret"
      scopes: "openid email profile offline_access"
      auth_url: "https://authentik.example.com/application/o/authorize/"
      token_url: "https://authentik.example.com/application/o/token/"
      api_url: "https://authentik.example.com/application/o/userinfo/"
      use_pkce: true
      use_refresh_token: true
      allow_sign_up: false
      allow_assign_grafana_admin: true
      auto_login: true
      skip_org_role_sync: true

    server:
      root_url: "https://grafana.example.com/"  # 必须正确配置
```

### 关键配置说明

| 配置项 | 说明 | 注意事项 |
|--------|------|----------|
| `root_url` | Grafana 访问地址 | **必须与实际域名一致**，否则会重定向失败 |
| `oauth_allow_insecure_email_lookup` | 邮箱查找 | 使用邮箱匹配用户时需设为 `true` |
| `use_pkce` | PKCE 支持 | 增强安全性 |

### LDAP 认证配置（备选）

```yaml
grafana:
  ldap:
    enabled: true
    config: |
      verbose_logging = true

      [[servers]]
      host = "ldap.example.com"
      port = 389
      use_ssl = false
      bind_dn = "cn=admin,dc=example,dc=com"
      bind_password = "examplePassword"
      search_filter = "(uid=%s)"
      search_base_dns = ["ou=foo,dc=example,dc=com"]

      [servers.attributes]
      name = "cn"
      surname = "sn"
      username = "uid"
      email = "mail"

      [[servers.group_mappings]]
      group_dn = "cn=g-admin,ou=Group,dc=example,dc=com"
      org_role = "Editor"
```

---

## JupyterHub 集成

### OAuth 认证配置

`values.yaml`:

```yaml
hub:
  config:
    Authenticator:
      admin_users:
        - "admin"
      allow_all: true
      auto_login: true

    GenericOAuthenticator:
      client_id: "your-client-id"
      client_secret: "your-client-secret"
      login_service: "OAuth"
      authorize_url: "https://authentik.example.com/application/o/authorize/"
      token_url: "https://authentik.example.com/application/o/token/"
      userdata_url: "https://authentik.example.com/application/o/userinfo/"
      oauth_callback_url: "https://jupyter.example.com/hub/oauth_callback"
      scope:
        - "openid"
        - "email"
        - "profile"
      username_claim: "sub"

    JupyterHub:
      authenticator_class: "generic-oauth"
      admin_access: true
```

### 配置说明

| 配置项 | 说明 | 可选值 |
|--------|------|--------|
| `allow_all` | 是否允许所有用户 | `true` / `false` |
| `username_claim` | 用户名字段 | `sub` / `email` / `preferred_username` |
| `oauth_callback_url` | 回调地址 | 必须在 Authentik 中配置 |

### LDAP 认证配置（备选）

```yaml
hub:
  config:
    LDAPAuthenticator:
      server_address: "ldap.example.com"
      use_ssl: true
      bind_dn_template:
        - "uid={username},ou=foo,ou=People,dc=example,dc=com"
      escape_userdn: true

    JupyterHub:
      authenticator_class: "ldapauthenticator.LDAPAuthenticator"
```

---

## OAuth vs LDAP 对比

| 特性 | OAuth | LDAP |
|------|-------|------|
| 配置复杂度 | 简单 | 中等 |
| 统一登录（SSO） | ✅ 支持 | ❌ 不支持 |
| 离线访问 | ✅ Refresh Token | ❌ 需在线验证 |
| 安全性 | ✅ PKCE 增强 | 一般 |
| 适用场景 | 现代 SSO 架构 | 传统 LDAP 环境 |

---

## 常见问题

### userinfo 路径问题

Authentik 的 userinfo 端点必须是 `/userinfo/`（带斜杠），否则会报错：
```
ERROR - OAUTH userinfo does not have username or email {}
```

### 重定向 URI 不匹配

Grafana 报错 `redirect_uri_mismatch`：确保 `root_url` 与实际访问域名一致。

### 回调地址配置

JupyterHub 的 `oauth_callback_url` 必须在 Authentik 应用的"重定向 URI"中添加。

---

## 参考文档

- [Airflow OAuth 文档](https://airflow.apache.org/docs/apache-airflow/2.8.3/security/webserver.html)
- [Grafana OAuth 配置](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/generic-oauth/)
- [JupyterHub OAuth 认证](https://z2jh.jupyter.org/en/stable/administrator/authentication.html)]]></content:encoded>
            <author>yupanzi</author>
            <category>Authentication</category>
            <category>OAuth2</category>
            <category>Authentik</category>
        </item>
        <item>
            <title><![CDATA[Sentry 在 K8s 中的部署与问题解决]]></title>
            <link>https://yupanzi.com/posts/sentry-k8s-deployment/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/sentry-k8s-deployment/</guid>
            <pubDate>Fri, 31 May 2024 09:58:29 GMT</pubDate>
            <description><![CDATA[使用 Helm 在 K8s 部署 Sentry，解决 ClickHouse 分布式表问题]]></description>
            <content:encoded><![CDATA[在 Kubernetes 集群中部署 Sentry 错误监控系统。官方没有提供 K8s 部署文档，但我们可以使用社区的 [Helm Chart](https://github.com/sentry-kubernetes/charts)。

## 版本信息

- App Version: 24.5.0
- Chart Version: 23.1.0

## 核心配置

### values.yaml

```yaml
# 文件存储配置
filestore:
  backend: filesystem
  filesystem:
    path: /var/lib/sentry/files
    persistence:
      accessMode: ReadWriteMany  # 需要 ReadWriteMany 存储
      enabled: true
      persistentWorkers: true
      size: 10Gi
      storageClass: "efs"  # 使用支持 RWX 的存储类

# Ingress 配置
nginx:
  ingress:
    enabled: true
    hostname: sentry.company.com
    ingressClassName: "alb"  # 使用 AWS ALB
    path: /
    pathType: Prefix

# 数据库配置
postgresql:
  postgresqlPassword: "examplePassword"
  postgresqlPostgresPassword: "examplePassword"

# Source Maps 支持
sourcemaps:
  enabled: true

# 初始管理员用户
user:
  create: true
  email: example@company.com
  password: examplePassword

# Zookeeper 数据清理
zookeeper:
  autopurge:
    purgeInterval: 3
    snapRetainCount: 3
```

### 配置说明

| 配置项 | 说明 | 注意事项 |
|--------|------|----------|
| filestore | 文件存储 | 必须使用 ReadWriteMany 存储类 |
| postgresql | 数据库密码 | 防止启动报错，[详见 issue](https://github.com/sentry-kubernetes/charts/issues/571#issuecomment-1039616281) |
| nginx.ingress | 入口配置 | 根据实际 Ingress Controller 调整 |
| zookeeper.autopurge | 自动清理 | 避免数据过大 |

## ClickHouse 报错修复

### 问题现象

部署后 `sentry-clickhouse` Pod 一直处于 `CrashLoopBackOff` 状态：

```bash
snuba.clickhouse.errors.ClickhouseWriterError: Method write is not supported
by storage Distributed with more than one shard and no sharding key provided
```

### 原因分析

分布式表 `metrics_raw_v2_dist` 缺少 sharding key 配置。相关 issue：
- [snuba#4897](https://github.com/getsentry/snuba/issues/4897)
- [charts#1272](https://github.com/sentry-kubernetes/charts/issues/1272)
- [charts#1042](https://github.com/sentry-kubernetes/charts/issues/1042)

### 解决步骤

进入 ClickHouse 容器重建表：

```bash
# 进入容器
kubectl exec -it sentry-clickhouse-0 -- bash

# 连接 ClickHouse
clickhouse-client -h sentry-clickhouse

# 删除旧表
DROP TABLE default.metrics_raw_v2_dist ON CLUSTER 'sentry-clickhouse' SYNC;

# 创建新表（带 sharding key）
CREATE TABLE default.metrics_raw_v2_dist ON CLUSTER 'sentry-clickhouse'
(
    `use_case_id` LowCardinality(String),
    `org_id` UInt64,
    `project_id` UInt64,
    `metric_id` UInt64,
    `timestamp` DateTime,
    `tags.key` Array(UInt64),
    `tags.value` Array(UInt64),
    `metric_type` LowCardinality(String),
    `set_values` Array(UInt64),
    `count_value` Float64,
    `distribution_values` Array(Float64),
    `materialization_version` UInt8,
    `retention_days` UInt16,
    `partition` UInt16,
    `offset` UInt64,
    `timeseries_id` UInt32
)
ENGINE = Distributed('sentry-clickhouse', 'default', 'metrics_raw_v2_local',
                     sipHash64('timeseries_id'));  -- 关键：添加 sharding key
```

执行后 Pod 会恢复正常。

## 替代方案

如果 Sentry 过于复杂，可以考虑 [GlitchTip](https://glitchtip.com/) - 一个轻量级的 Sentry 替代品。

## 注意事项

- 存储类必须支持 **ReadWriteMany**（如 EFS、NFS）
- PostgreSQL 密码务必提前配置，否则可能导致初始化失败
- ClickHouse 问题需要手动修复，Chart 未自动处理]]></content:encoded>
            <author>yupanzi</author>
            <category>Database</category>
            <category>Helm</category>
            <category>Kubernetes</category>
        </item>
        <item>
            <title><![CDATA[斐波那契数列的时间复杂度分析]]></title>
            <link>https://yupanzi.com/posts/fibonacci-sequence-time-complexity/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/fibonacci-sequence-time-complexity/</guid>
            <pubDate>Thu, 16 May 2024 16:34:22 GMT</pubDate>
            <description><![CDATA[对比递归、动态规划、矩阵快速幂等方法的时间复杂度和实现]]></description>
            <content:encoded><![CDATA[计算斐波那契数列的第 n 个数有多种方法，不同方法的时间复杂度差异很大。我们来看看常见的几种实现方式。

## 什么是斐波那契数列

除了第一个和第二个数外，任意一个数都等于前两个数之和：

```
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
```

## 四种实现方法对比

| 方法 | 时间复杂度 | 空间复杂度 | 特点 |
|------|-----------|-----------|------|
| 递归 | O(2^n) | O(n) | 简单但效率低 |
| 动态规划 | O(n) | O(n) | 避免重复计算 |
| 矩阵快速幂 | O(log n) | O(1) | 最快的精确方法 |
| Binet 公式 | O(1) | O(1) | 有精度限制 |

## 递归方法

最直观的实现，但效率很低：

```python
def fibonacci_recursive(n):
    if n <= 1:
        return n
    # 每次都会重复计算大量子问题
    return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
```

**问题**：计算 `f(5)` 时，`f(3)` 会被计算 2 次，`f(2)` 会被计算 3 次。

## 动态规划

用数组存储已计算的值，避免重复计算：

```python
def fibonacci_dp(n):
    if n <= 1:
        return n
    # 存储所有中间结果
    fib = [0] * (n+1)
    fib[1] = 1
    for i in range(2, n+1):
        fib[i] = fib[i-1] + fib[i-2]
    return fib[n]
```

**优化**：只需要保存前两个数，空间复杂度可以降到 O(1)。

## 矩阵快速幂

利用矩阵乘法性质，通过快速幂算法加速：

```python
def matrix_multiply(a, b):
    return [[sum(x * y for x, y in zip(row, col)) for col in zip(*b)] for row in a]

def matrix_power(matrix, n):
    result = [[1, 0], [0, 1]]  # 单位矩阵
    while n > 0:
        if n % 2 == 1:
            result = matrix_multiply(result, matrix)
        matrix = matrix_multiply(matrix, matrix)
        n //= 2
    return result

def fibonacci_matrix(n):
    if n <= 1:
        return n
    matrix = [[1, 1], [1, 0]]
    powered_matrix = matrix_power(matrix, n-1)
    return powered_matrix[0][0]
```

**原理**：利用 `[[1,1],[1,0]]^n` 的性质快速计算。

## Binet 公式

使用黄金分割比直接计算：

```python
import math

def fibonacci_binet(n):
    sqrt_5 = math.sqrt(5)
    phi = (1 + sqrt_5) / 2  # 黄金分割比
    return round((phi**n - (-1/phi)**n) / sqrt_5)
```

## 注意事项

- **递归方法**仅适合小数值，`n > 40` 时会非常慢
- **动态规划**是实际工作中最常用的方法，平衡了效率和易读性
- **矩阵快速幂**适合需要计算超大数值的场景
- **Binet 公式**在 `n` 很大时会因浮点数精度问题产生误差]]></content:encoded>
            <author>yupanzi</author>
            <category>Algorithm</category>
            <category>Python</category>
        </item>
        <item>
            <title><![CDATA[Base64 编码差异问题]]></title>
            <link>https://yupanzi.com/posts/base64-create-different/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/base64-create-different/</guid>
            <pubDate>Thu, 16 May 2024 16:23:13 GMT</pubDate>
            <description><![CDATA[Shell 和 Python 中 Base64 编码结果不同的原因及解决方法]]></description>
            <content:encoded><![CDATA[## 问题

为什么 Shell 中的 `echo 'abc' | base64` 和 Python 中的结果不一样？

## 原因

Shell 的 `echo` 命令默认会在输出末尾添加换行符 `\n`，这个换行符也会被 base64 编码。

## 解决方法

### Shell 中不添加换行符

使用 `echo -n` 选项：

```bash
echo -n 'abc' | base64
```

### Python 中不添加换行符

```python
import base64

data = 'abc'
encoded_data = base64.b64encode(data.encode('utf-8'))
print(encoded_data.decode('utf-8'))
```

现在两者结果一致！

### Python 中添加换行符（模拟默认 echo）

```python
import base64

data = 'abc\n'  # 添加换行符
encoded_data = base64.b64encode(data.encode('utf-8'))
print(encoded_data.decode('utf-8'))
```

## Python Base64 常用方法

### 标准编码/解码

```python
import base64

# 原始数据
data = "Python Base64 编码测试！"

# 编码
data_bytes = data.encode('utf-8')
encoded_data = base64.b64encode(data_bytes)
print("编码后:", encoded_data)

# 解码
decoded_data = base64.b64decode(encoded_data)
print("解码后:", decoded_data.decode('utf-8'))
```

### URL 安全编码/解码

将 `+` 和 `/` 替换为 `-` 和 `_`，适用于 URL 参数：

```python
# URL 安全编码
urlsafe_encoded = base64.urlsafe_b64encode(data_bytes)
print("URL安全编码:", urlsafe_encoded)

# URL 安全解码
urlsafe_decoded = base64.urlsafe_b64decode(urlsafe_encoded)
print("URL安全解码:", urlsafe_decoded.decode('utf-8'))
```

## 常用方法对比

| 方法 | 说明 |
|------|------|
| `b64encode(s)` | 标准 Base64 编码 |
| `b64decode(s)` | 标准 Base64 解码 |
| `urlsafe_b64encode(s)` | URL 安全编码（替换 +/） |
| `urlsafe_b64decode(s)` | URL 安全解码 |

## 注意事项

- Base64 是编码方式，不是加密方法
- 不应该用于保护敏感信息
- Shell 和 Python 编码差异主要源于换行符]]></content:encoded>
            <author>yupanzi</author>
            <category>Algorithm</category>
            <category>Python</category>
            <category>Shell</category>
        </item>
        <item>
            <title><![CDATA[Git 使用技巧与 CI/CD 实践]]></title>
            <link>https://yupanzi.com/posts/git-cicd-tips/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/git-cicd-tips/</guid>
            <pubDate>Thu, 16 May 2024 16:17:56 GMT</pubDate>
            <description><![CDATA[Git 常用操作技巧和 GitLab CI/CD 并行执行实践]]></description>
            <content:encoded><![CDATA[Git 版本控制和 CI/CD 流水线的常用技巧和最佳实践。

## Pull vs Fetch

`git pull` 和 `git fetch` 的区别：

```
git pull = git fetch + merge to local
```

| 命令 | 作用 |
|------|------|
| `git fetch` | 只下载远程更新到本地仓库，不合并 |
| `git pull` | 下载远程更新并自动合并到当前分支 |

**建议**：先 `fetch` 查看变更，再决定是否 `merge` 或 `rebase`。

> 参考: [Ruby China - git pull 和 git fetch 有什么区别](https://ruby-china.org/topics/15729)

---

## Rebase vs Merge

### Merge（合并）

- 创建新的合并提交
- 保留非线性历史
- 保持完整的分支记录
- 适合公共分支间合并

### Rebase（变基）

- 重写提交历史
- 创建线性历史
- 看起来像顺序开发
- 适合特性分支更新

### 使用原则

| 原则 | 说明 |
|------|------|
| 公共分支用 merge | 不要在公共分支（main/develop）上 rebase |
| 特性分支用 rebase | 更新特性分支或准备合并到主分支 |
| 团队统一规范 | 确保团队理解并遵守相同的工作流程 |
| 操作前备份 | rebase 前创建备份分支或推送到远程 |

---

## 切换默认分支（main → master）

### 1. 创建并推送 master 分支

```bash
# 从 main 创建 master
git branch master main

# 推送到远程
git push origin master
```

### 2. 在远程仓库更改默认分支

- **GitHub**: Settings → Branches → Default branch → 选择 master
- **GitLab**: Settings → Repository → Default Branch → 选择 master
- **Bitbucket**: Repository settings → Branches → Main branch → 选择 master

### 3. 删除旧的 main 分支（可选）

```bash
# 切换到 master
git checkout master

# 删除本地 main
git branch -d main

# 删除远程 main
git push origin --delete main
```

**注意**：更改默认分支会影响其他开发者和 CI/CD，需提前通知团队。

---

## 分支同步（develop → master）

### 方法一：Reset（重置，丢弃所有修改）

```bash
# 拉取最新 master
git checkout master
git pull origin master

# 重置 develop 到 master
git checkout develop
git reset --hard origin/master
```

**警告**：会丢弃 develop 上所有不在 master 的提交。

### 方法二：Merge（合并，保留历史）

```bash
git checkout develop
git merge master
```

保留 develop 的提交历史，将 master 的更改合并进来。

### 方法三：Rebase（变基，线性历史）

```bash
git checkout develop
git rebase master
```

将 develop 的更改重新应用到 master 之上，可能产生冲突。

### 方法四：重建分支（彻底同步）

```bash
git checkout master
git branch -D develop           # 删除旧 develop
git checkout -b develop         # 基于 master 创建新 develop
git push -f origin develop      # 强制推送
```

**警告**：强制推送会重写远程历史，需与团队沟通。

### 安全建议

操作前创建备份：

```bash
git checkout develop
git branch backup_develop  # 创建备份分支
```

出现问题时可用备份恢复。

---

## GitLab CI/CD 并行执行

### 并行执行原理

GitLab CI/CD 中，同一个 stage 的多个 job 默认会并行执行（前提是有足够的 Runner）。

### 方法一：同一 Stage 多个 Job

```yaml
stages:
  - build
  - test

job1:
  stage: build
  script:
    - echo "This is job1"

job2:
  stage: build
  script:
    - echo "This is job2"

test1:
  stage: test
  script:
    - echo "This is test1"

test2:
  stage: test
  script:
    - echo "This is test2"
```

- `job1` 和 `job2` 在 `build` 阶段并行执行
- `test1` 和 `test2` 在 `test` 阶段并行执行

### 方法二：使用 parallel 关键字

将单个作业分成多个并行实例：

```yaml
test:
  stage: test
  script:
    - echo "This is a parallel test"
  parallel: 3
```

这会启动 3 个并行的 `test` 作业实例。

### Runner 并发配置

确保 GitLab Runner 支持并发执行。

在 Runner 配置文件中设置 `concurrent` 值：

```toml
concurrent = 10
```

如果并发数设置过低，作业会排队等待，无法真正并行。

### 注意事项

- 并行执行需要足够的可用 Runner
- Runner 的 `concurrent` 值决定了最大并发作业数
- 不同 stage 之间是串行执行的
- 使用 `parallel` 关键字可以水平扩展单个作业]]></content:encoded>
            <author>yupanzi</author>
            <category>Git</category>
            <category>CI/CD</category>
            <category>GitLab</category>
        </item>
        <item>
            <title><![CDATA[Authentik 集成钉钉 OAuth2 登录]]></title>
            <link>https://yupanzi.com/posts/authentik-use-dingtalk-login/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/authentik-use-dingtalk-login/</guid>
            <pubDate>Wed, 10 Apr 2024 13:44:53 GMT</pubDate>
            <description><![CDATA[通过 AWS Serverless 实现 Authentik 集成钉钉 OAuth2 登录的完整方案]]></description>
            <content:encoded><![CDATA[## 背景

Authentik 是一个开源的身份验证和授权服务，支持多种身份验证方式。钉钉提供了 OAuth2 授权机制，但其接口是非标准的，需要通过自定义转换服务来适配。

本文介绍如何使用 Authentik 的 OAuth2 提供程序集成钉钉登录，让用户可以使用钉钉账户登录应用。

参考文档：[Authentik OAuth Sources](https://docs.goauthentik.io/integrations/sources/oauth/)

## 实现步骤

### 1. 创建钉钉应用

参考以下文档创建钉钉应用：
- [GitLab DingTalk 集成](https://docs.gitlab.com/ee/integration/ding_talk.html)
- [Casdoor DingTalk OAuth](https://casdoor.org/zh/docs/provider/oauth/DingTalk/)

#### 1.1 钉钉 OAuth 接口说明

官方文档：[获取登录用户的访问凭证](https://open.dingtalk.com/document/orgapp/obtain-identity-credentials)

**OAuth2 流程（钉钉版本）：**

<details>
<summary>接口示例</summary>

```text
钉钉 OAuth2 协议流程

1. 授权请求
GET https://login.dingtalk.com/oauth2/auth?
redirect_uri=https%3A%2F%2Fwww.example.com%2F&response_type=code&client_id=dingyourclientid&scope=openid&prompt=consent

2. 回调地址格式
https://www.example.com/?authCode=6b427e8bfab83e93bedd13f16a430702

3. 获取 Token
POST https://api.dingtalk.com/v1.0/oauth2/userAccessToken
Content-Type: application/json

{
  "clientId": "ding your id",
  "clientSecret": "your secret",
  "code": "6b427e8bfab83e93bedd13f16a430702",
  "grantType": "authorization_code"
}

响应：
{
  "expireIn": 7200,
  "accessToken": "a8f4e3215a703ce9a7164e91dbab53c0",
  "refreshToken": "b13e5a61b421342d95d86c9e64c275c6"
}

4. 获取用户信息
GET https://api.dingtalk.com/v1.0/contact/users/me
x-acs-dingtalk-access-token: a8f4e3215a703ce9a7164e91dbab53c0
Content-Type: application/json

响应：
{
  "nick": "AWIS ME",
  "unionId": "D578iS5hxxxx",
  "avatarUrl": "https://static-legacy.dingtalk.com/media/lADPGT5i9m5ZyXDNA4LNAtA_720.jpg",
  "openId": "WySPOpXqxE",
  "mobile": "1350xxxxxxxx",
  "stateCode": "86",
  "email": "xxxu@xxx.com"
}
```

</details>

**参考实现：**
- [Directus 讨论示例](https://github.com/directus/directus/discussions/11881)
- [apiproxy 实现](https://github.com/xu4wang/apiproxy/blob/main/src/handlers/oauth_dingtalk.ts)
- [Kratos 实现](https://github.com/ory/kratos/blob/eb67bed1f26d2c7ff10e5481b679b2213b44676d/selfservice/strategy/oidc/provider_dingtalk.go)

### 2. 配置 Authentik

参考：[Twitch 集成文档](https://docs.goauthentik.io/integrations/sources/twitch/)

**关键配置项：**

| 配置项 | 值 |
|--------|-----|
| 身份验证类型 | OpenID Connect |
| Scopes | `openid` |
| Authorization URL | `https://login.dingtalk.com/oauth2/auth?prompt=consent` |
| Token URL | 自定义转换服务 URL（见下文） |
| User Info URL | 自定义转换服务 URL（见下文） |

> **注意**：钉钉的 OAuth2 接口是非标准的（命名方法和参数格式有差异），需要自己实现转换服务。参考：[知乎文章](https://zhuanlan.zhihu.com/p/666423994)

### 3. 实现 OAuth2 转换服务

使用 AWS Serverless（Lambda）实现，将钉钉接口转换为标准 OAuth2 格式。

#### 3.1 Token 接口

<details>
<summary>📄 /auth/dingtalk/token</summary>

```python
import requests
import json
from base64 import b64decode
from urllib.parse import parse_qs

TOKEN_URL = 'https://api.dingtalk.com/v1.0/oauth2/userAccessToken'

def parse_form_data_to_json(form_data):
    parsed_data = parse_qs(form_data)
    result = {k: v[0] for k, v in parsed_data.items()}
    return result

def main(event, context):
    print(f"event:\n{event}")
    s = event.get("body")
    if event.get("isBase64Encoded") and s:
        s = b64decode(s).decode("utf-8")
    body = parse_form_data_to_json(s)

    headers = {"Content-Type": "application/json"}
    response = requests.post(TOKEN_URL, json={
        'clientId': body.get('client_id'),
        'clientSecret': body.get('client_secret'),
        'code': body.get('code'),
        'grantType': body.get('grant_type'),
    }, headers=headers)
    response.raise_for_status()
    res = response.json()

    result = {
        # 'refresh_token': res.get('refreshToken'),
        'access_token': res.get('accessToken'),
        'expires_in': res.get('expiresIn'),
        'token_type': 'Bearer',
    }

    return {"statusCode": 200, "body": json.dumps(result)}
```

</details>

#### 3.2 用户信息接口

<details>
<summary>📄 /auth/dingtalk/profile</summary>

```python
import requests
import json

URL = 'https://api.dingtalk.com/v1.0/contact/users/me'

def main(event, context):
    print(f"event:\n{event}")
    access_token = event.get('headers', {}).get('authorization', '')
    access_token = access_token.replace('Bearer ', '')
    print(access_token)

    headers = {
        "Content-Type": "application/json",
        'x-acs-dingtalk-access-token': access_token,
    }
    response = requests.get(URL, headers=headers)
    response.raise_for_status()
    user_info = response.json()
    print(user_info)

    result = {
        # 'issuer': userInfoURL,
        # 'picture': user_info.get('avatarUrl'),
        'sub': user_info['openId'],  # 关键字段，必须有
        'nickname': user_info['nick'],
        'name': user_info['nick'],
        'email': user_info['email']
    }
    return {"statusCode": 200, "body": json.dumps(result)}
```

</details>

## 常见问题

### 错误：Could not determine id.

**原因**：返回的用户信息中缺少 `sub` 字段。

**解决**：确保转换服务返回包含 `sub` 字段的 JSON，`sub` 通常对应钉钉的 `openId`。

**相关源码：**
- [OAuth 类型定义](https://github.com/goauthentik/authentik/blob/main/authentik/sources/oauth/types/oidc.py)
- [回调处理逻辑](https://github.com/goauthentik/authentik/blob/main/authentik/sources/oauth/views/callback.py#L59)

**错误日志示例：**

```json
{
  "auth_via": "unauthenticated",
  "domain_url": "example.com",
  "event": "Authentication Failure",
  "host": "example.com",
  "level": "warning",
  "logger": "authentik.sources.oauth.views.callback",
  "pid": 4721,
  "reason": "Could not determine id.",
  "request_id": "28a8d8818c63441da41051455c32d437",
  "schema_name": "public",
  "timestamp": "2024-04-09T10:30:48.464283"
}
```

## 总结

通过自定义转换服务，成功将钉钉的非标准 OAuth2 接口适配为 Authentik 可识别的标准格式，实现了钉钉登录集成。核心要点：

1. 钉钉接口参数命名与标准 OAuth2 不同（如 `clientId` vs `client_id`）
2. 必须在用户信息中返回 `sub` 字段
3. 使用 Serverless 服务作为中间层进行协议转换]]></content:encoded>
            <author>yupanzi</author>
            <category>AWS</category>
            <category>Authentication</category>
            <category>OAuth2</category>
            <category>Python</category>
        </item>
        <item>
            <title><![CDATA[Python 批量移动 S3 对象]]></title>
            <link>https://yupanzi.com/posts/chatgpt-help-s3-move-object/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/chatgpt-help-s3-move-object/</guid>
            <pubDate>Thu, 21 Mar 2024 14:46:34 GMT</pubDate>
            <description><![CDATA[使用 Python 和 boto3 批量重命名移动 S3 对象]]></description>
            <content:encoded><![CDATA[## 需求

将 S3 中的文件路径从：
```
s3://foobar/expense/2023/12/02 00:00:00/2023-12-02 00:00:00.json
```

移动为：
```
s3://foobar/expense/2023/12/02/2023-12-02.json
```

需要处理从某个日期到今天的所有文件。

## 实现方式

S3 没有直接的"移动"操作，需要先复制（COPY）再删除（DELETE）原对象。

### 安装依赖

```bash
pip install boto3
```

### Python 脚本

```python
import boto3
import datetime

# 初始化 S3 客户端
s3_client = boto3.client('s3')

# S3 桶名称
bucket_name = 'foobar'

# 设定日期范围
start_date = datetime.datetime(2023, 12, 1)
end_date = datetime.datetime.now()

# 遍历日期范围
current_date = start_date
while current_date <= end_date:
    # 构建原始和目标键
    original_key = f'expense/{current_date.year}/{current_date.month:02d}/{current_date.day:02d} 00:00:00/{current_date.strftime("%Y-%m-%d")} 00:00:00.json'
    new_key = f'expense/{current_date.year}/{current_date.month:02d}/{current_date.day:02d}/{current_date.strftime("%Y-%m-%d")}.json'

    # 复制对象
    copy_source = {
        'Bucket': bucket_name,
        'Key': original_key
    }
    try:
        s3_client.copy(copy_source, bucket_name, new_key)
        print(f'Copied: {original_key} to {new_key}')

        # 删除原始对象
        s3_client.delete_object(Bucket=bucket_name, Key=original_key)
        print(f'Deleted: {original_key}')
    except s3_client.exceptions.NoSuchKey:
        print(f'No such key: {original_key}')
    except Exception as e:
        print(f'Error: {e}')

    # 移至下一天
    current_date += datetime.timedelta(days=1)
```

## 注意事项

- 确保 AWS 凭证已配置（环境变量或 `.aws/credentials` 文件）
- 需要有足够的 S3 操作权限（复制和删除）
- 运行前建议备份重要数据
- 大量文件时可能需要处理 S3 分页响应]]></content:encoded>
            <author>yupanzi</author>
            <category>AWS</category>
            <category>Cloud</category>
            <category>Python</category>
            <category>S3</category>
        </item>
        <item>
            <title><![CDATA[Kubernetes 运维实践指南]]></title>
            <link>https://yupanzi.com/posts/kubernetes-ops-guide/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/kubernetes-ops-guide/</guid>
            <pubDate>Thu, 21 Mar 2024 09:25:03 GMT</pubDate>
            <description><![CDATA[Kubernetes 运维实践，包括 kubectl 技巧、JupyterHub 配置等]]></description>
            <content:encoded><![CDATA[Kubernetes 集群运维的实用技巧和最佳实践。

## Kubectl 常用技巧

### 批量删除 Job

删除所有以 `foo-bar` 开头的 Job：

```bash
kubectl get jobs -o name | grep 'foo-bar' | xargs kubectl delete
```

**命令说明：**

| 部分 | 作用 |
|------|------|
| `kubectl get jobs -o name` | 列出所有 Job 名称 |
| `grep 'foo-bar'` | 过滤以 `foo-bar` 开头的 Job |
| `xargs kubectl delete` | 批量删除 |

**预览删除列表（不执行删除）：**

```bash
kubectl get jobs -o name | grep 'foo-bar'
```

**指定命名空间：**

```bash
kubectl get jobs -n your-namespace -o name | grep 'foo-bar' | xargs kubectl delete -n your-namespace
```

### 设置默认 StorageClass

**1. 查看现有 StorageClass**

```bash
kubectl get storageclass
```

**2. 设置默认 StorageClass**

将 `my-storage-class` 设置为默认：

```bash
kubectl patch storageclass my-storage-class -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
```

**3. 移除其他默认标记**

如果之前有其他默认 StorageClass，需要移除：

```bash
kubectl patch storageclass old-default-storage-class -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
```

**4. 验证设置**

```bash
kubectl get storageclass
```

查看 `my-storage-class` 的注解是否为 `true`。

---

## 镜像管理

### 查询特定镜像仓库的所有镜像

查询 Kubernetes 集群上所有以 `docker.foobar.com` 开头的 Pod 镜像，并去重排序：

```bash
kubectl get pods --all-namespaces -o jsonpath="{..image}" | tr -s '[[:space:]]' '\n' | grep '^docker\.foobar\.com' | sort -u
```

**命令说明：**

| 命令部分 | 作用 |
|---------|------|
| `kubectl get pods --all-namespaces -o jsonpath="{..image}"` | 获取所有命名空间中 Pod 的镜像信息 |
| `tr -s '[[:space:]]' '\n'` | 将空格转换为换行符，每个镜像占一行 |
| `grep '^docker\.foobar\.com'` | 过滤以 `docker.foobar.com` 开头的镜像 |
| `sort -u` | 排序并去重（`-u` 参数表示唯一） |

**脚本方式：**

```bash
#!/bin/bash

# 查询并去重排序镜像
kubectl get pods --all-namespaces -o jsonpath="{..image}" | \
  tr -s '[[:space:]]' '\n' | \
  grep '^docker\.foobar\.com' | \
  sort -u
```

保存为 `get-images.sh` 后执行：

```bash
chmod +x get-images.sh
./get-images.sh
```

**注意：**
- 需要有查询所有命名空间 Pod 的权限
- 确保 kubectl 配置正确并能访问集群

---

## JupyterHub 在 K8s 上的配置

### LDAP 非加密端口支持

**问题**：JupyterHub 的 LDAP 认证默认只支持 LDAPS（加密端口），对于使用非加密 LDAP 端口的场景无法正常工作。

**原因**：`jupyterhub-ldapauthenticator` 包中的 `use_ssl` 参数逻辑有问题，导致非加密连接时使用了错误的绑定模式。

**解决方案**：通过 Dockerfile 修改认证器源码，修复 `use_ssl` 逻辑：

```Dockerfile
FROM jupyterhub/k8s-hub:latest

USER root
# 修复 LDAP authenticator use_ssl 问题
RUN FILEPATH=`python -c "import pkg_resources; import os; print(os.path.join(pkg_resources.get_distribution('jupyterhub-ldapauthenticator').location, 'ldapauthenticator', 'ldapauthenticator.py'))"` && \
    sed -i 's/ldap3.AUTO_BIND_NO_TLS if self.use_ssl else ldap3.AUTO_BIND_TLS_BEFORE_BIND/ldap3.AUTO_BIND_NO_TLS if not self.use_ssl else ldap3.AUTO_BIND_TLS_BEFORE_BIND/g' ${FILEPATH}

USER jovyan
```

这个修复将原来的 `if self.use_ssl` 改为 `if not self.use_ssl`，确保：
- 非加密连接时使用 `AUTO_BIND_NO_TLS`
- 加密连接时使用 `AUTO_BIND_TLS_BEFORE_BIND`

---

### ProfileList 配置

在 K8s 上部署 JupyterHub 时，可以让用户在启动 Notebook 时选择不同的资源配置（如 4G/8G 内存，不同节点类型）。

**配置方式**：通过 `KubeSpawner.profile_list` 配置多个资源配置选项。

**values.yaml 配置示例**：

```yaml
hub:
  extraConfig:
    profileList: |
      c.KubeSpawner.profile_list = [
          {
              'display_name': '小型环境 (4G 内存)',
              'description': '适合轻量级数据分析任务',
              'kubespawner_override': {
                  'cpu_limit': 1,
                  'cpu_guarantee': 0.5,
                  'mem_limit': '4G',
                  'mem_guarantee': '2G',
                  'node_selector': {
                      'disktype': 'ssd',
                      'workload': 'general'
                  }
              }
          },
          {
              'display_name': '大型环境 (8G 内存)',
              'description': '适合大规模数据处理和模型训练',
              'kubespawner_override': {
                  'cpu_limit': 2,
                  'cpu_guarantee': 1,
                  'mem_limit': '8G',
                  'mem_guarantee': '4G',
                  'node_selector': {
                      'disktype': 'ssd',
                      'workload': 'compute'
                  }
              }
          },
          {
              'display_name': 'GPU 环境',
              'description': '配备 GPU 用于深度学习',
              'kubespawner_override': {
                  'cpu_limit': 4,
                  'mem_limit': '16G',
                  'extra_resource_limits': {
                      'nvidia.com/gpu': '1'
                  },
                  'node_selector': {
                      'accelerator': 'nvidia-tesla-v100'
                  }
              }
          }
      ]
```

**配置项说明**：

| 配置项 | 说明 | 示例 |
|--------|------|------|
| `cpu_limit` | CPU 上限 | `2` (2 核) |
| `cpu_guarantee` | CPU 保证值 | `1` (1 核) |
| `mem_limit` | 内存上限 | `'8G'` |
| `mem_guarantee` | 内存保证值 | `'4G'` |

**节点选择**：

```python
'node_selector': {
    'disktype': 'ssd',        # 磁盘类型
    'workload': 'compute',    # 工作负载类型
    'zone': 'us-east-1a'      # 可用区
}
```

**Tolerations（容忍度）**：

```python
'tolerations': [
    {
        'key': 'gpu',
        'operator': 'Equal',
        'value': 'true',
        'effect': 'NoSchedule'
    }
]
```

**部署配置**：

```bash
RELEASE=jhub
NAMESPACE=jhub

helm upgrade --install $RELEASE jupyterhub/jupyterhub \
    --namespace $NAMESPACE \
    --create-namespace \
    --version=3.0.0 \
    --values config.yaml
```

---

## 注意事项

- 批量删除操作不可逆，执行前务必确认
- 同一时间只能有一个默认 StorageClass
- 修改 StorageClass 需要相应的集群权限
- 节点必须提前打好对应的标签：`kubectl label nodes node1 disktype=ssd`
- `cpu_limit` 和 `mem_limit` 会限制 Pod 的最大资源使用
- GPU 资源需要先安装 NVIDIA Device Plugin

## 参考文档

- [JupyterHub 配置参考](https://z2jh.jupyter.org/en/latest/resources/reference.html)
- [KubeSpawner profile_list](https://jupyterhub-kubespawner.readthedocs.io/en/latest/spawner.html#kubespawner.KubeSpawner.profile_list)]]></content:encoded>
            <author>yupanzi</author>
            <category>Kubernetes</category>
            <category>JupyterHub</category>
            <category>LDAP</category>
        </item>
        <item>
            <title><![CDATA[手机和电脑]]></title>
            <link>https://yupanzi.com/posts/mobile-and-pc/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/mobile-and-pc/</guid>
            <pubDate>Thu, 08 Feb 2024 11:27:48 GMT</pubDate>
            <description><![CDATA[手机偏娱乐与便携、电脑偏效率与处理，聊聊两者使用场景的根本差异。]]></description>
            <content:encoded><![CDATA[最近在手机和电脑间切换，发现手机主要用于娱乐，使用上主要是方便，而电脑则是用于工作，使用上主要是偏向效率。

这两点的根本不同不能过于颠倒，比如你说用手机工作，用电脑娱乐。这种总是有些拧巴，为什么？

手机当前主要是触摸屏幕，所以对于容错性比较高的场景适用，而且最好是没有输入键盘的操作

电脑由于大部分不是触屏，所以对于精确效率的场景比较合适，尤其是键盘这种输入设备的操作

手机主要在于便携和多传感器，可以认为是一个采集终端

电脑主要在于物理设备和硬件，可以看作是一个处理中心

仔细品，看看现在这两个设备上的应用有哪些不同，以及流行的分别是什么就知道了，同时在开发对应的应用的同时也要按照这种设计顺应不同的方向，别搞反了，除非你是主打“差异化”]]></content:encoded>
            <author>yupanzi</author>
            <category>thoughts</category>
            <category>productivity</category>
        </item>
        <item>
            <title><![CDATA[网络诊断与运维技巧]]></title>
            <link>https://yupanzi.com/posts/network-diagnostic-tips/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/network-diagnostic-tips/</guid>
            <pubDate>Wed, 03 Jan 2024 18:13:47 GMT</pubDate>
            <description><![CDATA[网络运维常用技巧，包括 HTTP 状态码、TCP 端口测试等]]></description>
            <content:encoded><![CDATA[网络运维中常用的诊断和调试技巧。

## HTTP 状态码

HTTP 状态码用来表示请求是否成功，以及出现了什么错误。

### 状态码分类

| 范围 | 类型 | 说明 |
|------|------|------|
| 1xx (100-199) | 信息性 | 请求正在处理 |
| 2xx (200-299) | 成功 | 请求正常处理完毕 |
| 3xx (300-399) | 重定向 | 需要进一步操作 |
| 4xx (400-499) | 客户端错误 | 请求有问题或无法实现 |
| 5xx (500-599) | 服务器错误 | 服务器处理时出错 |

### 不常见的状态码

| 状态码 | 说明 | 备注 |
|--------|------|------|
| 101 | 切换协议 | 服务器同意切换到请求的协议，常用于 WebSocket |
| 104 | 连接重置 | Nginx 定义，客户端取消了请求 |
| 429 | 请求过多 | 触发了频率限制 |
| 499 | 客户端关闭连接 | Nginx 定义，客户端在服务器响应前关闭了连接 |

### 104 连接重置示例

```text
('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))
```

常见场景：AWS NAT Gateway 会在 30 秒内关闭空闲连接，导致客户端收到 104 错误。

参考：[AWS NAT Gateway 文档](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html)

### 499 客户端关闭连接

这是 Nginx 自定义的状态码，表示客户端在服务器准备响应前就关闭了连接，通常发生在用户取消请求时。

---

## TCP 端口连通性测试

需要测试远程服务器的某个端口（如 MySQL 的 3306）是否可访问。介绍几种常用的测试方法。

### 快速测试方法

**使用 nc (推荐)**

最简洁的方式：

```bash
nc -zv 192.168.1.100 3306
```

输出：

```
Connection to 192.168.1.100 3306 port [tcp/mysql] succeeded!
```

**参数说明**：
- `-z`：扫描模式，不发送数据
- `-v`：显示详细信息

**使用 telnet**

传统方式：

```bash
telnet 192.168.1.100 3306
```

成功时会显示：

```
Trying 192.168.1.100...
Connected to 192.168.1.100.
```

**退出方式**：按 `Ctrl + ]`，然后输入 `quit`

### 批量测试端口

**nc 批量扫描**

```bash
# 测试多个端口
for port in 3306 80 443 22; do
    nc -zv -w 3 192.168.1.100 $port
done
```

**扫描端口范围**

```bash
# 扫描 3000-3010 端口
nc -zv 192.168.1.100 3000-3010
```

### 高级测试方法

**使用 nmap**

功能最强大的扫描工具：

```bash
# 扫描单个端口
nmap -p 3306 192.168.1.100

# 扫描多个端口
nmap -p 3306,80,443 192.168.1.100

# 扫描端口范围
nmap -p 3000-4000 192.168.1.100

# 快速扫描（跳过主机发现）
nmap -Pn -p 3306 192.168.1.100
```

输出示例：

```
PORT     STATE SERVICE
3306/tcp open  mysql
```

**使用 timeout 避免卡住**

```bash
# 3 秒超时
timeout 3 bash -c "cat < /dev/null > /dev/tcp/192.168.1.100/3306"

# 检查返回值
if [ $? -eq 0 ]; then
    echo "端口 3306 开放"
else
    echo "端口 3306 关闭或超时"
fi
```

**使用 /dev/tcp (内置方法)**

不需要额外工具：

```bash
# Bash 内置 TCP 测试
(echo > /dev/tcp/192.168.1.100/3306) &>/dev/null && echo "开放" || echo "关闭"
```

### 编写测试脚本

```bash
#!/bin/bash

host="192.168.1.100"
port=3306
timeout=3

# 测试端口
if timeout $timeout bash -c "cat < /dev/null > /dev/tcp/$host/$port" 2>/dev/null; then
    echo "✓ $host:$port 可访问"
    exit 0
else
    echo "✗ $host:$port 不可访问"
    exit 1
fi
```

使用：

```bash
chmod +x test-port.sh
./test-port.sh
```

### 方法对比

| 方法 | 优点 | 缺点 | 适用场景 |
|------|------|------|----------|
| `nc` | 简单快速 | 需要安装 | 日常测试 |
| `telnet` | 通用性好 | 难以脚本化 | 交互式测试 |
| `nmap` | 功能强大 | 较慢，需要安装 | 批量扫描 |
| `/dev/tcp` | 无需工具 | 仅 Bash 支持 | 脚本中使用 |
| `timeout + /dev/tcp` | 内置 + 超时控制 | 语法复杂 | 生产脚本 |

### 安装工具

**CentOS/RHEL**

```bash
yum install nc nmap telnet
```

**Ubuntu/Debian**

```bash
apt-get install netcat nmap telnet
```

**macOS**

```bash
brew install netcat nmap telnet
```

### 注意事项

- `telnet` 连接成功后需要手动退出，不适合脚本
- `nc` 的 `-w` 参数可以设置超时时间
- `nmap` 扫描大量端口可能触发防火墙告警
- `/dev/tcp` 方法在某些受限环境下可能不可用
- 使用 `timeout` 命令避免无限等待]]></content:encoded>
            <author>yupanzi</author>
            <category>Network</category>
            <category>Shell</category>
            <category>Troubleshooting</category>
        </item>
        <item>
            <title><![CDATA[ChatGPT 发布一周年感想]]></title>
            <link>https://yupanzi.com/posts/chatgpt-one-year/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/chatgpt-one-year/</guid>
            <pubDate>Thu, 30 Nov 2023 12:00:00 GMT</pubDate>
            <description><![CDATA[这是最好的时代，也是最坏的时代。]]></description>
            <content:encoded><![CDATA[2022 年 11 月 30 日，ChatGPT 全球发布，开创了大语言模型的新时代。

从最开始的毫不在乎到现在的离不开，感受到的不只是震撼。

以前使用 Siri 总是觉着很蠢听不懂人话，而现在的 ChatGPT 从直观反馈来说，应该是不仅仅能听懂还能理解（所谓的人类理解），就算你中文夹杂着英文也是可以的，感觉语言的巴别塔好像被攻破了。

但是我觉着，如果将 ChatGPT 作为“好用”工具，主要体现在两点，理解需求和知识集合，这两点缺一不可，同时也相辅相成。

### 一、理解需求

很多人凭着新鲜感试用了下，感觉回答的也就很一般，不好用，甚至觉着它不明白我要问什么。可是真的是这样吗？

首先，很多人连需求都没有理清楚，而且有时候说话是有上下文的，想想看，如果一个陌生人突然问你“吃饭了吗”，你知道他究竟是什么意思吗？

GPT4 出来的时候我问了问题，它会将你的需求描述一下，按照它的理解方式去执行，这一点就很厉害了，因为现实中很多人都做不到。

当然，缺点也很明显， token 长度，就如同人的记忆，当然还是越大越好，就好像你的朋友一样，越熟悉越明白。

### 二、知识集合

ChatGPT 如果只是能理解你说的话虽然是进步但是也不会特别好用，主要是 OpenAI 这个公司整理了很多高质量数据，互联网海洋般的数据。

这一点才是我主要付费的原因，得益于互联网开放的精神，可能也恰恰是这些高质量数据才能通过量变引起质变。

现在 ChatGPT 就像一个特别好的老师，拥有整个互联网知识，而如何向老师学习在于你是如何使用的。

你可以问问题，你也可以将你理解的问题找他对答案，着实有趣。

当然，还是有一些专有知识无从获取，这点它就不如人意了，会出现一本正经的胡说八道了，这点也是广为诟病的问题。

所以它可以辅助你，但你最好是个专家能够辨别出 1% 这种级别的错误。对于编程倒是很好确定，执行下就行了，但是对于无法验证的领域就有些危险了。


### 预测推理

预测是我使用后最明显的感受，打字机效果的输出。通过我粗浅的分析，感觉人类也只不过是做了一些高级的预测，就好像说话一样——你真的懂你为什么说出下一个字吗？是不是脱口而出呢？

而推理如同思考一样，人脑也是，如果想一想再回答，那么效果可能会相对好些，也许 GPT 这条路可能会对人类的模拟更加进了一步！

### 未来展望

语言使人类产生文明，那么以后的人工智能时代是不是能够产生新的文明呢？

目前还不得而知，但是有一点即将发生，那就是改变生产关系。]]></content:encoded>
            <author>yupanzi</author>
            <category>AI</category>
        </item>
        <item>
            <title><![CDATA[兜兜转转，还是回到了 Hexo]]></title>
            <link>https://yupanzi.com/posts/start-hexo-blog/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/start-hexo-blog/</guid>
            <pubDate>Thu, 30 Nov 2023 08:00:00 GMT</pubDate>
            <description><![CDATA[从 WordPress 到 Org-mode 再到 Hexo，折腾了一圈终于找到合适的]]></description>
            <content:encoded><![CDATA[## 又换博客系统了

说起来有点惭愧，这个博客从 WordPress 折腾到 Org-mode，现在又换成了 Hexo。

之前在 [部署博客主机](/2017/04/28/deploy-blog-server/) 那篇文章里说过，从虚拟主机的 WordPress 到 VPS 的 WordPress 再到 Org-mode，中间几次折腾把文章颠簸的不剩什么了。那时候还说"需要不再折腾"，结果现在还是换了。

不过这次真的是最后一次了（大概）。

## 为什么不继续用 Org-mode

Org-mode 确实强大，Emacs 用户的最爱。但是我遇到几个问题：

1. **生态太小**：相比 Hexo/Hugo，Org-mode 的主题插件太少了
2. **工具链重**：需要 Emacs + Org-mode + org-publish，配置复杂
3. **协作困难**：如果想在其他机器写文章，还得配置一套 Emacs 环境

说白了，我只是想写点技术笔记，不需要 Org-mode 那么强大的功能。

## 为什么选择 Hexo

其实之前就考虑过 Hexo，这次终于下决心了。主要原因：

- **简单**：Node.js 生态，npm 一键安装，不需要复杂配置
- **快速**：生成速度快，本地预览秒开
- **生态好**：主题插件丰富，社区活跃
- **纯 Markdown**：不需要学 Org 语法，任何编辑器都能写

## 基本使用

Hexo 的常用命令就几个，记住了基本够用：

```bash
# 创建新文章
hexo new "文章标题"

# 本地预览（支持热更新）
hexo server

# 生成静态文件
hexo generate  # 或简写 hexo g

# 部署到远程
hexo deploy    # 或简写 hexo d

# 一键生成并部署
hexo g -d
```

本地预览会在 `http://localhost:4000` 启动服务，修改文章后自动刷新，体验很流畅。

## 迁移过程

从 Org-mode 迁移到 Hexo 还挺顺利：

1. **文章转换**：Org 文件转 Markdown，手动改了下格式
2. **图片资源**：都是静态文件，直接复制过来就行
3. **域名部署**：还是用之前的 VPS，Nginx 配置都不用改

唯一麻烦的是历史文章不多了，之前几次折腾已经丢了不少。不过也好，算是一次清理，留下的都是还有价值的。

## 使用感受

用了几个月，真香。

最大的感受就是**简单**。写文章就在本地编辑器写，想用 VS Code 用 VS Code，想用 Vim 用 Vim。写完 `hexo g -d` 发布，整个流程顺畅。

所有文章就是 Markdown 文件，用 Git 管理，想改历史文章直接改文件，想回滚直接 `git revert`。不像 WordPress 还要在数据库里捣鼓，也不像 Org-mode 需要配置一堆 elisp。

而且静态网站部署简单，之前配好的 Nginx + Let's Encrypt 继续用，不需要数据库，不需要 PHP，省心。

## 给折腾党的建议

如果你也在各种博客系统之间纠结：

- **别花太多时间选主题**：先用个简洁的开始写，主题以后可以换
- **Git 管理很重要**：文章都是本地文件，一定要推到远程仓库备份
- **够用就好**：别像我一样折腾，Hexo 就挺好的，别再换了

最后，静态博客不是万能的，需要评论、搜索这些功能要用第三方服务。但如果只是想写点技术笔记，Hexo 够了。

这次应该不会再换了（真的）。

更多信息查看 [Hexo 官方文档](https://hexo.io/docs/)]]></content:encoded>
            <author>yupanzi</author>
            <category>Hexo</category>
            <category>Blog</category>
        </item>
        <item>
            <title><![CDATA[AWS 上传文件 5GB 限制问题]]></title>
            <link>https://yupanzi.com/posts/aws-upload-file-size-limit/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/aws-upload-file-size-limit/</guid>
            <pubDate>Wed, 10 Oct 2018 10:00:00 GMT</pubDate>
            <description><![CDATA[解决 AWS 上传文件时 5GB 大小限制的方法]]></description>
            <content:encoded><![CDATA[## AWS 上传文件大小 5GB 限制

在使用 AWS 上传大文件时,可能会遇到 5GB 的限制问题。以下是两种解决方案:

### 1. 在 boto3 中使用 copy 而不是 copy_object

参考: [GitHub Issue #1715](https://github.com/boto/boto3/issues/1715)

### 2. 使用 aws-cli 的 --expected-size 参数

参考: [GitHub Issue #1090](https://github.com/aws/aws-cli/issues/1090)]]></content:encoded>
            <author>yupanzi</author>
            <category>AWS</category>
            <category>boto3</category>
        </item>
        <item>
            <title><![CDATA[macOS 系统配置与技巧]]></title>
            <link>https://yupanzi.com/posts/macos-system-tips/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/macos-system-tips/</guid>
            <pubDate>Wed, 10 Oct 2018 10:00:00 GMT</pubDate>
            <description><![CDATA[macOS 系统常用配置技巧，包括显示隐藏文件、禁用 .DS_Store 等]]></description>
            <content:encoded><![CDATA[macOS 有一些系统级的配置技巧，可以提升日常使用和开发体验。

## 显示隐藏文件

macOS 默认隐藏所有以 `.` 开头的文件（dotfiles），比如 `.gitignore`、`.env` 等配置文件。

### 方法对比

| 方法 | 适用场景 | 持久性 |
|------|---------|--------|
| 快捷键 | Finder、文件对话框 | 临时 |
| 终端命令 | 全局设置 | 永久 |
| ls 命令 | 仅终端查看 | 临时 |

### 快捷键切换（推荐）

在 Finder 或文件对话框中按 **Command + Shift + .** 即可快速切换显示/隐藏：

- 再按一次恢复隐藏
- 立即生效，无需重启
- 最常用的方法

### 终端全局设置

**显示所有隐藏文件**：

```bash
# 修改 Finder 设置
defaults write com.apple.finder AppleShowAllFiles -bool true

# 重启 Finder 使设置生效
killall Finder
```

**恢复隐藏**：

```bash
# 恢复默认设置
defaults write com.apple.finder AppleShowAllFiles -bool false

# 重启 Finder
killall Finder
```

### 终端查看隐藏文件

如果只需要在终端查看，用 `ls` 命令即可：

```bash
# 显示所有文件（包括隐藏文件）
ls -la

# 或者不显示 . 和 ..
ls -A
```

### 让单个文件永久可见

**情况 1：文件名以 . 开头**

这种文件只能通过**重命名**去掉开头的点：

```bash
# 例如让 .env 文件可见
mv .env env
```

> **注意**：这是 Unix 规则，`chflags` 命令无法改变这类文件的可见性。

**情况 2：被隐藏标记隐藏**

用 `chflags` 命令取消隐藏标记：

```bash
# 取消隐藏（-R 表示递归处理文件夹）
chflags -R nohidden /path/to/file

# 重新隐藏
chflags hidden /path/to/file
```

### 注意事项

开启全局显示后，会看到很多系统和应用的内部文件（如 `.DS_Store`、`.Trash` 等），**操作时注意别误删或修改这些文件**。

---

## 禁用 .DS_Store 文件

### 什么是 .DS_Store

.DS_Store (Desktop Services Store) 是 macOS 系统用来存储文件夹的自定义属性的隐藏文件，比如文件图标的位置、文件夹视图设置或背景图片。这个文件在本地使用没什么问题，但在网络共享、Git 仓库或 USB 设备中可能会造成困扰。

详细信息请参考: [Wikipedia - .DS_Store](https://en.wikipedia.org/wiki/.DS_Store)

### 查询当前设置状态

在修改设置前，我们可以先查看当前的配置状态：

```bash
# 查询网络卷的 .DS_Store 禁用状态
defaults read com.apple.desktopservices DSDontWriteNetworkStores

# 查询 USB 设备的 .DS_Store 禁用状态
defaults read com.apple.desktopservices DSDontWriteUSBStores

# 如果返回 1 表示已禁用，0 或错误提示表示未禁用（默认）
```

### 禁用 .DS_Store 文件创建

**禁用网络卷上的 .DS_Store**

在网络共享文件夹（如 SMB、AFP、NFS）上禁用：

```bash
defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true
```

**禁用 USB 设备上的 .DS_Store**

在外接 USB 存储设备上禁用：

```bash
defaults write com.apple.desktopservices DSDontWriteUSBStores -bool true
```

**让设置生效**

修改设置后需要重启 Finder：

```bash
killall Finder
```

**注意**：macOS 目前**无法完全禁用本地磁盘**上的 .DS_Store 文件创建，这是系统 Finder 正常工作所必需的。上述命令只对网络卷和 USB 设备有效。

### 清理现有的 .DS_Store 文件

**清理特定目录**：

```bash
# 清理当前目录及子目录
find . -name ".DS_Store" -type f -delete

# 清理指定目录（将 /path/to/dir 替换为实际路径）
find /path/to/dir -name ".DS_Store" -type f -delete
```

**清理整个系统（需谨慎）**：

```bash
# 清理整个用户目录
find ~ -name ".DS_Store" -type f -delete

# 清理整个系统（需要 sudo 权限，执行前请三思）
sudo find / -name ".DS_Store" -depth -exec rm {} \;
```

### Git 仓库处理

如果你在使用 Git，建议将 .DS_Store 加入 `.gitignore`：

```bash
echo ".DS_Store" >> .gitignore
```

已经提交到仓库的 .DS_Store 可以这样移除：

```bash
# 从 Git 索引中删除，但保留本地文件
find . -name .DS_Store -print0 | xargs -0 git rm -f --ignore-unmatch --cached

# 提交更改
git commit -m "Remove .DS_Store files"
```

### 恢复默认设置

如果想恢复 macOS 的默认行为（允许创建 .DS_Store）：

```bash
# 恢复网络卷设置
defaults delete com.apple.desktopservices DSDontWriteNetworkStores

# 恢复 USB 设备设置
defaults delete com.apple.desktopservices DSDontWriteUSBStores

# 重启 Finder 使设置生效
killall Finder
```

### 适用版本

以上命令在 macOS 10.12 (Sierra) 及更高版本（包括最新的 macOS Sequoia 15.x）上均可正常使用。`defaults` 命令是 macOS 系统的标准配置工具，向后兼容性良好。

## 参考链接

- [Wikipedia - .DS_Store](https://en.wikipedia.org/wiki/.DS_Store)
- [How to disable the creation of .DS_Store files for Mac users folders](https://www.techrepublic.com/article/how-to-disable-the-creation-of-dsstore-files-for-mac-users-folders/)]]></content:encoded>
            <author>yupanzi</author>
            <category>macOS</category>
            <category>Terminal</category>
        </item>
        <item>
            <title><![CDATA[SSH 完全配置指南]]></title>
            <link>https://yupanzi.com/posts/ssh-complete-guide/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/ssh-complete-guide/</guid>
            <pubDate>Tue, 19 Jun 2018 10:00:00 GMT</pubDate>
            <description><![CDATA[SSH 密钥生成、免密登录、跳板机配置的完整指南]]></description>
            <content:encoded><![CDATA[本文整合了 SSH 的常用配置，包括密钥生成、免密登录和跳板机代理。

## 密钥生成

### 生成 RSA 密钥对

```bash
ssh-keygen -t rsa -C "your-email@example.com"
```

生成后的文件：
- 私钥：`~/.ssh/id_rsa`
- 公钥：`~/.ssh/id_rsa.pub`

### 生成 Ed25519 密钥（推荐）

Ed25519 更安全、更快：

```bash
ssh-keygen -t ed25519 -C "your-email@example.com"
```

---

## 免密登录配置

### 方法一：ssh-copy-id

```bash
ssh-copy-id user@remote-host
```

### 方法二：手动复制公钥

```bash
# 查看公钥
cat ~/.ssh/id_rsa.pub

# 复制到远程服务器的 ~/.ssh/authorized_keys
```

### 权限设置

SSH 对权限有严格要求，权限过于宽松会导致认证失败：

```bash
chmod 700 ~/.ssh
chmod 600 ~/.ssh/*
```

| 文件/目录 | 推荐权限 |
|-----------|----------|
| `~/.ssh/` | 700 |
| `~/.ssh/id_rsa` | 600 |
| `~/.ssh/id_rsa.pub` | 644 |
| `~/.ssh/authorized_keys` | 600 |
| `~/.ssh/config` | 600 |

---

## SSH 配置文件

`~/.ssh/config` 可以简化 SSH 连接命令。

### 基本配置

```
Host myserver
    HostName 192.168.1.100
    User admin
    Port 22
    IdentityFile ~/.ssh/id_rsa
```

使用：

```bash
# 不需要记 IP 和用户名
ssh myserver
```

### 多个服务器配置

```
# 开发服务器
Host dev
    HostName dev.example.com
    User developer
    IdentityFile ~/.ssh/id_rsa_dev

# 生产服务器
Host prod
    HostName prod.example.com
    User admin
    IdentityFile ~/.ssh/id_rsa_prod
    Port 2222
```

---

## 跳板机配置

### 场景

需要通过跳板机（堡垒机）访问内网服务器。

```
本地 → 跳板机(Jump Host) → 目标服务器
```

### ProxyCommand 配置

在 `~/.ssh/config` 中：

```
# 跳板机配置
Host jump
    HostName jump.example.com
    User jumpuser
    IdentityFile ~/.ssh/id_rsa

# 内网服务器（通过跳板机）
Host internal
    HostName 10.0.0.100
    User admin
    IdentityFile ~/.ssh/id_rsa
    ProxyCommand ssh -q -W %h:%p jump
```

使用：

```bash
# 自动通过 jump 跳转到 internal
ssh internal
```

### ProxyJump（更简洁）

OpenSSH 7.3+ 支持更简洁的语法：

```
Host internal
    HostName 10.0.0.100
    User admin
    ProxyJump jump
```

### 参数说明

| 参数 | 说明 |
|------|------|
| `-q` | 静默模式，不输出警告 |
| `-W %h:%p` | 转发 stdin/stdout 到目标主机和端口 |
| `%h` | 目标主机名 |
| `%p` | 目标端口 |

### 多级跳转

```
Host target
    HostName 10.0.0.200
    User admin
    ProxyJump jump1,jump2
```

---

## 常见问题

### sign_and_send_pubkey: signing failed

通常是权限问题，检查 `~/.ssh` 目录权限：

```bash
chmod 700 ~/.ssh
chmod 600 ~/.ssh/*
```

### 连接超时

添加 Keep Alive 设置：

```
Host *
    ServerAliveInterval 60
    ServerAliveCountMax 3
```

### 首次连接确认

跳过主机指纹确认（仅限受信任环境）：

```
Host *
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null
```

---

## 实用配置示例

```
# 全局设置
Host *
    ServerAliveInterval 60
    AddKeysToAgent yes

# 跳板机
Host bastion
    HostName bastion.example.com
    User ops
    IdentityFile ~/.ssh/id_ed25519

# 内网开发服务器
Host dev-*
    User developer
    ProxyJump bastion

Host dev-web
    HostName 10.0.1.10

Host dev-db
    HostName 10.0.1.20

# GitHub
Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_key
```

---

## 参考资料

- [SSH 原理与运用](http://www.ruanyifeng.com/blog/2011/12/ssh_remote_login.html)
- [SSH ProxyCommand 详解](http://mingxinglai.com/cn/2015/07/ssh-proxycommand/)
- [OpenSSH Config 手册](https://man.openbsd.org/ssh_config)]]></content:encoded>
            <author>yupanzi</author>
            <category>SSH</category>
            <category>Linux</category>
            <category>Network</category>
        </item>
        <item>
            <title><![CDATA[Ubuntu 系统配置技巧]]></title>
            <link>https://yupanzi.com/posts/ubuntu-system-tips/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/ubuntu-system-tips/</guid>
            <pubDate>Tue, 19 Jun 2018 10:00:00 GMT</pubDate>
            <description><![CDATA[Ubuntu 系统常用配置技巧，包括更改主机名、时钟同步等]]></description>
            <content:encoded><![CDATA[Ubuntu 系统日常运维中的一些常用配置技巧。

## 更改主机名

在 Ubuntu 系统中,可以通过以下步骤更改主机名:

### 方法一：修改配置文件

1. 编辑 hostname 文件:

```sh
sudo vi /etc/hostname
```

2. 编辑 hosts 文件:

```sh
sudo vi /etc/hosts
```

在 hosts 文件中修改或添加:

```
127.0.1.1 [hostname]
```

### 方法二：使用 hostnamectl 命令

```sh
sudo hostnamectl set-hostname [hostname]
```

使用 `hostnamectl` 命令是最简单和推荐的方法,它会自动更新所有相关的配置文件。

### 验证更改

重启系统后,可以使用以下命令验证主机名是否更改成功:

```sh
hostname
# 或
hostnamectl
```

---

## 时钟同步设置

### 方法一：使用 timedatectl

**查看同步状态**：

```bash
timedatectl status
```

**启用 NTP 同步**：

```bash
sudo timedatectl set-ntp true
```

这会启用 `systemd-timesyncd` 服务，自动与外部 NTP 服务器同步时间。

### 方法二：安装 NTP 服务

适用于需要更精细控制的场景。

**1. 安装 NTP**

```bash
sudo apt-get update
sudo apt-get install ntp
```

**2. 配置 NTP 服务器**

编辑配置文件：

```bash
sudo nano /etc/ntp.conf
```

添加或修改 NTP 服务器：

```conf
server 0.ubuntu.pool.ntp.org
server 1.ubuntu.pool.ntp.org
server 2.ubuntu.pool.ntp.org
server 3.ubuntu.pool.ntp.org
```

**3. 重启服务**

```bash
sudo systemctl restart ntp
```

**4. 查看同步状态**

```bash
ntpq -p
```

### 两种方法对比

| 方法 | 优点 | 缺点 |
|------|------|------|
| timedatectl | 系统自带，简单快速 | 功能相对简单 |
| NTP 服务 | 功能强大，可精细配置 | 需要额外安装 |

### 注意事项

- 确保防火墙允许 UDP 123 端口（NTP 端口）
- 受限网络环境需配置特定 NTP 服务器
- 两种方法不要同时使用，选择其一即可]]></content:encoded>
            <author>yupanzi</author>
            <category>Ubuntu</category>
            <category>Linux</category>
        </item>
        <item>
            <title><![CDATA[容器与 K8s 故障排查指南]]></title>
            <link>https://yupanzi.com/posts/container-troubleshooting/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/container-troubleshooting/</guid>
            <pubDate>Tue, 10 Oct 2017 10:00:00 GMT</pubDate>
            <description><![CDATA[Docker X11 显示、EKS 节点丢失、Helm Release 问题的解决方案]]></description>
            <content:encoded><![CDATA[本文整合了 Docker 和 Kubernetes 常见故障的排查方法。

## Docker X11 显示问题

在 Docker 容器中运行图形界面应用时，可能遇到：

```
Error: cannot open display localhost:11.0
```

### 解决方案

在 `docker run` 时添加 `--net=host` 参数：

```bash
docker run --net=host -e DISPLAY=$DISPLAY your-image
```

---

## EKS 节点丢失

### 问题现象

- EKS 控制台看不到节点
- `kubectl get nodes` 可以正常显示
- Pod 调度正常

### 解决方法

在 `aws-auth` ConfigMap 中添加节点的 IAM Role 映射：

```bash
kubectl edit configmap aws-auth -n kube-system
```

添加配置：

```yaml
data:
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      rolearn: arn:aws:iam::111122223333:role/my-node-role
      username: system:node:{{EC2PrivateDNSName}}
```

### 获取节点 IAM Role

```bash
# 获取实例 ID
kubectl get nodes -o wide

# 查询 IAM Role
aws ec2 describe-instances --instance-ids i-xxxxx \
  --query 'Reservations[0].Instances[0].IamInstanceProfile.Arn'
```

---

## Helm Release 找不到

### 问题现象

```bash
$ helm list -n jupyter
# 没有 foobar

$ helm install foobar jupyterhub/jupyterhub -n jupyter
Error: release foobar already exists
```

### 原因分析

Release 可能处于失败或 pending 状态。

### 解决方法

**1. 查看所有状态的 Release**

```bash
helm list --all-namespaces --all
```

**2. 处理不同状态**

| 状态 | 处理方法 |
|------|----------|
| `failed` | `helm rollback` 或 `helm delete` |
| `pending-install` | `helm delete` 然后重新安装 |
| `pending-upgrade` | `helm rollback` 或强制升级 |

**3. 手动清理**

Helm 3 将 release 信息存储在 Secret 中：

```bash
# 查找
kubectl get secret -n jupyter | grep foobar

# 删除
kubectl delete secret sh.helm.release.v1.foobar.v1 -n jupyter
```

**4. 重新安装**

```bash
helm install foobar jupyterhub/jupyterhub -n jupyter
```

### 预防措施

```bash
# 使用 --atomic，失败自动回滚
helm install foobar chart -n jupyter --atomic

# 设置超时
helm install foobar chart -n jupyter --timeout 5m
```

---

## 完整排查流程

```bash
# 1. 查看所有 release
helm list -n namespace --all

# 2. 查看详细状态
helm status release-name -n namespace

# 3. 查看历史
helm history release-name -n namespace

# 4. 尝试回滚
helm rollback release-name 1 -n namespace

# 5. 如果失败，手动清理
kubectl get secret -n namespace | grep release-name
kubectl delete secret sh.helm.release.v1.release-name.v1 -n namespace

# 6. 重新安装
helm install release-name chart -n namespace
```

---

## 参考链接

- [Docker X11 Display Issue](https://stackoverflow.com/questions/38249629)
- [EKS aws-auth ConfigMap](https://docs.aws.amazon.com/eks/latest/userguide/auth-configmap.html)
- [Helm Troubleshooting](https://helm.sh/docs/faq/troubleshooting/)]]></content:encoded>
            <author>yupanzi</author>
            <category>Docker</category>
            <category>Kubernetes</category>
            <category>Troubleshooting</category>
        </item>
        <item>
            <title><![CDATA[Emacs 使用技巧]]></title>
            <link>https://yupanzi.com/posts/emacs-tips/</link>
            <guid isPermaLink="false">https://yupanzi.com/posts/emacs-tips/</guid>
            <pubDate>Tue, 10 Oct 2017 10:00:00 GMT</pubDate>
            <description><![CDATA[Emacs 编辑器的实用技巧和配置]]></description>
            <content:encoded><![CDATA[## Buffer 操作

### 从终端 Buffer 切换到其他 Buffer

在终端模式下,C-x 键绑定变成了 C-c。

参考: [How to switch to a different buffer from a terminal buffer](https://stackoverflow.com/questions/2173356/how-to-switch-to-a-different-buffer-from-a-terminal-buffer)

### Buffer 关闭操作

- `C-x k` - 关闭当前 buffer
- `M-x kill-some-buffers` - 选择性关闭 buffers
- `M-x kill-matching-buffers` - 关闭匹配的 buffers

## Term-mode 配置

### 禁用 evil-mode

```elisp
(evil-set-initial-state 'term-mode 'emacs)
```

参考:
- [After-advice for disabling evil-mode in ansi-term has no effect](https://emacs.stackexchange.com/questions/32234/after-advice-for-disabling-evil-mode-in-ansi-term-has-no-effect)
- [Disabling a package in emacs term-mode](https://stackoverflow.com/questions/19623545/disabling-a-package-in-emacs-term-mode)
- [How to use terminal in Emacs effectively](http://blog.binchen.org/posts/how-to-use-terminal-in-emacs-effectively.html)

使用 `C-z` 可以在 evil-mode 和 term-mode 之间切换。

### 禁用行号显示

```elisp
(add-hook 'term-mode-hook (lambda () (linum-mode -1)))
```

参考: [How to disable global-linum-mode for certain mode](https://emacs.stackexchange.com/questions/17333/how-to-disable-global-linum-mode-for-certain-mode)

## JSON 格式化

### JSON reformat

- `C-c C-f` - 使用 json-reformat 格式化选中区域或整个 buffer

相关包:
- [json-reformat](https://github.com/gongo/json-reformat)
- [json-mode](https://github.com/joshwnj/json-mode)

json-mode 用于 .json 文件的编辑。]]></content:encoded>
            <author>yupanzi</author>
            <category>Emacs</category>
            <category>Editor</category>
        </item>
    </channel>
</rss>