<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Lim&apos;s Blog</title><description>一个小小的编程爱好者</description><link>https://blog.useforall.com/</link><language>zh_CN</language><templateTheme>Firefly</templateTheme><templateThemeVersion>6.3.8</templateThemeVersion><templateThemeUrl>https://github.com/CuteLeaf/Firefly</templateThemeUrl><lastBuildDate>2026年5月20日 08:58:24</lastBuildDate><item><title>Codex WebUI项目</title><link>https://blog.useforall.com/posts/codex-webui/</link><guid isPermaLink="true">https://blog.useforall.com/posts/codex-webui/</guid><description>一直想找一个好用的Codex WebUI项目，就是可以Docker部署然后在哪都能用的，但是找来找去，要么是bug多，要么是直接不接受pr，所以干脆我直接自己写了一个，还是比较满意的捏</description><pubDate>Wed, 20 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
一直想找一个好用的Codex WebUI项目，就是可以Docker部署然后在哪都能用的，但是找来找去，要么是bug多，要么是直接不接受pr，所以干脆我直接自己写了一个，还是比较满意的捏。有问题直接提issue即可，欢迎star和fork。
:::&lt;/p&gt;
&lt;h1&gt;项目地址&lt;/h1&gt;
&lt;p&gt;::github{repo=&quot;LimLLL/codex-webui&quot;}&lt;/p&gt;
&lt;h1&gt;Codex WebUI 项目说明&lt;/h1&gt;
&lt;p&gt;给 &lt;a href=&quot;https://github.com/openai/codex&quot;&gt;OpenAI Codex CLI&lt;/a&gt; 做的 Web 前端。把命令行交互搬到浏览器里，支持多线程并发、文件管理、终端、插件市场等。&lt;/p&gt;
&lt;p&gt;后端用 NestJS 通过 stdio JSON-RPC 和 &lt;code&gt;codex app-server&lt;/code&gt; 通信，前端 React + Vite，中间用 Socket.IO 实时推送。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/other/2026/05/20/6a0d746bcbaaf.png&quot; alt=&quot;main&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;功能&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;对话与线程&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多线程并发运行，互不干扰&lt;/li&gt;
&lt;li&gt;线程按工作区分组，支持归档、fork、回滚、重命名&lt;/li&gt;
&lt;li&gt;Markdown 渲染 + Shiki 代码高亮&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@&lt;/code&gt; 引用文件、粘贴图片&lt;/li&gt;
&lt;li&gt;追问（steer）和中断（stop）正在执行的 turn&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;审批流程&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;命令执行、文件变更的审批卡片，直接在页面上操作&lt;/li&gt;
&lt;li&gt;支持安全策略切换（sandbox 级别）&lt;/li&gt;
&lt;li&gt;多设备同时在线时的 CAS 防冲突&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;文件管理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/other/2026/05/20/6a0d746bd164d.png&quot; alt=&quot;sidebar-file&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;树形文件浏览器，支持拖拽移动&lt;/li&gt;
&lt;li&gt;Monaco Editor 代码查看&lt;/li&gt;
&lt;li&gt;Git diff 分栏对比（@git-diff-view）&lt;/li&gt;
&lt;li&gt;上传 / 下载 / 重命名 / 复制 / 移动 / 新建目录&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;终端&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/other/2026/05/20/6a0d746bc411f.png&quot; alt=&quot;sidebar-terminal&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多 tab 共享终端（node-pty + xterm.js）&lt;/li&gt;
&lt;li&gt;断线重连，输出不丢失&lt;/li&gt;
&lt;li&gt;headless VT 回放&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;集成与插件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/other/2026/05/20/6a0d746bc78c1.png&quot; alt=&quot;sidebar-intergration&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其他&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JWT + API Key 认证&lt;/li&gt;
&lt;li&gt;插件/MCP 服务器管理&lt;/li&gt;
&lt;li&gt;深色/浅色主题，中英文切换&lt;/li&gt;
&lt;li&gt;响应式布局，手机平板也能用&lt;/li&gt;
&lt;li&gt;Docker 一键部署&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;技术栈&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;浏览器
  React 19 · Vite 8 · TanStack (Router + Query + Virtual)
  Zustand · Socket.IO Client · Monaco Editor · xterm.js
  Tailwind CSS 4 · shadcn/ui · Framer Motion · dnd-kit
     ↕  REST + WebSocket
后端
  NestJS 11 · Fastify 5 · Socket.IO · node-pty
  SQLite (better-sqlite3 + Drizzle ORM) · Pino
     ↕  stdio JSON-RPC
  codex app-server（子进程）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;h3&gt;前置条件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Node.js &amp;gt;= 20&lt;/li&gt;
&lt;li&gt;pnpm &amp;gt;= 9&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/openai/codex&quot;&gt;Codex CLI&lt;/a&gt; 已安装并可用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Docker 部署（推荐）&lt;/h3&gt;
&lt;p&gt;直接从 GHCR 拉取镜像，无需本地构建：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建 .env
cat &amp;lt;&amp;lt;EOF &amp;gt; .env
WEBUI_API_KEY=your-secret-key
OPENAI_API_KEY=sk-xxx
EOF

# 启动（自动拉取多架构镜像）
docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者手动运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name codex-webui \
  -p 8172:8172 \
  -e WEBUI_API_KEY=your-secret-key \
  -e OPENAI_API_KEY=sk-xxx \
  -v codex_root:/root \
  -v codex_workspaces:/workspaces \
  ghcr.io/limlll/codex-webui:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;服务运行在 &lt;code&gt;http://localhost:8172&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;/root&lt;/code&gt; 卷持久化 codex/claude/MCP 配置及运行时工具链。首次启动自动释放内置 seed。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;本地开发&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/LimLLL/codex-webui.git
cd codex-webui
pnpm install

cp .env.example .env
# 编辑 .env，至少设置 WEBUI_API_KEY

# 启动后端（默认端口 8172）
pnpm start:dev

# 另一个终端，启动前端（端口 5173，自动代理到后端）
cd web &amp;amp;&amp;amp; pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开 &lt;code&gt;http://localhost:5173&lt;/code&gt; 即可使用。&lt;/p&gt;
&lt;h2&gt;环境变量&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;变量&lt;/th&gt;
&lt;th&gt;必填&lt;/th&gt;
&lt;th&gt;默认值&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WEBUI_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;登录密钥，同时用于派生 JWT 签名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PORT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8172&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;后端监听端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CODEX_BIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;&lt;code&gt;codex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;codex CLI 可执行文件路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CODEX_HOME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.codex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Codex 主目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WORKSPACE_ROOTS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;逗号分隔的允许访问目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LOG_LEVEL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;&lt;code&gt;info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pino 日志级别&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WEBUI_DB_PATH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CODEX_HOME/codex-webui.sqlite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SQLite 数据库路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WEBUI_UPLOAD_MAX_BYTES&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;&lt;code&gt;104857600&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;上传文件大小限制（100MB）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DEFAULT_TERMINAL_CWD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;终端默认工作目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WEBUI_TERMINAL_MAX_SESSIONS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;最大并发终端会话数（1-50）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WEBUI_TERMINAL_GRACE_MS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;&lt;code&gt;45000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;断开连接后终端保活时长&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WEBUI_TERMINAL_SCROLLBACK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;终端回滚缓冲区行数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;项目结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;├── src/                  # NestJS 后端
│   ├── codex/            # 进程管理、JSON-RPC 客户端
│   ├── threads/          # 线程 CRUD、WebSocket 网关
│   ├── files/            # 文件操作、路径安全校验
│   ├── terminal/         # 多 tab 终端（node-pty）
│   ├── auth/             # JWT + API Key 认证
│   ├── database/         # SQLite + Drizzle ORM
│   └── ...               # 其他模块
├── web/                  # React 前端
│   └── src/
│       ├── routes/       # TanStack Router 页面
│       ├── components/   # UI 组件
│       ├── stores/       # Zustand 状态管理
│       ├── hooks/        # 自定义 hooks
│       └── generated/    # Hey API SDK（自动生成）
├── Dockerfile            # 多阶段构建 + seed root
└── docker-compose.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常用命令&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm start:dev          # 后端开发模式
pnpm build              # 编译后端
pnpm test               # 运行测试
pnpm lint               # ESLint 检查
pnpm db:generate        # 生成数据库迁移
pnpm db:migrate         # 执行迁移
cd web &amp;amp;&amp;amp; pnpm dev      # 前端开发模式
cd web &amp;amp;&amp;amp; pnpm build    # 前端构建（输出到 public/）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;License&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;./LICENSE&quot;&gt;AGPL-3.0&lt;/a&gt;&lt;/p&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>域名被污染后批量清理 Google 索引</title><link>https://blog.useforall.com/posts/bulk-remove-polluted-google-index/</link><guid isPermaLink="true">https://blog.useforall.com/posts/bulk-remove-polluted-google-index/</guid><description>捡了个二手域名结果被外链攻击，Google Search Console 里躺着几万条垃圾 URL。用 Cloudflare Worker 返回 410 + Google Indexing API 多账号轮换批量清理，记录一下整个处理过程。</description><pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import ShowMoreCollapse from &quot;../../components/custom/showMoreCollapse/main.tsx&quot;;&lt;/p&gt;
&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
买了个域名发现被外链攻击，Google Search Console 的 Page Indexing 里堆了几万条垃圾记录。通过 Cloudflare Worker 全站返回 410 Gone + GCP 多项目批量创建服务账号 + Google Indexing API 多账号轮换的方式来批量清理这些索引。
:::&lt;/p&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;之前买了个域名，丢到 Google Search Console 里准备提交索引，妈的结果打开 Page Indexing 页面一看，几万条记录，全是垃圾 URL，但是很奇怪的点是WebArchive类的相关网站我都看了，没有任何历史记录。所以有点怀疑这个域名之前被人拿去做过 SEO 外链攻击，或者说域名本身就是被污染过的。&lt;/p&gt;
&lt;p&gt;Google 那边的 URL Removal Tool 一次只能删一条，几万条一条条删？我寿命不够用😓。所以只能另辟蹊径。&lt;/p&gt;
&lt;h2&gt;整体思路&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare Worker 全站 410&lt;/strong&gt;：让所有访问这个域名的请求都返回 &lt;code&gt;410 Gone&lt;/code&gt;，告诉 Google 爬虫&quot;这些页面永久不存在了&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Google Indexing API 批量通知&lt;/strong&gt;：主动通知 Google 这些 URL 已删除，加速去索引&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GCP 多项目扩容配额&lt;/strong&gt;：每个账号下单个项目每天只有 200 次 API 调用额度，不够用就多开几个项目&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Cloudflare Worker：全站 410&lt;/h2&gt;
&lt;p&gt;这一步很简单，在 Cloudflare 上创建一个 Worker，让所有请求都返回 410 状态码。410 和 404 的区别在于，410 是明确告诉搜索引擎&quot;这个资源已经永久消失了，不会再回来&quot;。虽然 Google 的 John Mueller &lt;a href=&quot;https://www.searchenginejournal.com/googles-john-mueller-clarifies-404-410-confusion-for-seo/513576/&quot;&gt;说过&lt;/a&gt;两者处理差异 &quot;so minimal&quot;，但 410 语义上更明确——你主动声明&quot;我故意删了&quot;，而不是&quot;不知道为啥找不到了&quot;。在批量清理污染域名这种场景下，用 410 更合理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default {
  async fetch(request) {
    return new Response(
      &apos;&amp;lt;!DOCTYPE html&amp;gt;&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;410 Gone&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;410 Gone&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;This page no longer exists.&amp;lt;/p&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&apos;,
      {
        status: 410,
        headers: { &apos;Content-Type&apos;: &apos;text/html; charset=utf-8&apos; },
      }
    );
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2026/03/17/69b8fb875865c.png&quot; alt=&quot;CleanShot 2026-03-17 at 14.53.44@2x&quot; /&gt;&lt;/p&gt;
&lt;p&gt;部署完之后：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;新增一个DNS Record&lt;/li&gt;
&lt;li&gt;把域名的路由指向这个 Worker，所有流量都会被拦截并返回 410。这一步主要是配合 Google 爬虫自然爬取时的去索引，但光靠爬虫被动发现太慢了，所以就有了后续的步骤。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2026/03/17/69b8fb87860fe.png&quot; alt=&quot;CleanShot 2026-03-17 at 14.54.52@2x&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;GCP 批量创建项目&lt;/h2&gt;
&lt;p&gt;Google Indexing API 每个项目每天只有 200 次调用配额，几万条 URL 靠一个项目得跑好久。解决办法也简单粗暴——多开项目，每个项目 200/天，开 N 个就是 N×200/天。&lt;/p&gt;
&lt;p&gt;下面这个脚本在 GCP Cloud Shell 里运行，并发创建多个项目，每个项目自动启用 Indexing API、创建服务账号、下载密钥文件：&lt;/p&gt;
&lt;p&gt;&amp;lt;ShowMoreCollapse client:visible collapsedHeight={700}&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
# 在 GCP Cloud Shell 中运行，并发创建多个项目 + 服务账号
#
# 使用方法:
#   1. 打开 Cloud Shell (https://shell.cloud.google.com)
#   2. 修改下方配置变量
#   3. bash setup-gcp-projects.sh
#   4. 下载生成的 JSON 文件放到本地 scripts/service_accounts/

# ===== 配置区 (必须修改) =====
NUM_PROJECTS=4                    # 要创建的项目数 (每个项目 200/天)
ORG_ID=&quot;&quot;                         # 组织数字 ID (运行 gcloud organizations list 查看)
BILLING_ACCOUNT=&quot;&quot;                # 账单账号 ID (运行 gcloud billing accounts list 查看)
# ===========================

if [ -z &quot;$ORG_ID&quot; ] || [ -z &quot;$BILLING_ACCOUNT&quot; ]; then
  echo &quot;请先填写 ORG_ID 和 BILLING_ACCOUNT&quot;
  echo &quot;&quot;
  echo &quot;查看组织 ID:&quot;
  gcloud organizations list
  echo &quot;&quot;
  echo &quot;查看账单账号 ID:&quot;
  gcloud billing accounts list
  exit 1
fi

OUTPUT_DIR=&quot;./service_accounts&quot;
mkdir -p &quot;$OUTPUT_DIR&quot;

# 单个项目的完整创建流程
setup_project() {
  local idx=$1
  local project_id=&quot;gsc-idx-${idx}-$(date +%s)&quot;
  local sa_email=&quot;gsc-sa@${project_id}.iam.gserviceaccount.com&quot;
  local log_prefix=&quot;[项目 $idx]&quot;

  echo &quot;$log_prefix 开始创建: $project_id&quot;

  # 1. 创建项目
  echo &quot;$log_prefix [1/6] 创建项目...&quot;
  if ! gcloud projects create &quot;$project_id&quot; --organization=&quot;$ORG_ID&quot; --quiet 2&amp;gt;&amp;amp;1; then
    echo &quot;$log_prefix 创建项目失败，终止&quot;
    return 1
  fi

  # 2. 关联账单
  echo &quot;$log_prefix [2/6] 关联账单...&quot;
  gcloud billing projects link &quot;$project_id&quot; --billing-account=&quot;$BILLING_ACCOUNT&quot; --quiet 2&amp;gt;&amp;amp;1

  # 3. 关闭策略限制 (允许创建 SA Key)
  echo &quot;$log_prefix [3/6] 关闭 SA Key 策略限制...&quot;
  cat &amp;gt; &quot;/tmp/policy-${idx}-a.yaml&quot; &amp;lt;&amp;lt; EOF
name: projects/${project_id}/policies/iam.disableServiceAccountKeyCreation
spec:
  rules:
  - enforce: false
EOF
  gcloud org-policies set-policy &quot;/tmp/policy-${idx}-a.yaml&quot; --project=&quot;$project_id&quot; --quiet 2&amp;gt;&amp;amp;1 || true

  cat &amp;gt; &quot;/tmp/policy-${idx}-b.yaml&quot; &amp;lt;&amp;lt; EOF
name: projects/${project_id}/policies/iam.managed.disableServiceAccountKeyCreation
spec:
  rules:
  - enforce: false
EOF
  gcloud org-policies set-policy &quot;/tmp/policy-${idx}-b.yaml&quot; --project=&quot;$project_id&quot; --quiet 2&amp;gt;&amp;amp;1 || true

  # 4. 启用 Indexing API
  echo &quot;$log_prefix [4/6] 启用 Indexing API...&quot;
  gcloud services enable indexing.googleapis.com --project=&quot;$project_id&quot; --quiet 2&amp;gt;&amp;amp;1

  # 5. 创建服务账号
  echo &quot;$log_prefix [5/6] 创建服务账号...&quot;
  gcloud iam service-accounts create gsc-sa --project=&quot;$project_id&quot; --display-name=&quot;GSC Indexing SA&quot; --quiet 2&amp;gt;&amp;amp;1

  # 6. 下载密钥 (无限重试直到策略生效)
  echo &quot;$log_prefix [6/6] 下载密钥 (等待策略生效)...&quot;
  local attempt=0
  while true; do
    attempt=$((attempt + 1))
    if gcloud iam service-accounts keys create &quot;${OUTPUT_DIR}/sa-${idx}.json&quot; \
      --iam-account=&quot;$sa_email&quot; --project=&quot;$project_id&quot; --quiet 2&amp;gt;&amp;amp;1; then
      echo &quot;$log_prefix 密钥下载成功 (第 ${attempt} 次尝试)&quot;
      break
    fi
    echo &quot;$log_prefix 策略未生效，等待 15 秒后重试 (第 ${attempt} 次)...&quot;
    sleep 15
  done

  echo &quot;$sa_email&quot; &amp;gt;&amp;gt; &quot;${OUTPUT_DIR}/.emails.txt&quot;
  echo &quot;$log_prefix 完成! -&amp;gt; sa-${idx}.json ($sa_email)&quot;
}

rm -f &quot;${OUTPUT_DIR}/.emails.txt&quot;

echo &quot;==========================================&quot;
echo &quot;  并发创建 $NUM_PROJECTS 个项目&quot;
echo &quot;==========================================&quot;

# 并发启动
pids=()
for i in $(seq 1 $NUM_PROJECTS); do
  setup_project &quot;$i&quot; &amp;amp;
  pids+=($!)
done

# 等待完成
fail_count=0
for pid in &quot;${pids[@]}&quot;; do
  if ! wait &quot;$pid&quot;; then
    fail_count=$((fail_count + 1))
  fi
done

echo &quot;&quot;
echo &quot;==========================================&quot;
echo &quot;  全部完成! 成功: $((NUM_PROJECTS - fail_count))/$NUM_PROJECTS&quot;
echo &quot;==========================================&quot;
echo &quot;&quot;
echo &quot;JSON 密钥文件:&quot;
ls -la &quot;$OUTPUT_DIR&quot;/sa-*.json 2&amp;gt;/dev/null
echo &quot;&quot;
echo &quot;==========================================&quot;
echo &quot;  重要: 将以下邮箱全部添加为 GSC 属性 Owner&quot;
echo &quot;==========================================&quot;
if [ -f &quot;${OUTPUT_DIR}/.emails.txt&quot; ]; then
  sort &quot;${OUTPUT_DIR}/.emails.txt&quot; | while read -r email; do
    echo &quot;  $email&quot;
  done
  rm -f &quot;${OUTPUT_DIR}/.emails.txt&quot;
fi
echo &quot;&quot;
echo &quot;操作步骤:&quot;
echo &quot;1. 下载 ${OUTPUT_DIR}/ 目录下所有 sa-*.json 文件&quot;
echo &quot;2. 放到本地项目 scripts/service_accounts/ 目录&quot;
echo &quot;3. 进入 GSC → 设置 → 用户和权限&quot;
echo &quot;4. 把上面每个邮箱都添加为 Owner&quot;
echo &quot;5. 运行 pnpm gsc:remove&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/p&gt;
&lt;p&gt;:::warning{title=&quot;注意&quot;}
脚本里的 &lt;code&gt;ORG_ID&lt;/code&gt; 和 &lt;code&gt;BILLING_ACCOUNT&lt;/code&gt; 必须填你自己的。没有组织的个人账号需要先创建一个组织，或者去掉 &lt;code&gt;--organization&lt;/code&gt; 参数手动创建项目。另外 GCP 免费账号有项目数量限制，创建太多可能会被拒绝。
:::&lt;/p&gt;
&lt;p&gt;脚本跑完后会在 &lt;code&gt;service_accounts/&lt;/code&gt; 目录下生成一堆 &lt;code&gt;sa-*.json&lt;/code&gt; 密钥文件，同时输出所有服务账号的邮箱地址。&lt;strong&gt;这些邮箱必须逐一添加到 GSC 属性的 Owner 权限里&lt;/strong&gt;，否则 API 调用会报权限错误。&lt;/p&gt;
&lt;h2&gt;批量调用 Indexing API 清理索引&lt;/h2&gt;
&lt;p&gt;前面两步都准备完成后，开始调用Indexing API。流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从 GSC 导出垃圾 URL 的 CSV 文件，放到 &lt;code&gt;scripts/gsc-data/&lt;/code&gt; 目录
&lt;ul&gt;
&lt;li&gt;记得需要点进具体的 &lt;code&gt;Why pages aren’t indexed&lt;/code&gt; 页面，才能看到 URL 列表&lt;/li&gt;
&lt;li&gt;点右上角的 &lt;code&gt;Export&lt;/code&gt;，选择 &lt;code&gt;Download CSV&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;把前面下载的服务账号 JSON 放到 &lt;code&gt;scripts/service_accounts/&lt;/code&gt; 目录&lt;/li&gt;
&lt;li&gt;运行脚本，自动轮换多个服务账号发送 &lt;code&gt;URL_DELETED&lt;/code&gt; 通知&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;ShowMoreCollapse client:visible collapsedHeight={700}&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * GSC Bulk URL Removal via Google Indexing API (Multi-Account)
 *
 * 从 GSC 导出的 CSV 中读取垃圾 URL，通过多个服务账号轮换批量发送 URL_DELETED 通知。
 * 每个服务账号 200/天配额，N 个账号 = N*200/天。
 *
 * 使用方法:
 *   1. 将 GSC 导出的 CSV 放到 scripts/gsc-data/
 *   2. 将服务账号 JSON 放到 scripts/service_accounts/
 *   3. 运行: node scripts/gsc-bulk-remove.js [--dry-run]
 */

import { JWT } from &apos;google-auth-library&apos;;
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from &apos;fs&apos;;
import { join, dirname } from &apos;path&apos;;
import { fileURLToPath } from &apos;url&apos;;

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const INDEXING_API_URL = &apos;https://indexing.googleapis.com/v3/urlNotifications:publish&apos;;
const INDEXING_SCOPE = &apos;https://www.googleapis.com/auth/indexing&apos;;
const PER_ACCOUNT_QUOTA = 200;
const REQUEST_DELAY_MS = 1000;
const DATA_DIR = join(__dirname, &apos;gsc-data&apos;);
const SERVICE_ACCOUNTS_DIR = join(__dirname, &apos;service_accounts&apos;);
const LEGACY_SA_FILE = join(__dirname, &apos;service_account.json&apos;);
const PROGRESS_FILE = join(__dirname, &apos;gsc-data&apos;, &apos;progress.json&apos;);

/** 发现所有可用的服务账号 JSON 文件 */
function discoverServiceAccounts() {
  const files = [];
  if (existsSync(SERVICE_ACCOUNTS_DIR)) {
    const saFiles = readdirSync(SERVICE_ACCOUNTS_DIR)
      .filter((f) =&amp;gt; f.endsWith(&apos;.json&apos;))
      .sort()
      .map((f) =&amp;gt; join(SERVICE_ACCOUNTS_DIR, f));
    files.push(...saFiles);
  }
  if (files.length === 0 &amp;amp;&amp;amp; existsSync(LEGACY_SA_FILE)) {
    files.push(LEGACY_SA_FILE);
  }
  return files;
}

/** 创建认证客户端 */
async function createAuthClient(keyFilePath) {
  const keys = JSON.parse(readFileSync(keyFilePath, &apos;utf-8&apos;));
  const client = new JWT({
    email: keys.client_email,
    key: keys.private_key,
    scopes: [INDEXING_SCOPE],
  });
  await client.authorize();
  return { client, email: keys.client_email };
}

/** 解析 GSC 导出的 CSV 文件 */
function parseCsvUrls(filePath) {
  const content = readFileSync(filePath, &apos;utf-8&apos;);
  const lines = content.split(&apos;\n&apos;).filter((line) =&amp;gt; line.trim());
  return lines
    .slice(1)
    .map((line) =&amp;gt; {
      const delimiter = line.includes(&apos;\t&apos;) ? &apos;\t&apos; : &apos;,&apos;;
      return line.split(delimiter)[0].trim().replace(/^[&quot;&apos;]|[&quot;&apos;]$/g, &apos;&apos;);
    })
    .filter((url) =&amp;gt; url.startsWith(&apos;http&apos;));
}

/** 加载/保存进度 */
function loadProgress() {
  if (existsSync(PROGRESS_FILE)) {
    const data = JSON.parse(readFileSync(PROGRESS_FILE, &apos;utf-8&apos;));
    if (!data.accounts) {
      data.accounts = {};
    }
    return data;
  }
  return { processed: [], lastDate: &apos;&apos;, accounts: {} };
}

function saveProgress(progress) {
  writeFileSync(PROGRESS_FILE, JSON.stringify(progress, null, 2));
}

/** 发送 URL_DELETED 通知 */
async function notifyUrlDeleted(client, url) {
  try {
    const res = await client.fetch(INDEXING_API_URL, {
      method: &apos;POST&apos;,
      headers: { &apos;Content-Type&apos;: &apos;application/json&apos; },
      body: JSON.stringify({ url, type: &apos;URL_DELETED&apos; }),
    });
    if (res.status === 200) return { success: true, status: 200 };
    const errorBody = typeof res.data === &apos;string&apos; ? res.data : JSON.stringify(res.data);
    return { success: false, status: res.status, error: errorBody };
  } catch (err) {
    return { success: false, error: err.message };
  }
}

const sleep = (ms) =&amp;gt; new Promise((resolve) =&amp;gt; setTimeout(resolve, ms));

async function main() {
  const isDryRun = process.argv.includes(&apos;--dry-run&apos;);
  console.log(&apos;=== GSC Bulk URL Removal Tool (Multi-Account) ===\n&apos;);

  if (isDryRun) console.log(&apos;DRY RUN 模式 - 不会发送实际请求\n&apos;);

  // 发现服务账号
  const saFiles = discoverServiceAccounts();
  if (saFiles.length === 0) {
    console.error(&apos;未找到服务账号文件。&apos;);
    console.error(`请将 JSON 文件放到: ${SERVICE_ACCOUNTS_DIR}/`);
    process.exit(1);
  }
  console.log(`找到 ${saFiles.length} 个服务账号 (总配额: ${saFiles.length * PER_ACCOUNT_QUOTA}/天)\n`);

  // 解析 CSV
  if (!existsSync(DATA_DIR)) {
    mkdirSync(DATA_DIR, { recursive: true });
    console.log(`已创建数据目录: ${DATA_DIR}`);
    console.log(&apos;请将 GSC 导出的 CSV 文件放入该目录后重新运行。&apos;);
    return;
  }

  const csvFiles = readdirSync(DATA_DIR).filter((f) =&amp;gt; f.endsWith(&apos;.csv&apos;));
  if (csvFiles.length === 0) {
    console.log(`在 ${DATA_DIR} 中未找到 CSV 文件。`);
    return;
  }

  let allUrls = [];
  for (const file of csvFiles) {
    const urls = parseCsvUrls(join(DATA_DIR, file));
    console.log(`${file}: ${urls.length} 个 URL`);
    allUrls = allUrls.concat(urls);
  }
  allUrls = [...new Set(allUrls)];
  console.log(`\n总计去重后: ${allUrls.length} 个 URL`);

  // 加载进度
  const progress = loadProgress();
  const today = new Date().toISOString().split(&apos;T&apos;)[0];
  if (progress.lastDate !== today) {
    progress.lastDate = today;
    progress.accounts = {};
  }

  const processedSet = new Set(progress.processed);
  const pendingUrls = allUrls.filter((url) =&amp;gt; !processedSet.has(url));
  console.log(`已处理: ${progress.processed.length} 个`);
  console.log(`待处理: ${pendingUrls.length} 个\n`);

  if (pendingUrls.length === 0) {
    console.log(&apos;所有 URL 已处理完毕!&apos;);
    return;
  }

  // 构建账号队列
  const accountQueue = [];
  for (const saFile of saFiles) {
    const saKey = saFile.split(&apos;/&apos;).pop();
    const used = progress.accounts[saKey] || 0;
    const remaining = PER_ACCOUNT_QUOTA - used;
    if (remaining &amp;gt; 0) accountQueue.push({ file: saFile, key: saKey, used, remaining });
  }

  const totalRemaining = accountQueue.reduce((sum, a) =&amp;gt; sum + a.remaining, 0);
  if (totalRemaining &amp;lt;= 0) {
    console.log(&apos;今日所有账号配额已用完。请明天再运行。&apos;);
    return;
  }

  console.log(&apos;账号配额:&apos;);
  for (const acc of accountQueue) {
    console.log(`  ${acc.key}: ${acc.remaining}/${PER_ACCOUNT_QUOTA} 剩余`);
  }

  const batch = pendingUrls.slice(0, totalRemaining);
  console.log(`\n本次将处理: ${batch.length} 个 URL\n`);

  if (isDryRun) {
    console.log(&apos;将要处理的 URL (前 20 个):&apos;);
    batch.slice(0, 20).forEach((url, i) =&amp;gt; console.log(`  ${i + 1}. ${url}`));
    if (batch.length &amp;gt; 20) console.log(`  ... 还有 ${batch.length - 20} 个`);
    return;
  }

  // 认证
  console.log(&apos;正在认证...&apos;);
  const clients = [];
  for (const acc of accountQueue) {
    try {
      const { client, email } = await createAuthClient(acc.file);
      clients.push({ ...acc, client, email });
      console.log(`  ${acc.key} (${email}): OK`);
    } catch (err) {
      console.log(`  ${acc.key}: 认证失败 - ${err.message}`);
    }
  }

  if (clients.length === 0) {
    console.error(&apos;\n所有账号认证失败，终止。&apos;);
    process.exit(1);
  }

  // 处理 URL，轮换账号
  let urlIndex = 0;
  let successCount = 0;
  let failCount = 0;

  for (const acc of clients) {
    if (urlIndex &amp;gt;= batch.length) break;
    const accBatch = batch.slice(urlIndex, urlIndex + acc.remaining);
    console.log(`\n--- ${acc.key} (${acc.email}) ---`);

    let processed = 0;
    for (let i = 0; i &amp;lt; accBatch.length; i++) {
      const url = accBatch[i];
      const result = await notifyUrlDeleted(acc.client, url);
      const globalIdx = urlIndex + i + 1;

      if (result.success) {
        successCount++;
        processed++;
        progress.processed.push(url);
        progress.accounts[acc.key] = (progress.accounts[acc.key] || 0) + 1;
        console.log(`  [${globalIdx}/${batch.length}] OK ${url}`);
      } else {
        const errMsg = result.error || `HTTP ${result.status}`;
        if (result.status === 429 || errMsg.includes(&apos;Quota exceeded&apos;)) {
          console.log(`\n  ${acc.key} 配额已用完，切换下一个账号`);
          break;
        }
        failCount++;
        processed++;
        console.log(`  [${globalIdx}/${batch.length}] FAIL ${url}`);
        console.log(`    ${errMsg}`);
      }

      if (globalIdx % 10 === 0) saveProgress(progress);
      if (i &amp;lt; accBatch.length - 1) await sleep(REQUEST_DELAY_MS);
    }
    urlIndex += processed;
  }

  saveProgress(progress);

  console.log(&apos;\n=== 完成 ===&apos;);
  console.log(`成功: ${successCount}`);
  console.log(`失败: ${failCount}`);
  console.log(`累计已处理: ${progress.processed.length}`);

  const left = allUrls.length - progress.processed.length;
  if (left &amp;gt; 0) {
    const dailyCapacity = saFiles.length * PER_ACCOUNT_QUOTA;
    console.log(`\n还剩 ${left} 个，按 ${dailyCapacity}/天 配额预计 ${Math.ceil(left / dailyCapacity)} 天完成`);
  }
}

main().catch(console.error);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/p&gt;
&lt;p&gt;脚本支持断点续传，每处理 10 条自动保存进度。第二天再跑会自动跳过已处理的 URL 并重置每日配额计数。先用 &lt;code&gt;--dry-run&lt;/code&gt; 跑一下看看数据对不对，没问题再去掉参数正式跑。&lt;/p&gt;
&lt;h2&gt;完整操作流程&lt;/h2&gt;
&lt;p&gt;总结一下从头到尾的操作步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;部署 Cloudflare Worker&lt;/strong&gt;：创建一个 Worker 返回 410，把域名路由指过去&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GCP 批量开项目&lt;/strong&gt;：打开 &lt;a href=&quot;https://shell.cloud.google.com&quot;&gt;Cloud Shell&lt;/a&gt;，填好配置跑 &lt;code&gt;setup-gcp-projects.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下载密钥文件&lt;/strong&gt;：把生成的 &lt;code&gt;sa-*.json&lt;/code&gt; 下载到本地 &lt;code&gt;scripts/service_accounts/&lt;/code&gt; 目录&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;添加 GSC 权限&lt;/strong&gt;：进 GSC → 设置 → 用户和权限，把每个服务账号邮箱都加为 Owner&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;导出垃圾 URL&lt;/strong&gt;：在 GSC 的 Page Indexing 页面导出 CSV，放到 &lt;code&gt;scripts/gsc-data/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跑脚本&lt;/strong&gt;：&lt;code&gt;node scripts/gsc-bulk-remove.js --dry-run&lt;/code&gt; 先看看，没问题就 &lt;code&gt;node scripts/gsc-bulk-remove.js&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每天重复第 6 步&lt;/strong&gt;：直到所有垃圾 URL 清理完毕&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip{title=&quot;配额计算&quot;}
假设你开了 4 个项目，每天就有 800 次配额。10 万条垃圾 URL 大概需要 125 天。如果嫌慢可以多开几个项目，但注意 GCP 的项目数量限制。实际上 Cloudflare Worker 的 410 也在持续生效，Google 爬虫自己爬到 410 也会逐渐去索引，两边同时进行会快很多。
:::&lt;/p&gt;
&lt;h2&gt;效果&lt;/h2&gt;
&lt;p&gt;跑了几天之后，GSC 的 Page Indexing 数字开始明显下降。410 + Indexing API 双管齐下，比单纯等 Google 爬虫自己发现要快不少。不过几万条数据量还是需要耐心，急不来的。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2026/03/17/69b8fd284bf99.png&quot; alt=&quot;CleanShot 2026-03-17 at 15.04.48@2x&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>免费 Exa/Tavily/SerpAPI/Outlook取件 API</title><link>https://blog.useforall.com/posts/exa-search-api-mcp-configuration-guide/</link><guid isPermaLink="true">https://blog.useforall.com/posts/exa-search-api-mcp-configuration-guide/</guid><description>写了个注册机批量注册了几钱个 Exa/Tavily/SerpAPI 账号，做成公益 API 代理服务供大家免费使用（仅收取Linux.do Credit)。本文介绍如何在 Gemini CLI、Claude Code、Codex CLI 等 AI 编程工具中配置这个 MCP 搜索服务。</description><pubDate>Fri, 13 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import Disclaimer from &quot;../../components/custom/Disclaimer/main.tsx&quot;;&lt;/p&gt;
&lt;p&gt;:::tip[公益服务]
Exa/Tavily/SerpAPI 是一个非常强大的第三方搜索 API，支持语义搜索、代码搜索、网页爬取和深度研究。但它的免费额度有限，单个账号根本不够用。所以我写了个注册机，批量注册了几千个账号，做了个代理池子，把所有账号的额度聚合起来，对外暴露一个统一的 MCP 端点。现在把这个服务公开出来（仅收取免费的Linux.do Credit），大家直接用就行，不用自己折腾注册机了。本文教你怎么在主流 AI 编程工具中接入这个服务。
:::&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2026-03-16: 不再提供免费服务了，请使用Linux.do Credit进行兑换。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;服务器地址&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://sc.eo.mk&quot;&gt;Service Center&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果没有Linux.do账号，请自行注册一个，然后联系我获取兑换码。&lt;/li&gt;
&lt;li&gt;Firecrawl待开发，主要是有点懒了&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Exa.ai&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2026/03/16/69b7674471d96.png&quot; alt=&quot;CleanShot 2026-03-16 at 10.11.08@2x&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;SerpAPI&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2026/03/16/69b7674471dba.png&quot; alt=&quot;CleanShot 2026-03-16 at 10.11.33@2x&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Tavily&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2026/03/16/69b7674471da5.png&quot; alt=&quot;CleanShot 2026-03-16 at 10.11.55@2x&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Outlook取件&lt;/h2&gt;
&lt;p&gt;:::important
本软件仅会存储你的outlook邮箱，不会存储任何邮件，如果你担心隐私问题、或担心博主会使用你的邮箱，那么请出门右转，不要使用。不想浪费时间回答任何隐私问题。&amp;lt;br/&amp;gt;
只支持&lt;code&gt;Graph API&lt;/code&gt;，&lt;code&gt;IMAP&lt;/code&gt;不打算支持，太麻烦了
:::
&lt;img src=&quot;https://lsky.useforall.com/Blog/2026/03/16/69b7674471dc8.png&quot; alt=&quot;CleanShot 2026-03-16 at 10.12.21@2x&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2026/03/16/69b7674471dd1.png&quot; alt=&quot;CleanShot 2026-03-16 at 10.13.11@2x&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;Disclaimer client:visible /&amp;gt;&lt;/p&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>PaddleOCR-VL 模型部署备忘录</title><link>https://blog.useforall.com/posts/paddleocr-vl-deployment-memo/</link><guid isPermaLink="true">https://blog.useforall.com/posts/paddleocr-vl-deployment-memo/</guid><description>记录 PaddleOCR-VL 模型的部署过程，因为官方文档写得稀烂，浪费了我4个小时才跑通。希望这篇文章能帮你少走弯路。</description><pubDate>Tue, 27 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import ShowMoreCollapse from &quot;../../components/custom/showMoreCollapse/main.tsx&quot;;&lt;/p&gt;
&lt;p&gt;:::warning[吐槽]
PaddleOCR 的文档从 2021 年写得稀巴烂，到 2026 年了，依旧他妈的稀巴烂。各种依赖冲突、版本不兼容、文档和实际代码对不上，浪费了我整整 4 个小时。写这篇文章就是为了让后来者少踩坑，因为指望百度更新文档是不可能的。
:::&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2026-04-30：更新uv源地址，修复 Flash Attention 安装问题；补充一键启动脚本；更新模型为 PaddleOCR-VL-1.5-0.9B；补充更多需要替换的参数说明。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;架构说明&lt;/h2&gt;
&lt;p&gt;PaddleOCR-VL 的部署需要&lt;strong&gt;两个独立的服务配合运行&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;vLLM 后端服务&lt;/strong&gt;（端口 8118）：负责 VL 模型的高性能推理&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PaddleX Serving 前端服务&lt;/strong&gt;（端口 8000）：负责文档预处理、版面分析，并调用 vLLM 后端&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;用户请求 → PaddleX Serving (8000) → vLLM Backend (8118)
                  ↓
             返回 OCR 结果
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;前置条件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Linux 服务器（本文以 Ubuntu 为例）&lt;/li&gt;
&lt;li&gt;NVIDIA GPU（需要 CUDA 支持）&lt;/li&gt;
&lt;li&gt;足够的显存（建议 8GB+）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Step 1：安装 uv 包管理器&lt;/h2&gt;
&lt;p&gt;为什么用 &lt;code&gt;uv&lt;/code&gt;？因为 &lt;code&gt;pip&lt;/code&gt; 在处理 Paddle 这种依赖地狱的时候慢得要死，&lt;code&gt;uv&lt;/code&gt; 至少能让你在绝望中少等一会儿。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 下载并安装 uv
# ⚠️ 替换：版本号 0.9.27 可根据需要更换为最新版本
wget https://speed.oo9.dpdns.org/gh/astral-sh/uv/releases/download/0.9.27/uv-x86_64-unknown-linux-gnu.tar.gz
tar -zxf uv-x86_64-unknown-linux-gnu.tar.gz
mv uv-x86_64-unknown-linux-gnu/* /usr/local/bin

# 替换uv源（海外机器无需配置）
mkdir -p ~/.config/uv &amp;amp;&amp;amp; cat &amp;gt; ~/.config/uv/uv.toml &amp;lt;&amp;lt;&apos;EOF&apos;
index-url = &quot;https://speed.oo9.dpdns.org/pypi/simple/&quot;
EOF
# 替换pip源（vLLM的安装脚本依赖pip，所以也要替换，海外机器无需配置）
mkdir -p ~/.pip &amp;amp;&amp;amp; cat &amp;gt; ~/.pip/pip.conf &amp;lt;&amp;lt;&apos;EOF&apos;
[global]
index-url = https://speed.oo9.dpdns.org/pypi/simple/
trusted-host = speed.oo9.dpdns.org
EOF

# 创建工作目录
mkdir -p /paddle
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 2：部署 vLLM 后端服务（端口 8118）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;必须先启动这个服务&lt;/strong&gt;，因为 PaddleX Serving 依赖它。&lt;/p&gt;
&lt;p&gt;&amp;lt;ShowMoreCollapse client:visible collapsedHeight={400}&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /paddle

# 创建 vLLM 专用虚拟环境
# ⚠️ 替换：Python 版本可根据需要调整，推荐 3.10-3.12
uv venv .venv_vllm --python 3.12
source .venv_vllm/bin/activate

# 先装 pip（是的，uv 环境里还得装 pip，因为后面的脚本依赖它）
uv pip install pip

# 安装 PaddleOCR
uv pip install -U &quot;paddleocr[doc-parser]&quot;

# 安装 Flash Attention（加速推理的关键）
# ⚠️ 替换：根据你的 CUDA 和 PyTorch 版本选择对应的 wheel
# 这里是 CUDA 12.8 + PyTorch 2.8 + Python 3.12 的版本
# 其他版本请去 https://github.com/mjun0812/flash-attention-prebuild-wheels/releases 找
uv pip install https://speed.oo9.dpdns.org/gh/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.3.14/flash_attn-2.8.2+cu128torch2.8-cp312-cp312-linux_x86_64.whl

# 安装 vLLM 依赖
paddleocr install_genai_server_deps vllm
# 补齐缺失的flashinfer-python
pip install flashinfer-python

# 安装 transformers（版本很重要，不要乱改）
uv pip install transformers==4.57.6

# 安装 Python 开发头文件（编译某些依赖需要）
# ⚠️ 替换：python3.12-dev 根据你的 Python 版本调整
apt update &amp;amp;&amp;amp; apt install -y python3.12-dev

# 启动 vLLM 服务
# ⚠️ 替换：
#   - --model_name → 可选模型：PaddleOCR-VL-0.9B、PaddleOCR-VL-1.5-0.9B、PaddleOCR-VL-2B 等
#   - --port 8118 → vLLM 后端端口，需要和 PaddleOCR-VL.yaml 中的配置一致
paddlex_genai_server --model_name PaddleOCR-VL-1.5-0.9B --backend vllm --port 8118
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/p&gt;
&lt;p&gt;:::warning[Flash Attention 的坑]
Flash Attention 的预编译 wheel 对 CUDA、PyTorch、Python 版本有严格要求。如果版本不匹配，要么安装失败，要么运行时 segfault。&lt;/p&gt;
&lt;p&gt;百度的文档？不存在的。他们只会告诉你 &lt;code&gt;pip install flash-attn&lt;/code&gt;，然后让你自己编译 2 小时，最后还可能失败。&lt;/p&gt;
&lt;p&gt;建议直接去 &lt;a href=&quot;https://github.com/mjun0812/flash-attention-prebuild-wheels/releases&quot;&gt;mjun0812 的预编译仓库&lt;/a&gt; 找对应版本。
:::&lt;/p&gt;
&lt;h2&gt;Step 3：部署 PaddleX Serving 前端服务（端口 8000）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;新开一个终端&lt;/strong&gt;，部署前端服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /paddle

# 创建 PaddleX 专用虚拟环境（不要和 vLLM 环境混用！）
# ⚠️ 替换：Python 版本可根据需要调整
uv venv .venv_paddle --python 3.12
source .venv_paddle/bin/activate

# 安装 PaddlePaddle GPU 版本
# ⚠️ 替换：cu126 表示 CUDA 12.6，根据你的 CUDA 版本选择对应的包
# 可选：cu118（CUDA 11.8）、cu120（CUDA 12.0）、cu126（CUDA 12.6）
uv pip install paddlepaddle-gpu==3.2.1 -i https://www.paddlepaddle.org.cn/packages/stable/cu126/

# 安装 PaddleOCR
uv pip install -U &quot;paddleocr[doc-parser]&quot; pip

# 安装 PaddleX Serving 组件
paddlex --install serving

# 启动服务
# ⚠️ 替换：
#   - gpu:0 → 改为你的 GPU 设备号，多卡可用 gpu:0,1
#   - --port 8000 → 改为你想要的端口号
paddlex --serve --device gpu:0 --pipeline PaddleOCR-VL --port 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note[关于 CUDA 版本]
查看你的 CUDA 版本：&lt;code&gt;nvcc --version&lt;/code&gt; 或 &lt;code&gt;nvidia-smi&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;百度的文档根本不会告诉你这些，你得自己去 &lt;a href=&quot;https://www.paddlepaddle.org.cn/install/quick&quot;&gt;PaddlePaddle 官网&lt;/a&gt; 找对应的安装命令。而且官网的安装命令还经常和实际的包名对不上，真是谢谢您嘞。
:::&lt;/p&gt;
&lt;h2&gt;Step 4：配置 PaddleOCR-VL.yaml（可选）&lt;/h2&gt;
&lt;p&gt;如果你需要自定义配置（比如调整 batch_size、阈值等），可以覆盖默认的配置文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ⚠️ 替换：路径中的 python3.12 根据你的 Python 版本调整
# 如果你用的是 conda/miniconda，路径类似：/root/miniconda3/lib/python3.12/site-packages/paddlex/configs/pipelines/
# 如果你用的是 uv venv，路径类似：/paddle/.venv_paddle/lib/python3.12/site-packages/paddlex/configs/pipelines/
cp /root/PaddleOCR-VL.yaml /paddle/.venv_paddle/lib/python3.12/site-packages/paddlex/configs/pipelines/ -r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置文件内容如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;ShowMoreCollapse client:visible collapsedHeight={500}&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# PaddleOCR-VL.yaml
# ⚠️ 替换：server_url 中的端口需要和 vLLM 服务端口一致

pipeline_name: PaddleOCR-VL

batch_size: 64

use_queues: True

use_doc_preprocessor: True
use_layout_detection: True
use_chart_recognition: False
format_block_content: False

SubModules:
  LayoutDetection:
    module_name: layout_detection
    model_name: PP-DocLayoutV2
    model_dir: null
    batch_size: 8
    threshold:
      0: 0.5 # abstract
      1: 0.5 # algorithm
      2: 0.5 # aside_text
      3: 0.5 # chart
      4: 0.5 # content
      5: 0.4 # formula
      6: 0.4 # doc_title
      7: 0.5 # figure_title
      8: 0.5 # footer
      9: 0.5 # footer
      10: 0.5 # footnote
      11: 0.5 # formula_number
      12: 0.5 # header
      13: 0.5 # header
      14: 0.5 # image
      15: 0.4 # formula
      16: 0.5 # number
      17: 0.4 # paragraph_title
      18: 0.5 # reference
      19: 0.5 # reference_content
      20: 0.45 # seal
      21: 0.5 # table
      22: 0.4 # text
      23: 0.4 # text
      24: 0.5 # vision_footnote
    layout_nms: True
    layout_unclip_ratio: [1.0, 1.0]
    layout_merge_bboxes_mode:
      0: &quot;union&quot; # abstract
      1: &quot;union&quot; # algorithm
      2: &quot;union&quot; # aside_text
      3: &quot;large&quot; # chart
      4: &quot;union&quot; # content
      5: &quot;large&quot; # display_formula
      6: &quot;large&quot; # doc_title
      7: &quot;union&quot; # figure_title
      8: &quot;union&quot; # footer
      9: &quot;union&quot; # footer
      10: &quot;union&quot; # footnote
      11: &quot;union&quot; # formula_number
      12: &quot;union&quot; # header
      13: &quot;union&quot; # header
      14: &quot;union&quot; # image
      15: &quot;large&quot; # inline_formula
      16: &quot;union&quot; # number
      17: &quot;large&quot; # paragraph_title
      18: &quot;union&quot; # reference
      19: &quot;union&quot; # reference_content
      20: &quot;union&quot; # seal
      21: &quot;union&quot; # table
      22: &quot;union&quot; # text
      23: &quot;union&quot; # text
      24: &quot;union&quot; # vision_footnote
  VLRecognition:
    module_name: vl_recognition
    model_name: PaddleOCR-VL-1.5-0.9B
    model_dir: null
    batch_size: 2048
    genai_config:
      backend: vllm-server
      # ⚠️ 替换：这里的端口必须和 Step 2 中 vLLM 服务的端口一致
      server_url: http://127.0.0.1:8118/v1

SubPipelines:
  DocPreprocessor:
    pipeline_name: doc_preprocessor
    batch_size: 8
    use_doc_orientation_classify: True
    use_doc_unwarping: True
    SubModules:
      DocOrientationClassify:
        module_name: doc_text_orientation
        model_name: PP-LCNet_x1_0_doc_ori
        model_dir: null
        batch_size: 8
      DocUnwarping:
        module_name: image_unwarping
        model_name: UVDoc
        model_dir: null

Serving:
  extra:
    max_num_input_imgs: null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/p&gt;
&lt;h2&gt;Step 5：测试服务&lt;/h2&gt;
&lt;p&gt;两个服务都启动后，可以用以下 Python 代码测试：&lt;/p&gt;
&lt;p&gt;&amp;lt;ShowMoreCollapse client:visible collapsedHeight={400}&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests
import base64
from concurrent.futures import ThreadPoolExecutor, as_completed

# ⚠️ 替换：改为你的 PaddleX Serving 地址和端口
API_URL = &quot;http://localhost:8000/layout-parsing&quot;

# ⚠️ 替换：改为你的 PDF 文件路径
with open(&apos;xxxx.pdf&apos;, &apos;rb&apos;) as f:
    data = f.read()

content = base64.b64encode(data).decode(&quot;ascii&quot;)

def test_ocr(file_data, file_type=0):
    headers = {
        &quot;Content-Type&quot;: &quot;application/json&quot;
    }
    payload = {
        &quot;file&quot;: file_data,
        &quot;fileType&quot;: file_type,
        &quot;useDocOrientationClassify&quot;: True,
        &quot;useLayoutDetection&quot;: True,
        &quot;useDocUnwarping&quot;: True,
        &quot;useChartRecognition&quot;: True,
        &quot;repetitionPenalty&quot;: 1,
        &quot;temperature&quot;: 0.1,
        &quot;topP&quot;: 1,
        &quot;minPixels&quot;: 147384,
        &quot;maxPixels&quot;: 2822400,
        &quot;layoutNms&quot;: True,
        &quot;visualize&quot;: False
    }
    response = requests.post(API_URL, json=payload, headers=headers)
    return response.json()

# 并发测试
with ThreadPoolExecutor(max_workers=4) as executor:
    futures = [executor.submit(test_ocr, content, 0) for _ in range(10)]
    for future in as_completed(futures):
        try:
            result = future.result()
            print(&quot;OCR 处理完成&quot;)
        except Exception as e:
            print(f&quot;OCR 处理失败: {e}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/p&gt;
&lt;h2&gt;需要根据实际情况替换的部分汇总&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;参数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;th&gt;示例值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;uv 版本&lt;/td&gt;
&lt;td&gt;uv 包管理器版本号&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.9.27&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python 版本&lt;/td&gt;
&lt;td&gt;虚拟环境 Python 版本&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3.12&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUDA 版本 (PaddlePaddle)&lt;/td&gt;
&lt;td&gt;PaddlePaddle 对应的 CUDA 版本&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cu126&lt;/code&gt;（CUDA 12.6）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flash Attention wheel&lt;/td&gt;
&lt;td&gt;预编译的 FA 包 URL&lt;/td&gt;
&lt;td&gt;根据 CUDA/PyTorch/Python 版本选择&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;python-dev 包名&lt;/td&gt;
&lt;td&gt;Python 开发头文件包&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python3.12-dev&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vLLM 端口&lt;/td&gt;
&lt;td&gt;vLLM 后端服务端口&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8118&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PaddleX Serving 端口&lt;/td&gt;
&lt;td&gt;前端服务端口（对外暴露）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU 设备号&lt;/td&gt;
&lt;td&gt;使用的 GPU 编号&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gpu:0&lt;/code&gt; 或 &lt;code&gt;gpu:0,1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模型名称&lt;/td&gt;
&lt;td&gt;PaddleOCR-VL 模型版本&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PaddleOCR-VL-1.5-0.9B&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YAML 配置路径&lt;/td&gt;
&lt;td&gt;PaddleOCR-VL.yaml 的位置&lt;/td&gt;
&lt;td&gt;根据 Python 环境路径调整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;server_url&lt;/td&gt;
&lt;td&gt;YAML 中 vLLM 服务地址&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://127.0.0.1:8118/v1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;一键启动脚本&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# 启动 vLLM 服务 (后台)
start_vllm() {
    source /paddle/.venv_vllm/bin/activate
    paddlex_genai_server --model_name PaddleOCR-VL-1.5-0.9B --backend vllm --port 8118
}

start_vllm &amp;amp;
VLLM_PID=$!

echo &quot;vLLM 服务已后台启动 (PID: $VLLM_PID)&quot;
echo &quot;  - vLLM:    http://localhost:8118&quot;
echo &quot;  - PaddleX: http://localhost:8000&quot;
echo &quot;使用 &apos;pkill -f paddlex&apos; 停止服务&quot;
echo &quot;&quot;

# 启动 PaddleX 服务 (前台，显示日志)
source /paddle/.venv_paddle/bin/activate
paddlex --serve --device gpu:0 --pipeline ~/xxxxx/PaddleOCR-VL.yaml --port 8000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;如果你按照这篇文章还是跑不起来，那大概率是版本又变了。去骂百度吧，反正他们也不会改文档的。&lt;/p&gt;
&lt;p&gt;祝你好运。&lt;/p&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>MacOS绕过SSL Pining问题</title><link>https://blog.useforall.com/posts/macos-bypass-ssl-pining/</link><guid isPermaLink="true">https://blog.useforall.com/posts/macos-bypass-ssl-pining/</guid><description>主要是用 Frida 来绕过 MacOS 上的 SSL Pining 问题，方便调试一些应用。主要是做一个备忘录，方便以后查阅，并不涉及非常深入的用法。</description><pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import ShowMoreCollapse from &quot;../../components/custom/showMoreCollapse/main.tsx&quot;;&lt;/p&gt;
&lt;p&gt;:::info[本文主要内容]
主要是用 &lt;code&gt;Frida&lt;/code&gt; 来绕过 &lt;code&gt;MacOS&lt;/code&gt; 上的 &lt;code&gt;SSL Pining&lt;/code&gt; 问题，方便调试一些应用。主要是做一个备忘录，方便以后查阅，并不涉及非常深入的用法。
:::&lt;/p&gt;
&lt;p&gt;最近在尝试逆向部分AI平台时，有些平台只提供了客户端版本，没有网页版本，就导致调试起来比较麻烦。然后发现有些应用启用了 &lt;code&gt;SSL Pining&lt;/code&gt;，导致 ProxyMan 抓包时会报错 &lt;code&gt;SSL Pinning Failed&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Electron 应用绕过 SSL Pining&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;首先需要先判断应用是否基于 &lt;code&gt;Electron&lt;/code&gt;，可以通过查看应用包内是否有 &lt;code&gt;Electron Framework&lt;/code&gt; 来判断。可以通过 &lt;code&gt;ls /Applications/xxx.app/Contents/Frameworks&lt;/code&gt;来判断，出现 &lt;code&gt;Electron Framework.framework&lt;/code&gt; 就说明是 &lt;code&gt;Electron&lt;/code&gt; 应用。&lt;/li&gt;
&lt;li&gt;先使用最简单的办法，直接二进制方式启动程序，然后添加 &lt;code&gt;/Applications/xxx.app/Contents/MacOS/xxx --ignore-certificate-errors&lt;/code&gt; 参数，忽略证书错误。
&lt;ul&gt;
&lt;li&gt;此方法针对的主要是 Electron 主进程绕过 SSL Pining 问题，渲染进程或者其他子进程此方法无效。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果你发现是 Electron 的其他进程存在 SSL Pining 问题，可以使用 &lt;code&gt;inspect&lt;/code&gt; 邪修办法尝试解决（不一定有效）。
&lt;ul&gt;
&lt;li&gt;修改启动参数为：&lt;code&gt;/Applications/xxx.app/Contents/MacOS/xxx --inspect --ignore-certificate-errors&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Chrome 访问 &amp;lt;a href=&quot;chrome://inspect&quot; target=&quot;_blank&quot;&amp;gt;chrome://inspect&amp;lt;/a&amp;gt;，此时应该会出现一个设备，如下图，点击 &lt;code&gt;inspect&lt;/code&gt; 即可：
&lt;img src=&quot;https://lsky.useforall.com/Blog/2026/01/20/696f4da62f638.png&quot; alt=&quot;CleanShot 2026-01-20 at 17.40.26@2x&quot; /&gt;&lt;/li&gt;
&lt;li&gt;出现DevTools后，打开 &lt;code&gt;Console&lt;/code&gt;，然后输入以下代码：
&amp;lt;ShowMoreCollapse client:visible collapsedHeight={500}&amp;gt;&lt;pre&gt;&lt;code&gt;const tls = require(&apos;tls&apos;)
const https = require(&apos;https&apos;)

// 保存原始函数
const originalConnect = tls.connect
const originalRequest = https.request
const originalGet = https.get

// Hook tls.connect
tls.connect = function(...args) {
  let options = args[0]

  if (typeof options === &apos;object&apos;) {
    options.rejectUnauthorized = false
    options.checkServerIdentity = () =&amp;gt; undefined
  }

  console.log(&apos;🔓 TLS connect intercepted:&apos;, options?.host || options?.servername)
  return originalConnect.apply(this, args)
}

// Hook https.request
https.request = function(url, options, callback) {
  if (typeof url === &apos;string&apos;) {
    if (typeof options === &apos;object&apos;) {
      options.rejectUnauthorized = false
    } else {
      options = { rejectUnauthorized: false }
    }
  } else if (typeof url === &apos;object&apos;) {
    url.rejectUnauthorized = false
  }

  console.log(&apos;🔓 HTTPS request intercepted&apos;)
  return originalRequest.apply(this, arguments)
}

// Hook https.get
https.get = function(url, options, callback) {
  if (typeof url === &apos;string&apos;) {
    if (typeof options === &apos;object&apos;) {
      options.rejectUnauthorized = false
    } else {
      options = { rejectUnauthorized: false }
    }
  } else if (typeof url === &apos;object&apos;) {
    url.rejectUnauthorized = false
  }

  console.log(&apos;🔓 HTTPS get intercepted&apos;)
  return originalGet.apply(this, arguments)
}

console.log(&apos;✅ All TLS/HTTPS hooks installed - SSL verification disabled&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&amp;lt;/ShowMoreCollapse&amp;gt;
&lt;ul&gt;
&lt;li&gt;运行完成后，此时应该就可以绕过 SSL Pining 了，ProxyMan 应该会显示明文了。如果还是不行，那么只能使用 Frida 来进行动态注入了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;使用 Frida 绕过 SSL Pining&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装 Frida：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install frida-tools
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写 Frida 脚本 &lt;code&gt;ssl_kill.js&lt;/code&gt;，内容如下：
&amp;lt;ShowMoreCollapse client:visible collapsedHeight={500}&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Process.enumerateModules()
  .filter(m =&amp;gt; m.name.toLowerCase().includes(&quot;ssl&quot;))
  .forEach(m =&amp;gt; console.log(&quot;ssl module:&quot;, m.name, m.base));

function findSymbol(name) {
  // 先全局搜（如果你的 Frida 支持的话）
  if (Module.findExportByName &amp;amp;&amp;amp; typeof Module.findExportByName === &quot;function&quot;) {
    const p = Module.findExportByName(null, name);
    if (p) return p;
  }

  // 再在可能的 libssl 名字里搜（macOS 上名字经常不是 libssl.dylib）
  const candidates = [&quot;libssl.dylib&quot;, &quot;libssl.1.1.dylib&quot;, &quot;libssl.3.dylib&quot;];
  for (const m of candidates) {
    const mod = Process.findModuleByName(m);
    if (!mod) continue;
    const p = mod.findExportByName(name);
    if (p) return p;
  }
  return null;
}

var SSL_get_verify_result = findSymbol(&quot;SSL_get_verify_result&quot;);
var SSL_set_verify = findSymbol(&quot;SSL_set_verify&quot;);
var SSL_CTX_set_verify = findSymbol(&quot;SSL_CTX_set_verify&quot;);

console.log(&quot;&amp;gt;&amp;gt;&amp;gt; 开始注入 SSL Bypass...&quot;);

// 1. Hook SSL_get_verify_result
// 这个函数通常在握手完成后被调用，返回值 0 表示验证成功 (X509_V_OK)
if (SSL_get_verify_result) {
    Interceptor.attach(SSL_get_verify_result, {
        onLeave: function(retval) {
            // 无论原来验证结果如何，强行篡改为 0 (成功)
            retval.replace(ptr(0));
        }
    });
    console.log(&quot;[+] Hooked SSL_get_verify_result (强制返回验证成功)&quot;);
} else {
    console.log(&quot;[-] 未找到 SSL_get_verify_result (可能被剥离或改名)&quot;);
}

// 2. Hook SSL_set_verify
// 这个函数用来设置验证模式。我们将模式强制设为 0 (SSL_VERIFY_NONE)
if (SSL_set_verify) {
    Interceptor.attach(SSL_set_verify, {
        onEnter: function(args) {
            // args[1] 是 mode 参数，强制设为 0
            args[1] = ptr(0);
        }
    });
    console.log(&quot;[+] Hooked SSL_set_verify (强制禁用验证模式)&quot;);
} else {
    console.log(&quot;[-] 未找到 SSL_set_verify&quot;);
}

// 3. Hook SSL_CTX_set_verify
// 上下文级别的设置，同样强制设为 0
if (SSL_CTX_set_verify) {
    Interceptor.attach(SSL_CTX_set_verify, {
        onEnter: function(args) {
            // args[1] 是 mode 参数
            args[1] = ptr(0);
        }
    });
    console.log(&quot;[+] Hooked SSL_CTX_set_verify (强制禁用上下文验证)&quot;);
} else {
    console.log(&quot;[-] 未找到 SSL_CTX_set_verify&quot;);
}

console.log(&quot;&amp;gt;&amp;gt;&amp;gt; 注入完成，请检查 Proxyman 是否出现明文数据。&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 Frida 注入脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo frida -n &quot;应用进程名&quot; -l ssl_kill.js
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在没关闭 &lt;code&gt;SIP&lt;/code&gt; 的情况下，大概率会注入失败，报错 &lt;code&gt;Failed to attach: unable to access process with pid 5131 from the current user account&lt;/code&gt;，这种情况可以重新签名二进制文件来绕过限制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先找到应用的二进制文件路径，比如 &lt;code&gt;/Applications/xxx.app/Contents/Resources/bin/xxx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;然后执行以下命令重新签名：&lt;pre&gt;&lt;code&gt;# 进入应用目录
cd /Applications/xxx.app/Contents/Resources/xxx/xxx
# 删除原有签名
sudo xattr -cr xxx
# 创建一个plist文件，内容如下
echo &apos;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&amp;gt;
&amp;lt;plist version=&quot;1.0&quot;&amp;gt;
&amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.get-task-allow&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
&amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;&apos; &amp;gt; debug.plist

# 重新签名
codesign -f -s - --entitlements debug.plist /Applications/xxx.app/Contents/Resources/bin/xxx/xxx
codesign -d --entitlements - /Applications/xxx.app/Contents/Resources/bin/xxx/xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;最后执行 &lt;code&gt;sudo frida -l ssl_kill.js -n &quot;应用进程名&quot;&lt;/code&gt; 重新注入脚本。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Openlist备忘录</title><link>https://blog.useforall.com/posts/openlist-memo/</link><guid isPermaLink="true">https://blog.useforall.com/posts/openlist-memo/</guid><description>一些使用时候的注意和配置事项，以及一些网盘的配置备注</description><pubDate>Mon, 19 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import ShowMoreCollapse from &quot;../../components/custom/showMoreCollapse/main.tsx&quot;;&lt;/p&gt;
&lt;p&gt;:::info[本文主要内容]
一些使用时候的注意和配置事项，以及一些网盘的配置备注
:::&lt;/p&gt;
&lt;h2&gt;配置Docker Openlist走Clash代理&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;编辑 &lt;code&gt;docker-compose.yaml&lt;/code&gt;，部署 &lt;code&gt;Clash&lt;/code&gt; 并添加环境变量：
&amp;lt;ShowMoreCollapse client:visible&amp;gt;&lt;pre&gt;&lt;code&gt;services:
  openlist:
    image: &apos;openlistteam/openlist:latest-aio&apos;
    #image: xhofe/alist:v3.45.0-aio
    container_name: openlist
    volumes:
      - ./data:/opt/openlist/data
      #- ./data:/opt/alist/data
    ports:
      - 65007:5244 # webui
      - 65010:5246 # s3
    environment:
      - PUID=0
      - PGID=0
      - UMASK=022
      - http_proxy=http://clash:7890
      - https_proxy=http://clash:7890
      - all_proxy=http://clash:7890
      - no_proxy=localhost,127.0.0.0,127.0.1.1,127.0.1.1,127.0.0.1,local.home,172.18.0.1
    restart: unless-stopped

  clash:
    image: metacubex/mihomo
    #network_mode: bridge
    restart: always
    volumes:
      - ./clash:/root/.config/mihomo
    container_name: clash2 # 容器名称
    ports:
      # 主机http代理端口:容器http代理端口，默认7890
      # 主机sock5代理端口:容器sock5代理端口，默认7890
      # 主机REST API端口:容器REST API端口，默认9090
      - 7890:7890
      - 7891:9090
&lt;/code&gt;&lt;/pre&gt;
&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/li&gt;
&lt;li&gt;按照上面方法需要注意一下几点：
&lt;ul&gt;
&lt;li&gt;no_proxy中需要添加Openlist和Clash的容器IP地址，防止代理回环&lt;/li&gt;
&lt;li&gt;这种方法只会代理Openlist的下载请求，也就是在网盘配置里开启&lt;code&gt;Web代理&lt;/code&gt;选项（&lt;code&gt;Webdav&lt;/code&gt; 设置 &lt;code&gt;本地代理&lt;/code&gt; 没有进行测试）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;优化海外VPS对于115网盘的上传/固定下载IP&lt;/h2&gt;
&lt;h3&gt;上传速度&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;创建一个 &lt;code&gt;ip_test.sh&lt;/code&gt; 文件，&lt;code&gt;nano ip_test.sh &amp;amp;&amp;amp; chmod +x ip_test.sh &amp;amp;&amp;amp; ./ip_test.sh&lt;/code&gt;：
&amp;lt;ShowMoreCollapse client:visible&amp;gt;&lt;pre&gt;&lt;code&gt;#!/bin/bash

# --- IP 地址列表 ---
# 将您提供的所有 IP 地址粘贴到这里，每行一个
# 脚本会自动去重
RAW_IP_LIST=&quot;
183.36.31.78
120.241.7.134
183.36.31.53
120.241.14.63
163.177.84.75
219.133.47.8
163.177.84.77
183.36.31.126
163.177.84.39
120.241.7.154
&quot;

# 使用 sort -u 进行去重，并去除空行
UNIQUE_IPS=$(echo &quot;${RAW_IP_LIST}&quot; | grep -v &apos;^$&apos; | sort -u)

# 检查是否成功获取到 IP
if [ -z &quot;$UNIQUE_IPS&quot; ]; then
    echo &quot;错误：未能从列表中提取到任何有效的 IP 地址。请检查 RAW_IP_LIST 的内容。&quot;
    exit 1
fi

echo &quot;=================================================&quot;
echo &quot;           开始测试指定 IP 地址的延迟            &quot;
echo &quot;=================================================&quot;
echo &quot;&quot;
echo &quot;将测试的独特 IP 地址列表：&quot;
echo &quot;${UNIQUE_IPS}&quot;
echo &quot;&quot;

# 循环测试每个独特的 IP 地址
for IP in ${UNIQUE_IPS}; do
    echo &quot;=================================================&quot;
    echo &quot;正在测试 IP: ${IP} 的延迟 (ping 5 次)...&quot;
    ping -c 5 ${IP}
    echo &quot;&quot;
    echo &quot;正在测试 IP: ${IP} 的归属地&quot;
    # mtr -rwc 10 表示发送 10 个包并生成报告
    curl cip.cc/${IP}
    echo &quot;&quot;
done

echo &quot;=================================================&quot;
echo &quot;              所有 IP 地址测试完成！             &quot;
echo &quot;=================================================&quot;
&lt;/code&gt;&lt;/pre&gt;
&amp;lt;/ShowMoreCollapse&amp;gt;
:::note[IP来源]
RAW_IP_LIST可以去 &amp;lt;a href=&quot;https://www.itdog.cn/ping/fhnfile.oss-cn-shenzhen.aliyuncs.com&quot; target=&quot;_blank&quot;&amp;gt;itdog&amp;lt;/a&amp;gt; 获取最新的115网盘oss的IP地址。
- fhnfile.oss-cn-shenzhen.aliyuncs.com
- oss-cn-shenzhen.aliyuncs.com
记住，只用复制国内的IP地址即可。海外的IP地址无需复制。
:::&lt;/li&gt;
&lt;li&gt;根据脚本输出的内容，选择延迟最低的IP地址，填写到 &lt;code&gt;docker-compose.yaml&lt;/code&gt; 中 &lt;code&gt;Openlist&lt;/code&gt; 服务的 &lt;code&gt;extra_hosts&lt;/code&gt; 选项中，覆盖默认的 &lt;code&gt;fhnfile.oss-cn-shenzhen.aliyuncs.com&lt;/code&gt; IP地址。&lt;pre&gt;&lt;code&gt;services:
  openlist:
    ...
    extra_hosts:
      - &quot;fhnfile.oss-cn-shenzhen.aliyuncs.com:选择的最佳IP地址&quot;
      - &quot;oss-cn-shenzhen.aliyuncs.com:选择的最佳IP地址&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;如果你有阿里云的深证VPS，那么此时可以直接走内网传输
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在你的VPS上运行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nslookup fhnfile.oss-cn-shenzhen-internal.aliyuncs.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nslookup oss-cn-shenzhen-internal.aliyuncs.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后自行ping一下解析出来的内网IP，哪个快用哪个&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;extra_hosts&lt;/code&gt; 为：&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;extra_hosts:
  - &quot;fhnfile.oss-cn-shenzhen.aliyuncs.com:内网IP地址&quot;
  - &quot;oss-cn-shenzhen.aliyuncs.com:内网IP地址&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;下载固定IP地址防止115风控&lt;/h3&gt;
&lt;p&gt;:::note
前面你没使用Clash代理的话，那么这里不用看。因为 &lt;code&gt;Openlist&lt;/code&gt; 的环境代理只会代理网页预览/下载（开启 &lt;code&gt;Web代理&lt;/code&gt; 选项）。
:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Clash的配置文件中添加以下内容，优选115网盘的下载IP地址&lt;pre&gt;&lt;code&gt;mixed-port: 7890
allow-lan: true
mode: Rule
log-level: info
unified-delay: true
global-client-fingerprint: chrome
# external-ui: /usr/local/etc/clash/ui
external-controller: &quot;0.0.0.0:9090&quot;
# 监听所有网络接口，而不仅仅是 localhost

ipv6: false

proxies:


rules:
  - IP-CIDR,127.0.0.1/32,DIRECT,no-resolve
  - IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
  - DOMAIN-KEYWORD,115,home # 这个必须添加
  - DOMAIN-KEYWORD,115cdn.net,home # 这个必须添加
  - MATCH,DIRECT
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;现在你在网页浏览内容或者下载文件，&lt;code&gt;Openlist&lt;/code&gt; 会通过 &lt;code&gt;Clash&lt;/code&gt; 使用固定的IP地址进行下载然后传回给你，防止115网盘的风控。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>精简 Rust 二进制文件：一份简单的优化指南</title><link>https://blog.useforall.com/posts/optimize-rust-binary-size/</link><guid isPermaLink="true">https://blog.useforall.com/posts/optimize-rust-binary-size/</guid><description>Rust 因其出色的性能、内存安全以及强大的工具链而备受青睐。然而，初次接触 Rust 的开发者可能会注意到，即使是一个简单的 &quot;Hello, World!&quot; 程序，其编译后的二进制文件也比 C 或 Go 等语言的对应产物要大。这主要是由 Rust 的静态链接策略、丰富的标准库以及为支持健全的错误处理（如栈展开）而包含的元数据所导致的。</description><pubDate>Sat, 29 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Rust 因其出色的性能、内存安全以及强大的工具链而备受青睐。然而，初次接触 Rust 的开发者可能会注意到，即使是一个简单的 &quot;Hello, World!&quot; 程序，其编译后的二进制文件也比 C 或 Go 等语言的对应产物要大。这主要是由 Rust 的静态链接策略、丰富的标准库以及为支持健全的错误处理（如栈展开）而包含的元数据所导致的。&lt;/p&gt;
&lt;p&gt;幸运的是，Cargo 和 Rust 编译器提供了丰富的配置选项，允许开发者对编译产物的大小进行深度控制。本指南将系统性地介绍一系列优化技巧，从基础配置到高级策略，帮助你有效地为 Rust 程序“瘦身”。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;1. 基础优化：&lt;code&gt;Cargo.toml&lt;/code&gt; 配置&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;最直接、最有效的优化始于项目的 &lt;code&gt;Cargo.toml&lt;/code&gt; 文件。通过调整 &lt;code&gt;release&lt;/code&gt; 构建配置，可以在不修改任何业务代码的情况下显著减小二进制体积。&lt;/p&gt;
&lt;p&gt;将以下配置添加到你的 &lt;code&gt;Cargo.toml&lt;/code&gt; 文件中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[profile.release]
# 开启链接时优化 (Link-Time Optimization)，允许编译器跨 crate 进行优化
lto = true

# 优化级别，&apos;z&apos; 表示“尽一切可能减小大小”
opt-level = &quot;z&quot;

# 移除调试符号信息。等同于在编译后运行 `strip` 命令
strip = true

# 将代码生成单元减少到 1，为 LTO 提供最大的优化空间，但这会减慢编译速度
codegen-units = 1

# 配置 panic 时的行为为直接终止程序，而不是“栈展开”
# 这可以移除与栈展开相关的元数据和逻辑
panic = &quot;abort&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;配置项解析与示例：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;lto = true&lt;/code&gt;&lt;/strong&gt;: 链接时优化（LTO）是尺寸优化的关键。它允许链接器在合并所有依赖项的最终阶段，通盘考虑整个程序的代码，从而执行更激进的死代码删除（Dead Code Elimination）和函数内联。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;opt-level = &quot;z&quot;&lt;/code&gt;&lt;/strong&gt;: 标准的优化级别是 &lt;code&gt;3&lt;/code&gt;（速度优先）或 &lt;code&gt;s&lt;/code&gt;（尺寸优先）。&lt;code&gt;z&lt;/code&gt; 是 &lt;code&gt;s&lt;/code&gt; 的一个更极端版本，它会指示编译器采用一切手段来减小生成的代码体积，即使这可能带来微小的性能损失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;strip = true&lt;/code&gt;&lt;/strong&gt;: 默认情况下，发布构建会保留一些调试信息。此选项会在编译完成后自动剥离所有不必要的符号表和调试信息。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;手动操作对比&lt;/strong&gt;：如果不设置此项，你需要在编译后手动运行 &lt;code&gt;strip target/release/your_binary&lt;/code&gt; 来达到同样的效果。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;panic = &quot;abort&quot;&lt;/code&gt;&lt;/strong&gt;: Rust 的默认 &lt;code&gt;panic&lt;/code&gt; 行为是 &lt;code&gt;unwind&lt;/code&gt;（栈展开），它会清理调用栈上的所有资源。这个过程需要额外的代码来支持。设置为 &lt;code&gt;abort&lt;/code&gt; 后，程序在遇到不可恢复的错误时会立即退出，省去了这部分开销。这是一个在尺寸和崩溃后行为之间的权衡。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实践效果&lt;/strong&gt;：仅应用上述配置，一个基础的 Actix Web &quot;Hello World&quot; 项目的二进制文件大小可以从 8.5MB 减小到约 3.5MB，效果非常显著。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;2. 定位体积来源：使用 &lt;code&gt;cargo-bloat&lt;/code&gt; 进行分析&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;在进行更深入的优化前，首先需要知道体积究竟消耗在哪里。&lt;code&gt;cargo-bloat&lt;/code&gt; 是一个不可或缺的分析工具，它可以清晰地展示二进制文件中每个函数和依赖项所占用的空间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;安装与使用：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装工具：&lt;pre&gt;&lt;code&gt;cargo install cargo-bloat
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;在项目根目录下运行分析（确保已使用 release 配置编译过项目）：&lt;pre&gt;&lt;code&gt;cargo bloat --release --crates
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;示例输出与解读：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cargo bloat --release --crates
    Finished release [optimized] target(s) in 0.06s
   Compiling url v2.2.2
    Finished release [optimized] target(s) in 1.44s
 File Size: 3.43 MiB
  Text Size: 3.31 MiB
.text             Size Crate
2.10 MiB (63.5%)  std
373.1KiB (11.1%)  tokio
207.3KiB ( 6.1%)  hyper
119.8KiB ( 3.5%)  actix_http
...
8.70 KiB ( 0.3%)  my_project   &amp;lt;-- 你自己的代码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从这份报告中可以清晰地看到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;std&lt;/code&gt; (标准库) 占据了绝大部分空间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tokio&lt;/code&gt; 和 &lt;code&gt;hyper&lt;/code&gt; 等异步运行时和 HTTP 库是主要的体积来源。&lt;/li&gt;
&lt;li&gt;项目自身的代码 (&lt;code&gt;my_project&lt;/code&gt;) 占比其实很小。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这份数据为你指明了优化的方向：&lt;strong&gt;管理和精简依赖项&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;3. 依赖项管理：精简 &lt;code&gt;features&lt;/code&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;大型 Rust 库为了保持灵活性，通常会通过 &lt;code&gt;features&lt;/code&gt; 来控制功能的开启。默认情况下，你可能会引入许多不需要的功能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优化策略：&lt;/strong&gt;
在 &lt;code&gt;Cargo.toml&lt;/code&gt; 中，为依赖项设置 &lt;code&gt;default-features = false&lt;/code&gt;，然后只启用你确实需要的功能。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 1: &lt;code&gt;tokio&lt;/code&gt;&lt;/strong&gt;
默认的 &lt;code&gt;tokio&lt;/code&gt; 引入了多线程运行时、所有 IO 驱动和宏，体积较大。如果你的应用只是一个简单的 TCP 客户端，可以这样配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Before:
# tokio = { version = &quot;1&quot;, features = [&quot;full&quot;] }

# After:
tokio = { version = &quot;1&quot;, default-features = false, features = [&quot;macros&quot;, &quot;rt&quot;, &quot;net&quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rt&lt;/code&gt; 开启了单线程运行时，&lt;code&gt;net&lt;/code&gt; 提供了 TCP/UDP 支持。这样就避免了引入多线程和文件系统等不必要的部分。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;示例 2: &lt;code&gt;reqwest&lt;/code&gt;&lt;/strong&gt;
如果你只用 &lt;code&gt;reqwest&lt;/code&gt; 发送 JSON 数据，并不需要 &lt;code&gt;gzip&lt;/code&gt; 或 &lt;code&gt;brotli&lt;/code&gt; 压缩支持：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Before:
# reqwest = &quot;0.11&quot;

# After:
reqwest = { version = &quot;0.11&quot;, default-features = false, features = [&quot;json&quot;, &quot;rustls-tls&quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们明确指定了 &lt;code&gt;json&lt;/code&gt; 支持，并选用了 &lt;code&gt;rustls-tls&lt;/code&gt; 作为 TLS 后端，它通常比原生的 &lt;code&gt;native-tls&lt;/code&gt; (OpenSSL) 产物体积更小。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;4. 高级与外部工具&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;当上述方法达到极限时，还可以借助一些外部工具和更底层的技术。&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;4.1 使用 UPX 进行可执行文件压缩&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;UPX (Ultimate Packer for eXecutables) 是一个流行的可执行文件压缩器。它通过压缩算法处理二进制文件，并在运行时自动解压到内存中执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;安装 (以 macOS 和 Debian/Ubuntu 为例):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# macOS
brew install upx

# Debian/Ubuntu
sudo apt-get install upx-ucl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用与效果演示：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 编译你的项目
cargo build --release

# 2. 查看原始大小
ls -lh target/release/my_project
# -rwxr-xr-x  1 user  staff   3.5M Nov 23 14:30 target/release/my_project

# 3. 使用 UPX 进行最高级别压缩
upx --best --lzma target/release/my_project

# 4. 查看压缩后的大小
ls -lh target/release/my_project
# -rwxr-xr-x  1 user  staff   1.1M Nov 23 14:32 target/release/my_project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;权衡&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：压缩率极高，操作简单。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：会增加程序启动时的解压延迟（通常是毫秒级），且某些安全软件可能对加壳程序产生误报。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;4.2 终极方案: &lt;code&gt;#![no_std]&lt;/code&gt;&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;这是最彻底的优化方式，通常用于嵌入式系统或操作系统开发。它会完全移除对标准库 &lt;code&gt;std&lt;/code&gt; 的依赖。这意味着你将失去堆分配（如 &lt;code&gt;Vec&lt;/code&gt;, &lt;code&gt;String&lt;/code&gt;）、文件 IO、网络、线程等所有由操作系统提供的抽象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;no_std&lt;/code&gt; &quot;Hello World&quot; 示例：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;main.rs&lt;/code&gt; 文件内容:&lt;/strong&gt;&lt;pre&gt;&lt;code&gt;#![no_std]
#![no_main]

use core::panic::PanicInfo;

// 定义 panic 处理器
#[panic_handler]
fn panic(_info: &amp;amp;PanicInfo) -&amp;gt; ! {
    loop {}
}

// 定义程序入口点
#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    // 在这里不能使用 println! 等依赖 std 的宏
    // 如果要输出，需要直接调用系统调用 (syscall)
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构建&lt;/strong&gt;: 这种方式构建出的二进制文件极小，通常只有几 KB。但开发复杂度极高，因为它要求开发者直接与底层 API 甚至系统调用打交道。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;结论&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;优化 Rust 二进制文件体积是一个系统性的过程，开发者可以根据项目需求选择合适的优化深度。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对于绝大多数应用&lt;/strong&gt;：从 &lt;code&gt;Cargo.toml&lt;/code&gt; 的 &lt;code&gt;[profile.release]&lt;/code&gt; 配置入手，结合 &lt;code&gt;cargo-bloat&lt;/code&gt; 分析并精简依赖项的 &lt;code&gt;features&lt;/code&gt;，通常就能获得满意的结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于分发敏感的应用 (如 CLI 工具)&lt;/strong&gt;：在完成上述步骤后，可以额外使用 UPX 进行压缩，以获得最佳的分发体验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对于极端环境 (如嵌入式)&lt;/strong&gt;：&lt;code&gt;#![no_std]&lt;/code&gt; 是最终选择，但这需要完全不同的开发模式和知识体系。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过合理运用这些工具和技术，你可以有效地控制 Rust 项目的最终产物体积，使其在保持高性能和安全性的同时，也兼具轻量化的优势。&lt;/p&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>使用CF Worker搭建属于自己的加速服务</title><link>https://blog.useforall.com/posts/cf-worker-build-your-own-accelerate-service/</link><guid isPermaLink="true">https://blog.useforall.com/posts/cf-worker-build-your-own-accelerate-service/</guid><description>本文主要介绍如何使用CF Worker搭建属于自己的加速服务，并通过白嫖Hurricane Electric免费IPv6来白嫖域名实现近乎永续的加速服务。适用于Github、Homebrew、Docker、HuggingFace等多种场景（万物皆可加速）</description><pubDate>Sun, 23 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import SslForm from &quot;../../components/custom/Cloudflare/addSSL.tsx&quot;;
import XgetGrid from &quot;../../components/custom/Cloudflare/xget/main.tsx&quot;;&lt;/p&gt;
&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
本文主要介绍如何使用CF Worker搭建属于自己的加速服务，并通过白嫖Hurricane Electric免费IPv6来白嫖域名实现近乎永续的加速服务。适用于Github、Homebrew、Docker、HuggingFace等多种场景（万物皆可加速）
:::&lt;/p&gt;
&lt;p&gt;CF Worker 部署了个 Xget，一个巨强的服务加速的项目，真的做到了万物皆可加速，感谢大佬的付出。然后白嫖了 HE 的 IPv6 用 rdns 托管在了 CF 上（&lt;a href=&quot;/posts/Use-he-net-to-get-almost-unlimited-and-permanent-domains/&quot;&gt;教程在此&lt;/a&gt;）。
::github{repo=&quot;xixu-me/Xget&quot;}
只要 HE 不倒闭（话说 HE 倒闭概率应该比大公司还低吧😂）或者 HE 不收回免费IPv6，CF 不封我号，此服务永久可用（封了也没事儿，在注册就行了，毕竟我自己也要用）。已经优选过了，可以不用浪费自己的梯子流量，把域名加入 direct 即可。支持 ipv4 和 IPv6 访问。&lt;/p&gt;
&lt;p&gt;如果懒得自己搭建，可以使用成品。提供两个加速地址：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IPv6 反向解析域名&lt;/strong&gt;：&lt;code&gt;https://proxy.a.a.a.a.4.a.f.e.0.7.4.0.1.0.0.2.ip6.arpa&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;短域名&lt;/strong&gt;：&lt;code&gt;https://speed.oo9.dpdns.org&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;测速链接：&lt;a href=&quot;https://speed.oo9.dpdns.org/ubuntu/ubuntu/pool/universe/t/texlive-extra/texlive-extra_2025.20250927.orig.tar.xz&quot;&gt;texlive-extra_2025.20250927.orig.tar.xz&lt;/a&gt;
&amp;lt;XgetGrid client:only={&quot;react&quot;} /&amp;gt;&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare Worker&lt;/strong&gt;：Cloudflare 提供的无服务器计算平台。建议注册小号，这个本质上属于滥用，有封号风险。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;获取 HE 免费的IPv6&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;前往 &lt;a href=&quot;https://tunnelbroker.net/&quot;&gt;Hurricane Electric 免费 IPv6 申请页面&lt;/a&gt; 注册账号并登录。&lt;/li&gt;
&lt;li&gt;剩余的请参考&lt;a href=&quot;/posts/Use-he-net-to-get-almost-unlimited-and-permanent-domains/&quot;&gt;此教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;完成托管HE的域名到CF。&lt;/li&gt;
&lt;li&gt;请在下方组件完成 SSL 证书申请，否则搭建完成后会证书错误。
&amp;lt;SslForm client:visible /&amp;gt;
:::tip
前端会报Cors错误，所有写成了API，后端不会存储你的任何信息，请放心使用。违法犯罪的事情咱不做
:::
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/11/12/6914a2e1f1028.png&quot; alt=&quot;CleanShot 2025-11-12 at 23.06.52@2x&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;部署CF Worker&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fork Xget 仓库&lt;/strong&gt;：&lt;a href=&quot;https://github.com/xixu-me/Xget/fork&quot;&gt;Fork xixu-me/Xget&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取 Cloudflare 凭证&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;访问&lt;a href=&quot;https://dash.cloudflare.com/?to=/:account/api-tokens&quot;&gt;帐户 API 令牌&lt;/a&gt;创建并记录 API 令牌，使用“编辑 Cloudflare Workers”模板&lt;/li&gt;
&lt;li&gt;访问 &lt;a href=&quot;https://dash.cloudflare.com/?to=/:account/workers-and-pages&quot;&gt;Workers 和 Pages&lt;/a&gt; 记录 Account ID
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/11/12/691499ca5b710.png&quot; alt=&quot;CleanShot 2025-11-12 at 22.27.38@2x&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置 GitHub Secrets&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;进入你的 GitHub 存储库 → Settings → Secrets and variables → Actions&lt;/li&gt;
&lt;li&gt;添加以下 Secrets：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CLOUDFLARE_API_TOKEN&lt;/code&gt;：你的 API 令牌&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CLOUDFLARE_ACCOUNT_ID&lt;/code&gt;：你的 Account ID
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/11/12/6914991dc942d.png&quot; alt=&quot;CleanShot 2025-11-12 at 22.25.31@2x&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;触发部署&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;推送代码到 &lt;code&gt;main&lt;/code&gt; 分支会自动触发部署，或者直接去 &lt;code&gt;Action&lt;/code&gt; 手动触发。&lt;/li&gt;
&lt;li&gt;仅修改文档文件（&lt;code&gt;.md&lt;/code&gt;）、&lt;code&gt;LICENSE&lt;/code&gt;、&lt;code&gt;.gitignore&lt;/code&gt; 等不会触发部署&lt;/li&gt;
&lt;li&gt;也可以在 GitHub Actions 页面手动触发部署&lt;/li&gt;
&lt;li&gt;请确保至少触发一次Action后再进行优选&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优选&lt;/strong&gt;（可选）：
&lt;ul&gt;
&lt;li&gt;添加一条 &lt;code&gt;Record&lt;/code&gt;，类型为 &lt;code&gt;CNAME&lt;/code&gt;，推荐指向 &lt;code&gt;www.dbs.com.sg&lt;/code&gt; 或者去 &lt;a href=&quot;https://www.cnae.top/&quot;&gt;&lt;code&gt;https://www.cnae.top/&lt;/code&gt;&lt;/a&gt; 自己选一个&lt;/li&gt;
&lt;li&gt;前往 &lt;code&gt;Workers Routes&lt;/code&gt; 页面，添加一条路由，格式为 &lt;code&gt;你的域名/*&lt;/code&gt;，选择刚刚创建的 Worker&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Vapoursynth视频压制笔记</title><link>https://blog.useforall.com/posts/vapoursynth-video-encoding-notes/</link><guid isPermaLink="true">https://blog.useforall.com/posts/vapoursynth-video-encoding-notes/</guid><description>本文主要介绍如何使用Vapoursynth进行视频压制的相关笔记和备忘，以及如何在Linux上压制视频的一些经验分享。</description><pubDate>Sun, 16 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import ShowMoreCollapse from &quot;../../components/custom/showMoreCollapse/main.tsx&quot;;&lt;/p&gt;
&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
本文主要介绍如何使用Vapoursynth进行视频压制的相关笔记和备忘，以及如何在Linux上压制视频的一些经验分享。
:::&lt;/p&gt;
&lt;p&gt;说来也在猫站（&lt;a href=&quot;https://pterclub.net&quot;&gt;PTer&lt;/a&gt;）压制组呆了一年多了，惭愧的是确实没学到什么高级技巧，目前也只会压制一些简单的x264视频，所以本文也只是记录一些常用的命令和脚本，方便以后查阅。同时顺便记录下我是如何折腾在Linux上Build一个docker环境来压制视频的。这么折腾是因为部分软件只能在Windows上运行，为了顺利在Linux上运行，也是废了一番功夫。&lt;/p&gt;
&lt;h2&gt;Docker环境搭建&lt;/h2&gt;
&lt;p&gt;待更新...&lt;/p&gt;
&lt;h2&gt;脚本类&lt;/h2&gt;
&lt;h3&gt;自动生成不同视频源的对比代码脚本&lt;/h3&gt;
&lt;p&gt;:::tip{title=&quot;脚本功能简介&quot;}
该脚本会扫描指定目录下的所有子文件夹，查找主视频文件（支持BDMV/MKV/MP4格式），并生成相应的VapourSynth代码片段，追加到指定的&lt;code&gt;.vpy&lt;/code&gt;文件末尾。支持BDMV结构的智能识别和递归查找其他视频格式。手动查找每个源最大m2ts再复制地址再写到代码里，这种手动行为是非常不健康的！！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 compare_code_gen.py -h
usage: compare_code_gen.py [-h] compare_vpy_file source_directory

自动扫描指定目录下的所有子文件夹，查找主视频文件(BDMV/MKV/MP4)，并生成VapourSynth代码追加到.vpy文件中。

positional arguments:
  compare_vpy_file  要修改的目标 .vpy 文件路径
  source_directory  包含多个BD或Remux子文件夹的源目录 (e.g., &apos;silver/bd&apos;)

options:
  -h, --help        show this help message and exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::
&amp;lt;ShowMoreCollapse collapsedHeight={&apos;400px&apos;} client:visible&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import argparse
import json
from pathlib import Path
import os

def find_bd_root(parent_path: Path) -&amp;gt; Path | None:
    &quot;&quot;&quot;
    在给定的父路径下，查找包含 BDMV 文件夹的子目录（即蓝光根目录）。
    只查找一层深度。
    &quot;&quot;&quot;
    if not parent_path.is_dir():
        return None

    for item in parent_path.iterdir():
        if item.is_dir():
            if (item / &quot;BDMV&quot;).is_dir():
                return item
            elif (item.name in [&quot;BDMV&quot;, &quot;bdmv&quot;]) and (item.is_dir()):
                return parent_path

    return None

def find_largest_m2ts(bd_root_path: Path) -&amp;gt; Path | None:
    &quot;&quot;&quot;
    在给定的 BD 根目录下，查找 BDMV/STREAM/ 文件夹中最大的 .m2ts 文件。
    &quot;&quot;&quot;
    stream_dir = bd_root_path / &quot;BDMV&quot; / &quot;STREAM&quot;

    if not stream_dir.is_dir():
        return None
    m2ts_files = list(stream_dir.glob(&quot;*.m2ts&quot;))

    if not m2ts_files:
        print(f&quot;  -&amp;gt; 警告: 在 &apos;{stream_dir}&apos; 中未找到任何 .m2ts 文件。&quot;)
        return None

    return max(m2ts_files, key=lambda p: p.stat().st_size)

def find_main_video_file(parent_path: Path) -&amp;gt; Path | None:
    &quot;&quot;&quot;
    智能地在给定目录中查找主视频文件。
    优先级: BDMV -&amp;gt; MKV (递归) -&amp;gt; MP4 (递归)
    &quot;&quot;&quot;
    # 策略 1: 查找 BDMV 结构 (非递归)
    bd_root = find_bd_root(parent_path)
    if bd_root:
        print(f&quot;  -&amp;gt; 检测到 BDMV 结构，位于: {bd_root}&quot;)
        return find_largest_m2ts(bd_root)
    # 策略 2: 按优先级递归查找其他容器格式
    print(&quot;  -&amp;gt; 未检测到 BDMV 结构，开始递归查找其他视频文件...&quot;)
    for ext in [&quot;mkv&quot;, &quot;mp4&quot;]:
        print(f&quot;  -&amp;gt; 正在递归搜索 .{ext} 文件...&quot;)
        files = list(parent_path.glob(f&quot;**/*.{ext}&quot;))

        if files:
            largest_file = max(files, key=lambda p: p.stat().st_size)
            relative_path = largest_file.relative_to(parent_path)
            print(f&quot;  -&amp;gt; 找到最大的 {ext.upper()} 文件: {relative_path}&quot;)
            return largest_file
        else:
            print(f&quot;  -&amp;gt; 目录中未找到任何 .{ext} 文件。&quot;)

    return None

def generate_vpy_snippet(var_name: str, video_path: Path) -&amp;gt; tuple[str, str]:
    &quot;&quot;&quot;
    根据变量名和视频文件路径生成 VapourSynth 代码片段。
    返回 (代码片段, 使用的安全变量名)。
    &quot;&quot;&quot;
    absolute_path = os.path.abspath(video_path)
    # 确保变量名在VapourSynth中有效
    safe_var_name = var_name.replace(&apos;-&apos;, &apos;_&apos;).replace(&apos;.&apos;, &apos;_&apos;)

    source_line = f&apos;{safe_var_name} = core.lsmas.LWLibavSource(r&quot;{absolute_path}&quot;)&apos;
    fps_line = f&apos;{safe_var_name} = core.std.AssumeFPS({safe_var_name}, fpsnum=24000, fpsden=1001)&apos;
    depth_line = f&apos;{safe_var_name} = mvf.Depth({safe_var_name}, 16)&apos;

    snippet = f&quot;{source_line}\n{fps_line}\n{depth_line}&quot;
    return snippet, safe_var_name

def main():
    &quot;&quot;&quot;主执行函数&quot;&quot;&quot;
    parser = argparse.ArgumentParser(
        description=&quot;自动扫描指定目录下的所有子文件夹，查找主视频文件(BDMV/MKV/MP4)，并生成VapourSynth代码追加到.vpy文件中。&quot;
    )
    parser.add_argument(&quot;compare_vpy_file&quot;, help=&quot;要修改的目标 .vpy 文件路径&quot;)
    parser.add_argument(
        &quot;source_directory&quot;,
        help=&quot;包含多个BD或Remux子文件夹的源目录 (e.g., &apos;silver/bd&apos;)&quot;
    )
    args = parser.parse_args()
    vpy_file = Path(args.compare_vpy_file)
    if not vpy_file.exists():
        print(f&quot;信息: vpy 文件 &apos;{vpy_file}&apos; 不存在，将自动创建。&quot;)
        vpy_file.touch()
    source_dir = Path(args.source_directory)
    if not source_dir.is_dir():
        print(f&quot;错误: 源目录 &apos;{source_dir}&apos; 不存在或不是一个目录。&quot;)
        return
    sub_directories = sorted([d for d in source_dir.iterdir() if d.is_dir()])

    print(f&quot;将在 &apos;{source_dir}&apos; 目录下扫描 {len(sub_directories)} 个子文件夹...&quot;)
    generated_snippets = []
    successful_vars = [] # &amp;lt;--- 新增：用于存储成功生成的安全变量名

    for sub_dir in sub_directories:
        print(f&quot;\n--- 正在处理: {sub_dir} ---&quot;)

        main_video = find_main_video_file(sub_dir)

        if main_video:
            var_name = sub_dir.name.upper()

            # 生成代码片段，并获取使用到的安全变量名
            snippet, safe_name = generate_vpy_snippet(var_name, main_video)

            print(f&quot;最终找到的主视频文件: {main_video}&quot;)
            print(f&quot;将使用变量名: {safe_name}&quot;)

            generated_snippets.append(snippet)
            successful_vars.append(safe_name) # &amp;lt;--- 新增：记录变量名
        else:
            print(f&quot;警告: 在 &apos;{sub_dir}&apos; 下未找到支持的视频文件 (BDMV/MKV/MP4)，已跳过。&quot;)

    if not generated_snippets:
        print(&quot;\n没有生成任何代码片段，vpy 文件未被修改。&quot;)
        return

    # --- 新增：生成 FrameInfo 和 set_output 代码块 ---
    frame_info_lines = []
    set_output_lines = []
    for i, var in enumerate(successful_vars):
        frame_info_lines.append(f&quot;{var} = awf.FrameInfo({var}, &apos;{var}&apos;)&quot;)
        set_output_lines.append(f&quot;{var}.set_output({i})&quot;)

    all_new_code = &quot;\n\n&quot;.join(generated_snippets)
    frame_info_code = &quot;\n&quot;.join(frame_info_lines)
    set_output_code = &quot;\n&quot;.join(set_output_lines)
    # --- 结束新增部分 ---

    try:
        fixed_import = &quot;&quot;&quot;import vapoursynth as vs
import awsmfunc as awf
import kagefunc as kgf
import mvsfunc as mvf
import adptvgrnMod
from rekt import rektlvls
from vsutil import get_y
from screenshot import GetSnapShot, upload_to_lsky_album
core = vs.core\n&quot;&quot;&quot;

        original_content = &quot;&quot;
        # 只有在文件存在且不为空时才读取
        if vpy_file.exists() and vpy_file.stat().st_size &amp;gt; 0:
            original_content = vpy_file.read_text(encoding=&apos;utf-8&apos;)
            # 如果原始文件已有import，避免重复添加
            if original_content.strip().startswith(&quot;import vapoursynth&quot;):
                fixed_import = &quot;&quot;

        # 拼接所有部分
        new_content = (
            fixed_import +
            original_content.strip() +
            &quot;\n\n# --- 自动生成的源滤镜代码 ---\n&quot; +
            all_new_code +
            &quot;\n\n# --- 自动生成的 FrameInfo 和 Output ---\n&quot; +
            frame_info_code +
            &quot;\n\n&quot; +
            set_output_code +
            &quot;\n\n# --- 自动生成的代码结束 ---\n&quot;
        )

        vpy_file.write_text(new_content, encoding=&apos;utf-8&apos;)
        print(f&quot;\n成功！已将生成的代码追加到 &apos;{vpy_file}&apos; 的末尾。&quot;)

    except IOError as e:
        print(f&quot;错误: 读写文件 &apos;{vpy_file}&apos; 时发生错误: {e}&quot;)
    except Exception as e:
        print(f&quot;发生未知错误: {e}&quot;)

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/p&gt;
&lt;h3&gt;x264自动压制脚本&lt;/h3&gt;
&lt;p&gt;:::tip{title=&quot;脚本功能简介&quot;}
该脚本使用VapourSynth的vspipe和x264进行视频压制，支持多CRF值和Zone设置。自动输入log文件、压制多个crf时合并log为同一个文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 x264_code_generation.py -h
usage: x264_code_generation.py [-h] vpy_file crf_value zone total_frames output_path

视频处理脚本

positional arguments:
  vpy_file      输入的 vpy 文件路径
  crf_value     CRF 值 (JSON 格式的数组, e.g., &apos;[18, 18.5]&apos;)
  zone          Zone 值 (JSON 格式的数组, e.g., &apos;[[103,543], [1000,2000]]&apos;)
  total_frames  视频总帧数
  output_path   输出文件目录

options:
  -h, --help    show this help message and exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::
&amp;lt;ShowMoreCollapse collapsedHeight={&apos;400px&apos;} client:visible&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import argparse
import json
from pathlib import Path
import os
import subprocess
import sys
import time

def main(vpy_file, crf_value, zone, total_frames, output_path):
    start_time = time.time()
    # ... (命令定义部分保持不变) ...
    vspipe_cmd = [&apos;vspipe&apos;, &apos;-c&apos;, &apos;y4m&apos;, vpy_file, &apos;-&apos;]
    x264_cmd = [
        &apos;x264&apos;,
        &apos;--output-depth&apos;, &apos;8&apos;,
        &apos;--crf&apos;, str(crf_value),
        &apos;--preset&apos;, &apos;veryslow&apos;,
        &apos;--profile&apos;, &apos;high&apos;,
        &apos;--level&apos;, &apos;4.1&apos;,
        &apos;--vbv-bufsize&apos;, &apos;78125&apos;,
        &apos;--vbv-maxrate&apos;, &apos;62500&apos;,
        &apos;--merange&apos;, &apos;32&apos;,
        &apos;--bframes&apos;, &apos;16&apos;,
        &apos;--deblock&apos;, &apos;-3:-3&apos;,
        &apos;--no-fast-pskip&apos;,
        &apos;--rc-lookahead&apos;, &apos;250&apos;,
        &apos;--qcomp&apos;, &apos;0.60&apos;,
        &apos;--psy-rd&apos;, &apos;1.0:0.00&apos;,
        &apos;--aq-mode&apos;, &apos;3&apos;,
        &apos;--aq-strength&apos;, &apos;0.75&apos;,
        &apos;--me&apos;, &apos;umh&apos;,
        &apos;--b-adapt&apos;, &apos;2&apos;,
        &apos;--direct&apos;, &apos;auto&apos;,
        &apos;--subme&apos;, &apos;11&apos;,
        &apos;--trellis&apos;, &apos;2&apos;,
        &apos;--no-dct-decimate&apos;,
        &apos;--no-mbtree&apos;,
        &apos;--colormatrix&apos;, &apos;bt709&apos;,
        &apos;--colorprim&apos;, &apos;bt709&apos;,
        &apos;--transfer&apos;, &apos;bt709&apos;,
        &apos;--ipratio&apos;, &apos;1.30&apos;,
        &apos;--pbratio&apos;, &apos;1.20&apos;,
        &apos;--output&apos;, output_path,
        &apos;--frames&apos;, str(total_frames),
        &apos;--demuxer&apos;, &apos;y4m&apos;,
        &apos;-&apos;
    ]
    if zone:
        tmp = []
        for z in zone:
            tmp.append(f&quot;{z[0]},{z[1]},b=1.3&quot;)
        output_index = x264_cmd.index(&apos;--output&apos;)
        x264_cmd.insert(output_index, &apos;--zones&apos;)
        x264_cmd.insert(output_index + 1, &apos;/&apos;.join(tmp))

    # --- 优化后的执行与日志记录模块 ---
    log_file_path = Path(output_path).with_suffix(&apos;.txt&apos;)
    full_command_str = &apos; &apos;.join(vspipe_cmd) + &apos; | &apos; + &apos; &apos;.join(x264_cmd)
    print(f&apos;--------------------------- CRF {crf_value} begin ---------------------------&apos;)
    print(f&quot;执行命令: {full_command_str}&quot;)
    print(f&quot;输出文件: {output_path}&quot;)
    print(f&quot;日志文件: {log_file_path}&quot;)

    try:
        with open(log_file_path, &apos;w&apos;, encoding=&apos;utf-8&apos;) as log_file:
            # --- 新增功能：将命令写入日志文件顶部 ---
            # log_file.write(f&quot;--- Command for CRF {crf_value} ---\n&quot;)
            log_file.write(full_command_str + &quot;\n&quot;)
            # log_file.write(&quot;--- Log Output ---\n&quot;)
            # --- 新增功能结束 ---

            vspipe_process = subprocess.Popen(vspipe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            x264_process = subprocess.Popen(
                x264_cmd,
                stdin=vspipe_process.stdout,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                encoding=&apos;utf-8&apos;,
                errors=&apos;replace&apos;
            )
            vspipe_process.stdout.close()

            for line in iter(x264_process.stdout.readline, &apos;&apos;):
                # **新的、更可靠的进度行判断逻辑**
                # 进度行通常以 &apos;[&apos; 开头，并包含 &apos;fps&apos; 和 &apos;eta&apos;
                line_stripped = line.strip()
                if line_stripped.startswith(&apos;[&apos;) and &apos;fps&apos; in line_stripped and &apos;eta&apos; in line_stripped:
                    # 这是进度行，我们在控制台动态刷新它
                    sys.stdout.write(&quot; &quot; * 80 + &quot;\r&quot;) # 清除旧行内容
                    sys.stdout.write(line_stripped + &apos;\r&apos;)
                    sys.stdout.flush()
                    # 并且，我们不把它写入日志文件
                else:
                    # 这是永久信息行 (如 x264 [info] 或最终总结)
                    # 在控制台正常打印
                    print(line_stripped)
                    # 并把它写入日志文件
                    log_file.write(line)

            print() # 进度条结束后换行，保持终端整洁

            # 等待进程结束
            vspipe_process.wait()
            x264_process.wait()

            # **新的、更智能的错误处理逻辑**
            # 如果 x264 成功退出，那么 vspipe 的 SIGPIPE(-13) 错误是正常的，可以忽略
            if x264_process.returncode == 0 and vspipe_process.returncode in [-13, 141]: # 141 is 128+13, another way SIGPIPE is reported
                print(&quot;VSPipe 进程被正常中止 (SIGPIPE)，这是预期行为。&quot;)
            elif vspipe_process.returncode != 0:
                # 只有在 x264 也失败，或者 vspipe 报了其他错误时，才报告 vspipe 错误
                vspipe_stderr = vspipe_process.stderr.read().decode(&apos;utf-8&apos;, errors=&apos;replace&apos;)
                error_message = f&quot;VSPipe 错误 (返回码: {vspipe_process.returncode}):\n{vspipe_stderr}&quot;
                print(error_message)
                log_file.write(f&quot;\n\n--- VSPipe Error ---\n{error_message}\n&quot;)

            if x264_process.returncode != 0:
                print(f&quot;x264 编码失败，返回码: {x264_process.returncode}&quot;)
            else:
                print(f&quot;nCRF {crf_value} 编码成功!&quot;)

    except FileNotFoundError:
        print(&quot;错误: &apos;vspipe&apos; 或 &apos;x264&apos; 命令未找到。请确保它们已安装并在系统 PATH 中。&quot;)
    except Exception as e:
        print(f&quot;发生未知错误: {e}&quot;)

    print(f&apos;--------------------------- CRF {crf_value} End (Time: {time.time() - start_time:.2f} 秒) -----------------------------&apos;)
    print(&quot;\n&quot;)

# __main__ 部分保持不变
if __name__ == &quot;__main__&quot;:
    parser = argparse.ArgumentParser(description=&quot;视频处理脚本&quot;)
    # ... 其余部分完全相同 ...
    parser.add_argument(&quot;vpy_file&quot;, help=&quot;输入的 vpy 文件路径&quot;)
    parser.add_argument(&quot;crf_value&quot;, type=json.loads, help=&quot;CRF 值 (JSON 格式的数组, e.g., &apos;[18, 18.5]&apos;)&quot;)
    parser.add_argument(&quot;zone&quot;, type=json.loads, help=&quot;Zone 值 (JSON 格式的数组, e.g., &apos;[[103,543], [1000,2000]]&apos;)&quot;)
    parser.add_argument(&quot;total_frames&quot;, type=int, help=&quot;视频总帧数&quot;)
    parser.add_argument(&quot;output_path&quot;, help=&quot;输出文件目录&quot;)
    args = parser.parse_args()

    vpy_path = Path(args.vpy_file)
    if not vpy_path.exists():
        print(f&quot;错误: vpy 文件不存在 -&amp;gt; {vpy_path}&quot;)
        exit(1)
    if not vpy_path.name.endswith(&quot;.vpy&quot;):
        print(&quot;错误: 输入文件不是 vpy 文件&quot;)
        exit(1)

    output_dir = Path(args.output_path)
    output_dir.mkdir(parents=True, exist_ok=True)

    vpy_file_path = str(vpy_path.resolve())
    output_dir_path = str(output_dir.resolve())

    for i in args.crf_value:
        output_file = Path(output_dir_path) / f&quot;crf{i}.mkv&quot;
        if (output_file.exists()):
            # 删除已存在的文件和日志
            os.remove(output_file)
            log_file = output_file.with_suffix(&apos;.txt&apos;)
            if log_file.exists():
                os.remove(log_file)
        main(vpy_file_path, i, args.zone, args.total_frames, str(output_file))

    # 把所有log文件合并成一个，之间添加三行换行
    combined_log_path = output_dir / &quot;combined_log.txt&quot;
    if len(args.crf_value) &amp;gt; 0:
        with open(combined_log_path, &apos;a&apos;, encoding=&apos;utf-8&apos;) as combined_log:
            for i in args.crf_value:
                log_file = output_dir / f&quot;crf{i}.txt&quot;
                if log_file.exists():
                    with open(log_file, &apos;r&apos;, encoding=&apos;utf-8&apos;) as lf:
                        combined_log.write(lf.read())
                        combined_log.write(&quot;\n\n\n&quot;) # 三行换行分隔
                # 删除文件
                if log_file.exists():
                    os.remove(log_file)
        print(f&quot;所有日志已合并到 {combined_log_path}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/p&gt;
&lt;h3&gt;AI修复字幕语法错误&lt;/h3&gt;
&lt;p&gt;:::tip{title=&quot;脚本功能简介&quot;}
该脚本使用OpenAI的API对SRT字幕文件进行语法和拼写错误的修复，特别关注OCR过程中常见的字符混淆问题。支持并行处理多个字幕块以提高效率。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 ai-fix-srt.py -h
usage: ai-fix-srt.py [-h] [-o OUTPUT_FILE] [-w WORKERS] input_file

Correct spelling and grammar in an SRT file using OpenAI in parallel.

positional arguments:
  input_file            The path to the input SRT file.

options:
  -h, --help            show this help message and exit
  -o OUTPUT_FILE, --output_file OUTPUT_FILE
                        The path to save the corrected SRT file. Defaults to &apos;input_filename_corrected.srt&apos;.
  -w WORKERS, --workers WORKERS
                        Number of parallel requests to send to OpenAI. Default: 50.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::
&amp;lt;ShowMoreCollapse collapsedHeight={&apos;400px&apos;} client:visible&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import srt
import time
import argparse
import concurrent.futures
from openai import OpenAI
from pathlib import Path
from tqdm import tqdm

# --- 配置 ---
# 建议使用环境变量，但也可以直接在此处设置 API 密钥
API_KEY = &quot;xxxxxx&quot;
# API_KEY = os.getenv(&quot;OPENAI_API_KEY&quot;)
BASE_URL = &apos;https://api.openai.com/v1&apos;  # 如果使用自定义的 OpenAI 兼容 API，请修改此处
MODEL_NAME = &quot;gemini-2.5-pro&quot;
CHUNK_SIZE = 100       # 每次处理的字幕数量
MAX_WORKERS = 50           # 并行请求的数量，可根据你的 API 速率限制调整
SYSTEM_PROMPT = &quot;&quot;&quot;
You are an expert proofreader and editor specializing in UK English. Your task is to correct spelling, grammar, and punctuation errors in the provided SRT subtitle block that are ocr&apos;d from a .sup file. Especially focus on `i`, `l`, &apos;I`, &apos;L` chaos.

Follow these rules STRICTLY:
1.  **Target Language**: All corrections must adhere to standard UK English (e.g., use &apos;colour&apos;, not &apos;color&apos;; &apos;realise&apos;, not &apos;realize&apos;; &apos;dialogue&apos;, not &apos;dialog&apos;).
2.  **Preserve Structure**: You MUST return the corrected text in the exact same SRT format. This includes preserving all subtitle index numbers, timestamps, formatting tag, and line breaks within the text. DO NOT alter, add, or remove any timestamps or index numbers.
3.  **Content Focus**: Only modify the subtitle text itself for corrections. If a line of text is already correct, leave it unchanged.
4.  **No Extra Commentary**: Do not add any explanations, apologies, or comments before or after the SRT block. Your entire response must be ONLY the corrected SRT data.
5.  Do not change any names, places, or specific terminology unless they are clearly misspelled.

Here is an example of the required input and output format:

--- EXAMPLE INPUT ---
3
00:00:19,895 --&amp;gt; 00:00:22,273
&amp;lt;i&amp;gt;The whole famlly&apos;s together.&amp;lt;/i&amp;gt;
&amp;lt;i&amp;gt;Mom makes braciole.&amp;lt;/i&amp;gt;

4
00:00:22,440 --&amp;gt; 00:00:25,276
&amp;lt;i&amp;gt;Dad puts the jersey on.&amp;lt;/i&amp;gt;
&amp;lt;i&amp;gt;We&apos;re aiI watchlng the game.&amp;lt;/i&amp;gt;


--- EXAMPLE OUTPUT ---
3
00:00:19,895 --&amp;gt; 00:00:22,273
&amp;lt;i&amp;gt;The whole family&apos;s together.&amp;lt;/i&amp;gt;
&amp;lt;i&amp;gt;Mom makes braciole.&amp;lt;/i&amp;gt;

4
00:00:22,440 --&amp;gt; 00:00:25,276
&amp;lt;i&amp;gt;Dad puts the jersey on.&amp;lt;/i&amp;gt;
&amp;lt;i&amp;gt;We&apos;re all watching the game.&amp;lt;/i&amp;gt;

&quot;&quot;&quot;

def correct_chunk_task(client: OpenAI, srt_chunk_string: str, chunk_index: int):
    &quot;&quot;&quot;
    单个并行任务：调用 OpenAI API 校对一个 SRT 块。
    返回一个元组 (chunk_index, corrected_srt_string)。
    &quot;&quot;&quot;
    try:
        response = client.chat.completions.create(
            model=MODEL_NAME,
            messages=[
                {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: SYSTEM_PROMPT},
                {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: f&quot;Now, please process the following SRT block:\n\n{srt_chunk_string}&quot;}
            ],
            temperature=0.1,
        )
        corrected_text = response.choices[0].message.content.strip()
        return chunk_index, corrected_text
    except Exception as e:
        # 如果 API 调用失败，打印错误并返回 None，以便主线程可以处理回退
        print(f&quot;\nError processing chunk {chunk_index + 1}: {e}&quot;)
        return chunk_index, None

def process_srt_file(input_path, output_path, max_workers):
    &quot;&quot;&quot;
    使用并行处理读取、校对并保存 SRT 文件。
    &quot;&quot;&quot;
    if not API_KEY:
        print(&quot;Error: OPENAI_API_KEY environment variable not set.&quot;)
        return

    try:
        with open(input_path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
            content = f.read()
        original_subs = list(srt.parse(content))
        print(f&quot;Successfully loaded {len(original_subs)} subtitles from &apos;{input_path}&apos;.&quot;)
    except Exception as e:
        print(f&quot;Error reading or parsing SRT file: {e}&quot;)
        return

    # 将字幕分割成块
    chunks_of_subs = [original_subs[i:i + CHUNK_SIZE] for i in range(0, len(original_subs), CHUNK_SIZE)]
    total_chunks = len(chunks_of_subs)
    print(f&quot;Divided into {total_chunks} chunks for processing with up to {max_workers} parallel workers.&quot;)

    client = OpenAI(api_key=API_KEY, base_url=BASE_URL)

    # 创建一个列表来按顺序存储结果
    corrected_results = [None] * total_chunks

    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        # 提交所有任务
        future_to_chunk = {
            executor.submit(correct_chunk_task, client, srt.compose(chunk), i): chunk
            for i, chunk in enumerate(chunks_of_subs)
        }

        # 使用 tqdm 创建进度条，当任务完成时更新
        progress_bar = tqdm(concurrent.futures.as_completed(future_to_chunk), total=total_chunks, desc=&quot;Correcting Chunks&quot;)

        for future in progress_bar:
            original_chunk = future_to_chunk[future]
            try:
                index, corrected_srt_string = future.result()

                if corrected_srt_string:
                    # 验证返回的 SRT 是否有效
                    try:
                        corrected_chunk_subs = list(srt.parse(corrected_srt_string))
                        if len(corrected_chunk_subs) != len(original_chunk):
                            print(f&quot;\nWarning: Mismatch in subtitle count for chunk {index + 1}. Using original.&quot;)
                            corrected_results[index] = original_chunk # 回退到原始块
                        else:
                            corrected_results[index] = corrected_chunk_subs
                    except Exception as parse_error:
                        print(f&quot;\nError parsing corrected SRT for chunk {index + 1}: {parse_error}. Using original.&quot;)
                        corrected_results[index] = original_chunk # 回退到原始块
                else:
                    # API 调用失败，使用原始块
                    corrected_results[index] = original_chunk

            except Exception as exc:
                # 任务本身抛出异常
                print(f&quot;\nA task generated an exception: {exc}. The original chunk will be used.&quot;)
                # 找到这个异常 future 对应的索引并回退
                for f, c in future_to_chunk.items():
                    if f == future:
                        # 这是个笨办法，但能用。更好的方法是在提交时就把索引和future关联起来
                        # (我们已经在 correct_chunk_task 返回值里做了，这里是双保险)
                        for i, chunk_list in enumerate(chunks_of_subs):
                            if chunk_list == c:
                                corrected_results[i] = chunk_list
                                break
                        break

    # 组合所有处理过的块
    final_subs = []
    for chunk in corrected_results:
        if chunk:
            final_subs.extend(chunk)

    try:
        final_srt_content = srt.compose(final_subs)
        with open(output_path, &apos;w&apos;, encoding=&apos;utf-8&apos;) as f:
            f.write(final_srt_content)
        print(f&quot;\nProcessing complete. Corrected subtitles saved to &apos;{output_path}&apos;.&quot;)
    except Exception as e:
        print(f&quot;\nError writing the final SRT file: {e}&quot;)

if __name__ == &quot;__main__&quot;:
    parser = argparse.ArgumentParser(description=&quot;Correct spelling and grammar in an SRT file using OpenAI in parallel.&quot;)
    parser.add_argument(&quot;input_file&quot;, type=str, help=&quot;The path to the input SRT file.&quot;)
    parser.add_argument(&quot;-o&quot;, &quot;--output_file&quot;, type=str, help=&quot;The path to save the corrected SRT file. Defaults to &apos;input_filename_corrected.srt&apos;.&quot;)
    parser.add_argument(&quot;-w&quot;, &quot;--workers&quot;, type=int, default=MAX_WORKERS, help=f&quot;Number of parallel requests to send to OpenAI. Default: {MAX_WORKERS}.&quot;)

    args = parser.parse_args()

    input_path = Path(args.input_file)
    if not input_path.is_file():
        print(f&quot;Error: Input file not found at &apos;{input_path}&apos;&quot;)
    else:
        if args.output_file:
            output_path = Path(args.output_file)
        else:
            output_path = input_path.with_name(f&quot;{input_path.stem}_corrected{input_path.suffix}&quot;)

        process_srt_file(str(input_path), str(output_path), args.workers)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/p&gt;
&lt;h3&gt;自动检测Band/Block脚本&lt;/h3&gt;
&lt;p&gt;:::tip{title=&quot;脚本功能简介&quot;}
该脚本使用awsmfunc库中的banddtct函数自动检测视频中的Band/Block区域，方便后续进行针对性处理。这只是一个实例脚本，具体参数和预处理步骤请根据实际视频源进行调整。识别完成后，会自动在视频文件位置生成banding.txt文件，记录检测到的band/block区域信息。
:::
&amp;lt;ShowMoreCollapse collapsedHeight={&apos;400px&apos;} client:visible&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import vapoursynth as vs
import awsmfunc as awf
import kagefunc as kgf
import mvsfunc as mvf
import adptvgrnMod
from rekt import rektlvls
from vsutil import get_y
from screenshot import GetSnapShot
core = vs.core


source = core.lsmas.LWLibavSource(&apos;/demux/complete/Return/extract/video.h264&apos;).std.Crop(0, 0, 20, 20)
source = awf.fb(source, top=1, bottom=1, mode=&apos;fillmargins&apos;)
source = awf.zresize(source, top=1, bottom=1)
source = core.std.AssumeFPS(source, fpsnum=24000, fpsden=1001)
source = mvf.Depth(source, 16)

# src.set_output()
awf.banddtct(source,thr=100,min_zone_len=20)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/p&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>使用Docker搭建Minecraft服务器</title><link>https://blog.useforall.com/posts/minecraft/minecraft-server-with-docker/</link><guid isPermaLink="true">https://blog.useforall.com/posts/minecraft/minecraft-server-with-docker/</guid><description>本文主要介绍如何使用Docker搭建MCSManager并开一个自己Minecraft服务器，涵盖了基础设置、数据持久化、网络配置以及Build一个自己的Minecraft服务器镜像等内容。</description><pubDate>Sun, 09 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import ShowMoreCollapse from &quot;../../../components/custom/showMoreCollapse/main.tsx&quot;;
import PurpurVersionInfo from &quot;../../../components/custom/purpurVersion/main.tsx&quot;;&lt;/p&gt;
&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
本文主要介绍如何使用Docker搭建MCSManager并开一个自己Minecraft服务器，涵盖了基础设置、数据持久化、网络配置以及Build一个自己的Minecraft服务器镜像等内容。
:::&lt;/p&gt;
&lt;p&gt;最后一次玩MC是在初二吧，记得当时还是1.4左右版本，记得当时玩的服务器好像叫什么秋月之光，应该在当时算大型服务器了吧，当年辛辛苦苦得到了一个信标，被人家偷走了，因此还砸坏了电脑，然后被打了一顿😂。&lt;/p&gt;
&lt;p&gt;最近闲来无事，心血来潮，想搭建一个公益Minecraft养老生存服务器（其实是想废物利用下没用上的服务器），但是由于以前也没开过MC服务器，不知道要装什么插件，索性就放弃了。相较于开服，我还是更喜欢当个玩家（如果你看到这篇文章，愿意当服主的话，请联系我！！此消息永久有效）。此文只是记录一下使用Docker搭建Minecraft服务器的过程，方便以后回顾。&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Docker&lt;/strong&gt;：容器化平台。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCSManager&lt;/strong&gt;：Minecraft服务器管理面板，支持多种Minecraft服务器类型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minecraft服务器镜像&lt;/strong&gt;：本文使用GitHub Action打包一个自己的服务器镜像（可以自行增加常用插件在内）&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;部署MCSManager&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;创建&lt;code&gt;docker-compose.yaml&lt;/code&gt;文件：&lt;pre&gt;&lt;code&gt;mkdir -p ~/docker_data/mcsm &amp;amp;&amp;amp; cd ~/docker_data/mcsm &amp;amp;&amp;amp; nano docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;services:
  web:
    image: githubyumao/mcsmanager-web:latest
    ports:
      - 65004:23333 # 注意修改为自己的端口号
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./web/data:/opt/mcsmanager/web/data
      - ./web/logs:/opt/mcsmanager/web/logs
    network_mode: bridge
    restart: unless-stopped

  daemon:
    image: githubyumao/mcsmanager-daemon:latest
    restart: unless-stopped
    ports:
      - 65005:24444 # 注意修改为自己的端口号
      - 65008:25565 # 注意修改为自己的端口号
    environment:

      - MCSM_DOCKER_WORKSPACE_PATH=/home/ubuntu/docker_data/mcsm/daemon/data/InstanceData
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - ./daemon/data:/opt/mcsmanager/daemon/data
      - ./daemon/logs:/opt/mcsmanager/daemon/logs
      - /var/run/docker.sock:/var/run/docker.sock
    network_mode: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;查看生成的daemon节点key，后续需要填入MCSManager面板中：&lt;pre&gt;&lt;code&gt;cat daemon/data/Config/global.json
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;version&quot;: 2,
  &quot;ip&quot;: &quot;&quot;,
  &quot;port&quot;: 24444,
  &quot;prefix&quot;: &quot;&quot;,

  &quot;key&quot;: &quot;xxxxxxx&quot;,
  &quot;maxFileTask&quot;: 2,
  &quot;maxZipFileSize&quot;: 200,
  &quot;language&quot;: &quot;zh_cn&quot;,
  &quot;defaultInstancePath&quot;: &quot;&quot;,
  &quot;allocatablePortRange&quot;: [
      10010,
      65500
  ],
  &quot;currentAllocatablePort&quot;: 10095,
  &quot;portAssignInterval&quot;: 5,
  &quot;uploadSpeedRate&quot;: 0,
  &quot;downloadSpeedRate&quot;: 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;启动MCSManager服务&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;访问MCSManager面板，地址为&lt;code&gt;http://你的服务器IP:65004&lt;/code&gt;，按照web流程配置账户信息，然后进行Nginx反代（我反正添加本地节点有点问题，反代过后添加节点没问题）&lt;/li&gt;
&lt;li&gt;按照如下图片配置Nginx Proxy Manager反代
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/11/09/69108c5c73b95.png&quot; alt=&quot;CleanShot 2025-11-09 at 20.35.39@2x&quot; /&gt;
&lt;ul&gt;
&lt;li&gt;前往&lt;code&gt;Advanced&lt;/code&gt; Tab下的&lt;code&gt;Custom Nginx Configuration&lt;/code&gt;，添加以下代码：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;include /data/nginx/custom/cloudflare_ips.conf;

# 传输时默认开启gzip压缩
gzip on;

# 传输时会被压缩的类型(应当依据文件压缩效果添加)
gzip_types text/plain text/css application/javascript application/xml application/json;

# 反向代理时，启用压缩
gzip_proxied any;

# 传输时压缩等级，等级越高压缩消耗CPU越多，最高9级，通常5级就够了
gzip_comp_level 5;

# 传输时大小达到1k才压缩，压缩小内容无意义
gzip_min_length 1k;

# 最大文件上传大小限制。设置0为不限制
client_max_body_size 0;

# 关闭缓存
proxy_request_buffering off;
proxy_buffering off;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;前往面板，按图添加节点
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/11/09/69109d27ad665.png&quot; alt=&quot;CleanShot 2025-11-09 at 20.43.57@2x&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;使用Github Action构建Minecraft服务器镜像&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;这里以purpur端为例，其他端也可以参考类似的思路进行构建。需要什么插件可以自己加进去
我最终是推送到我自建的&lt;code&gt;gitea&lt;/code&gt;，如果你要推送到&lt;code&gt;ghcr&lt;/code&gt;，直接让AI改一下这个文件即可&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;随便开个空仓库，开启Action&lt;/li&gt;
&lt;li&gt;创建&lt;code&gt;purpur/Dockerfile&lt;/code&gt;文件&lt;pre&gt;&lt;code&gt;mkdir purpur &amp;amp;&amp;amp; nano purpur/Dockerfile
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;ARG BASE_IMAGE=container-registry.oracle.com/graalvm/jdk:17

# 使用变量作为基础镜像
FROM ${BASE_IMAGE}

# 定义构建参数，可以从 build-push-action 传入
ARG MC_VERSION=1.20.4
ARG PURPUR_JAR_NAME=purpur.jar

# 安装 curl 用于下载，并清理缓存
RUN microdnf install curl &amp;amp;&amp;amp; \
    microdnf clean all

# 下载 Purpur 服务端
RUN curl -L -o /app/${PURPUR_JAR_NAME} \
    &quot;https://api.purpurmc.org/v2/purpur/${MC_VERSION}/latest/download&quot;

# 复制启动脚本到镜像中
COPY start.sh /app/start.sh

# 给予启动脚本执行权限
RUN chmod +x /app/start.sh

WORKDIR /workspace

# 声明服务器数据卷，用于持久化世界、插件等数据
VOLUME /workspace

# 暴露 Minecraft 服务器端口
EXPOSE 25565

# 设置容器启动时执行的命令
CMD [&quot;/app/start.sh&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;创建&lt;code&gt;purpur/start.sh&lt;/code&gt;启动脚本&lt;pre&gt;&lt;code&gt;nano purpur/start.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh

# 自动同意 Minecraft EULA
echo &quot;eula=true&quot; &amp;gt; eula.txt

# 设置默认内存，如果环境变量 MEMORY 未设置，则使用 4G
MEMORY=${MEMORY:-4G}

# 使用 Aikar&apos;s Flags 优化启动参数
# exec 会让 Java 进程替换掉 shell 进程，成为容器的主进程 (PID 1)，这样可以更好地接收 Docker 的停止信号
exec java -Xms${MEMORY} -Xmx${MEMORY} \
    -XX:+UseG1GC \
    -XX:+ParallelRefProcEnabled \
    -XX:MaxGCPauseMillis=200 \
    -XX:+UnlockExperimentalVMOptions \
    -XX:+DisableExplicitGC \
    -XX:+AlwaysPreTouch \
    -XX:G1NewSizePercent=30 \
    -XX:G1MaxNewSizePercent=40 \
    -XX:G1HeapRegionSize=8M \
    -XX:G1ReservePercent=20 \
    -XX:G1HeapWastePercent=5 \
    -XX:G1MixedGCCountTarget=4 \
    -XX:InitiatingHeapOccupancyPercent=15 \
    -XX:G1MixedGCLiveThresholdPercent=90 \
    -XX:G1RSetUpdatingPauseTimePercent=5 \
    -XX:SurvivorRatio=32 \
    -XX:+PerfDisableSharedMem \
    -XX:MaxTenuringThreshold=1 \
    -Dusing.aikars.flags=https://mcflags.emc.gs \
    -Daikars.new.flags=true \
    -jar /app/purpur.jar --nogui
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;创建一个&lt;code&gt;purpur.yml&lt;/code&gt;文件
&amp;lt;ShowMoreCollapse collapsedHeight={&apos;800px&apos;} client:visible&amp;gt;&lt;pre&gt;&lt;code&gt;# .github/workflows/build-purpur.yml
# ----------------------------------------------------------------
# 这是一个用于构建 Minecraft Purpur 服务端 Docker 镜像的 GitHub Actions 工作流。
#
# 功能特性:
# 1. 多架构构建: 支持 amd64 和 arm64 架构。
# 2. 动态 Java 版本: 自动根据输入的 Minecraft 版本选择 Java 17 或 Java 21 作为基础镜像。
#    - MC &amp;gt;= 1.20.5 使用 Java 21
#    - MC &amp;lt; 1.20.5 使用 Java 17
# 3. 手动触发与版本控制: 支持手动触发并输入版本号，也支持通过 push 自动触发。
#    - 最终镜像标签为 &amp;lt;MC_VERSION&amp;gt;，例如 &quot;1.20.5&quot;。
#    - 各架构的中间镜像标签为 &amp;lt;MC_VERSION&amp;gt;-&amp;lt;arch&amp;gt;，例如 &quot;1.20.5-amd64&quot;。
# 4. 串行执行: 构建任务将按顺序执行（先完成 amd64，再开始 arm64），便于排错和控制并发。
# 5. 推送到私有 Gitea 仓库: 自动构建并推送到你自建的 Gitea 容器镜像仓库。
# ----------------------------------------------------------------

name: Build Purpur Docker Image &amp;amp; Push to Gitea

on:
  # 允许在 GitHub Actions 页面手动触发
  workflow_dispatch:
    inputs:
      mc_version:
        description: &apos;Minecraft Version (e.g., 1.20.4, 1.20.5)&apos;
        required: true
        default: &apos;1.20.4&apos;

# 全局环境变量
env:
  # 当 push 触发时使用的默认 Minecraft 版本
  DEFAULT_MC_VERSION: &quot;1.20.4&quot;
  # Gitea 上的镜像名称
  GITEA_IMAGE_NAME: &quot;purpur&quot;
  # 第一个需要 Java 21 的 Minecraft 版本，用于版本比较
  JAVA_21_CUTOFF_VERSION: &quot;1.20.5&quot;

jobs:
  build-and-push-arch:
    strategy:
      fail-fast: false
      # 设置最大并行数为 1，实现串行执行
      max-parallel: 1
      matrix:
        include:
          - arch: amd64
            runner: ubuntu-latest
          - arch: arm64
            runner: ubuntu-22.04-arm

    runs-on: ${{ matrix.runner }}
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Determine Versions
        id: get_versions
        run: |
          # 步骤 1: 确定 Minecraft 版本
          # 如果是手动触发，则使用输入的版本号；否则，使用环境变量中定义的默认版本号
          MC_VERSION=${{ github.event.inputs.mc_version || env.DEFAULT_MC_VERSION }}
          echo &quot;MC_VERSION=$MC_VERSION&quot; &amp;gt;&amp;gt; $GITHUB_ENV
          echo &quot;Determined Minecraft version: $MC_VERSION&quot;

          # 步骤 2: 根据 Minecraft 版本判断需要的 Java 版本
          # 使用 dpkg 工具比较版本号，这比简单的字符串比较更可靠
          if dpkg --compare-versions &quot;$MC_VERSION&quot; &quot;ge&quot; &quot;${{ env.JAVA_21_CUTOFF_VERSION }}&quot;; then
            JAVA_TAG=&quot;21&quot;
            echo &quot;Version $MC_VERSION requires Java 21.&quot;
          else
            JAVA_TAG=&quot;17&quot;
            echo &quot;Version $MC_VERSION requires Java 17.&quot;
          fi

          # 步骤 3: 设置基础镜像环境变量，供后续步骤使用
          BASE_IMAGE=&quot;container-registry.oracle.com/graalvm/jdk:$JAVA_TAG&quot;
          echo &quot;BASE_IMAGE=$BASE_IMAGE&quot; &amp;gt;&amp;gt; $GITHUB_ENV
          echo &quot;Using base image: $BASE_IMAGE&quot;

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Gitea Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ secrets.GITEA_HOST }}
          username: ${{ secrets.GITEA_USERNAME }}
          password: ${{ secrets.GITEA_REGISTRY_PAT }}

      - name: Build and push Docker image
        id: buildx1
        continue-on-error: true
        uses: docker/build-push-action@v5
        with:
          context: ./purpur
          file: ./purpur/Dockerfile
          platforms: linux/${{ matrix.arch }}
          build-args: |
            MC_VERSION=${{ env.MC_VERSION }}
            # 将动态决定的基础镜像名传递给 Dockerfile
            BASE_IMAGE=${{ env.BASE_IMAGE }}
          tags: ${{ secrets.GITEA_HOST }}/${{ secrets.GITEA_USERNAME }}/${{ env.GITEA_IMAGE_NAME }}:${{ env.MC_VERSION }}-${{ matrix.arch }}
          push: true
          # 禁用 provenance 和 sbom，避免创建额外的 manifest
          provenance: false
          sbom: false
          cache-from: type=gha
          cache-to: type=gha,mode=max
      - name: Wait to retry
        if: steps.buildx1.outcome != &apos;success&apos;
        run: |
          sleep 10
      - name: Build and push Docker image
        if: steps.buildx1.outcome != &apos;success&apos;
        uses: docker/build-push-action@v5
        with:
          context: ./purpur
          file: ./purpur/Dockerfile
          platforms: linux/${{ matrix.arch }}
          build-args: |
            MC_VERSION=${{ env.MC_VERSION }}
            # 将动态决定的基础镜像名传递给 Dockerfile
            BASE_IMAGE=${{ env.BASE_IMAGE }}
          tags: ${{ secrets.GITEA_HOST }}/${{ secrets.GITEA_USERNAME }}/${{ env.GITEA_IMAGE_NAME }}:${{ env.MC_VERSION }}-${{ matrix.arch }}
          push: true
          # 禁用 provenance 和 sbom，避免创建额外的 manifest
          provenance: false
          sbom: false
          cache-from: type=gha
          cache-to: type=gha,mode=max

  merge-manifest:
    needs: build-and-push-arch
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Log in to Gitea Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ secrets.GITEA_HOST }}
          username: ${{ secrets.GITEA_USERNAME }}
          password: ${{ secrets.GITEA_REGISTRY_PAT }}

      - name: Determine Minecraft Version
        # 在这个 job 中也需要确定版本号，以保持与上一个 job 一致
        run: |
          MC_VERSION=${{ github.event.inputs.mc_version || env.DEFAULT_MC_VERSION }}
          echo &quot;MC_VERSION=$MC_VERSION&quot; &amp;gt;&amp;gt; $GITHUB_ENV

      - name: Create and push manifest list with version tag
        run: |
          IMAGE_BASE=&quot;${{ secrets.GITEA_HOST }}/${{ secrets.GITEA_USERNAME }}/${{ env.GITEA_IMAGE_NAME }}&quot;
          VERSION_TAG=&quot;${IMAGE_BASE}:${{ env.MC_VERSION }}&quot;
          AMD_TAG=&quot;${IMAGE_BASE}:${{ env.MC_VERSION }}-amd64&quot;
          ARM_TAG=&quot;${IMAGE_BASE}:${{ env.MC_VERSION }}-arm64&quot;

          echo &quot;--- Creating Manifest List for $VERSION_TAG ---&quot;

          # 清理所有可能存在的旧 manifest（忽略错误）
          echo &quot;Cleaning up old manifests...&quot;
          docker manifest rm ${VERSION_TAG} 2&amp;gt;/dev/null || true
          docker manifest rm ${AMD_TAG} 2&amp;gt;/dev/null || true
          docker manifest rm ${ARM_TAG} 2&amp;gt;/dev/null || true

          # 检查架构特定镜像状态
          echo &quot;Checking image status...&quot;
          docker manifest inspect ${AMD_TAG} || echo &quot;❌ amd64 image not found or is manifest list&quot;
          docker manifest inspect ${ARM_TAG} || echo &quot;❌ arm64 image not found or is manifest list&quot;

          # 检查架构镜像是否存在
          if docker manifest inspect ${AMD_TAG} &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; docker manifest inspect ${ARM_TAG} &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
            echo &quot;Both architecture images exist, creating manifest list...&quot;

            # 由于架构标签可能是 manifest list，我们使用 --amend 来处理
            docker manifest create ${VERSION_TAG} \
              --amend ${AMD_TAG} \
              --amend ${ARM_TAG}
            docker manifest push ${VERSION_TAG}
            echo &quot;✅ Gitea manifest pushed successfully&quot;
          else
            echo &quot;❌ Architecture images not ready, skipping Gitea manifest creation&quot;
          fi

      - name: Inspect final manifest
        run: |
          echo &quot;--- Inspecting Final Manifest ---&quot;
          docker manifest inspect ${{ secrets.GITEA_HOST }}/${{ secrets.GITEA_USERNAME }}/${{ env.GITEA_IMAGE_NAME }}:${{ env.MC_VERSION }}

&lt;/code&gt;&lt;/pre&gt;
&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/li&gt;
&lt;li&gt;提交代码到GitHub，然后手动去触发Action构建镜像，输入你要的Minecraft版本，以下是当前支持版本（点击复制）：
&amp;lt;PurpurVersionInfo client:visible /&amp;gt;&lt;/li&gt;
&lt;li&gt;打包完成后，前往MCSManager面板，按照图片添加一个实例：
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/11/09/691099eb2a6d7.png&quot; alt=&quot;CleanShot 2025-11-09 at 21.32.10@2x&quot; /&gt;&lt;/li&gt;
&lt;li&gt;运行完成后，还需要配置&lt;code&gt;端口映射&lt;/code&gt;以及&lt;code&gt;环境变量&lt;/code&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/11/09/69109b5f9bb49.png&quot; alt=&quot;CleanShot 2025-11-09 at 21.42.24@2x&quot; /&gt;&lt;/li&gt;
&lt;li&gt;最后愉快的启动服务器吧！&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>关于 Docker Gitea 非 rootless 版无法获取SSH log信息解决办法</title><link>https://blog.useforall.com/posts/solve-docker-gitea-ssh-log-issue/</link><guid isPermaLink="true">https://blog.useforall.com/posts/solve-docker-gitea-ssh-log-issue/</guid><description>非rootless版本（也就是默认的gitea/gitea:latest无法获取到SSH登录日志信息，哪怕设置ENABLE_SSH_LOG为true也无济于事。本文介绍一种解决办法，通过获取Docker容器的日志文件来实现SSH log的记录，从而可以进行fail2ban的配置</description><pubDate>Mon, 03 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
非rootless版本（也就是默认的&lt;code&gt;gitea/gitea:latest&lt;/code&gt;无法获取到SSH登录日志信息，哪怕设置&lt;code&gt;ENABLE_SSH_LOG&lt;/code&gt;为&lt;code&gt;true&lt;/code&gt;也无济于事。本文介绍一种解决办法，通过获取Docker容器的日志文件来实现SSH log的记录，从而可以进行fail2ban的配置
:::&lt;/p&gt;
&lt;p&gt;Gitea在部署标准版的情况下，无论你如何配置&lt;code&gt;app.ini&lt;/code&gt;都无法获取到SSH登录日志信息，主要原因是Gitea容器内并没有&lt;code&gt;sshd&lt;/code&gt;服务，所以无法获取到相关日志信息。官网的描述很简单的，只需要改下 &lt;code&gt;app.ini&lt;/code&gt; 中 &lt;code&gt;log&lt;/code&gt; 部分下的 &lt;code&gt;MODE&lt;/code&gt;为&lt;code&gt;file&lt;/code&gt;，并开启 &lt;code&gt;ENABLE_SSH_LOG&lt;/code&gt;，但是请放心做无用功，哪怕如此修改后，&lt;code&gt;gitea.log&lt;/code&gt; 依旧不会有 &lt;code&gt;SSH&lt;/code&gt; 相关的log，因为&lt;code&gt;ENABLE_SSH_LOG&lt;/code&gt;  仅对 Gitea 内建 &lt;code&gt;SSH&lt;/code&gt; 有效，抱歉的是，标准版中的 OpenSSH 不可与内建 SSH 调和。同时在其 github 存在一个 &lt;a href=&quot;https://github.com/go-gitea/gitea/issues/14490&quot;&gt;issue&lt;/a&gt; ，回复是“如果你没用它自带的 SSH，凭什么指望 Gitea 的日志会提供那些信息？直接设置 fail2ban 去用 sshd 的日志就行了”。
但是，天不绝人之路，通过&lt;code&gt;docker compose logs -f gitea&lt;/code&gt;查看可以发现，docker logs内是有SSH相关的爆破信息的，因此解决方法就很简单了，定时将docker logs内的SSH信息提取出来，写入到一个单独的log文件中，然后让fail2ban去监控这个log文件即可。&lt;/p&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;定时提取SSH log&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;创建脚本文件&lt;pre&gt;&lt;code&gt;nano ~/docker_data/gitea/gitea_docker_log.sh &amp;amp;&amp;amp; chmod +x ~/docker_data/gitea/gitea_docker_log.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
# --- 配置 ---

# Gitea 容器的名称
CONTAINER_NAME=&quot;gitea&quot;

# 日志文件在宿主机上的完整路径
LOG_FILE=&quot;/root/docker_data/gitea/gitea.log&quot;

# --- 脚本主体 ---
LOG_DIR=$(dirname &quot;$LOG_FILE&quot;)

# 确保日志目录存在
mkdir -p &quot;$LOG_DIR&quot;
echo &quot;脚本已启动，将监控容器 &apos;$CONTAINER_NAME&apos; 并将日志写入 &apos;$LOG_FILE&apos;&quot;

# 无限循环，确保在容器重启后能自动重新连接
while true; do
    # 使用 docker inspect 检查容器是否存在且在运行，这比解析 ps 输出更可靠
    if docker inspect -f &apos;{{.State.Running}}&apos; &quot;$CONTAINER_NAME&quot; 2&amp;gt;/dev/null | grep -q &quot;true&quot;; then
        echo &quot;[$(date &apos;+%Y-%m-%d %H:%M:%S&apos;)] 已连接到 &apos;$CONTAINER_NAME&apos; 容器日志流...&quot;

        # 使用 docker logs 持续跟踪日志（只取过去15秒的日志），并将标准错误(2)也重定向到标准输出(1)
        # 然后通过 sed 高效地格式化时间戳，最后追加到日志文件
        # sed -u: 使用无缓冲模式，确保日志实时写入
        # s/T/ /: 将日期和时间之间的 &apos;T&apos; 替换为空格
        # s/\..*Z$//: 删除小数点及其后面的所有内容，即毫秒和&apos;Z&apos;
        docker logs --timestamps --follow &quot;$CONTAINER_NAME&quot; 2&amp;gt;&amp;amp;1 | \
            sed -u &apos;s/T/ /;s/\..*Z$//&apos; &amp;gt;&amp;gt; &quot;$LOG_FILE&quot;

        # 如果 docker logs 命令退出 (通常是因为容器停止了)，会执行到这里
        echo &quot;[$(date &apos;+%Y-%m-%d %H:%M:%S&apos;)] 与 &apos;$CONTAINER_NAME&apos; 的日志流断开。正在重试...&quot;
    else
        echo &quot;[$(date &apos;+%Y-%m-%d %H:%M:%S&apos;)] 容器 &apos;$CONTAINER_NAME&apos; 未运行或不存在，等待 10 秒...&quot;
    fi

    # 短暂休眠，避免在容器不存在时消耗过多 CPU
    sleep 10
done
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;创建systemd服务&lt;pre&gt;&lt;code&gt;nano /etc/systemd/system/gitea_docker_log.service
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Gitea Log Monitor
After=docker.service
Requires=docker.service

[Service]

ExecStart=/root/docker_data/gitea/gitea_log_monitor.sh
Restart=always
User=root

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;启动服务&lt;pre&gt;&lt;code&gt;systemctl daemon-reload
systemctl enable gitea_docker_log.service
systemctl start gitea_docker_log.service
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;检查服务状态&lt;pre&gt;&lt;code&gt;systemctl status gitea_docker_log.service
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;配置fail2ban监控SSH log&lt;/h2&gt;
&lt;p&gt;不要使用官方的&lt;a href=&quot;https://docs.gitea.com/administration/fail2ban-setup&quot;&gt;过滤规则&lt;/a&gt;，根本就是乱写，完全抓不到任何一个爆破行为，下面是我实际使用的过滤规则&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建fail2ban过滤器&lt;pre&gt;&lt;code&gt;nano /etc/fail2ban/filter.d/gitea.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[Definition]
failregex = ^.*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from &amp;lt;HOST&amp;gt; .*
            ^.* Invalid user .* from &amp;lt;HOST&amp;gt; .*
            ^.* &amp;lt;HOST&amp;gt; not allowed because not listed in AllowUsers.*
            ^Disconnected from invalid user .* &amp;lt;HOST&amp;gt; port.*
            ^Connection closed by invalid user .* &amp;lt;HOST&amp;gt; port.*
            ^Timeout before authentication for connection from &amp;lt;HOST&amp;gt; .*
ignoreregex =
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;创建fail2ban jail&lt;pre&gt;&lt;code&gt;nano /etc/fail2ban/jail.d/gitea-docker.local
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[gitea-docker]

# 是否启用此防护规则。true 表示启用。
enabled = true

# [关键] 指定 iptables 防火墙规则应该应用在哪条链上。
# 对于 Docker 容器，所有外部到容器的流量都经过宿主机的 FORWARD 链。
# 因此，必须设置为 FORWARD 才能正确地封禁访问 Docker 容器的 IP。
chain = FORWARD

# 指定用于解析日志文件的过滤器（filter）。
# &quot;gitea&quot; 对应于 /etc/fail2ban/filter.d/gitea.conf 文件。
# 这个过滤器定义了如何从日志中识别出失败的登录尝试（例如，通过正则表达式匹配错误信息）。
filter = gitea

# [关键] 指定要监控的日志文件的完整路径。
# 这里指向的是你在宿主机上，通过脚本收集并整合的 Gitea 容器日志。
# Fail2Ban 将会实时读取这个文件来检测攻击行为。

logpath = /root/docker_data/gitea/gitea.log

# [安全策略] 最大重试次数。
# 在 `findtime` 定义的时间窗口内，如果同一个 IP 地址的失败尝试次数达到 3 次，
# Fail2Ban 就会触发封禁动作。这是一个比较严格的设置，能快速响应攻击。
maxretry = 3

# [安全策略] 观察时间窗口。
# Fail2Ban 只会计算最近 10 分钟内的失败次数。
# 例如，一个 IP 在 10:00 失败了 2 次，在 10:11 又失败了 1 次，这不会触发封禁，
# 因为在任何一个 10 分钟的窗口内，失败次数都没有达到 3 次。可以自行改大
findtime = 10m

# [安全策略] 封禁时长。
# 一旦一个 IP 被封禁，它将在 146 小时（约 6 天）内无法访问任何服务。
# 这是一个长期封禁，能非常有效地阻止持续的暴力破解或扫描攻击。
bantime = 146h

# [安全策略] 封禁动作。
# 定义当触发封禁时，具体执行什么操作。
# &quot;iptables-allports&quot; 是一个预定义的动作，会使用 iptables 封禁该 IP 的所有端口。
# &quot;blocktype=DROP&quot; 是传递给这个动作的参数，指定封禁模式为 DROP。
# DROP 模式会直接丢弃来自该 IP 的数据包，让攻击者连接超时，比 REJECT (拒绝) 更隐蔽。
banaction = iptables-allports[blocktype=DROP]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;重启fail2ban服务&lt;pre&gt;&lt;code&gt;systemctl restart fail2ban
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;检查fail2ban状态&lt;pre&gt;&lt;code&gt;fail2ban-client status &amp;amp;&amp;amp; fail2ban-client status gitea-docker
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;配置&lt;code&gt;logrotate&lt;/code&gt;服务&lt;pre&gt;&lt;code&gt;nano /etc/logrotate.d/gitea_docker_log
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/root/docker_data/gitea/gitea.log {
    # 每天轮转一次
    daily

    # 保留最近 3 份的归档日志
    rotate 3

    # 归档时使用 gzip 压缩
    compress

    # 推迟到下一次轮转时再压缩，避免服务切换文件句柄时出问题
    delaycompress

    # 如果日志文件不存在，不报错
    missingok

    # 如果日志文件为空，不进行轮转
    notifempty

    # 复制log文件并清空原文件内容
    copytruncate
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>使用he.net获取近乎无限且永久的域名</title><link>https://blog.useforall.com/posts/Use-he-net-to-get-almost-unlimited-and-permanent-domains/</link><guid isPermaLink="true">https://blog.useforall.com/posts/Use-he-net-to-get-almost-unlimited-and-permanent-domains/</guid><description>本文介绍如何使用从Hurricane Electric得到的免费 ::/64 ::/48 IPv6 地址块来注册和管理域名，并托管到Cloudflare，实现近乎无限且永久的域名使用。</description><pubDate>Wed, 29 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import Ipv6RdnsConverter from &quot;../../components/custom/ipv6-block-to-rdns/main.tsx&quot;;
import ShowMoreCollapse from &quot;../../components/custom/showMoreCollapse/main.tsx&quot;;&lt;/p&gt;
&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
本文介绍如何使用从Hurricane Electric得到的免费 &lt;code&gt;::/64&lt;/code&gt; &lt;code&gt;::/48&lt;/code&gt; IPv6 地址块来注册和管理域名，并托管到Cloudflare，实现近乎&lt;code&gt;无限&lt;/code&gt;且&lt;code&gt;永久&lt;/code&gt;的域名使用。
:::&lt;/p&gt;
&lt;p&gt;:::tip
此为邪修办法，仅供学习交流使用，请勿用于商业用途。当然了，这么长的域名也没什么商业价值😂，应该不会真的有人拿来用吧。我主要是拿来做优选域名的，这样甚至都不用花钱去买xyz域名了。主要he不倒闭，就能一直用
:::&lt;/p&gt;
&lt;p&gt;前段时间在IDC Flare上看到一个&lt;a href=&quot;https://idcflare.com/t/topic/11815&quot;&gt;帖子&lt;/a&gt;通过he.net来白嫖域名，感觉挺有意思的，就尝试了一下。由于原帖教程写的乱七八糟的，也没有什么排版，所以我就重新整理了一下，写成了这篇文章，顺便当个备忘录吧。注意，以下所有教程均使用&lt;code&gt;::48&lt;/code&gt;地址块作为示例，&lt;code&gt;::64&lt;/code&gt;同理，只是地址段少一些，最终域名长度长一些而已。&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VPS&lt;/strong&gt;：此服务器必须要有公网ipv4（话说应该没有无ipv4的服务器吧😂）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hurricane Electric账号&lt;/strong&gt;：获取ipv6 block。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare账号&lt;/strong&gt;：提供免费DNS托管服务&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;获取Hurricane Electric IPv6地址块&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;注册HE账号并获取IPv6地址块，教程参考&lt;a href=&quot;/posts/20/#%E5%9F%BA%E6%9C%AC%E9%83%A8%E7%BD%B2&quot;&gt;这里&lt;/a&gt;2&lt;/li&gt;
&lt;li&gt;答题升级账号，升级过后第二天，就可以分配&lt;code&gt;::48&lt;/code&gt;地址块了。获取到以后，请复制完整的IPv6地址块儿，例如&lt;code&gt;2001:470:aa::/48&lt;/code&gt;&lt;pre&gt;&lt;code&gt;问题:How many bits are in an IPv6 address?
答案:128

问题:Which of the following choices is a valid IPv6 address?
答案:2001:0db8:0:ff2e::1

问题:How many /64 subnets are available in a /48 prefix?
答案:65536

问题:How many available IPv6 addresses are there in a /64 allocation?
答案:18,446,744,073,709,551,616

问题:What operating systems currently support IPv6?
答案:All of the these support IPv6

问题:What features of IPv6 do you see yourself using?
答案:Larger Address Space

问题:If you were setting up a network of 12 million hosts that all needed their own unique globally routable addresses, which protocol would you use?
答案:IPv6
Question 3

问题:Do you plan on making sure your workplace is IPv6 ready before IPv4 exhaustion?
答案:Yes

问题:Do you think that any of your co-workers or friends would find Hurricane Electric&apos;s IPv6 certification useful?
答案:Very much

问题:Do you think you would benefit from using Hurricane Electric&apos;s free IPv6 certification process at work?
答案:Very much

问题:Are you enjoying the Hurricane Electric IPv6 certification process?
答案:Very much

问题:Did you like this level in the Hurricane Electric IPv6 certification process?
答案:Yes

问题:Is the price right for the Hurricane Electric free IPv6 certification process?
答案:A great bargain

问题:Does the network equipment you currently use support IPv6?
答案:All

问题:With regards to the server configuration portion, what level of difficulty would you rate it?
答案:Trivial

问题:Hurricane Electric would like to make sure you are completely happy with our free IPv6 certification process. Please rate your satisfaction on a scale of 1 to 5; 5 being completely satisfied, 1 being completely unsatisfied.
答案:5 - Completely Satisfied

问题:Have you asked your provider about when they plan on supporting IPv6?
答案:Yes

问题:Do you think that Internet oriented companies (software, hardware, or service providers) need to be IPv6 ready before IPv4 exhaustion?
答案:Yes

问题:Do you feel like you learned (or refreshed) your knowledge by completing the Hurricane Electric IPv6 certification process?
答案:Very much

问题:Are you able to understand the material?
答案:Yes

问题:Do you feel good about the Hurricane Electric free IPv6 certification process?
答案:Yes

问题:Have you asked your domain name registrar if they support IPv6?
答案:Yes

问题:You request IPv6 glue for your nameservers through:
答案:The registrar of the domain used by your name servers

问题:IPv6 glue for nameservers resides on which nameservers?
答案:The TLD/ccTLDs&apos;

问题:Which of the following queries proves working IPv6 glue?
答案:dig AAAA ns1.exampledomain.tld @tld.server

问题:Which TLD listed below is authoritative for .com &amp;amp; .net IPv6 Glue?
答案:A.GTLD-SERVERS.net

问题:What is another name sometimes used for A or AAAA nameserver glue records found in the top level domain zone files?
答案:host records

问题:What is a registrar?
答案:An organization that is able to register domains

问题:What is a registry?
答案:An organization responsible for operating the authorative nameservers and database for a top level domain

问题:Why does getting AAAA records for your nameservers in the corresponding TLD (top level domain) zone matter?
答案:It enables entirely native IPv6 DNS queries and makes it possible for IPv6 only hosts to reach the nameservers for your domain, since they can&apos;t use glue that is just an A record.

问题:IPv6 AAAA records have been added for several of the root nameservers.
答案:True

===========
问题:What command do you use to ping an IPv6 address on Free Open Source UNIX platforms such as Linux, FreeBSD, etc?
答案:ping6

问题:What command do you use to traceroute to an IPv6 address on Free Open Source UNIX platforms such as Linux, FreeBSD, etc?
答案:traceroute6

问题:What command do you use to ping an IPv6 address on a Microsoft Windows platform?
答案:ping

问题:What command do you use to traceroute to an IPv6 address on a Microsoft Windows platform?
答案:tracert

问题:IPv6 addresses are written using what number base?
答案:hexadecimal (base 16)

问题:Hexadecimal digits are represented by:
答案:0 to 9 and A to F

问题:On Redhat, CentOS, and Fedora Core systems that don&apos;t accept ::/0 as the IPv6 default route, which of the following should you use instead?
答案:2000::/3

问题:When configuring forward DNS entries for use with an IPv6 address, what record type do you use?
答案:AAAA

问题:When configuring reverse DNS with BIND for addresses in the IPv6 allocation 2001:A:B:C::/64, what is the correct format for the zone?
答案:C.0.0.0.B.0.0.0.A.0.0.0.1.0.0.2.ip6.arpa

问题:What is the IPv6 default route?
答案:::/0

问题:What is the IPv6 localhost address?
答案:::1/128

问题:Which of the following is a link-local address?
答案:fe80::20c:dbff:fefb:232b

问题:Which of the following URLs specifies a literal IPv6 address correctly?
答案:http://[2001:470:0:64::2]

问题:Which of the following URLs specifies a literal IPv6 address and port number correctly?
答案:https://[2001:0db8:85a3:08d3:1319:8a2e:0370:7344]:443

问题:If you run native IPv4 and IPv6 at the same time this is called:
答案:Dual stack

问题:How do you use the dig command to get the IPv6 address record for domain he.net?
答案:dig he.net AAAA

问题:How do you use the dig command to get the PTR record for the IPv6 address 2001:470:0:76::2?
答案:dig -x 2001:470:0:76::2

问题:What command shows IPv6 addresses configured on ethernet interfaces under UNIX (Linux, FreeBSD, etc.)?
答案:ifconfig

问题:What command shows IPv6 addresses configured on ethernet interfaces under Microsoft Windows?
答案:ipconfig

问题:Under FreeBSD, what does the generic tunneling interface start with?
答案:gif

问题:Under Linux, what kernel module needs to be loaded to support IPv6 networking?
答案:ipv6

问题:Are routers allowed to fragment IPv6 packets?
答案:No

问题:How many bytes are in an IPv6 address?
答案:16

问题:How many /48 subnets are available in a /32 prefix?
答案:65536

问题:Which protocol is used for manually configured tunnels?
答案:6in4

问题:Which of the following is the IPv6 documentation prefix?
答案:2001:db8::/32

问题:Which of the following is the IPv6 link-local prefix?
答案:fe80::/10

问题:Which of the following is the IPv6 multicast prefix?
答案:ff00::/8

问题:Which of the following is the IPv6 ULA (unique local addresses) prefix?
答案:fc00::/7

问题:Which of the following is a subnet of 2001:db8::/32?
答案:2001:db8:7fa5::/48

问题:On Linux, how would you traceroute to the IPv6 address of he.net?
答案:traceroute6 he.net

问题:On Windows Vista, how would you traceroute to the IPv6 address of he.net?
答案:tracert he.net

问题:On Linux, what is the IPv6 ping command?
答案:ping6

问题:Which command forces the UNIX command ssh to use IPv6 to connect to example.com (useful for domains with both A and AAAA records)?
答案:ssh -6 example.com

问题:You would force the UNIX command ssh to use IPv4 (useful if it had both A and AAAA records) to connect to example.com using which command?
答案:ssh -4 example.com

问题:Which command forces the UNIX command wget to use IPv6 to make a HTTP GET request to he.net (useful for domains with both A and AAAA records)?
答案:wget -6 he.net

问题:Which command forces the UNIX command wget to use IPv4 to make a HTTP GET request to he.net (useful for domains with both A and AAAA records)?
答案:wget -4 he.net

问题:Which command forces the UNIX command mtr to use IPv6 to traceroute to he.net (useful for domains with both A and AAAA records)?
答案:mtr -6 he.net

问题:Which command forces the UNIX command mtr to use IPv4 to traceroute to he.net (useful for domains with both A and AAAA records)?
答案:mtr -4 he.net

问题:When using basic auto-configuration, what is used from the host to configure the last 64bits of the IPv6 address?
答案:The MAC address of the ethernet interface

问题:A MAC address is only 48bits. So when using basic auto-configuration, what is used to fill in the missing 16bits?
答案:FFFE

问题:On many routers, which one of the following commands is used to configure an IPv6 address on an interface?
答案:ipv6 address 2001:A:B:C::1/64

问题:What is the length of an IPv6 packet header?
答案:40 bytes

问题:Which of the following organizations assigns IPv6 addresses?
答案:All of the above

问题:What protocol number is used for 6in4 IPv4 packets?
答案:41

问题:Which of the following is the 6to4 IPv6 prefix?
答案:2002::/16

问题:Which of the following well-known prefixes is used for Teredo?
答案:2001:0000::/32

问题:Which of the following is an IPv4-mapped IPv6 address?
答案:::ffff:10.10.10.2

问题:On operating systems that support it, IPv4-mapped IPv6 addresses are used to:
答案:map IPv4 addresses to an IPv6 address to make it so that IPv6 socket system calls can be used with both IPv4 or IPv6 addresses

问题:Which of the following is an IPv4-compatible IPv6 address?
答案:::10.10.10.2

问题:IPv4-compatible IPv6 addresses are deprecated in RFC 4291.
答案:True

问题:Should you ever see packets with IPv4-mapped IPv6 addresses on the wire (outside of a host)?
答案:No

问题:Which version of OSPF supports IPv6?
答案:OSPFv3

问题:Which of the following can be used by an IPv6 host to learn the address of a default gateway?
答案:neighbor discovery protocol

问题:Which of the following can be used by a host to learn its own IPv6 address?
答案:stateless autoconfiguration

问题:If you translate IPv4 packets to IPv6 or IPv6 packets to IPv4, this is called:
答案:NAT64/DNS64/DS-Lite

问题:On many routers, what command shows IPv6 routes?
答案:show ipv6 route

问题:On many routers, what command shows IPv6 BGP sessions?
答案:show ipv6 bgp summary

问题:On many routers, what command shows IPv6 BGP routes?
答案:sh ipv6 bgp
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;添加域名&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;输入复制的IPv6地址块，进行转换，然后复制转换出来的数据
&amp;lt;Ipv6RdnsConverter client:visible/&amp;gt;
::tip
转换时会自动补0，无需手动补齐
:::&lt;/li&gt;
&lt;li&gt;前往Cloudflare添加域名，输入刚才转换出来的内容，进行注册
&lt;img src=&quot;https://lsky.useforall.com/other/2025/10/29/6901b5add55ce.png&quot; alt=&quot;CleanShot 2025-10-29 at 14.33.10@2x&quot; /&gt;&lt;/li&gt;
&lt;li&gt;注册完成后，前往Hurricane Electric控制台，把上一步骤最后获得的NameServer填入到rDNS部分（最后不要忘记点&lt;code&gt;Save&lt;/code&gt;!）
&lt;img src=&quot;https://lsky.useforall.com/other/2025/10/29/6901b68991bc5.png&quot; alt=&quot;CleanShot 2025-10-29 at 14.36.59@2x&quot; /&gt;&lt;/li&gt;
&lt;li&gt;基本五分钟内，Cloudflare里就会显示active了，如果一直没有action，请从头检查一遍。&lt;/li&gt;
&lt;li&gt;至此，你就已经获得了一个近乎&lt;code&gt;永久&lt;/code&gt;的域名了，但是我为什么说是可以获得&lt;code&gt;无限&lt;/code&gt;的域名呢。注意到我为什么一直在强调地址块儿吗？因为你可以随意划分子网段，就又可以获得新的域名了。例如地址块&lt;code&gt;2001:470:aa::/48&lt;/code&gt;，我可以再划分出&lt;code&gt;2001:470:aa:1::/64&lt;/code&gt;，&lt;code&gt;2001:470:aa:2::/64&lt;/code&gt;，&lt;code&gt;2001:470:aa:3::/64&lt;/code&gt;......下面我以&lt;code&gt;2001:470:aa:1::/64&lt;/code&gt;为例，继续演示如何获得新的域名。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;获取更多域名&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;重复上面的&lt;a href=&quot;#%E6%B7%BB%E5%8A%A0%E5%9F%9F%E5%90%8D&quot;&gt;步骤1&lt;/a&gt;，把&lt;code&gt;2001:470:aa:1::/64&lt;/code&gt;转换成rDNS格式&lt;/li&gt;
&lt;li&gt;重复上面的步骤2，在Cloudflare注册新的域名&lt;/li&gt;
&lt;li&gt;此时，不要把分配的ns服务器填到Hurricane Electric控制台，而是直接在Cloudflare里，&lt;code&gt;::/48&lt;/code&gt;IPv6段对应的域名下下添加&lt;code&gt;NS&lt;/code&gt;记录，指向刚才注册的域名的ns服务器
&lt;img src=&quot;https://lsky.useforall.com/other/2025/10/29/6901bb6670935.png&quot; alt=&quot;CleanShot 2025-10-29 at 14.56.28@2x&quot; /&gt;&lt;/li&gt;
&lt;li&gt;等待生效即可。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;::/64&lt;/code&gt;段同理。生成更长的域名也可以，操作都是一样的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;虽然这些域名也没啥用，但是拿来当Cloudflare优选域名还是可以的。毕竟不要钱，而且还想生成多少就多少，只要he.net不倒闭或者不收回ipv6地址块儿就行了。（感觉he倒的可能性比大部分大型公司概率都低捏）。&lt;/p&gt;
&lt;p&gt;:::note{title=&quot;为什么可以这么做从而获得一个域名 - Gemini 2.5 Pro回答&quot;}&lt;/p&gt;
&lt;h3&gt;核心概念：所有权与授权 (Delegation)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;传统域名 (&lt;code&gt;.com&lt;/code&gt;, &lt;code&gt;.net&lt;/code&gt;):&lt;/strong&gt; 你通过&lt;strong&gt;注册商&lt;/strong&gt;（如 GoDaddy）向&lt;strong&gt;注册局&lt;/strong&gt;（如 Verisign）&lt;strong&gt;付费购买&lt;/strong&gt;一个域名的使用权。你拥有的是一个“名字”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;rDNS 域名 (&lt;code&gt;.ip6.arpa&lt;/code&gt;):&lt;/strong&gt; 你&lt;strong&gt;不购买&lt;/strong&gt;这个域名。你获得它的控制权，是因为你&lt;strong&gt;被分配了对应的 IP 地址块&lt;/strong&gt;。&lt;strong&gt;IP 地址的所有权 = 对应 rDNS 域名的控制权&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;整个过程是一条清晰的&lt;strong&gt;授权链 (Chain of Delegation)&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;第一步：全球最高权威 - IANA&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;IANA (互联网号码分配局)&lt;/strong&gt; 是互联网世界的最高管理者。它管理着所有的 IP 地址资源和根域名，包括特殊的 &lt;code&gt;.arpa&lt;/code&gt; 顶级域。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.ip6.arpa&lt;/code&gt; 这个顶级域就是 IANA 专门设立用来做 IPv6 反向解析的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以把 &lt;strong&gt;IANA&lt;/strong&gt; 想象成&lt;strong&gt;全球土地管理局&lt;/strong&gt;，它拥有地球上所有的土地（IP 地址），并且制定了“门牌号反查房主”的规则（rDNS）。&lt;/p&gt;
&lt;h3&gt;第二步：区域权威 - RIRs (如 ARIN)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;IANA 不会直接把 IP 地址分给个人或小公司，太琐碎了。它会把巨大的 IP 地址块（比如 &lt;code&gt;2001::/16&lt;/code&gt;）和对应的 &lt;code&gt;.ip6.arpa&lt;/code&gt; 管理权，&lt;strong&gt;授权&lt;/strong&gt;给五大&lt;strong&gt;区域互联网注册机构 (RIRs)&lt;/strong&gt;。例如，北美的 ARIN、欧洲的 RIPE NCC 等。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;ShowMoreCollapse client:visible&amp;gt;
2.  所以，当 IANA 把 &lt;code&gt;2001:470::/32&lt;/code&gt; 这个巨大的地址块分给 ARIN 时，它同时也把 &lt;code&gt;0.7.4.0.1.0.0.2.ip6.arpa&lt;/code&gt; 这个 rDNS 区域的管理权&lt;strong&gt;委托 (Delegate)&lt;/strong&gt; 给了 ARIN。&lt;/p&gt;
&lt;p&gt;现在，&lt;strong&gt;ARIN&lt;/strong&gt; 就像是&lt;strong&gt;北美洲土地管理局&lt;/strong&gt;。它管理着北美洲的所有土地（IPs），并且有权决定这片土地上“门牌号反查房主”的具体记录。&lt;/p&gt;
&lt;h3&gt;第三步：本地权威 - Hurricane Electric (HE)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;ARIN 也不会直接给终端用户分 IP。它会把更小一些的地址块（比如 HE 拥有的 &lt;code&gt;2001:470::/32&lt;/code&gt;）分配给像 HE 这样的大型网络服务提供商 (ISP/LIR)。&lt;/li&gt;
&lt;li&gt;同样，当 ARIN 把 &lt;code&gt;2001:470::/32&lt;/code&gt; 分给 HE 时，也把 &lt;code&gt;0.7.4.0.1.0.0.2.ip6.arpa&lt;/code&gt; 的管理权&lt;strong&gt;委托&lt;/strong&gt;给了 HE。&lt;/li&gt;
&lt;li&gt;HE 的 DNS 服务器上现在就有了权威记录，告诉全世界：“&lt;code&gt;0.7.4.0.1.0.0.2.ip6.arpa&lt;/code&gt; 这个区域归我管！”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在，&lt;strong&gt;HE&lt;/strong&gt; 就像是一个&lt;strong&gt;大型房地产开发商&lt;/strong&gt;。它从洲际管理局那里拿到了一大片地，现在有权将这片地分割成小块（比如 &lt;code&gt;/48&lt;/code&gt; 的地块）分给客户。&lt;/p&gt;
&lt;h3&gt;第四步：最终用户 - 你&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;你向 HE 申请了一个 &lt;code&gt;/48&lt;/code&gt; 的地址块，比如 &lt;code&gt;2001:470:aa::/48&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;HE 把这个 IP 地址块的使用权给了你。根据“IP所有权 = rDNS控制权”的原则，HE &lt;strong&gt;也必须把这个 IP 块对应的 rDNS 区域的管理权交给你&lt;/strong&gt;。这个区域就是 &lt;code&gt;a.a.0.0.0.7.4.0.1.0.0.2.ip6.arpa&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;但是，HE 如何把管理权交给你呢？&lt;/strong&gt; 它不可能让你直接修改它的 DNS 服务器。它通过一种标准的 DNS 机制——&lt;strong&gt;NS 记录委托&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在，&lt;strong&gt;你&lt;/strong&gt;就是这个&lt;strong&gt;地块的主人&lt;/strong&gt;。开发商（HE）给了你地契，并且在自己的总规划图上标注：“这块地 (&lt;code&gt;/48&lt;/code&gt;) 的具体事务（DNS 解析），别问我，去问地主指定的新管家（Cloudflare）。”&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;你的操作与授权链的对应关系&lt;/h3&gt;
&lt;p&gt;现在我们把你教程里的步骤串起来，就一目了然了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;你去 Cloudflare 添加站点&lt;/strong&gt; (&lt;code&gt;a.a...ip6.arpa&lt;/code&gt;)。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;你在做什么：&lt;/strong&gt; 你在告诉 Cloudflare：“你好，我马上要雇佣你做我的新管家，来管理我的这块地。你的门牌号（NameServer 地址）是多少？”&lt;/li&gt;
&lt;li&gt;Cloudflare 回答：“好的，我的门牌号是 &lt;code&gt;dave.ns.cloudflare.com&lt;/code&gt; 和 &lt;code&gt;macy.ns.cloudflare.com&lt;/code&gt;。你快去开发商那里登记一下，告诉他们以后有事找我。”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;你回到 HE 控制台，在 rDNS 部分填入 Cloudflare 的 NameServer。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;你在做什么：&lt;/strong&gt; 这就是最关键的&lt;strong&gt;行使授权&lt;/strong&gt;的一步！你对开发商 HE 说：“关于我那块地 (&lt;code&gt;a.a...ip6.arpa&lt;/code&gt;) 的所有 DNS 查询，请不要再由你来回答了。请你&lt;strong&gt;委托&lt;/strong&gt;给我的新管家 &lt;code&gt;dave.ns.cloudflare.com&lt;/code&gt;。”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HE 在后台做什么：&lt;/strong&gt; HE 在它自己的 DNS 服务器（&lt;code&gt;0.7.4.0.1.0.0.2.ip6.arpa&lt;/code&gt; 区域的权威服务器）里，为你对应的子域添加了两条 &lt;code&gt;NS&lt;/code&gt; 记录：&lt;pre&gt;&lt;code&gt;a.a.0.0.0.7.4.0.1.0.0.2.ip6.arpa. IN NS dave.ns.cloudflare.com.
a.a.0.0.0.7.4.0.1.0.0.2.ip6.arpa. IN NS macy.ns.cloudflare.com.
&lt;/code&gt;&lt;/pre&gt;
这个记录向全世界宣告：想知道 &lt;code&gt;a.a...ip6.arpa&lt;/code&gt; 及其所有子域（比如 &lt;code&gt;1.0.0.0.a.a...ip6.arpa&lt;/code&gt;）的任何信息，请去问 &lt;code&gt;dave.ns.cloudflare.com&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cloudflare 显示 Active。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;发生了什么：&lt;/strong&gt; Cloudflare 会定期检查全球 DNS 系统。它发现 HE 已经成功地将对你那个 rDNS 域名的查询权指向了自己。授权验证通过，它正式上岗，开始为你管理这个“域名”。
&amp;lt;/ShowMoreCollapse&amp;gt;
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Nginx Proxy Manager获取客户端真实IP（统一解决办法）</title><link>https://blog.useforall.com/posts/nginx-proxy-manager-get-real-client-ip-a-unified-solution/</link><guid isPermaLink="true">https://blog.useforall.com/posts/nginx-proxy-manager-get-real-client-ip-a-unified-solution/</guid><description>本文介绍如何在Nginx Proxy Manager中获取客户端的真实IP地址，适用于各种后端服务。配置成功后，仅需添加一行代码即可实现真实IP的传递。无需大改所有的反代配置。</description><pubDate>Tue, 28 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
本文介绍如何在Nginx Proxy Manager中获取客户端的真实IP地址，适用于各种后端服务。配置成功后，仅需添加一行代码即可实现真实IP的传递。无需大改所有的反代配置。
:::&lt;/p&gt;
&lt;p&gt;以前基本没用过Cloudflare的CDN，cf对我来说，更多的就是DNS解析服务而已，所以一直以来在Nginx Proxy Manager（以下简称NPM）中配置反代时，其默认的&lt;code&gt;$remote_addr&lt;/code&gt;配置就足够用了，因为它获取到的就是客户端的真实IP地址。但是最近由于一些需求，开始使用Cloudflare的CDN服务，发现NPM中获取到的&lt;code&gt;$remote_addr&lt;/code&gt;变成了Cloudflare的节点IP地址，而不是客户端的真实IP地址。&lt;/p&gt;
&lt;p&gt;很多教程都是直接在每个反代配置中添加&lt;code&gt;set_real_ip_from&lt;/code&gt;和&lt;code&gt;real_ip_header&lt;/code&gt;，如果你套CDN的站点比较少，那倒也无所谓，但是如果你有几十个站点都需要配置，那就非常麻烦了。经过一番研究，终于找到了一种统一的解决办法，只需要在NPM的全局配置中添加一行代码，就可以让所有的反代配置都能获取到客户端的真实IP地址。NPM支持使用自定义配置文件（实际上就是nginx的配置方法，NPM不过就是一个Nginx的管理面板而已）。&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nginx Proxy Manager&lt;/strong&gt;：反向代理软件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;update_cf_ips.sh&lt;/strong&gt;：更新Cloudflare的IP段信息到自定义nginx配置文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;部署Nginx Proxy Manager&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;参考&lt;a href=&quot;/posts/3/#%E9%85%8D%E7%BD%AE-nginx-proxy-manager&quot;&gt;这里&lt;/a&gt;，本文默认你也是Docker部署的NPM。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;配置获取真实IP&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;创建并复制粘贴&lt;code&gt;update_cf_ips.sh&lt;/code&gt;脚本：&lt;pre&gt;&lt;code&gt;cd /root/docker_data/nginx &amp;amp;&amp;amp; nano update_cf_ips.sh &amp;amp;&amp;amp; chmod +x update_cf_ips.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# ==============================================================================
#          Cloudflare IP Updater for Nginx Proxy Manager (NPM)
# ==============================================================================
# 功能:
#   1. 自动从 Cloudflare 官方源获取最新的 IPv4 和 IPv6 地址列表。
#   2. 生成 Nginx `real_ip` 配置，以正确获取通过 CF CDN 访问的真实用户 IP。
#   3. 仅在 IP 列表发生变化时，才更新配置文件并重新加载 Nginx。
#   4. 在无更新时，显示上次的更新时间。
# ==============================================================================

# --- 用户配置区 ---

# Nginx Proxy Manager (NPM) 的自定义配置目录在宿主机上的绝对路径
NPM_CUSTOM_DIR=&quot;/root/docker_data/nginx/data/nginx/custom&quot;

# NPM 的 Docker 容器名称或 ID
NPM_CONTAINER_NAME=&quot;nginx-app-1&quot;

# --- 脚本核心逻辑 ---

# --- 颜色定义 ---
GREEN=&apos;\033[0;32m&apos;
YELLOW=&apos;\033[0;33m&apos;
RED=&apos;\033[0;31m&apos;
CYAN=&apos;\033[0;36m&apos;
RESET=&apos;\033[0m&apos;

# --- 日志函数 ---
log_info() { echo -e &quot;  ${CYAN}*${RESET} $1&quot;; }
log_success() { echo -e &quot;    ${GREEN}✓ SUCCESS:${RESET} $1&quot;; }
log_warn() { echo -e &quot;    ${YELLOW}‼ WARN:${RESET} $1&quot;; }
log_error() { echo -e &quot;    ${RED}✗ ERROR:${RESET} $1&quot; &amp;gt;&amp;amp;2; }

# 脚本退出时执行的清理函数
cleanup() {
    rm -f &quot;$TMP_FILE&quot;
    tput cnorm
}

# --- 主程序 ---

trap cleanup EXIT
set -e

CONFIG_FILE_PATH=&quot;$NPM_CUSTOM_DIR/cloudflare_ips.conf&quot;
TMP_FILE=$(mktemp)

echo -e &quot;${GREEN}=====================================================${RESET}&quot;
echo -e &quot;${CYAN}  Cloudflare IP Updater for Nginx Proxy Manager${RESET}&quot;
echo -e &quot;${GREEN}=====================================================${RESET}&quot;
echo -e &quot;执行时间: $(date +&quot;%Y-%m-%d %H:%M:%S&quot;)&quot;

# --- 步骤 1: 环境检查 ---
echo -e &quot;\n${CYAN}===[ 步骤 1/4: 环境检查 ]===${RESET}&quot;
log_info &quot;检查自定义配置目录: $NPM_CUSTOM_DIR&quot;
if [ ! -d &quot;$NPM_CUSTOM_DIR&quot; ]; then
    log_error &quot;目录不存在！请检查脚本中的 NPM_CUSTOM_DIR 变量。&quot;
    exit 1
fi
log_success &quot;目录存在。&quot;

log_info &quot;检查网络下载工具 (curl/wget)...&quot;
if command -v curl &amp;amp;&amp;gt; /dev/null; then

    FETCH_CMD=&quot;curl -s -f -L&quot;
    log_success &quot;将使用 curl 进行下载。&quot;
elif command -v wget &amp;amp;&amp;gt; /dev/null; then
    FETCH_CMD=&quot;wget -qO-&quot;
    log_success &quot;将使用 wget 进行下载。&quot;
else
    log_error &quot;系统中未找到 curl 或 wget，无法下载 IP 列表。&quot;
    exit 1
fi

# --- 步骤 2: 下载并生成配置 ---
echo -e &quot;\n${CYAN}===[ 步骤 2/4: 下载并生成配置 ]===${RESET}&quot;
CF_IPV4_URL=&quot;https://www.cloudflare.com/ips-v4&quot;
CF_IPV6_URL=&quot;https://www.cloudflare.com/ips-v6&quot;

log_info &quot;正在从 Cloudflare 下载最新的 IP 列表...&quot;
{
    echo &quot;# Cloudflare IP Ranges&quot;
    echo &quot;# Auto-generated on $(date)&quot;
    echo &quot;&quot;
    {
        $FETCH_CMD $CF_IPV4_URL
        echo &quot;&quot;
        $FETCH_CMD $CF_IPV6_URL
    } | sed &apos;/^$/d&apos; | sed &apos;s/^/set_real_ip_from /; s/$/;/&apos;
    echo &quot;&quot;
    echo &quot;# 指定 Nginx 从 &apos;CF-Connecting-IP&apos; 请求头中获取真实的用户 IP 地址&quot;
    echo &quot;real_ip_header CF-Connecting-IP;&quot;
} &amp;gt; &quot;$TMP_FILE&quot;

if [ $? -eq 0 ] &amp;amp;&amp;amp; [ -s &quot;$TMP_FILE&quot; ]; then
    log_success &quot;IP 列表下载并格式化完成。&quot;
else
    log_error &quot;下载或处理 IP 列表失败，临时文件为空或下载出错。&quot;
    exit 1
fi

# --- 步骤 3: 对比并更新配置 ---
echo -e &quot;\n${CYAN}===[ 步骤 3/4: 对比并更新配置 ]===${RESET}&quot;
# 【关键修改】这里的 log_info 消息被更新了
log_info &quot;对比新旧配置文件的核心内容...&quot;

get_core_config() {
    grep -E &quot;set_real_ip_from|real_ip_header&quot; &quot;$1&quot;
}

if [ ! -f &quot;$CONFIG_FILE_PATH&quot; ] || ! cmp -s &amp;lt;(get_core_config &quot;$TMP_FILE&quot;) &amp;lt;(get_core_config &quot;$CONFIG_FILE_PATH&quot;); then
    log_success &quot;检测到 IP 列表有更新 (或首次运行)，准备更新配置文件。&quot;
    mv &quot;$TMP_FILE&quot; &quot;$CONFIG_FILE_PATH&quot;
    log_success &quot;配置文件已更新: $CONFIG_FILE_PATH&quot;
    UPDATE_NEEDED=true
else
    # 【关键修改】从旧文件中提取上次更新时间并显示
    last_update_time=$(grep &quot;Auto-generated on&quot; &quot;$CONFIG_FILE_PATH&quot; | sed &apos;s/# Auto-generated on //&apos;)
    if [ -n &quot;$last_update_time&quot; ]; then
        # 如果成功提取到时间，就显示出来
        log_warn &quot;Cloudflare IP 列表无变化。上次更新于: ${last_update_time}&quot;
    else
        # 如果没找到时间戳行（以防万一），就显示通用消息
        log_warn &quot;Cloudflare IP 列表无变化，无需更新。&quot;
    fi
    UPDATE_NEEDED=false
fi

# --- 步骤 4: 重载 Nginx ---
echo -e &quot;\n${CYAN}===[ 步骤 4/4: 重载 Nginx 服务 ]===${RESET}&quot;
if [ &quot;$UPDATE_NEEDED&quot; = true ]; then
    log_info &quot;正在测试 Nginx 配置...&quot;
    if docker exec &quot;$NPM_CONTAINER_NAME&quot; nginx -t &amp;amp;&amp;gt; /dev/null; then
        log_success &quot;Nginx 配置测试通过。&quot;
        log_info &quot;正在重新加载 Nginx 配置...&quot;
        if docker exec &quot;$NPM_CONTAINER_NAME&quot; nginx -s reload &amp;amp;&amp;gt; /dev/null; then
            log_success &quot;Nginx 配置已成功重新加载。&quot;
        else
            log_error &quot;Nginx reload 命令执行失败！&quot;
            exit 1
        fi
    else
        log_error &quot;新的 Nginx 配置测试失败！请检查 $CONFIG_FILE_PATH 文件内容。&quot;
        exit 1
    fi
else
    log_warn &quot;无需重载 Nginx。&quot;
fi

# --- 任务完成 ---
echo -e &quot;\n${GREEN}=====================================================${RESET}&quot;
echo -e &quot;${GREEN}          🎉 脚本执行成功，任务完成！ 🎉           ${RESET}&quot;
echo -e &quot;${GREEN}=====================================================${RESET}&quot;

exit 0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;设置&lt;code&gt;crontab&lt;/code&gt;定时运行&lt;pre&gt;&lt;code&gt;crontab -e
&lt;/code&gt;&lt;/pre&gt;
添加以下内容，每天凌晨2点运行一次：&lt;pre&gt;&lt;code&gt;# 每周一凌晨3点，自动更新 Nginx Proxy Manager 的 Cloudflare IP 列表
0 3 * * 1 /root/docker_data/nginx/update_cf_ips.sh &amp;gt;&amp;gt; /var/log/update_cf_ips.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
:::tip{title=&quot;注&quot;}
记得修改脚本地址为你实际的脚本地址
:::&lt;/li&gt;
&lt;li&gt;手动运行脚本测试&lt;pre&gt;&lt;code&gt;./update_cf_ips.sh &amp;amp;&amp;amp; ls -alh /root/docker_data/nginx/data/nginx/custom/cloudflare_ips.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;最后前往NPM面板，在你套了Cloudflare CDN的反代配置中，&lt;code&gt;Advanced&lt;/code&gt; Tab下的&lt;code&gt;Custom Nginx Configuration&lt;/code&gt;里添加一行代码：&lt;pre&gt;&lt;code&gt;include /data/nginx/custom/cloudflare_ips.conf;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;结束。至此，所有套了CF CDN的网站，都能获取到真实IP，而不需要繁杂的单独配置了。其他CDN提供商也可以参考同样的思路。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Docker部署Milvus并使用Nginx Proxy Manager反代gRPC协议</title><link>https://blog.useforall.com/posts/docker-deploy-milvus-with-nginx-proxy-manager-grpc/</link><guid isPermaLink="true">https://blog.useforall.com/posts/docker-deploy-milvus-with-nginx-proxy-manager-grpc/</guid><description>本文介绍如何使用Docker部署Milvus向量数据库，并使用Nginx Proxy Manager反代gRPC协议，同时使用Cloudflare CDN功能。</description><pubDate>Mon, 27 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要信息&quot;}
本文介绍如何使用Docker部署Milvus(v2.5.19)向量数据库，并使用Nginx Proxy Manager反代gRPC协议，同时使用Cloudflare CDN功能。
:::&lt;/p&gt;
&lt;p&gt;为什么要这么麻烦？主要原因是我的甲骨文新加坡西服务器太闲了，想物尽其用。然后最近开始使用Claude Code进行Vibe Coding，发现一个项目叫&lt;a href=&quot;https://github.com/zilliztech/claude-context&quot;&gt;claude-context&lt;/a&gt;。一直苦于这类AI Agent不能很好的获取上下文，这个项目应该是用了类似语义检索（虽然不是LSP）和向量检索的方式来实现的。然后这个项目使用的是Milvus作为向量数据库，所以就想把Milvus部署起来试试。&lt;/p&gt;
&lt;p&gt;本文仅仅涉及Milvus的Standalone模式，集群部署过于复杂，目前暂时也用不上。以后用上了再更新部署教程吧...&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nginx Proxy Manager&lt;/strong&gt;：反向代理 Milvus gRPC端口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Milvus&lt;/strong&gt;：生产级向量数据库，支持高效的向量检索。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;部署Milvus&lt;/h2&gt;
&lt;h3&gt;基本配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;创建对应文件夹和文件&lt;pre&gt;&lt;code&gt;mkdir -p ~/docker_data/milvus &amp;amp;&amp;amp; cd ~/docker_data/milvus
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;下载Milvus的&lt;code&gt;milvus.yaml&lt;/code&gt;文件&lt;pre&gt;&lt;code&gt;wget https://raw.githubusercontent.com/milvus-io/milvus/v2.5.19/configs/milvus.yaml -O milvus.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;修改&lt;code&gt;milvus.yaml&lt;/code&gt;文件，配置一些基础信息。&lt;code&gt;nano milvus.yaml&lt;/code&gt;(使用&lt;code&gt;ctrl+w&lt;/code&gt;进行搜索)&lt;pre&gt;&lt;code&gt;# Licensed to the LF AI &amp;amp; Data foundation under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# &quot;License&quot;); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Related configuration of etcd, used to store Milvus metadata &amp;amp; service discovery.
etcd:
  # Endpoints used to access etcd service. You can change this parameter as the endpoints of your own etcd cluster.
  # Environment variable: ETCD_ENDPOINTS
  # etcd preferentially acquires valid address from environment variable ETCD_ENDPOINTS when Milvus is started.
  endpoints: localhost:2379
  # Root prefix of the key to where Milvus stores data in etcd.
  # It is recommended to change this parameter before starting Milvus for the first time.
  # To share an etcd instance among multiple Milvus instances, consider changing this to a different value for each Milvus instance before you start them.
  # Set an easy-to-identify root path for Milvus if etcd service already exists.
  # Changing this for an already running Milvus instance may result in failures to read legacy data.
  rootPath: by-dev
  # Sub-prefix of the key to where Milvus stores metadata-related information in etcd.
  # Caution: Changing this parameter after using Milvus for a period of time will affect your access to old data.
  # It is recommended to change this parameter before starting Milvus for the first time.
  metaSubPath: meta
  # Sub-prefix of the key to where Milvus stores timestamps in etcd.
  # Caution: Changing this parameter after using Milvus for a period of time will affect your access to old data.
  # It is recommended not to change this parameter if there is no specific reason.
  kvSubPath: kv
  log:
    level: info # Only supports debug, info, warn, error, panic, or fatal. Default &apos;info&apos;.
    # path is one of:
    #  - &quot;default&quot; as os.Stderr,
    #  - &quot;stderr&quot; as os.Stderr,
    #  - &quot;stdout&quot; as os.Stdout,
    #  - file path to append server logs to.
    # please adjust in embedded Milvus: /tmp/milvus/logs/etcd.log
    path: stdout
  ssl:
    enabled: false # Whether to support ETCD secure connection mode
    tlsCert: /path/to/etcd-client.pem # path to your cert file
    tlsKey: /path/to/etcd-client-key.pem # path to your key file
    tlsCACert: /path/to/ca.pem # path to your CACert file
    # TLS min version
    # Optional values: 1.0, 1.1, 1.2, 1.3。
    # We recommend using version 1.2 and above.
    tlsMinVersion: 1.3
  requestTimeout: 10000 # Etcd operation timeout in milliseconds
  use:
    embed: false # Whether to enable embedded Etcd (an in-process EtcdServer).
  data:
    dir: default.etcd # Embedded Etcd only. please adjust in embedded Milvus: /tmp/milvus/etcdData/
  auth:
    enabled: false # Whether to enable authentication
    userName:  # username for etcd authentication
    password:  # password for etcd authentication

metastore:
  type: etcd # Default value: etcd, Valid values: [etcd, tikv]
  snapshot:
    ttl: 86400 # snapshot ttl in seconds
    reserveTime: 3600 # snapshot reserve time in seconds

# Related configuration of tikv, used to store Milvus metadata.
# Notice that when TiKV is enabled for metastore, you still need to have etcd for service discovery.
# TiKV is a good option when the metadata size requires better horizontal scalability.
tikv:
  endpoints: 127.0.0.1:2389 # Note that the default pd port of tikv is 2379, which conflicts with etcd.
  rootPath: by-dev # The root path where data is stored in tikv
  metaSubPath: meta # metaRootPath = rootPath + &apos;/&apos; + metaSubPath
  kvSubPath: kv # kvRootPath = rootPath + &apos;/&apos; + kvSubPath
  requestTimeout: 10000 # ms, tikv request timeout
  snapshotScanSize: 256 # batch size of tikv snapshot scan
  ssl:
    enabled: false # Whether to support TiKV secure connection mode
    tlsCert:  # path to your cert file
    tlsKey:  # path to your key file
    tlsCACert:  # path to your CACert file

localStorage:
  # Local path to where vector data are stored during a search or a query to avoid repetitve access to MinIO or S3 service.
  # Caution: Changing this parameter after using Milvus for a period of time will affect your access to old data.
  # It is recommended to change this parameter before starting Milvus for the first time.
  path: /var/lib/milvus/data/

# Related configuration of MinIO/S3/GCS or any other service supports S3 API, which is responsible for data persistence for Milvus.
# We refer to the storage service as MinIO/S3 in the following description for simplicity.
minio:
  # IP address of MinIO or S3 service.
  # Environment variable: MINIO_ADDRESS
  # minio.address and minio.port together generate the valid access to MinIO or S3 service.
  # MinIO preferentially acquires the valid IP address from the environment variable MINIO_ADDRESS when Milvus is started.
  # Default value applies when MinIO or S3 is running on the same network with Milvus.
  address: 172.17.0.1
  port: 65010 # Port of MinIO or S3 service.
  # Access key ID that MinIO or S3 issues to user for authorized access.
  # Environment variable: MINIO_ACCESS_KEY_ID or minio.accessKeyID
  # minio.accessKeyID and minio.secretAccessKey together are used for identity authentication to access the MinIO or S3 service.
  # This configuration must be set identical to the environment variable MINIO_ACCESS_KEY_ID, which is necessary for starting MinIO or S3.
  # The default value applies to MinIO or S3 service that started with the default docker-compose.yml file.
  accessKeyID: xxxxx
  # Secret key used to encrypt the signature string and verify the signature string on server. It must be kept strictly confidential and accessible only to the MinIO or S3 server and users.
  # Environment variable: MINIO_SECRET_ACCESS_KEY or minio.secretAccessKey
  # minio.accessKeyID and minio.secretAccessKey together are used for identity authentication to access the MinIO or S3 service.
  # This configuration must be set identical to the environment variable MINIO_SECRET_ACCESS_KEY, which is necessary for starting MinIO or S3.
  # The default value applies to MinIO or S3 service that started with the default docker-compose.yml file.
  secretAccessKey: xxxxxx
  useSSL: false # Switch value to control if to access the MinIO or S3 service through SSL.
  ssl:
    tlsCACert: /path/to/public.crt # path to your CACert file
  # Name of the bucket where Milvus stores data in MinIO or S3.
  # Milvus 2.0.0 does not support storing data in multiple buckets.
  # Bucket with this name will be created if it does not exist. If the bucket already exists and is accessible, it will be used directly. Otherwise, there will be an error.
  # To share an MinIO instance among multiple Milvus instances, consider changing this to a different value for each Milvus instance before you start them. For details, see Operation FAQs.
  # The data will be stored in the local Docker if Docker is used to start the MinIO service locally. Ensure that there is sufficient storage space.
  # A bucket name is globally unique in one MinIO or S3 instance.
  bucketName: milvus
  # Root prefix of the key to where Milvus stores data in MinIO or S3.
  # It is recommended to change this parameter before starting Milvus for the first time.
  # To share an MinIO instance among multiple Milvus instances, consider changing this to a different value for each Milvus instance before you start them. For details, see Operation FAQs.
  # Set an easy-to-identify root key prefix for Milvus if etcd service already exists.
  # Changing this for an already running Milvus instance may result in failures to read legacy data.
  rootPath: files
  # Whether to useIAM role to access S3/GCS instead of access/secret keys
  # For more information, refer to
  # aws: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use.html
  # gcp: https://cloud.google.com/storage/docs/access-control/iam
  # aliyun (ack): https://www.alibabacloud.com/help/en/container-service-for-kubernetes/latest/use-rrsa-to-enforce-access-control
  # aliyun (ecs): https://www.alibabacloud.com/help/en/elastic-compute-service/latest/attach-an-instance-ram-role
  useIAM: false
  # Cloud Provider of S3. Supports: &quot;aws&quot;, &quot;gcp&quot;, &quot;aliyun&quot;.
  # Cloud Provider of Google Cloud Storage. Supports: &quot;gcpnative&quot;.
  # You can use &quot;aws&quot; for other cloud provider supports S3 API with signature v4, e.g.: minio
  # You can use &quot;gcp&quot; for other cloud provider supports S3 API with signature v2
  # You can use &quot;aliyun&quot; for other cloud provider uses virtual host style bucket
  # You can use &quot;gcpnative&quot; for the Google Cloud Platform provider. Uses service account credentials
  # for authentication.
  # When useIAM enabled, only &quot;aws&quot;, &quot;gcp&quot;, &quot;aliyun&quot; is supported for now
  cloudProvider: aws
  # The JSON content contains the gcs service account credentials.
  # Used only for the &quot;gcpnative&quot; cloud provider.
  gcpCredentialJSON:
  # Custom endpoint for fetch IAM role credentials. when useIAM is true &amp;amp; cloudProvider is &quot;aws&quot;.
  # Leave it empty if you want to use AWS default endpoint
  iamEndpoint:
  logLevel: fatal # Log level for aws sdk log. Supported level:  off, fatal, error, warn, info, debug, trace
  region:  # Specify minio storage system location region
  useVirtualHost: false # Whether use virtual host mode for bucket
  requestTimeoutMs: 10000 # minio timeout for request time in milliseconds
  # The maximum number of objects requested per batch in minio ListObjects rpc,
  # 0 means using oss client by default, decrease these configration if ListObjects timeout
  listObjectsMaxKeys: 0

# Milvus supports four MQ: rocksmq(based on RockDB), natsmq(embedded nats-server), Pulsar and Kafka.
# You can change your mq by setting mq.type field.
# If you don&apos;t set mq.type field as default, there is a note about enabling priority if we config multiple mq in this file.
# 1. standalone(local) mode: rocksmq(default) &amp;gt; natsmq &amp;gt; Pulsar &amp;gt; Kafka
# 2. cluster mode:  Pulsar(default) &amp;gt; Kafka (rocksmq and natsmq is unsupported in cluster mode)
mq:
  # Default value: &quot;default&quot;
  # Valid values: [default, pulsar, kafka, rocksmq, natsmq]
  type: default
  enablePursuitMode: true # Default value: &quot;true&quot;
  pursuitLag: 10 # time tick lag threshold to enter pursuit mode, in seconds
  pursuitBufferSize: 8388608 # pursuit mode buffer size in bytes
  pursuitBufferTime: 60 # pursuit mode buffer time in seconds
  mqBufSize: 16 # MQ client consumer buffer length
  dispatcher:
    mergeCheckInterval: 0.1 # the interval time(in seconds) for dispatcher to check whether to merge
    targetBufSize: 16 # the lenth of channel buffer for targe
    maxTolerantLag: 3 # Default value: &quot;3&quot;, the timeout(in seconds) that target sends msgPack

# Related configuration of pulsar, used to manage Milvus logs of recent mutation operations, output streaming log, and provide log publish-subscribe services.
pulsar:
  # IP address of Pulsar service.
  # Environment variable: PULSAR_ADDRESS
  # pulsar.address and pulsar.port together generate the valid access to Pulsar.
  # Pulsar preferentially acquires the valid IP address from the environment variable PULSAR_ADDRESS when Milvus is started.
  # Default value applies when Pulsar is running on the same network with Milvus.
  address: localhost
  port: 6650 # Port of Pulsar service.
  webport: 80 # Web port of of Pulsar service. If you connect direcly without proxy, should use 8080.
  # The maximum size of each message in Pulsar. Unit: Byte.
  # By default, Pulsar can transmit at most 2MB of data in a single message. When the size of inserted data is greater than this value, proxy fragments the data into multiple messages to ensure that they can be transmitted correctly.
  # If the corresponding parameter in Pulsar remains unchanged, increasing this configuration will cause Milvus to fail, and reducing it produces no advantage.
  maxMessageSize: 2097152
  # Pulsar can be provisioned for specific tenants with appropriate capacity allocated to the tenant.
  # To share a Pulsar instance among multiple Milvus instances, you can change this to an Pulsar tenant rather than the default one for each Milvus instance before you start them. However, if you do not want Pulsar multi-tenancy, you are advised to change msgChannel.chanNamePrefix.cluster to the different value.
  tenant: public
  namespace: default # A Pulsar namespace is the administrative unit nomenclature within a tenant.
  requestTimeout: 60 # pulsar client global request timeout in seconds
  enableClientMetrics: false # Whether to register pulsar client metrics into milvus metrics path.

# If you want to enable kafka, needs to comment the pulsar configs
# kafka:
#   brokerList: localhost:9092
#   saslUsername:
#   saslPassword:
#   saslMechanisms:
#   securityProtocol:
#   ssl:
#     enabled: false # whether to enable ssl mode
#     tlsCert:  # path to client&apos;s public key (PEM) used for authentication
#     tlsKey:  # path to client&apos;s private key (PEM) used for authentication
#     tlsCaCert:  # file or directory path to CA certificate(s) for verifying the broker&apos;s key
#     tlsKeyPassword:  # private key passphrase for use with ssl.key.location and set_ssl_cert(), if any
#   readTimeout: 10

rocksmq:
  # Prefix of the key to where Milvus stores data in RocksMQ.
  # Caution: Changing this parameter after using Milvus for a period of time will affect your access to old data.
  # It is recommended to change this parameter before starting Milvus for the first time.
  # Set an easy-to-identify root key prefix for Milvus if etcd service already exists.
  path: /var/lib/milvus/rdb_data
  lrucacheratio: 0.06 # rocksdb cache memory ratio
  rocksmqPageSize: 67108864 # The maximum size of messages in each page in RocksMQ. Messages in RocksMQ are checked and cleared (when expired) in batch based on this parameters. Unit: Byte.
  retentionTimeInMinutes: 4320 # The maximum retention time of acked messages in RocksMQ. Acked messages in RocksMQ are retained for the specified period of time and then cleared. Unit: Minute.
  retentionSizeInMB: 8192 # The maximum retention size of acked messages of each topic in RocksMQ. Acked messages in each topic are cleared if their size exceed this parameter. Unit: MB.
  compactionInterval: 86400 # Time interval to trigger rocksdb compaction to remove deleted data. Unit: Second
  compressionTypes: 0,0,7,7,7 # compaction compression type, only support use 0,7. 0 means not compress, 7 will use zstd. Length of types means num of rocksdb level.

# natsmq configuration.
# more detail: https://docs.nats.io/running-a-nats-service/configuration
natsmq:
  server:
    port: 4222 # Listening port of the NATS server.
    storeDir: /var/lib/milvus/nats # Directory to use for JetStream storage of nats
    maxFileStore: 17179869184 # Maximum size of the &apos;file&apos; storage
    maxPayload: 8388608 # Maximum number of bytes in a message payload
    maxPending: 67108864 # Maximum number of bytes buffered for a connection Applies to client connections
    initializeTimeout: 4000 # waiting for initialization of natsmq finished
    monitor:
      trace: false # If true enable protocol trace log messages
      debug: false # If true enable debug log messages
      logTime: true # If set to false, log without timestamps.
      logFile: /tmp/milvus/logs/nats.log # Log file path relative to .. of milvus binary if use relative path
      logSizeLimit: 536870912 # Size in bytes after the log file rolls over to a new one
    retention:
      maxAge: 4320 # Maximum age of any message in the P-channel
      maxBytes:  # How many bytes the single P-channel may contain. Removing oldest messages if the P-channel exceeds this size
      maxMsgs:  # How many message the single P-channel may contain. Removing oldest messages if the P-channel exceeds this limit

# Related configuration of rootCoord, used to handle data definition language (DDL) and data control language (DCL) requests
rootCoord:
  dmlChannelNum: 16 # The number of DML-Channels to create at the root coord startup.
  # The maximum number of partitions in each collection.
  # New partitions cannot be created if this parameter is set as 0 or 1.
  # Range: [0, INT64MAX]
  maxPartitionNum: 1024
  # The minimum row count of a segment required for creating index.
  # Segments with smaller size than this parameter will not be indexed, and will be searched with brute force.
  minSegmentSizeToEnableIndex: 1024
  enableActiveStandby: false
  maxDatabaseNum: 64 # Maximum number of database
  maxGeneralCapacity: 65536 # upper limit for the sum of of product of partitionNumber and shardNumber
  gracefulStopTimeout: 5 # seconds. force stop node without graceful stop
  ip:  # TCP/IP address of rootCoord. If not specified, use the first unicastable address
  port: 53100 # TCP port of rootCoord
  grpc:
    serverMaxSendSize: 536870912 # The maximum size of each RPC request that the rootCoord can send, unit: byte
    serverMaxRecvSize: 268435456 # The maximum size of each RPC request that the rootCoord can receive, unit: byte
    clientMaxSendSize: 268435456 # The maximum size of each RPC request that the clients on rootCoord can send, unit: byte
    clientMaxRecvSize: 536870912 # The maximum size of each RPC request that the clients on rootCoord can receive, unit: byte

# Related configuration of proxy, used to validate client requests and reduce the returned results.
proxy:
  timeTickInterval: 200 # The interval at which proxy synchronizes the time tick, unit: ms.
  healthCheckTimeout: 3000 # ms, the interval that to do component healthy check
  msgStream:
    timeTick:
      bufSize: 512 # The maximum number of messages can be buffered in the timeTick message stream of the proxy when producing messages.
  maxNameLength: 255 # The maximum length of the name or alias that can be created in Milvus, including the collection name, collection alias, partition name, and field name.
  maxFieldNum: 64 # The maximum number of field can be created when creating in a collection. It is strongly DISCOURAGED to set maxFieldNum &amp;gt;= 64.
  maxVectorFieldNum: 4 # The maximum number of vector fields that can be specified in a collection. Value range: [1, 10].
  maxShardNum: 16 # The maximum number of shards can be created when creating in a collection.
  maxDimension: 32768 # The maximum number of dimensions of a vector can have when creating in a collection.
  # Whether to produce gin logs.\n
  # please adjust in embedded Milvus: false
  ginLogging: true
  ginLogSkipPaths: / # skip url path for gin log
  maxTaskNum: 1024 # The maximum number of tasks in the task queue of the proxy.
  ddlConcurrency: 16 # The concurrent execution number of DDL at proxy.
  dclConcurrency: 16 # The concurrent execution number of DCL at proxy.
  mustUsePartitionKey: false # switch for whether proxy must use partition key for the collection
  # maximum number of result entries, typically Nq * TopK * GroupSize.
  # It costs additional memory and time to process a large number of result entries.
  # If the number of result entries exceeds this limit, the search will be rejected.
  # Disabled if the value is less or equal to 0.
  maxResultEntries: -1
  accessLog:
    enable: false # Whether to enable the access log feature.
    minioEnable: false # Whether to upload local access log files to MinIO. This parameter can be specified when proxy.accessLog.filename is not empty.
    localPath: /tmp/milvus_access # The local folder path where the access log file is stored. This parameter can be specified when proxy.accessLog.filename is not empty.
    filename:  # The name of the access log file. If you leave this parameter empty, access logs will be printed to stdout.
    maxSize: 64 # The maximum size allowed for a single access log file. If the log file size reaches this limit, a rotation process will be triggered. This process seals the current access log file, creates a new log file, and clears the contents of the original log file. Unit: MB.
    rotatedTime: 0 # The maximum time interval allowed for rotating a single access log file. Upon reaching the specified time interval, a rotation process is triggered, resulting in the creation of a new access log file and sealing of the previous one. Unit: seconds
    remotePath: access_log/ # The path of the object storage for uploading access log files.
    remoteMaxTime: 0 # The time interval allowed for uploading access log files. If the upload time of a log file exceeds this interval, the file will be deleted. Setting the value to 0 disables this feature.
    formatters:
      base:
        format: &quot;[$time_now] [ACCESS] &amp;lt;$user_name: $user_addr&amp;gt; $method_name [status: $method_status] [code: $error_code] [sdk: $sdk_version] [msg: $error_msg] [traceID: $trace_id] [timeCost: $time_cost]&quot;
      query:
        format: &quot;[$time_now] [ACCESS] &amp;lt;$user_name: $user_addr&amp;gt; $method_name [status: $method_status] [code: $error_code] [sdk: $sdk_version] [msg: $error_msg] [traceID: $trace_id] [timeCost: $time_cost] [database: $database_name] [collection: $collection_name] [partitions: $partition_name] [expr: $method_expr] [params: $query_params]&quot;
        methods: &quot;Query, Delete&quot;
      search:
        format: &quot;[$time_now] [ACCESS] &amp;lt;$user_name: $user_addr&amp;gt; $method_name [status: $method_status] [code: $error_code] [sdk: $sdk_version] [msg: $error_msg] [traceID: $trace_id] [timeCost: $time_cost] [database: $database_name] [collection: $collection_name] [partitions: $partition_name] [expr: $method_expr] [nq: $nq] [params: $search_params]&quot;
        methods: &quot;HybridSearch, Search&quot;
    cacheSize: 0 # Size of log of write cache, in byte. (Close write cache if size was 0)
    cacheFlushInterval: 3 # time interval of auto flush write cache, in seconds. (Close auto flush if interval was 0)
  connectionCheckIntervalSeconds: 120 # the interval time(in seconds) for connection manager to scan inactive client info
  connectionClientInfoTTLSeconds: 86400 # inactive client info TTL duration, in seconds
  maxConnectionNum: 10000 # the max client info numbers that proxy should manage, avoid too many client infos
  gracefulStopTimeout: 30 # seconds. force stop node without graceful stop
  slowQuerySpanInSeconds: 5 # query whose executed time exceeds the `slowQuerySpanInSeconds` can be considered slow, in seconds.
  queryNodePooling:
    size: 10 # the size for shardleader(querynode) client pool
  http:
    enabled: true # Whether to enable the http server
    debug_mode: false # Whether to enable http server debug mode
    port:  # high-level restful api
    acceptTypeAllowInt64: true # high-level restful api, whether http client can deal with int64
    enablePprof: true # Whether to enable pprof middleware on the metrics port
    enableWebUI: true # Whether to enable setting the WebUI middleware on the metrics port
  ip:  # TCP/IP address of proxy. If not specified, use the first unicastable address
  port: 19530 # TCP port of proxy
  internalPort: 19529
  grpc:
    serverMaxSendSize: 268435456 # The maximum size of each RPC request that the proxy can send, unit: byte
    serverMaxRecvSize: 67108864 # The maximum size of each RPC request that the proxy can receive, unit: byte
    clientMaxSendSize: 268435456 # The maximum size of each RPC request that the clients on proxy can send, unit: byte
    clientMaxRecvSize: 67108864 # The maximum size of each RPC request that the clients on proxy can receive, unit: byte

# Related configuration of queryCoord, used to manage topology and load balancing for the query nodes, and handoff from growing segments to sealed segments.
queryCoord:
  taskMergeCap: 1
  taskExecutionCap: 256
  # Switch value to control if to automatically replace a growing segment with the corresponding indexed sealed segment when the growing segment reaches the sealing threshold.
  # If this parameter is set false, Milvus simply searches the growing segments with brute force.
  autoHandoff: true
  autoBalance: true # Switch value to control if to automatically balance the memory usage among query nodes by distributing segment loading and releasing operations evenly.
  autoBalanceChannel: true # Enable auto balance channel
  balancer: ScoreBasedBalancer # auto balancer used for segments on queryNodes
  globalRowCountFactor: 0.1 # the weight used when balancing segments among queryNodes
  scoreUnbalanceTolerationFactor: 0.05 # the least value for unbalanced extent between from and to nodes when doing balance
  reverseUnBalanceTolerationFactor: 1.3 # the largest value for unbalanced extent between from and to nodes after doing balance
  overloadedMemoryThresholdPercentage: 90 # The threshold of memory usage (in percentage) in a query node to trigger the sealed segment balancing.
  balanceIntervalSeconds: 60 # The interval at which query coord balances the memory usage among query nodes.
  memoryUsageMaxDifferencePercentage: 30 # The threshold of memory usage difference (in percentage) between any two query nodes to trigger the sealed segment balancing.
  rowCountFactor: 0.4 # the row count weight used when balancing segments among queryNodes
  segmentCountFactor: 0.4 # the segment count weight used when balancing segments among queryNodes
  globalSegmentCountFactor: 0.1 # the segment count weight used when balancing segments among queryNodes
  # the channel count weight used when balancing channels among queryNodes,
  #             A higher value reduces the likelihood of assigning channels from the same collection to the same QueryNode. Set to 1 to disable this feature.
  collectionChannelCountFactor: 10
  segmentCountMaxSteps: 50 # segment count based plan generator max steps
  rowCountMaxSteps: 50 # segment count based plan generator max steps
  randomMaxSteps: 10 # segment count based plan generator max steps
  growingRowCountWeight: 4 # the memory weight of growing segment row count
  delegatorMemoryOverloadFactor: 0.1 # the factor of delegator overloaded memory
  balanceCostThreshold: 0.001 # the threshold of balance cost, if the difference of cluster&apos;s cost after executing the balance plan is less than this value, the plan will not be executed
  checkSegmentInterval: 1000
  checkChannelInterval: 1000
  checkBalanceInterval: 300
  autoBalanceInterval: 3000 # the interval for triggerauto balance
  checkIndexInterval: 10000
  channelTaskTimeout: 60000 # 1 minute
  segmentTaskTimeout: 120000 # 2 minute
  distPullInterval: 500
  heartbeatAvailableInterval: 10000 # 10s, Only QueryNodes which fetched heartbeats within the duration are available
  loadTimeoutSeconds: 600
  distRequestTimeout: 5000 # the request timeout for querycoord fetching data distribution from querynodes, in milliseconds
  heatbeatWarningLag: 5000 # the lag value for querycoord report warning when last heatbeat is too old, in milliseconds
  checkHandoffInterval: 5000
  enableActiveStandby: false
  checkInterval: 1000
  checkHealthInterval: 3000 # 3s, the interval when query coord try to check health of query node
  checkHealthRPCTimeout: 2000 # 100ms, the timeout of check health rpc to query node
  brokerTimeout: 5000 # 5000ms, querycoord broker rpc timeout
  collectionRecoverTimes: 3 # if collection recover times reach the limit during loading state, release it
  observerTaskParallel: 16 # the parallel observer dispatcher task number
  checkAutoBalanceConfigInterval: 10 # the interval of check auto balance config
  checkNodeSessionInterval: 60 # the interval(in seconds) of check querynode cluster session
  gracefulStopTimeout: 5 # seconds. force stop node without graceful stop
  enableStoppingBalance: true # whether enable stopping balance
  channelExclusiveNodeFactor: 4 # the least node number for enable channel&apos;s exclusive mode
  collectionObserverInterval: 200 # the interval of collection observer
  checkExecutedFlagInterval: 100 # the interval of check executed flag to force to pull dist
  updateCollectionLoadStatusInterval: 5 # 5m, max interval of updating collection loaded status for check health
  cleanExcludeSegmentInterval: 60 # the time duration of clean pipeline exclude segment which used for filter invalid data, in seconds
  ip:  # TCP/IP address of queryCoord. If not specified, use the first unicastable address
  port: 19531 # TCP port of queryCoord
  grpc:
    serverMaxSendSize: 536870912 # The maximum size of each RPC request that the queryCoord can send, unit: byte
    serverMaxRecvSize: 268435456 # The maximum size of each RPC request that the queryCoord can receive, unit: byte
    clientMaxSendSize: 268435456 # The maximum size of each RPC request that the clients on queryCoord can send, unit: byte
    clientMaxRecvSize: 536870912 # The maximum size of each RPC request that the clients on queryCoord can receive, unit: byte

# Related configuration of queryNode, used to run hybrid search between vector and scalar data.
queryNode:
  stats:
    publishInterval: 1000 # The interval that query node publishes the node statistics information, including segment status, cpu usage, memory usage, health status, etc. Unit: ms.
  segcore:
    knowhereThreadPoolNumRatio: 4 # The number of threads in knowhere&apos;s thread pool. If disk is enabled, the pool size will multiply with knowhereThreadPoolNumRatio([1, 32]).
    chunkRows: 128 # Row count by which Segcore divides a segment into chunks.
    interimIndex:
      # Whether to create a temporary index for growing segments and sealed segments not yet indexed, improving search performance.
      # Milvus will eventually seals and indexes all segments, but enabling this optimizes search performance for immediate queries following data insertion.
      # This defaults to true, indicating that Milvus creates temporary index for growing segments and the sealed segments that are not indexed upon searches.
      enableIndex: true
      nlist: 128 # interim index nlist, recommend to set sqrt(chunkRows), must smaller than chunkRows/8
      nprobe: 16 # nprobe to search small index, based on your accuracy requirement, must smaller than nlist
      subDim: 4 # interim index sub dim, recommend to (subDim % vector dim == 0)
      refineRatio: 4.5 # interim index parameters, should set to be &amp;gt;= 1.0
      indexBuildRatio: 0.1 # the ratio of building interim index rows count with max row count of a flush segment, should set to be &amp;lt; 1.0
      refineQuantType: NONE # Data representation of SCANN_DVR index, options: &apos;NONE&apos;, &apos;FLOAT16&apos;, &apos;BFLOAT16&apos; and &apos;UINT8&apos;
      refineWithQuant: true # whether to use refineQuantType to refine for faster but loss a little precision
      denseVectorIndexType: IVF_FLAT_CC # Dense vector intermin index type
      memExpansionRate: 1.15 # extra memory needed by building interim index
      buildParallelRate: 0.5 # the ratio of building interim index parallel matched with cpu num
    multipleChunkedEnable: true # Enable multiple chunked search
    deleteDumpBatchSize: 10000 # Batch size for delete snapshot dump in segcore.
    knowhereScoreConsistency: false # Enable knowhere strong consistency score computation logic
    jsonKeyStatsCommitInterval: 200 # the commit interval for the JSON key Stats to commit
  loadMemoryUsageFactor: 1 # The multiply factor of calculating the memory usage while loading segments
  enableDisk: false # enable querynode load disk index, and search on disk index
  maxDiskUsagePercentage: 95
  cache:
    memoryLimit: 2147483648 # 2 GB, 2 * 1024 *1024 *1024
    readAheadPolicy: willneed # The read ahead policy of chunk cache, options: `normal, random, sequential, willneed, dontneed`
    # options: async, sync, disable.
    # Specifies the necessity for warming up the chunk cache.
    # 1. If set to &quot;sync&quot; or &quot;async&quot; the original vector data will be synchronously/asynchronously loaded into the
    # chunk cache during the load process. This approach has the potential to substantially reduce query/search latency
    # for a specific duration post-load, albeit accompanied by a concurrent increase in disk usage;
    # 2. If set to &quot;disable&quot; original vector data will only be loaded into the chunk cache during search/query.
    warmup: disable
  mmap:
    vectorField: false # Enable mmap for loading vector data
    vectorIndex: false # Enable mmap for loading vector index
    scalarField: false # Enable mmap for loading scalar data
    scalarIndex: false # Enable mmap for loading scalar index
    chunkCache: true # Enable mmap for chunk cache (raw vector retrieving).
    # Enable memory mapping (mmap) to optimize the handling of growing raw data.
    # By activating this feature, the memory overhead associated with newly added or modified data will be significantly minimized.
    # However, this optimization may come at the cost of a slight decrease in query latency for the affected data segments.
    growingMmapEnabled: false
    fixedFileSizeForMmapAlloc: 1 # tmp file size for mmap chunk manager
    maxDiskUsagePercentageForMmapAlloc: 50 # disk percentage used in mmap chunk manager
  lazyload:
    enabled: false # Enable lazyload for loading data
    waitTimeout: 30000 # max wait timeout duration in milliseconds before start to do lazyload search and retrieve
    requestResourceTimeout: 5000 # max timeout in milliseconds for waiting request resource for lazy load, 5s by default
    requestResourceRetryInterval: 2000 # retry interval in milliseconds for waiting request resource for lazy load, 2s by default
    maxRetryTimes: 1 # max retry times for lazy load, 1 by default
    maxEvictPerRetry: 1 # max evict count for lazy load, 1 by default
  indexOffsetCacheEnabled: false # enable index offset cache for some scalar indexes, now is just for bitmap index, enable this param can improve performance for retrieving raw data from index
  grouping:
    enabled: true
    maxNQ: 1000
    topKMergeRatio: 20
  scheduler:
    receiveChanSize: 10240
    unsolvedQueueSize: 10240
    # maxReadConcurrentRatio is the concurrency ratio of read task (search task and query task).
    # Max read concurrency would be the value of hardware.GetCPUNum * maxReadConcurrentRatio.
    # It defaults to 2.0, which means max read concurrency would be the value of hardware.GetCPUNum * 2.
    # Max read concurrency must greater than or equal to 1, and less than or equal to hardware.GetCPUNum * 100.
    # (0, 100]
    maxReadConcurrentRatio: 1
    cpuRatio: 10 # ratio used to estimate read task cpu usage.
    maxTimestampLag: 86400
    scheduleReadPolicy:
      # fifo: A FIFO queue support the schedule.
      # user-task-polling:
      #         The user&apos;s tasks will be polled one by one and scheduled.
      #         Scheduling is fair on task granularity.
      #         The policy is based on the username for authentication.
      #         And an empty username is considered the same user.
      #         When there are no multi-users, the policy decay into FIFO&quot;
      name: fifo
      taskQueueExpire: 60 # Control how long (many seconds) that queue retains since queue is empty
      enableCrossUserGrouping: false # Enable Cross user grouping when using user-task-polling policy. (Disable it if user&apos;s task can not merge each other)
      maxPendingTaskPerUser: 1024 # Max pending task per user in scheduler
  levelZeroForwardPolicy: FilterByBF # delegator level zero deletion forward policy, possible option[&quot;FilterByBF&quot;, &quot;RemoteLoad&quot;]
  streamingDeltaForwardPolicy: FilterByBF # delegator streaming deletion forward policy, possible option[&quot;FilterByBF&quot;, &quot;Direct&quot;]
  forwardBatchSize: 4194304 # the batch size delegator uses for forwarding stream delete in loading procedure
  exprCache:
    enabled: false # enable expression result cache
    capacityBytes: 268435456 # max capacity in bytes for expression result cache
  dataSync:
    flowGraph:
      maxQueueLength: 16 # The maximum size of task queue cache in flow graph in query node.
      maxParallelism: 1024 # Maximum number of tasks executed in parallel in the flowgraph
  enableSegmentPrune: false # use partition stats to prune data in search/query on shard delegator
  queryStreamBatchSize: 4194304 # return min batch size of stream query
  queryStreamMaxBatchSize: 134217728 # return max batch size of stream query
  bloomFilterApplyParallelFactor: 4 # parallel factor when to apply pk to bloom filter, default to 4*CPU_CORE_NUM
  workerPooling:
    size: 10 # the size for worker querynode client pool
  idfOracle:
    enableDisk: true
    writeConcurrency: 4
  ip:  # TCP/IP address of queryNode. If not specified, use the first unicastable address
  port: 21123 # TCP port of queryNode
  grpc:
    serverMaxSendSize: 536870912 # The maximum size of each RPC request that the queryNode can send, unit: byte
    serverMaxRecvSize: 268435456 # The maximum size of each RPC request that the queryNode can receive, unit: byte
    clientMaxSendSize: 268435456 # The maximum size of each RPC request that the clients on queryNode can send, unit: byte
    clientMaxRecvSize: 536870912 # The maximum size of each RPC request that the clients on queryNode can receive, unit: byte

indexCoord:
  bindIndexNodeMode:
    enable: false
    address: localhost:22930
    withCred: false
    nodeID: 0
  segment:
    minSegmentNumRowsToEnableIndex: 1024 # It&apos;s a threshold. When the segment num rows is less than this value, the segment will not be indexed

indexNode:
  scheduler:
    buildParallel: 1
  ip:  # TCP/IP address of indexNode. If not specified, use the first unicastable address
  port: 21121 # TCP port of indexNode
  grpc:
    serverMaxSendSize: 536870912 # The maximum size of each RPC request that the indexNode can send, unit: byte
    serverMaxRecvSize: 268435456 # The maximum size of each RPC request that the indexNode can receive, unit: byte
    clientMaxSendSize: 268435456 # The maximum size of each RPC request that the clients on indexNode can send, unit: byte
    clientMaxRecvSize: 536870912 # The maximum size of each RPC request that the clients on indexNode can receive, unit: byte

dataCoord:
  channel:
    watchTimeoutInterval: 300 # Timeout on watching channels (in seconds). Datanode tickler update watch progress will reset timeout timer.
    legacyVersionWithoutRPCWatch: 2.4.1 # Datanodes &amp;lt;= this version are considered as legacy nodes, which doesn&apos;t have rpc based watch(). This is only used during rolling upgrade where legacy nodes won&apos;t get new channels
    balanceSilentDuration: 300 # The duration after which the channel manager start background channel balancing
    balanceInterval: 360 # The interval with which the channel manager check dml channel balance status
    checkInterval: 1 # The interval in seconds with which the channel manager advances channel states
    notifyChannelOperationTimeout: 5 # Timeout notifing channel operations (in seconds).
  segment:
    maxSize: 1024 # The maximum size of a segment, unit: MB. datacoord.segment.maxSize and datacoord.segment.sealProportion together determine if a segment can be sealed.
    diskSegmentMaxSize: 2048 # Maximum size of a segment in MB for collection which has Disk index
    sealProportion: 0.12 # The minimum proportion to datacoord.segment.maxSize to seal a segment. datacoord.segment.maxSize and datacoord.segment.sealProportion together determine if a segment can be sealed.
    sealProportionJitter: 0.1 # segment seal proportion jitter ratio, default value 0.1(10%), if seal proportion is 12%, with jitter=0.1, the actuall applied ratio will be 10.8~12%
    assignmentExpiration: 2000 # Expiration time of the segment assignment, unit: ms
    allocLatestExpireAttempt: 200 # The time attempting to alloc latest lastExpire from rootCoord after restart
    maxLife: 86400 # The max lifetime of segment in seconds, 24*60*60
    # If a segment didn&apos;t accept dml records in maxIdleTime and the size of segment is greater than
    # minSizeFromIdleToSealed, Milvus will automatically seal it.
    # The max idle time of segment in seconds, 10*60.
    maxIdleTime: 600
    minSizeFromIdleToSealed: 16 # The min size in MB of segment which can be idle from sealed.
    # The max number of binlog (which is equal to the binlog file num of primary key) for one segment,
    # the segment will be sealed if the number of binlog file reaches to max value.
    maxBinlogFileNumber: 32
    smallProportion: 0.5 # The segment is considered as &quot;small segment&quot; when its # of rows is smaller than
    # (smallProportion * segment max # of rows).
    # A compaction will happen on small segments if the segment after compaction will have
    compactableProportion: 0.85
    # over (compactableProportion * segment max # of rows) rows.
    # MUST BE GREATER THAN OR EQUAL TO &amp;lt;smallProportion&amp;gt;!!!
    # During compaction, the size of segment # of rows is able to exceed segment max # of rows by (expansionRate-1) * 100%.
    expansionRate: 1.25
  sealPolicy:
    channel:
      # The size threshold in MB, if the total size of growing segments of each shard
      # exceeds this threshold, the largest growing segment will be sealed.
      growingSegmentsMemSize: 4096
      # If the total entry number of l0 logs of each shard
      # exceeds this threshold, the earliest growing segments will be sealed.
      blockingL0EntryNum: 5000000
      # The size threshold in MB, if the total entry number of l0 logs of each shard
      # exceeds this threshold, the earliest growing segments will be sealed.
      blockingL0SizeInMB: 64
  autoUpgradeSegmentIndex: false # whether auto upgrade segment index to index engine&apos;s version
  forceRebuildSegmentIndex: false # force rebuild segment index to specify index engine&apos;s version
  # if param forceRebuildSegmentIndex is enabled, the vector index will be rebuilt to aligned with targetVecIndexVersion.
  # if param forceRebuildSegmentIndex is not enabled, the newly created vector index will be aligned with the newer one of index engine&apos;s version and targetVecIndexVersion.
  # if param targetVecIndexVersion is not set, the default value is -1, which means no target vec index version, then the vector index will be aligned with index engine&apos;s version
  targetVecIndexVersion: -1
  segmentFlushInterval: 2 # the minimal interval duration(unit: Seconds) between flushing operation on same segment
  # Switch value to control if to enable segment compaction.
  # Compaction merges small-size segments into a large segment, and clears the entities deleted beyond the rentention duration of Time Travel.
  enableCompaction: true
  compaction:
    # Switch value to control if to enable automatic segment compaction during which data coord locates and merges compactable segments in the background.
    # This configuration takes effect only when dataCoord.enableCompaction is set as true.
    enableAutoCompaction: true
    indexBasedCompaction: true
    # compaction task prioritizer, options: [default, level, mix].
    # default is FIFO.
    # level is prioritized by level: L0 compactions first, then mix compactions, then clustering compactions.
    # mix is prioritized by level: mix compactions first, then L0 compactions, then clustering compactions.
    taskPrioritizer: default
    taskQueueCapacity: 100000 # compaction task queue size
    rpcTimeout: 10
    maxParallelTaskNum: 10
    dropTolerance: 86400 # Compaction task will be cleaned after finish longer than this time(in seconds)
    gcInterval: 1800 # The time interval in seconds for compaction gc
    scheduleInterval: 500 # The time interval in milliseconds for scheduling compaction tasks. If the configuration setting is below 100ms, it will be adjusted upwards to 100ms
    mix:
      triggerInterval: 60 # The time interval in seconds to trigger mix compaction
    levelzero:
      triggerInterval: 10 # The time interval in seconds for trigger L0 compaction
      forceTrigger:
        minSize: 8388608 # The minimum size in bytes to force trigger a LevelZero Compaction, default as 8MB
        maxSize: 67108864 # The maxmum size in bytes to force trigger a LevelZero Compaction, default as 64MB
        deltalogMinNum: 10 # The minimum number of deltalog files to force trigger a LevelZero Compaction
        deltalogMaxNum: 30 # The maxmum number of deltalog files to force trigger a LevelZero Compaction, default as 30
    expiry:
      tolerance: -1 # tolerant duration in hours for expiry data, negative value means disable force expiry compaction
    single:
      ratio:
        threshold: 0.2 # The ratio threshold of a segment to trigger a single compaction, default as 0.2
      deltalog:
        maxsize: 16777216 # The deltalog size of a segment to trigger a single compaction, default as 16MB
        maxnum: 200 # The deltalog count of a segment to trigger a compaction, default as 200
      expiredlog:
        maxsize: 10485760 # The expired log size of a segment to trigger a compaction, default as 10MB
    clustering:
      enable: true # Enable clustering compaction
      autoEnable: false # Enable auto clustering compaction
      triggerInterval: 600 # clustering compaction trigger interval in seconds
      minInterval: 3600 # The minimum interval between clustering compaction executions of one collection, to avoid redundant compaction
      maxInterval: 259200 # If a collection haven&apos;t been clustering compacted for longer than maxInterval, force compact
      newDataSizeThreshold: 512m # If new data size is large than newDataSizeThreshold, execute clustering compaction
      preferSegmentSizeRatio: 0.8
      maxSegmentSizeRatio: 1
      maxTrainSizeRatio: 0.8 # max data size ratio in Kmeans train, if larger than it, will down sampling to meet this limit
      maxCentroidsNum: 10240 # maximum centroids number in Kmeans train
      minCentroidsNum: 16 # minimum centroids number in Kmeans train
      minClusterSizeRatio: 0.01 # minimum cluster size / avg size in Kmeans train
      maxClusterSizeRatio: 10 # maximum cluster size / avg size in Kmeans train
      maxClusterSize: 5g # maximum cluster size in Kmeans train
  syncSegmentsInterval: 300 # The time interval for regularly syncing segments
  index:
    memSizeEstimateMultiplier: 2 # When the memory size is not setup by index procedure, multiplier to estimate the memory size of index data
  enableGarbageCollection: true # Switch value to control if to enable garbage collection to clear the discarded data in MinIO or S3 service.
  gc:
    interval: 3600 # The interval at which data coord performs garbage collection, unit: second.
    missingTolerance: 86400 # The retention duration of the unrecorded binary log (binlog) files. Setting a reasonably large value for this parameter avoids erroneously deleting the newly created binlog files that lack metadata. Unit: second.
    dropTolerance: 10800 # The retention duration of the binlog files of the deleted segments before they are cleared, unit: second.
    removeConcurrent: 32 # number of concurrent goroutines to remove dropped s3 objects
    scanInterval: 168 # orphan file (file on oss but has not been registered on meta) on object storage garbage collection scanning interval in hours
  enableActiveStandby: false
  brokerTimeout: 5000 # 5000ms, dataCoord broker rpc timeout
  autoBalance: true # Enable auto balance
  checkAutoBalanceConfigInterval: 10 # the interval of check auto balance config
  import:
    filesPerPreImportTask: 2 # The maximum number of files allowed per pre-import task.
    taskRetention: 10800 # The retention period in seconds for tasks in the Completed or Failed state.
    maxSizeInMBPerImportTask: 6144 # To prevent generating of small segments, we will re-group imported files. This parameter represents the sum of file sizes in each group (each ImportTask).
    scheduleInterval: 2 # The interval for scheduling import, measured in seconds.
    checkIntervalHigh: 2 # The interval for checking import, measured in seconds, is set to a high frequency for the import checker.
    checkIntervalLow: 120 # The interval for checking import, measured in seconds, is set to a low frequency for the import checker.
    maxImportFileNumPerReq: 1024 # The maximum number of files allowed per single import request.
    maxImportJobNum: 1024 # Maximum number of import jobs that are executing or pending.
    waitForIndex: true # Indicates whether the import operation waits for the completion of index building.
  gracefulStopTimeout: 5 # seconds. force stop node without graceful stop
  slot:
    clusteringCompactionUsage: 16 # slot usage of clustering compaction job.
    mixCompactionUsage: 8 # slot usage of mix compaction job.
    l0DeleteCompactionUsage: 8 # slot usage of l0 compaction job.
    indexTaskSlotUsage: 64 # slot usage of index task per 512mb
    statsTaskSlotUsage: 8 # slot usage of stats task per 512mb
    analyzeTaskSlotUsage: 65535 # slot usage of analyze task
  jsonStatsTriggerCount: 10 # jsonkey stats task count per trigger
  jsonStatsTriggerInterval: 10 # jsonkey task interval per trigger
  enabledJSONKeyStatsInSort: false # Indicates whether to enable JSON key stats task with sort
  jsonKeyStatsMemoryBudgetInTantivy: 16777216 # the memory budget for the JSON index In Tantivy, the unit is bytes
  ip:  # TCP/IP address of dataCoord. If not specified, use the first unicastable address
  port: 13333 # TCP port of dataCoord
  grpc:
    serverMaxSendSize: 536870912 # The maximum size of each RPC request that the dataCoord can send, unit: byte
    serverMaxRecvSize: 268435456 # The maximum size of each RPC request that the dataCoord can receive, unit: byte
    clientMaxSendSize: 268435456 # The maximum size of each RPC request that the clients on dataCoord can send, unit: byte
    clientMaxRecvSize: 536870912 # The maximum size of each RPC request that the clients on dataCoord can receive, unit: byte

dataNode:
  dataSync:
    flowGraph:
      maxQueueLength: 16 # Maximum length of task queue in flowgraph
      maxParallelism: 1024 # Maximum number of tasks executed in parallel in the flowgraph
    maxParallelSyncMgrTasks: 256 # The max concurrent sync task number of datanode sync mgr globally
    skipMode:
      enable: true # Support skip some timetick message to reduce CPU usage
      skipNum: 4 # Consume one for every n records skipped
      coldTime: 60 # Turn on skip mode after there are only timetick msg for x seconds
  segment:
    # The maximum size of each binlog file in a segment buffered in memory. Binlog files whose size exceeds this value are then flushed to MinIO or S3 service.
    # Unit: Byte
    # Setting this parameter too small causes the system to store a small amount of data too frequently. Setting it too large increases the system&apos;s demand for memory.
    insertBufSize: 16777216
    deleteBufBytes: 16777216 # Max buffer size in bytes to flush del for a single channel, default as 16MB
    syncPeriod: 600 # The period to sync segments if buffer is not empty.
  memory:
    forceSyncEnable: true # Set true to force sync if memory usage is too high
    forceSyncSegmentNum: 1 # number of segments to sync, segments with top largest buffer will be synced.
    checkInterval: 3000 # the interal to check datanode memory usage, in milliseconds
    forceSyncWatermark: 0.5 # memory watermark for standalone, upon reaching this watermark, segments will be synced.
  timetick:
    interval: 500
  channel:
    # specify the size of global work pool of all channels
    # if this parameter &amp;lt;= 0, will set it as the maximum number of CPUs that can be executing
    # suggest to set it bigger on large collection numbers to avoid blocking
    workPoolSize: -1
    # specify the size of global work pool for channel checkpoint updating
    # if this parameter &amp;lt;= 0, will set it as 10
    updateChannelCheckpointMaxParallel: 10
    updateChannelCheckpointInterval: 60 # the interval duration(in seconds) for datanode to update channel checkpoint of each channel
    updateChannelCheckpointRPCTimeout: 20 # timeout in seconds for UpdateChannelCheckpoint RPC call
    maxChannelCheckpointsPerPRC: 128 # The maximum number of channel checkpoints per UpdateChannelCheckpoint RPC.
    channelCheckpointUpdateTickInSeconds: 10 # The frequency, in seconds, at which the channel checkpoint updater executes updates.
  import:
    concurrencyPerCPUCore: 4 # The execution concurrency unit for import/pre-import tasks per CPU core.
    maxImportFileSizeInGB: 16 # The maximum file size (in GB) for an import file, where an import file refers to either a Row-Based file or a set of Column-Based files.
    readBufferSizeInMB: 64 # The insert buffer size (in MB) during import.
    readDeleteBufferSizeInMB: 16 # The delete buffer size (in MB) during import.
  compaction:
    levelZeroBatchMemoryRatio: 0.5 # The minimal memory ratio of free memory for level zero compaction executing in batch mode
    levelZeroMaxBatchSize: -1 # Max batch size refers to the max number of L1/L2 segments in a batch when executing L0 compaction. Default to -1, any value that is less than 1 means no limit. Valid range: &amp;gt;= 1.
    useMergeSort: false # Whether to enable mergeSort mode when performing mixCompaction.
    maxSegmentMergeSort: 30 # The maximum number of segments to be merged in mergeSort mode.
  gracefulStopTimeout: 1800 # seconds. force stop node without graceful stop
  slot:
    slotCap: 16 # The maximum number of tasks(e.g. compaction, importing) allowed to run concurrently on a datanode
  clusteringCompaction:
    memoryBufferRatio: 0.3 # The ratio of memory buffer of clustering compaction. Data larger than threshold will be flushed to storage.
    workPoolSize: 8 # worker pool size for one clustering compaction job.
  bloomFilterApplyParallelFactor: 4 # parallel factor when to apply pk to bloom filter, default to 4*CPU_CORE_NUM
  storage:
    deltalog: json # deltalog format, options: [json, parquet]
  ip:  # TCP/IP address of dataNode. If not specified, use the first unicastable address
  port: 21124 # TCP port of dataNode
  grpc:
    serverMaxSendSize: 536870912 # The maximum size of each RPC request that the dataNode can send, unit: byte
    serverMaxRecvSize: 268435456 # The maximum size of each RPC request that the dataNode can receive, unit: byte
    clientMaxSendSize: 268435456 # The maximum size of each RPC request that the clients on dataNode can send, unit: byte
    clientMaxRecvSize: 536870912 # The maximum size of each RPC request that the clients on dataNode can receive, unit: byte

# This topic introduces the message channel-related configurations of Milvus.
msgChannel:
  chanNamePrefix:
    # Root name prefix of the channel when a message channel is created.
    # It is recommended to change this parameter before starting Milvus for the first time.
    # To share a Pulsar instance among multiple Milvus instances, consider changing this to a name rather than the default one for each Milvus instance before you start them.
    cluster: by-dev
    # Sub-name prefix of the message channel where the root coord publishes time tick messages.
    # The complete channel name prefix is ${msgChannel.chanNamePrefix.cluster}-${msgChannel.chanNamePrefix.rootCoordTimeTick}
    # Caution: Changing this parameter after using Milvus for a period of time will affect your access to old data.
    # It is recommended to change this parameter before starting Milvus for the first time.
    rootCoordTimeTick: rootcoord-timetick
    # Sub-name prefix of the message channel where the root coord publishes its own statistics messages.
    # The complete channel name prefix is ${msgChannel.chanNamePrefix.cluster}-${msgChannel.chanNamePrefix.rootCoordStatistics}
    # Caution: Changing this parameter after using Milvus for a period of time will affect your access to old data.
    # It is recommended to change this parameter before starting Milvus for the first time.
    rootCoordStatistics: rootcoord-statistics
    # Sub-name prefix of the message channel where the root coord publishes Data Manipulation Language (DML) messages.
    # The complete channel name prefix is ${msgChannel.chanNamePrefix.cluster}-${msgChannel.chanNamePrefix.rootCoordDml}
    # Caution: Changing this parameter after using Milvus for a period of time will affect your access to old data.
    # It is recommended to change this parameter before starting Milvus for the first time.
    rootCoordDml: rootcoord-dml
    replicateMsg: replicate-msg
    # Sub-name prefix of the message channel where the query node publishes time tick messages.
    # The complete channel name prefix is ${msgChannel.chanNamePrefix.cluster}-${msgChannel.chanNamePrefix.queryTimeTick}
    # Caution: Changing this parameter after using Milvus for a period of time will affect your access to old data.
    # It is recommended to change this parameter before starting Milvus for the first time.
    queryTimeTick: queryTimeTick
    # Sub-name prefix of the message channel where the data coord publishes time tick messages.
    # The complete channel name prefix is ${msgChannel.chanNamePrefix.cluster}-${msgChannel.chanNamePrefix.dataCoordTimeTick}
    # Caution: Changing this parameter after using Milvus for a period of time will affect your access to old data.
    # It is recommended to change this parameter before starting Milvus for the first time.
    dataCoordTimeTick: datacoord-timetick-channel
    # Sub-name prefix of the message channel where the data coord publishes segment information messages.
    # The complete channel name prefix is ${msgChannel.chanNamePrefix.cluster}-${msgChannel.chanNamePrefix.dataCoordSegmentInfo}
    # Caution: Changing this parameter after using Milvus for a period of time will affect your access to old data.
    # It is recommended to change this parameter before starting Milvus for the first time.
    dataCoordSegmentInfo: segment-info-channel
  subNamePrefix:
    # Subscription name prefix of the data coord.
    # Caution: Changing this parameter after using Milvus for a period of time will affect your access to old data.
    # It is recommended to change this parameter before starting Milvus for the first time.
    dataCoordSubNamePrefix: dataCoord
    # Subscription name prefix of the data node.
    # Caution: Changing this parameter after using Milvus for a period of time will affect your access to old data.
    # It is recommended to change this parameter before starting Milvus for the first time.
    dataNodeSubNamePrefix: dataNode

# Configures the system log output.
log:
  # Milvus log level. Option: debug, info, warn, error, panic, and fatal.
  # It is recommended to use debug level under test and development environments, and info level in production environment.
  level: info
  file:
    # Root path to the log files.
    # The default value is set empty, indicating to output log files to standard output (stdout) and standard error (stderr).
    # If this parameter is set to a valid local path, Milvus writes and stores log files in this path.
    # Set this parameter as the path that you have permission to write.
    rootPath:
    maxSize: 300 # The maximum size of a log file, unit: MB.
    maxAge: 10 # The maximum retention time before a log file is automatically cleared, unit: day. The minimum value is 1.
    maxBackups: 20 # The maximum number of log files to back up, unit: day. The minimum value is 1.
  format: text # Milvus log format. Option: text and JSON
  stdout: true # Stdout enable or not

grpc:
  log:
    level: WARNING
  gracefulStopTimeout: 3 # second, time to wait graceful stop finish
  client:
    compressionEnabled: false
    dialTimeout: 200
    keepAliveTime: 10000
    keepAliveTimeout: 20000
    maxMaxAttempts: 10
    initialBackoff: 0.2
    maxBackoff: 10
    backoffMultiplier: 2
    minResetInterval: 1000
    maxCancelError: 32
    minSessionCheckInterval: 200

# Configure external tls.
tls:
  serverPemPath: configs/cert/server.pem
  serverKeyPath: configs/cert/server.key
  caPemPath: configs/cert/ca.pem

# Configure internal tls.
internaltls:
  serverPemPath: configs/cert/server.pem
  serverKeyPath: configs/cert/server.key
  caPemPath: configs/cert/ca.pem
  sni: localhost # The server name indication (SNI) for internal TLS, should be the same as the name provided by the certificates ref: https://en.wikipedia.org/wiki/Server_Name_Indication

common:
  defaultPartitionName: _default # Name of the default partition when a collection is created
  defaultIndexName: _default_idx # Name of the index when it is created with name unspecified
  entityExpiration: -1 # Entity expiration in seconds, CAUTION -1 means never expire
  indexSliceSize: 16 # Index slice size in MB
  threadCoreCoefficient:
    highPriority: 10 # This parameter specify how many times the number of threads is the number of cores in high priority pool
    middlePriority: 5 # This parameter specify how many times the number of threads is the number of cores in middle priority pool
    lowPriority: 1 # This parameter specify how many times the number of threads is the number of cores in low priority pool
    chunkCache: 10 # This parameter specify how many times the number of threads is the number of cores in chunk cache pool
  buildIndexThreadPoolRatio: 0.75
  DiskIndex:
    MaxDegree: 56
    SearchListSize: 100
    PQCodeBudgetGBRatio: 0.125
    BuildNumThreadsRatio: 1
    SearchCacheBudgetGBRatio: 0.1
    LoadNumThreadRatio: 8
    BeamWidthRatio: 4
  gracefulTime: 5000 # milliseconds. it represents the interval (in ms) by which the request arrival time needs to be subtracted in the case of Bounded Consistency.
  gracefulStopTimeout: 1800 # seconds. it will force quit the server if the graceful stop process is not completed during this time.
  storageType: remote # please adjust in embedded Milvus: local, available values are [local, remote, opendal], value minio is deprecated, use remote instead
  # Default value: auto
  # Valid values: [auto, avx512, avx2, avx, sse4_2]
  # This configuration is only used by querynode and indexnode, it selects CPU instruction set for Searching and Index-building.
  simdType: auto
  # This parameter controls the write mode of the local disk, which is used to write temporary data downloaded from remote storage.
  # Currently, only QueryNode uses &apos;common.diskWrite*&apos; parameters. Support for other components will be added in the future.
  # The options include &apos;direct&apos; and &apos;buffered&apos;. The default value is &apos;buffered&apos;.
  diskWriteMode: buffered
  # Disk write buffer size in KB, only used when disk write mode is &apos;direct&apos;, default is 64KB.
  # Current valid range is [4, 65536]. If the value is not aligned to 4KB, it will be rounded up to the nearest multiple of 4KB.
  diskWriteBufferSizeKb: 64
  # This parameter controls the number of writer threads used for disk write operations. The valid range is [0, hardware_concurrency].
  # It is designed to limit the maximum concurrency of disk write operations to reduce the impact on disk read performance.
  # For example, if you want to limit the maximum concurrency of disk write operations to 1, you can set this parameter to 1.
  # The default value is 0, which means the caller will perform write operations directly without using an additional writer thread pool.
  # In this case, the maximum concurrency of disk write operations is determined by the caller&apos;s thread pool size.
  diskWriteNumThreads: 0
  diskWriteRateLimiter:
    refillPeriodUs: 100000 # refill period in microseconds if disk rate limiter is enabled, default is 100000us (100ms)
    avgKBps: 262144 # average kilobytes per second if disk rate limiter is enabled, default is 262144KB/s (256MB/s)
    maxBurstKBps: 524288 # max burst kilobytes per second if disk rate limiter is enabled, default is 524288KB/s (512MB/s)
    # amplification ratio for high priority tasks if disk rate limiter is enabled, value &amp;lt;= 0 means ratio limit is disabled.
    # The ratio is the multiplication factor of the configured bandwidth.
    # For example, if the rate limit is 100KB/s, and the high priority ratio is 2, then the high priority tasks will be limited to 200KB/s.
    highPriorityRatio: -1
    middlePriorityRatio: -1 # amplification ratio for middle priority tasks if disk rate limiter is enabled, value &amp;lt;= 0 means ratio limit is disabled
    lowPriorityRatio: -1 # amplification ratio for low priority tasks if disk rate limiter is enabled, value &amp;lt;= 0 means ratio limit is disabled
  security:
    authorizationEnabled: true
    # The superusers will ignore some system check processes,
    # like the old password verification when updating the credential

    superUsers: root
    # default password for root user. The maximum length is 72 characters.
    # Large numeric passwords require double quotes to avoid yaml parsing precision issues.

    defaultRootPassword: &quot;xxxxxx&quot;
    rootShouldBindRole: false # Whether the root user should bind a role when the authorization is enabled.
    enablePublicPrivilege: true # Whether to enable public privilege
    rbac:
      overrideBuiltInPrivilegeGroups:
        enabled: false # Whether to override build-in privilege groups
      cluster:
        readonly:
          privileges: ListDatabases,SelectOwnership,SelectUser,DescribeResourceGroup,ListResourceGroups,ListPrivilegeGroups # Cluster level readonly privileges
        readwrite:
          privileges: ListDatabases,SelectOwnership,SelectUser,DescribeResourceGroup,ListResourceGroups,ListPrivilegeGroups,FlushAll,TransferNode,TransferReplica,UpdateResourceGroups # Cluster level readwrite privileges
        admin:
          privileges: ListDatabases,SelectOwnership,SelectUser,DescribeResourceGroup,ListResourceGroups,ListPrivilegeGroups,FlushAll,TransferNode,TransferReplica,UpdateResourceGroups,BackupRBAC,RestoreRBAC,CreateDatabase,DropDatabase,CreateOwnership,DropOwnership,ManageOwnership,CreateResourceGroup,DropResourceGroup,UpdateUser,RenameCollection,CreatePrivilegeGroup,DropPrivilegeGroup,OperatePrivilegeGroup # Cluster level admin privileges
      database:
        readonly:
          privileges: ShowCollections,DescribeDatabase # Database level readonly privileges
        readwrite:
          privileges: ShowCollections,DescribeDatabase,AlterDatabase # Database level readwrite privileges
        admin:
          privileges: ShowCollections,DescribeDatabase,AlterDatabase,CreateCollection,DropCollection # Database level admin privileges
      collection:
        readonly:
          privileges: Query,Search,IndexDetail,GetFlushState,GetLoadState,GetLoadingProgress,HasPartition,ShowPartitions,DescribeCollection,DescribeAlias,GetStatistics,ListAliases # Collection level readonly privileges
        readwrite:
          privileges: Query,Search,IndexDetail,GetFlushState,GetLoadState,GetLoadingProgress,HasPartition,ShowPartitions,DescribeCollection,DescribeAlias,GetStatistics,ListAliases,Load,Release,Insert,Delete,Upsert,Import,Flush,Compaction,LoadBalance,CreateIndex,DropIndex,CreatePartition,DropPartition # Collection level readwrite privileges
        admin:
          privileges: Query,Search,IndexDetail,GetFlushState,GetLoadState,GetLoadingProgress,HasPartition,ShowPartitions,DescribeCollection,DescribeAlias,GetStatistics,ListAliases,Load,Release,Insert,Delete,Upsert,Import,Flush,Compaction,LoadBalance,CreateIndex,DropIndex,CreatePartition,DropPartition,CreateAlias,DropAlias # Collection level admin privileges
    internaltlsEnabled: false
    tlsMode: 0
  session:
    ttl: 30 # ttl value when session granting a lease to register service
    retryTimes: 30 # retry times when session sending etcd requests
  locks:
    metrics:
      enable: false # whether gather statistics for metrics locks
    threshold:
      info: 500 # minimum milliseconds for printing durations in info level
      warn: 1000 # minimum milliseconds for printing durations in warn level
    maxWLockConditionalWaitTime: 600 # maximum seconds for waiting wlock conditional
  storage:
    scheme: s3
    enablev2: false
  # Whether to disable the internal time messaging mechanism for the system.
  # If disabled (set to false), the system will not allow DML operations, including insertion, deletion, queries, and searches.
  # This helps Milvus-CDC synchronize incremental data
  ttMsgEnabled: true
  traceLogMode: 0 # trace request info
  bloomFilterSize: 100000 # bloom filter initial size
  bloomFilterType: BlockedBloomFilter # bloom filter type, support BasicBloomFilter and BlockedBloomFilter
  maxBloomFalsePositive: 0.001 # max false positive rate for bloom filter
  bloomFilterApplyBatchSize: 1000 # batch size when to apply pk to bloom filter
  collectionReplicateEnable: false # Whether to enable collection replication.
  usePartitionKeyAsClusteringKey: false # if true, do clustering compaction and segment prune on partition key field
  useVectorAsClusteringKey: false # if true, do clustering compaction and segment prune on vector field
  enableVectorClusteringKey: false # if true, enable vector clustering key and vector clustering compaction
  localRPCEnabled: false # enable local rpc for internal communication when mix or standalone mode.
  sync:
    taskPoolReleaseTimeoutSeconds: 60 # The maximum time to wait for the task to finish and release resources in the pool
  enabledOptimizeExpr: true # Indicates whether to enable optimize expr
  enabledJSONKeyStats: false # Indicates sealedsegment whether to enable JSON key stats
  enabledGrowingSegmentJSONKeyStats: false # Indicates growingsegment whether to enable JSON key stats
  enableConfigParamTypeCheck: true # Indicates whether to enable config param type check
  clusterID: 0 # cluster id

# QuotaConfig, configurations of Milvus quota and limits.
# By default, we enable:
#   1. TT protection;
#   2. Memory protection.
#   3. Disk quota protection.
# You can enable:
#   1. DML throughput limitation;
#   2. DDL, DQL qps/rps limitation;
#   3. DQL Queue length/latency protection;
#   4. DQL result rate protection;
# If necessary, you can also manually force to deny RW requests.
quotaAndLimits:
  enabled: true # `true` to enable quota and limits, `false` to disable.
  # quotaCenterCollectInterval is the time interval that quotaCenter
  # collects metrics from Proxies, Query cluster and Data cluster.
  # seconds, (0 ~ 65536)
  quotaCenterCollectInterval: 3
  forceDenyAllDDL: false # true to force deny all DDL requests, false to allow.
  limits:
    allocRetryTimes: 15 # retry times when delete alloc forward data from rate limit failed
    allocWaitInterval: 1000 # retry wait duration when delete alloc forward data rate failed, in millisecond
    complexDeleteLimitEnable: false # whether complex delete check forward data by limiter
    maxCollectionNum: 65536
    maxCollectionNumPerDB: 65536 # Maximum number of collections per database.
    maxInsertSize: -1 # maximum size of a single insert request, in bytes, -1 means no limit
    maxResourceGroupNumOfQueryNode: 1024 # maximum number of resource groups of query nodes
    maxGroupSize: 10 # maximum size for one single group when doing search group by
  ddl:
    enabled: false # Whether DDL request throttling is enabled.
    # Maximum number of collection-related DDL requests per second.
    # Setting this item to 10 indicates that Milvus processes no more than 10 collection-related DDL requests per second, including collection creation requests, collection drop requests, collection load requests, and collection release requests.
    # To use this setting, set quotaAndLimits.ddl.enabled to true at the same time.
    collectionRate: -1
    # Maximum number of partition-related DDL requests per second.
    # Setting this item to 10 indicates that Milvus processes no more than 10 partition-related requests per second, including partition creation requests, partition drop requests, partition load requests, and partition release requests.
    # To use this setting, set quotaAndLimits.ddl.enabled to true at the same time.
    partitionRate: -1
    db:
      collectionRate: -1 # qps of db level , default no limit, rate for CreateCollection, DropCollection, LoadCollection, ReleaseCollection
      partitionRate: -1 # qps of db level, default no limit, rate for CreatePartition, DropPartition, LoadPartition, ReleasePartition
  indexRate:
    enabled: false # Whether index-related request throttling is enabled.
    # Maximum number of index-related requests per second.
    # Setting this item to 10 indicates that Milvus processes no more than 10 partition-related requests per second, including index creation requests and index drop requests.
    # To use this setting, set quotaAndLimits.indexRate.enabled to true at the same time.
    max: -1
    db:
      max: -1 # qps of db level, default no limit, rate for CreateIndex, DropIndex
  flushRate:
    enabled: true # Whether flush request throttling is enabled.
    # Maximum number of flush requests per second.
    # Setting this item to 10 indicates that Milvus processes no more than 10 flush requests per second.
    # To use this setting, set quotaAndLimits.flushRate.enabled to true at the same time.
    max: -1
    collection:
      max: 0.1 # qps, default no limit, rate for flush at collection level.
    db:
      max: -1 # qps of db level, default no limit, rate for flush
  compactionRate:
    enabled: false # Whether manual compaction request throttling is enabled.
    # Maximum number of manual-compaction requests per second.
    # Setting this item to 10 indicates that Milvus processes no more than 10 manual-compaction requests per second.
    # To use this setting, set quotaAndLimits.compaction.enabled to true at the same time.
    max: -1
    db:
      max: -1 # qps of db level, default no limit, rate for manualCompaction
  dbRate:
    enabled: false # Whether DB request throttling is enabled
    # Maximum number of db-related requests per second.
    # Setting this item to 10 indicates that Milvus processes no more than 10 db-related requests per second, including db creation/drop/alter requests.
    # To use this setting, set quotaAndLimits.dbRate.enabled to true at the same time.
    #
    max: -1
  dml:
    enabled: false # Whether DML request throttling is enabled.
    insertRate:
      # Highest data insertion rate per second.
      # Setting this item to 5 indicates that Milvus only allows data insertion at the rate of 5 MB/s.
      # To use this setting, set quotaAndLimits.dml.enabled to true at the same time.
      max: -1
      db:
        max: -1 # MB/s, default no limit
      collection:
        # Highest data insertion rate per collection per second.
        # Setting this item to 5 indicates that Milvus only allows data insertion to any collection at the rate of 5 MB/s.
        # To use this setting, set quotaAndLimits.dml.enabled to true at the same time.
        max: -1
      partition:
        max: -1 # MB/s, default no limit
    upsertRate:
      max: -1 # MB/s, default no limit
      db:
        max: -1 # MB/s, default no limit
      collection:
        max: -1 # MB/s, default no limit
      partition:
        max: -1 # MB/s, default no limit
    deleteRate:
      # Highest data deletion rate per second.
      # Setting this item to 0.1 indicates that Milvus only allows data deletion at the rate of 0.1 MB/s.
      # To use this setting, set quotaAndLimits.dml.enabled to true at the same time.
      max: -1
      db:
        max: -1 # MB/s, default no limit
      collection:
        # Highest data deletion rate per second.
        # Setting this item to 0.1 indicates that Milvus only allows data deletion from any collection at the rate of 0.1 MB/s.
        # To use this setting, set quotaAndLimits.dml.enabled to true at the same time.
        max: -1
      partition:
        max: -1 # MB/s, default no limit
    bulkLoadRate:
      max: -1 # MB/s, default no limit, not support yet. TODO: limit bulkLoad rate
      db:
        max: -1 # MB/s, default no limit, not support yet. TODO: limit db bulkLoad rate
      collection:
        max: -1 # MB/s, default no limit, not support yet. TODO: limit collection bulkLoad rate
      partition:
        max: -1 # MB/s, default no limit, not support yet. TODO: limit partition bulkLoad rate
  dql:
    enabled: false # Whether DQL request throttling is enabled.
    searchRate:
      # Maximum number of vectors to search per second.
      # Setting this item to 100 indicates that Milvus only allows searching 100 vectors per second no matter whether these 100 vectors are all in one search or scattered across multiple searches.
      # To use this setting, set quotaAndLimits.dql.enabled to true at the same time.
      max: -1
      db:
        max: -1 # vps (vectors per second), default no limit
      collection:
        # Maximum number of vectors to search per collection per second.
        # Setting this item to 100 indicates that Milvus only allows searching 100 vectors per second per collection no matter whether these 100 vectors are all in one search or scattered across multiple searches.
        # To use this setting, set quotaAndLimits.dql.enabled to true at the same time.
        max: -1
      partition:
        max: -1 # vps (vectors per second), default no limit
    queryRate:
      # Maximum number of queries per second.
      # Setting this item to 100 indicates that Milvus only allows 100 queries per second.
      # To use this setting, set quotaAndLimits.dql.enabled to true at the same time.
      max: -1
      db:
        max: -1 # qps, default no limit
      collection:
        # Maximum number of queries per collection per second.
        # Setting this item to 100 indicates that Milvus only allows 100 queries per collection per second.
        # To use this setting, set quotaAndLimits.dql.enabled to true at the same time.
        max: -1
      partition:
        max: -1 # qps, default no limit
  limitWriting:
    # forceDeny false means dml requests are allowed (except for some
    # specific conditions, such as memory of nodes to water marker), true means always reject all dml requests.
    forceDeny: false
    ttProtection:
      enabled: false
      # maxTimeTickDelay indicates the backpressure for DML Operations.
      # DML rates would be reduced according to the ratio of time tick delay to maxTimeTickDelay,
      # if time tick delay is greater than maxTimeTickDelay, all DML requests would be rejected.
      # seconds
      maxTimeTickDelay: 300
    memProtection:
      # When memory usage &amp;gt; memoryHighWaterLevel, all dml requests would be rejected;
      # When memoryLowWaterLevel &amp;lt; memory usage &amp;lt; memoryHighWaterLevel, reduce the dml rate;
      # When memory usage &amp;lt; memoryLowWaterLevel, no action.
      enabled: true
      dataNodeMemoryLowWaterLevel: 0.85 # (0, 1], memoryLowWaterLevel in DataNodes
      dataNodeMemoryHighWaterLevel: 0.95 # (0, 1], memoryHighWaterLevel in DataNodes
      queryNodeMemoryLowWaterLevel: 0.85 # (0, 1], memoryLowWaterLevel in QueryNodes
      queryNodeMemoryHighWaterLevel: 0.95 # (0, 1], memoryHighWaterLevel in QueryNodes
    growingSegmentsSizeProtection:
      # No action will be taken if the growing segments size is less than the low watermark.
      # When the growing segments size exceeds the low watermark, the dml rate will be reduced,
      # but the rate will not be lower than minRateRatio * dmlRate.
      enabled: false
      minRateRatio: 0.5
      lowWaterLevel: 0.2
      highWaterLevel: 0.4
    diskProtection:
      enabled: true # When the total file size of object storage is greater than `diskQuota`, all dml requests would be rejected;
      diskQuota: -1 # MB, (0, +inf), default no limit
      diskQuotaPerDB: -1 # MB, (0, +inf), default no limit
      diskQuotaPerCollection: -1 # MB, (0, +inf), default no limit
      diskQuotaPerPartition: -1 # MB, (0, +inf), default no limit
    l0SegmentsRowCountProtection:
      enabled: false # switch to enable l0 segment row count quota
      lowWaterLevel: 30000000 # l0 segment row count quota, low water level
      highWaterLevel: 50000000 # l0 segment row count quota, high water level
    deleteBufferRowCountProtection:
      enabled: false # switch to enable delete buffer row count quota
      lowWaterLevel: 32768 # delete buffer row count quota, low water level
      highWaterLevel: 65536 # delete buffer row count quota, high water level
    deleteBufferSizeProtection:
      enabled: false # switch to enable delete buffer size quota
      lowWaterLevel: 134217728 # delete buffer size quota, low water level
      highWaterLevel: 268435456 # delete buffer size quota, high water level
  limitReading:
    # forceDeny false means dql requests are allowed (except for some
    # specific conditions, such as collection has been dropped), true means always reject all dql requests.
    forceDeny: false

trace:
  # trace exporter type, default is stdout,
  # optional values: [&apos;noop&apos;,&apos;stdout&apos;, &apos;jaeger&apos;, &apos;otlp&apos;]
  exporter: noop
  # fraction of traceID based sampler,
  # optional values: [0, 1]
  # Fractions &amp;gt;= 1 will always sample. Fractions &amp;lt; 0 are treated as zero.
  sampleFraction: 0
  jaeger:
    url:  # when exporter is jaeger should set the jaeger&apos;s URL
  otlp:
    endpoint:  # example: &quot;127.0.0.1:4317&quot; for grpc, &quot;127.0.0.1:4318&quot; for http
    method:  # otlp export method, acceptable values: [&quot;grpc&quot;, &quot;http&quot;],  using &quot;grpc&quot; by default
    secure: true
  initTimeoutSeconds: 10 # segcore initialization timeout in seconds, preventing otlp grpc hangs forever

#when using GPU indexing, Milvus will utilize a memory pool to avoid frequent memory allocation and deallocation.
#here, you can set the size of the memory occupied by the memory pool, with the unit being MB.
#note that there is a possibility of Milvus crashing when the actual memory demand exceeds the value set by maxMemSize.
#if initMemSize and MaxMemSize both set zero,
#milvus will automatically initialize half of the available GPU memory,
#maxMemSize will the whole available GPU memory.
gpu:
  initMemSize: 2048 # Gpu Memory Pool init size
  maxMemSize: 4096 # Gpu Memory Pool Max size

# Any configuration related to the streaming node server.
streamingNode:
  ip:  # TCP/IP address of streamingNode. If not specified, use the first unicastable address
  port: 22222 # TCP port of streamingNode
  grpc:
    serverMaxSendSize: 268435456 # The maximum size of each RPC request that the streamingNode can send, unit: byte
    serverMaxRecvSize: 268435456 # The maximum size of each RPC request that the streamingNode can receive, unit: byte
    clientMaxSendSize: 268435456 # The maximum size of each RPC request that the clients on streamingNode can send, unit: byte
    clientMaxRecvSize: 268435456 # The maximum size of each RPC request that the clients on streamingNode can receive, unit: byte

# Any configuration related to the streaming service.
streaming:
  walBalancer:
    # The interval of balance task trigger at background, 1 min by default.
    # It&apos;s ok to set it into duration string, such as 30s or 1m30s, see time.ParseDuration
    triggerInterval: 1m
    # The initial interval of balance task trigger backoff, 50 ms by default.
    # It&apos;s ok to set it into duration string, such as 30s or 1m30s, see time.ParseDuration
    backoffInitialInterval: 50ms
    backoffMultiplier: 2 # The multiplier of balance task trigger backoff, 2 by default
  walBroadcaster:
    concurrencyRatio: 1 # The concurrency ratio based on number of CPU for wal broadcaster, 1 by default.
  txn:
    defaultKeepaliveTimeout: 10s # The default keepalive timeout for wal txn, 10s by default

# Any configuration related to the knowhere vector search engine
knowhere:
  enable: true # When enable this configuration, the index parameters defined following will be automatically populated as index parameters, without requiring user input.
  DISKANN:
    build:
      max_degree: 56 # Maximum degree of the Vamana graph
      pq_code_budget_gb_ratio: 0.125 # Size limit on the PQ code (compared with raw data)
      search_cache_budget_gb_ratio: 0.1 # Ratio of cached node numbers to raw data
      search_list_size: 100 # Size of the candidate list during building graph
    search:
      beam_width_ratio: 4 # Ratio between the maximum number of IO requests per search iteration and CPU number
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;编辑&lt;code&gt;docker-compose.yaml&lt;/code&gt;&lt;pre&gt;&lt;code&gt;services:
  etcd:
    container_name: milvus-etcd
    image: quay.io/coreos/etcd:v3.5.18
    environment:
      - ETCD_AUTO_COMPACTION_MODE=revision
      - ETCD_AUTO_COMPACTION_RETENTION=1000
      - ETCD_QUOTA_BACKEND_BYTES=4294967296
      - ETCD_SNAPSHOT_COUNT=50000
    volumes:
      - ./etcd:/etcd
    command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
    healthcheck:
      test: [&quot;CMD&quot;, &quot;etcdctl&quot;, &quot;endpoint&quot;, &quot;health&quot;]
      interval: 30s
      timeout: 20s
      retries: 3


  standalone:
    container_name: milvus-standalone
    image: milvusdb/milvus:v2.5.19
    command: [&quot;milvus&quot;, &quot;run&quot;, &quot;standalone&quot;]
    security_opt:
    - seccomp:unconfined
    environment:
      - ETCD_ENDPOINTS=etcd:2379
      - TIMEZONE=Asia/Shanghai
    volumes:
      - ./milvus.yaml:/milvus/configs/milvus.yaml
      - ./milvus:/var/lib/milvus
    healthcheck:
      test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:9091/healthz&quot;]
      interval: 30s
      start_period: 90s
      timeout: 20s
      retries: 3
    ports:
      - &quot;65011:19530&quot; # gRPC
      - &quot;65012:9091&quot; # HTTP管理面板（不知道是不是有bug，设置了用户密码依旧能直接访问，有知道咋解决的大佬请评论下咋弄））
    depends_on:
      - &quot;etcd&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;启动Milvus&lt;pre&gt;&lt;code&gt;docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;配置Nginx Proxy Manager反代gRPC协议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;如图配置：
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/10/27/68ff7bfb98be1.png&quot; alt=&quot;CleanShot 2025-10-27 at 21.56.11@2x&quot; /&gt;&lt;pre&gt;&lt;code&gt;# 第一行可以删除，主要是用于在用cf cdn后让npm获取真实ip的
# 如果需要的话，请参考https://blog.useforall.com/posts/nginx-proxy-manager-get-real-client-ip-a-unified-solution进行配置
include /data/nginx/custom/cloudflare_ips.conf;
underscores_in_headers on;
location / {
    # 验证是否为 gRPC 请求 (可选但推荐)
    if ($content_type !~ &quot;application/grpc&quot;) {
        return 404;
    }

    # 超时和保活设置
    grpc_read_timeout 300s;
    grpc_send_timeout 300s;
    grpc_socket_keepalive on;

    grpc_pass grpc://172.17.0.1:65011;
}
access_log off;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;去Cloudflare打开gRPC开关
&lt;img src=&quot;https://lsky.useforall.com/other/2025/10/27/68ff7c2d3f6ec.png&quot; alt=&quot;CleanShot 2025-10-27 at 22.05.10@2x&quot; /&gt;
:::tip{title=&quot;注意&quot;}
有些人这个地方可能会是&lt;code&gt;Join Beta&lt;/code&gt;，点击加入即可。我有些域名直接可以开，有些显示要加入Beta。具体看个人情况。
加入Beta后会显示：&lt;pre&gt;&lt;code&gt;Thanks for your interest! You will be able to enable gRPC support once you have been admitted to the beta.
&lt;/code&gt;&lt;/pre&gt;
具体要多久也不清楚，可能几分钟，也可能几小时不等。
:::&lt;/li&gt;
&lt;li&gt;最后请参照&lt;a href=&quot;https://milvus.io/docs/connect-to-milvus-server.md&quot;&gt;官方教程&lt;/a&gt;连接Milvus数据库&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Docker部署n8n并破解所有Enterprise功能</title><link>https://blog.useforall.com/posts/21/</link><guid isPermaLink="true">https://blog.useforall.com/posts/21/</guid><description>修改代码强制开启n8n的Enterprise功能</description><pubDate>Thu, 04 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要信息&quot;}
修改代码强制开启n8n的Enterprise功能
:::&lt;/p&gt;
&lt;p&gt;由于n8n都是开源的，因此修改代码来获取所有Enterprise License才有的功能也比较容易。最开始打算通过自建授权服务器来着，但是license检验的库是闭源的，不是很好搞。截止本文时间，n8n目前最新commit为&lt;code&gt;4313aa315f623f5d88381cdc98aacff6a25dc9ba&lt;/code&gt;，代码修改也是基于此版本，后续如有变动，参照修改即可。&lt;/p&gt;
&lt;p&gt;:::warning{title=&quot;注意&quot;}
对于开源软件，本人不支持、不推荐使用开心版和破解版。商业化对于开源软件是必要，对开源生态是必要的，对开源软件的良好发展是必要的（LskyPro+这种代码写的稀烂、文档写的稀烂、商业化后就转为闭源的软件除外）
:::&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Docker&lt;/strong&gt;：用来打包n8n镜像。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;n8n源码&lt;/strong&gt;：可以从这里&lt;a href=&quot;https://github.com/n8n-io/n8n&quot;&gt;github&lt;/a&gt;获取。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;修改代码&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;git clone --depth 1 https://github.com/n8n-io/n8n &amp;amp;&amp;amp; cd n8n&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改&lt;code&gt;license.ts&lt;/code&gt;文件，点击按钮查看diff&lt;pre&gt;&lt;code&gt;diff --git a/packages/cli/src/license.ts b/packages/cli/src/license.ts
index 6441f74003..82a30bfbe5 100644
--- a/packages/cli/src/license.ts
+++ b/packages/cli/src/license.ts
@@ -218,7 +218,11 @@ export class License implements LicenseProvider {
        }

        isLicensed(feature: BooleanLicenseFeature) {
-		return this.manager?.hasFeatureEnabled(feature) ?? false;
+		// 🚀 BYPASS: Always return true for all features except showNonProdBanner
+		if (feature === LICENSE_FEATURES.SHOW_NON_PROD_BANNER) {
+			return false;
+		}
+		return true;
        }

        /** @deprecated Use `LicenseState.isSharingLicensed` instead. */
@@ -346,7 +350,18 @@ export class License implements LicenseProvider {
        }

        getValue&amp;lt;T extends keyof FeatureReturnType&amp;gt;(feature: T): FeatureReturnType[T] {
-		return this.manager?.getFeatureValue(feature) as FeatureReturnType[T];
+		// 🚀 BYPASS: Return Enterprise values
+		if (feature === &apos;planName&apos;) {
+			return &apos;Enterprise&apos; as FeatureReturnType[T];
+		}
+
+		// Return unlimited for all quotas
+		if (Object.values(LICENSE_QUOTAS).includes(feature as any)) {
+			return UNLIMITED_LICENSE_QUOTA as FeatureReturnType[T];
+		}
+
+		// Return true for all boolean features
+		return true as FeatureReturnType[T];
        }

        getManagementJwt(): string {
@@ -384,36 +399,43 @@ export class License implements LicenseProvider {

        /** @deprecated Use `LicenseState` instead. */
        getUsersLimit() {
-		return this.getValue(LICENSE_QUOTAS.USERS_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
+		// 🚀 BYPASS: Unlimited users
+		return UNLIMITED_LICENSE_QUOTA;
        }

        /** @deprecated Use `LicenseState` instead. */
        getTriggerLimit() {
-		return this.getValue(LICENSE_QUOTAS.TRIGGER_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
+		// 🚀 BYPASS: Unlimited triggers
+		return UNLIMITED_LICENSE_QUOTA;
        }

        /** @deprecated Use `LicenseState` instead. */
        getVariablesLimit() {
-		return this.getValue(LICENSE_QUOTAS.VARIABLES_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
+		// 🚀 BYPASS: Unlimited variables
+		return UNLIMITED_LICENSE_QUOTA;
        }

        /** @deprecated Use `LicenseState` instead. */
        getAiCredits() {
-		return this.getValue(LICENSE_QUOTAS.AI_CREDITS) ?? 0;
+		// 🚀 BYPASS: Unlimited AI credits
+		return UNLIMITED_LICENSE_QUOTA;
        }

        /** @deprecated Use `LicenseState` instead. */
        getWorkflowHistoryPruneLimit() {
-		return this.getValue(LICENSE_QUOTAS.WORKFLOW_HISTORY_PRUNE_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
+		// 🚀 BYPASS: Unlimited workflow history
+		return UNLIMITED_LICENSE_QUOTA;
        }

        /** @deprecated Use `LicenseState` instead. */
        getTeamProjectLimit() {
-		return this.getValue(LICENSE_QUOTAS.TEAM_PROJECT_LIMIT) ?? 0;
+		// 🚀 BYPASS: Unlimited team projects
+		return UNLIMITED_LICENSE_QUOTA;
        }

        getPlanName(): string {
-		return this.getValue(&apos;planName&apos;) ?? &apos;Community&apos;;
+		// 🚀 BYPASS: Always return Enterprise
+		return &apos;Enterprise&apos;;
        }

        getInfo(): string {
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;修改完成后，利用&lt;code&gt;DND&lt;/code&gt;进行打包。先build一个用于打包的镜像&lt;pre&gt;&lt;code&gt;FROM node:22.16-bookworm

# 安装 Docker CLI 所需的依赖
# RUN 是在构建镜像时执行的命令
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends \
    ca-certificates \
    curl \
    gnupg \
    git \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

# 添加 Docker 官方的 GPG 密钥
RUN install -m 0755 -d /etc/apt/keyrings
RUN curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
RUN chmod a+r /etc/apt/keyrings/docker.asc

# 设置 Docker 的 apt 软件源
RUN echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release &amp;amp;&amp;amp; echo &quot;$VERSION_CODENAME&quot;) stable&quot; | \
  tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null

# 更新软件源信息，并安装 Docker CLI
# 注意：我们只安装 docker-ce-cli，不需要完整的 docker-ce 引擎
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y --no-install-recommends \
    docker-ce-cli \
    docker-buildx-plugin \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* \
    &amp;amp;&amp;amp; npm install -g pnpm


# 设置工作目录
WORKDIR /app
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;sudo docker build -t node-docker-builder -f Dockerfile .
cd n8n
sudo docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock -v &quot;$(pwd)&quot;:/app node-docker-builder bash
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;进入容器内部后，输入&lt;code&gt;DOCKER_BUILD=true pnpm build:docker&lt;/code&gt;开始打包&lt;/li&gt;
&lt;li&gt;打包完成后，参考官方教程进行部署，下面是我的&lt;code&gt;docker-compose.yaml&lt;/code&gt;&lt;pre&gt;&lt;code&gt;cd n8n &amp;amp;&amp;amp; nano docker-compose.yaml &amp;amp;&amp;amp; mkdir data local-files &amp;amp;&amp;amp; sudo chown -R 1000:1000 ./data ./local-files &amp;amp;&amp;amp; nano .env
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;services:
  n8n:
    image: n8nio/n8n:local
    container_name: n8n
    restart: always
    ports:
      - &quot;65035:5678&quot;
    environment:
      # 这些环境变量对于 n8n 在反代后正常工作至关重要，必须保留
      - N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
      - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
      - N8N_RUNNERS_ENABLED=true
      - NODE_ENV=production
      - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
      - TZ=${GENERIC_TIMEZONE}
      - NODE_TLS_REJECT_UNAUTHORIZED=0
    volumes:
      - ./data:/home/node/.n8n
      - ./local-files:/files
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;SUBDOMAIN=n8n
DOMAIN_NAME=换成你的一级域名
GENERIC_TIMEZONE=Asia/Shanghai
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;运行：&lt;code&gt;sudo docker compose up -d&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;License Key获取与填入&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;其实这一步不做也无所谓，因为就算是没License，默认Enterprise功能也都是开启的。有强迫症的话，可以接着下去&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;前往&lt;code&gt;https://n8n.xxx.com&lt;/code&gt;，首次进入会要求注册
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/09/04/68b985d47a5bc.png&quot; alt=&quot;CleanShot 2025-09-04 at 20.27.17@2x.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;随后会进入如下界面，点击箭头处按钮获取一个Community License
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/09/04/68b9863e72a22.png&quot; alt=&quot;CleanShot 2025-09-04 at 20.28.55@2x.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;随后点击邮件里的链接去激活，默认就是Enterprise License。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Oracle服务器使用Hurricane Electric的64位ipv6</title><link>https://blog.useforall.com/posts/20/</link><guid isPermaLink="true">https://blog.useforall.com/posts/20/</guid><description>详细介绍了从创建 HE 隧道到在 Ubuntu 22.04 上配置 Netplan 的完整流程，并特别指出了**防火墙放行**和**使用内网 IP 配置**这两个关键“坑点”</description><pubDate>Tue, 02 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要信息&quot;}
本文解决了 Oracle Cloud 服务器原生不支持分配 /64 IPv6 地址块的问题。通过使用 Hurricane Electric (HE) 的免费隧道服务，可以在 Oracle 服务器上成功配置一个完整的 /64 IPv6 段，从而拥有海量的 IPv6 地址资源（例如用于搭建代理池）。文章详细介绍了从创建 HE 隧道到在 Ubuntu 22.04 上配置 Netplan 的完整流程，并特别指出了&lt;strong&gt;防火墙放行&lt;/strong&gt;和&lt;strong&gt;使用内网 IP 配置&lt;/strong&gt;这两个关键“坑点”。
:::&lt;/p&gt;
&lt;p&gt;我有一个美国服务器直接分配了::/64地址块，因此拥有了近乎无限的ipv6代理池，但是问题在于我新加坡服务器调用美国服务器延迟还是有点高（200-250ms左右），这就让我想起来了我有个Oracle新加坡服务器。但Oracle服务器本身原生支持ipv6，为什么要多此一举？因为Oracle服务器不支持把::/64整段ipv6分配给Instance，只能分配::/128的ipv6。本文就是为了解决这个问题而写的。同时解决一些部署上的小坑点。笔者服务器系统是&lt;code&gt;Ubuntu 22.02&lt;/code&gt;，仅供参考。&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VPS&lt;/strong&gt;：此服务器必须要有公网ipv4（话说应该没有无ipv4的服务器吧😂）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hurricane Electric账号&lt;/strong&gt;：获取ipv6 block。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;基本部署&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;获取HE账号，前往&lt;a href=&quot;https://tunnelbroker.net/&quot;&gt;官网&lt;/a&gt;注册。&lt;/li&gt;
&lt;li&gt;登录后点击&lt;code&gt;Tunnelbroker&lt;/code&gt;，然后点击&lt;code&gt;Create Regular Tunnel&lt;/code&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/09/02/68b7036bee630.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;输入VPS的公网ipv4地址，请确保ipv4的ICMP已打开，并选择离你VPS近的区域。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/09/02/68b7046d99c37.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;创建完成后，记住给的&lt;code&gt;Server IPv4 Address&lt;/code&gt;地址，然后去Oracle OCI控制放行这个ip
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/09/02/68b70821e3aa9.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/09/02/68b70804496c8.png&quot; alt=&quot;CleanShot 2025-09-02 at 23.06.33@2x.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;点击&lt;code&gt;Example Configurations&lt;/code&gt;，选择你对应的系统，&lt;code&gt;Ubuntu 22.02&lt;/code&gt;默认用的&lt;code&gt;Netplan&lt;/code&gt;所以我选择&lt;code&gt;Netplan&lt;/code&gt;，请根据你自己的系统修改。复制里面的内容
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/09/02/68b708d647383.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;回到VPS，&lt;code&gt;sudo nano /etc/netplan/99-he-tunnel.yaml&lt;/code&gt;，把第五步复制的内容粘贴进去&lt;pre&gt;&lt;code&gt;network:
  version: 2
  tunnels:
    he-ipv6:
      mode: sit
      remote: 216.218.221.42
      local: xx.xx.xx.xx # 修改为你内网ipv4地址！！不要使用vps的公网地址
      addresses:
        - &quot;xxxx:xx:xx:xx::2/64&quot;
      routes:
        - to: default
          via: &quot;xxxx:xx:xx:xx::1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;启用：&lt;code&gt;sudo netplan apply&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;到这就全部结束了，怎么做成代理池可以继续参考这篇&lt;a href=&quot;https://blog.useforall.com/post/18#%E7%BB%84%E4%BB%B6%E8%AF%B4%E6%98%8E&quot;&gt;文章&lt;/a&gt;了。&lt;/p&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Docker部署LskyPro+开心版</title><link>https://blog.useforall.com/posts/19/</link><guid isPermaLink="true">https://blog.useforall.com/posts/19/</guid><description>本文提供了一套完整的 Docker 部署“兰空图床付费开心版”的教程，旨在解决官方 Dockerfile 无法打包及 PostgreSQL 数据库兼容性等问题</description><pubDate>Fri, 29 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要信息&quot;}
本文提供了一套完整的 Docker 部署“兰空图床付费开心版”的教程，旨在解决官方 Dockerfile 无法打包及 PostgreSQL 数据库兼容性等问题。主要内容包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;修复 Dockerfile&lt;/strong&gt;：提供了一个可以直接构建成功的、包含所有必要依赖（如&lt;code&gt;vips&lt;/code&gt;, &lt;code&gt;imagick&lt;/code&gt;）的 &lt;code&gt;Dockerfile&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;适配 PostgreSQL&lt;/strong&gt;：通过修改数据库迁移文件（Migrations），解决了在 PostgreSQL 下因 &lt;code&gt;json&lt;/code&gt; 类型不兼容导致的部署问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供完整部署方案&lt;/strong&gt;：包含 &lt;code&gt;docker-compose.yaml&lt;/code&gt; 配置文件和详细的 Nginx Proxy Manager 配置建议，支持更多图片格式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提供迁移指南&lt;/strong&gt;：为使用 PostgreSQL 的用户提供了一套详细的、从开源版手动迁移数据（图片记录和缩略图）到付费版的 SQL 和 Python 脚本方案。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;2025-09-21: 更新打包好的镜像地址，原Github账号死了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;前端时间在Linux Do看到个开心版帖子：&lt;a href=&quot;https://linux.do/t/topic/882900&quot;&gt;AcoFork 的兰空图床开心版 1panel 部署教程&lt;/a&gt;。但是我习惯Docker部署，所以折腾了一下。但是遇到了各种问题，首先是源码里提供的&lt;code&gt;Dockerfile&lt;/code&gt;无法直接打包，会报错。此外由于MySQL占用较大，我想换&lt;code&gt;PostgreSQL&lt;/code&gt;数据库，但是由于LskyPro初始化列的时候，会把在其他数据库里为&lt;code&gt;text&lt;/code&gt;类型的列建成&lt;code&gt;json&lt;/code&gt;类型不同，且没有进行处理，导致部署完成后有各种问题，且居然在含有json的列取&lt;code&gt;distinct *&lt;/code&gt;，还是比较匪夷所思的。本文都会进行修复。&lt;/p&gt;
&lt;h1&gt;免费公益图床&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;需要Linux.Do三级账号，且分数达到60分&lt;/li&gt;
&lt;li&gt;前往&lt;a href=&quot;https://cdk.linux.do/receive/075668c9-20de-470c-af3a-0ee004e05c41&quot;&gt;Linux.Do分发站&lt;/a&gt;领取账号&lt;/li&gt;
&lt;li&gt;100个账号，每个账号默认20GB，最大允许50MB图片&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;已打包好的Docker镜像(amd64/arm64)&lt;/strong&gt;：&lt;code&gt;docker pull ghcr.io/walrus8364/lsky-pro:latest&lt;/code&gt;。不保证&lt;code&gt;arm64&lt;/code&gt;可用性（因为我没用arm部署）
如果使用已打包好的镜像，请跳转&lt;a href=&quot;#%E9%83%A8%E7%BD%B2%E5%AE%B9%E5%99%A8&quot;&gt;部署容器&lt;/a&gt;部分
:::note{title=&quot;和原版区别&quot;}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;修复PG数据库问题&lt;/li&gt;
&lt;li&gt;完全重构相册分享页面UI。&lt;a href=&quot;https://lsky.useforall.com/shares/66533d04e0384cdabe64d6d841bd698d&quot;&gt;新分享页面&lt;/a&gt;
&lt;ol&gt;
&lt;li&gt;点击查看大图支持左右切换图片&lt;/li&gt;
&lt;li&gt;支持按ID或上传时间进行&lt;/li&gt;
&lt;li&gt;支持下载单个图片，同时新增&lt;code&gt;下载全部&lt;/code&gt;按钮&lt;/li&gt;
&lt;li&gt;支持聚类模式，实现&lt;a href=&quot;https://slow.pics/&quot;&gt;SlowPics&lt;/a&gt;的所有功能（图片命名规则需满足&lt;code&gt;AAA-x1&lt;/code&gt;, &lt;code&gt;AAA-x2&lt;/code&gt;, &lt;code&gt;BBB-1&lt;/code&gt;, &lt;code&gt;BBB-x2&lt;/code&gt;这种形式才可使用聚类模式）&lt;/li&gt;
&lt;li&gt;网站公告和站内公告的&lt;code&gt;code block&lt;/code&gt;改为使用shiki代替highlight.js的代码高亮
&lt;ul&gt;
&lt;li&gt;相册简介数据类型修改为TEXT，用以支持长MarkDown内容，使用marked解析，并支持shiki的代码高亮
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LskyPro+付费版源码&lt;/strong&gt;：前往此位大佬的博客进行下载。&lt;a href=&quot;https://www.2x.nz/posts/lskypro-local/#%E5%AE%89%E8%A3%85%E5%8C%85%E4%BD%93&quot;&gt;lskypro+本地无需授权开心版！&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;代码修复&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;本文假设下载好了源码，同时解压内容到了一个名为&lt;code&gt;code&lt;/code&gt;的文件夹&lt;/li&gt;
&lt;li&gt;修复&lt;code&gt;code/docker/Dockerfile&lt;/code&gt;。复制下面代码粘贴即可&lt;pre&gt;&lt;code&gt;FROM php:8.4-fpm

ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Shanghai

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    wget \
    gnupg \
    # software-properties-common \
    curl \
    zip \
    unzip \
    git \
    cron \
    supervisor \
    nginx \
    sqlite3 \
    netcat-openbsd \
    procps \
    htop \
    build-essential \
    pkg-config \
    autoconf \
    automake \
    libtool \
    imagemagick \
    libmagickwand-dev \
    libmagickcore-dev \
    libvips-dev \
    libvips-tools \
    libvips42 \
    libpng-dev \
    libjpeg-dev \
    libjpeg62-turbo-dev \
    libfreetype6-dev \
    libwebp-dev \
    libavif-dev \
    libtiff-dev \
    libgif-dev \
    libheif-dev \
    librsvg2-dev \
    libraw-dev \
    libzip-dev \
    zlib1g-dev \
    libonig-dev \
    libxml2-dev \
    libicu-dev \
    libxslt1-dev \
    libffi-dev \
    libsqlite3-dev \
    libpq-dev \
    fonts-dejavu-core \
    fontconfig \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*


# 将所有 PHP 扩展相关的操作合并到一个 RUN 指令中
RUN set -ex \
# 1. 安装稳定且可以通过 pecl 安装的扩展
    &amp;amp;&amp;amp; pecl install imagick redis \
    # 2. 从源码编译安装 vips 扩展
    &amp;amp;&amp;amp; git clone https://github.com/libvips/php-vips-ext.git \
    &amp;amp;&amp;amp; cd php-vips-ext \
    &amp;amp;&amp;amp; phpize \
    &amp;amp;&amp;amp; ./configure \
    &amp;amp;&amp;amp; make \
    &amp;amp;&amp;amp; make install \
    &amp;amp;&amp;amp; cd .. \
    &amp;amp;&amp;amp; rm -rf php-vips-ext \
    &amp;amp;&amp;amp; docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
    &amp;amp;&amp;amp; docker-php-ext-configure intl \
    &amp;amp;&amp;amp; docker-php-ext-install -j$(nproc) \
        pdo_mysql \
        pdo_sqlite \
        pdo_pgsql \
        mysqli \
        gd \
        zip \
        bcmath \
        opcache \
        exif \
        intl \
        xsl \
        pcntl \
        ftp \
        ffi \
    &amp;amp;&amp;amp; docker-php-ext-enable imagick redis vips \
    &amp;amp;&amp;amp; rm -rf /tmp/pear

COPY docker/config/php.ini /usr/local/etc/php/conf.d/custom.ini
COPY docker/config/www.conf /usr/local/etc/php-fpm.d/www.conf
COPY docker/config/nginx.conf /etc/nginx/sites-available/default
COPY docker/config/supervisord.conf /etc/supervisor/conf.d/supervisord.conf

# 删除默认的 Docker 配置文件，避免配置冲突
RUN rm -f /usr/local/etc/php-fpm.d/zz-docker.conf

WORKDIR /var/www/html

COPY . .

RUN chown -R www-data:www-data /var/www/html \
    &amp;amp;&amp;amp; chmod -R 755 /var/www/html/storage \
    &amp;amp;&amp;amp; chmod -R 755 /var/www/html/bootstrap/cache

COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

RUN mkdir -p /var/log/supervisor \
    &amp;amp;&amp;amp; mkdir -p /var/www/html/database \
    &amp;amp;&amp;amp; chown -R www-data:www-data /var/www/html/database

EXPOSE 80

# 添加健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:80/ || exit 1

ENTRYPOINT [&quot;/entrypoint.sh&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;按下图，修改&lt;code&gt;code/database/migrations&lt;/code&gt;下相关文件的表结构
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/08/29/68b0837f6345e.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;打包镜像：&lt;code&gt;cd code &amp;amp;&amp;amp; docker build -f docker/Dockerfile -t lsky-pro:latest .&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;部署容器&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;创建&lt;code&gt;data&lt;/code&gt;文件夹，并同时创建&lt;code&gt;docker-compose.yaml&lt;/code&gt;和&lt;code&gt;.env&lt;/code&gt;文件&lt;pre&gt;&lt;code&gt;cd code &amp;amp;&amp;amp; mkdir data &amp;amp;&amp;amp; touch docker-compose.yaml &amp;amp;&amp;amp; touch .env`
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;services:
  lsky-pro:
    image: lsky-pro:latest
    container_name: lsky-pro
    ports:
      - &quot;9999:80&quot; # 修改port mapping
    env_file: .env
    environment:
      - APP_NAME=兰空图床
      - APP_URL=http://localhost:9999 # 改为你自己的域名
      - APP_LICENSE_KEY=xxxx-xxxx-xxxx-xxxx # 随便填
      - ADMIN_USERNAME=admin # 不设置则默认为 admin
      - ADMIN_EMAIL=admin@example.com # 不设置则默认为 admin@example.com
      - ADMIN_PASSWORD=admin123 # 不设置则默认为 admin123
    volumes:
      - data:/var/www/html
    restart: unless-stopped
    network_mode: bridge

volumes:
  data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ./data
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;配置&lt;code&gt;Openlist&lt;/code&gt;：参考&lt;a href=&quot;https://blog.useforall.com/post/4#%E9%85%8D%E7%BD%AEOpenlist&quot;&gt;Docker部署Lsky图床并配合Openlist网盘存储#配置Openlist&lt;/a&gt;部分&lt;/li&gt;
&lt;li&gt;配置&lt;code&gt;Nginx Proxy Manager&lt;/code&gt;：大部分设置请参考&lt;a href=&quot;https://blog.useforall.com/post/4#%E9%85%8D%E7%BD%AENginx%20Proxy%20Manager&quot;&gt;Docker部署Lsky图床并配合Openlist网盘存储#配置Openlist#配置Nginx Proxy Manager&lt;/a&gt;部分。有一小点的变化：
&lt;ol&gt;
&lt;li&gt;因为付费版支持更多的图片格式，因此要修改&lt;code&gt;localtion ~ ^/(.*\.(jpeg|jpg|png|gif|tif|bmp|ico|psd|webp))$&lt;/code&gt;为&lt;code&gt;localtion ~* ^/(.*\.(jpeg|jpg|png|gif|tif|tiff|bmp|ico|psd|webp|avif|heic|heif|jp2|j2k|jp2k|jpf|jpm|jpg2|j2c|jpc|jpx))$&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;开源版手动迁移至付费版&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;由于表结构变化非常大，且官方没有提供迁移办法，因此提供一个基础的迁移方法。&amp;lt;span style=&apos;color: lightblue&apos;&amp;gt;此处默认开源版和付费版都是PG数据库部署的&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;先确认开源版&lt;code&gt;角色组&lt;/code&gt;的&lt;code&gt;路径命名规则&lt;/code&gt;和&lt;code&gt;文件命名规则&lt;/code&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/08/29/68b0888cb7990.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;付费版&lt;/code&gt;内的&lt;code&gt;储存管理&lt;/code&gt;里的&lt;code&gt;命名规则&lt;/code&gt;需要设置为&lt;code&gt;开源版&lt;/code&gt;的&lt;code&gt;路径命名规则/文件命名规则&lt;/code&gt;。其余配置请和开源版的储存策略一致
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/08/29/68b0892f203db.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;span style=&apos;color: lightblue&apos;&amp;gt;同时假设你和我一样只有一个本地储存和一个Openlist WebDAV储存&amp;lt;/span&amp;gt;
&lt;ol&gt;
&lt;li&gt;在&lt;code&gt;开源版&lt;/code&gt;的&lt;code&gt;public&lt;/code&gt;下，DataGrip里运行下面的sql：&lt;pre&gt;&lt;code&gt;select
    user_id,
    group_id,
    strategy_id,
    split_part(origin_name, &apos;.&apos;, 1) as name,
    &apos;&apos; as intro,
    origin_name as filename,
    concat_ws(&apos;/&apos;, path, name) as pathname,
    mimetype,
    extension,
    md5,
    sha1,
    &apos;{}&apos; as exif,
    size, width, height,
    false as is_public,
    &apos;normal&apos; as status,
    uploaded_ip::inet as uploaded_ip,
    null as expired_at,
    null as deleted_at,
    created_at,
    updated_at
from images;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;去付费版数据库的&lt;code&gt;photos&lt;/code&gt;这个表，把数据粘贴进去即可&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;迁移开源版的&lt;code&gt;thumbnails&lt;/code&gt;文件
&lt;ol&gt;
&lt;li&gt;在&lt;code&gt;收费版&lt;/code&gt;的&lt;code&gt;public&lt;/code&gt;运行下面的sql&lt;pre&gt;&lt;code&gt;select
    json_agg(
        json_build_object(
            -- 请注意替换为你开源版本地实际缩略图路径
            &apos;old_thumbnail_path&apos;, concat(&apos;/home/ubuntu/docker_data/lskypro/web/public/thumbnails/&apos;, md5, &apos;.png&apos;),
            -- 请注意替换为你付费版本地实际缩略图路径
            &apos;new_thumbnail_path&apos;, concat(&apos;/home/ubuntu/docker_data/lskyproplus/data/storage/app/public/thumbnails/&apos;, pathname)
        )
    )
from photos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;复制python代码并替换&lt;code&gt;data&lt;/code&gt;变量为上面sql的运行结果&lt;pre&gt;&lt;code&gt;nano thumbnail-migration.py
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from pathlib import Path
import shutil

data = []

for i in data:
    old_path = i[&quot;old_thumbnail_path&quot;]
    new_path = i[&quot;new_thumbnail_path&quot;]
    # 创建新目录
    if Path(new_path).parent.exists() is False:
        Path(new_path).parent.mkdir(parents=True, exist_ok=True)
    # 复制旧文件到新文件夹
    try:
        shutil.copy(old_path, new_path)
        print(f&quot;Copied: {old_path} -&amp;gt; {new_path}&quot;)
    except Exception as e:
        print(f&quot;Error copying {old_path} to {new_path}: {e}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;运行：&lt;code&gt;python thumbnail-migration.py&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;开始愉快的使用吧&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;a href=&quot;https://blog.useforall.com/c/lsy-pro-plus-api&quot;&gt;API文档&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe src=&apos;https://blog.useforall.com/c/lsy-pro-plus-api&apos; width=&apos;100%&apos; height=&apos;750px&apos;/&amp;gt;&lt;/p&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>创建自己的2^64个IP的代理池</title><link>https://blog.useforall.com/posts/18/</link><guid isPermaLink="true">https://blog.useforall.com/posts/18/</guid><description>在 /64 IPv6 地址块的服务器上部署 go-proxy-ipv6-pool 随机出口 IP 代理服务。解决运行后却遭遇的 connection timed out 或 cannot assign requested address 错误。这通常不是代理程序本身的问题，而是服务器网络环境没有进行正确配置。</description><pubDate>Tue, 22 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
在 &lt;code&gt;/64&lt;/code&gt; IPv6 地址块的服务器上部署 &lt;a href=&quot;https://github.com/XiaoMiku01/go-proxy-ipv6-pool&quot;&gt;&lt;code&gt;go-proxy-ipv6-pool&lt;/code&gt;&lt;/a&gt; 随机出口 IP 代理服务。解决运行后却遭遇的 &lt;code&gt;connection timed out&lt;/code&gt; 或 &lt;code&gt;cannot assign requested address&lt;/code&gt; 错误。这通常不是代理程序本身的问题，而是服务器网络环境没有进行正确配置。
:::&lt;/p&gt;
&lt;p&gt;本文将详细记录从零开始部署一个 Go IPv6 代理池，并彻底解决其网络连接问题的全过程。&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Golang&lt;/strong&gt;：运行项目。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ndppd&lt;/strong&gt;：类似于 IPv4 中 &lt;code&gt;ARP&lt;/code&gt; 协议的作用，IPv6 中需要使用 &lt;code&gt;ND&lt;/code&gt; 协议来发现邻居并确定可用路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;基本步骤&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;第一步：基础环境和代理程序准备&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;首先，我们需要准备好 Go 环境并下载代理程序项目。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 安装 Go 语言环境&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 下载 Go (请到官网 https://go.dev/dl/ 获取最新版链接)
wget https://go.dev/dl/go1.22.3.linux-amd64.tar.gz

# 解压到 /usr/local
sudo tar -C /usr/local -xzf go1.22.3.linux-amd64.tar.gz

# 将 Go 的二进制文件路径添加到环境变量
echo &apos;export PATH=$PATH:/usr/local/go/bin&apos; &amp;gt;&amp;gt; ~/.profile
source ~/.profile

# 验证安装
go version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 克隆项目并准备运行脚本&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 克隆项目
git clone https://github.com/XiaoMiku01/go-proxy-ipv6-pool.git
cd go-proxy-ipv6-pool

# 创建一个便捷的 run.sh 脚本
cat &amp;lt;&amp;lt; &apos;EOF&apos; &amp;gt; run.sh
#!/bin/bash

# 将 Go 的缓存目录设置在当前项目下，避免污染系统
export GOMODCACHE=&quot;$(pwd)/.gomodcache&quot;
export GOCACHE=&quot;$(pwd)/.gocache&quot;
mkdir -p $GOMODCACHE
mkdir -p $GOCACHE

echo &quot;--- Using local Go caches ---&quot;
echo &quot;Module Cache: $GOMODCACHE&quot;
echo &quot;Build Cache:  $GOCACHE&quot;
echo &quot;-----------------------------&quot;

# 以 root 权限运行 Go 程序，并将所有脚本参数传递给它
# 注意：后面的网络配置需要程序以 root 权限运行来绑定 IP
# 如果不使用自动绑定IP的修改版，可以不用 sudo
sudo go run . &quot;$@&quot;
EOF

# 赋予脚本执行权限
chmod +x run.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;第二步：核心网络配置&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;这是解决所有网络问题的核心。我们需要从内核 Socket 层、路由层到数据链路层进行“三板斧”配置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 内核参数：允许绑定非本地 IP (解决 &lt;code&gt;cannot assign requested address&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：默认情况下，Linux 内核不允许程序绑定到一个尚未明确分配给网络接口的 IP 地址。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：修改 &lt;code&gt;net.ipv6.ip_nonlocal_bind&lt;/code&gt; 参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 临时启用（用于测试，重启后失效）
sudo sysctl -w net.ipv6.ip_nonlocal_bind=1

## 同时修改下其他参数
sudo sysctl -w net.ipv6.conf.all.forwarding=1
sudo sysctl -w net.ipv6.conf.default.forwarding=1

# 永久生效
sudo tee -a /etc/sysctl.conf &amp;lt;&amp;lt;EOF
net.ipv6.ip_nonlocal_bind=1
net.ipv6.conf.all.forwarding=1
net.ipv6.conf.default.forwarding=1
EOF \
&amp;amp;&amp;amp; sudo sysctl -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 本地路由：告知内核 /64 网段归本机所有 (解决出站路由)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：内核需要知道它可以从 &lt;code&gt;/64&lt;/code&gt; 地址池中的任意 IP 发出数据包。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：添加一条本地路由规则。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 临时添加（用于测试，重启后失效）
# 将 &amp;lt;your_ipv6_cidr&amp;gt; 替换为你的网段
# 将 &amp;lt;interface&amp;gt; 替换为你的网卡名，例如 eth0
sudo ip route add local &amp;lt;your_ipv6_cidr&amp;gt; dev &amp;lt;interface&amp;gt;


# 永久生效
sudo nano /etc/network/interfaces.d/xx-cloud-init

# 找到 iface eth0 inet6 static 这部分，在它的末尾添加 post-up 命令。post-up 的意思是“当这个接口成功启动后，执行以下命令”。
# ... (前面的配置不变) ...

# control-alias eth0
iface eth0 inet6 static
    address xxxxx
    gateway xxxxx
    # 在这里添加 post-up 命令
    post-up ip route add local &amp;lt;your_ipv6_cidr&amp;gt; dev &amp;lt;interface&amp;gt;

# 重启服务
sudo systemctl restart networking.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. NDP 代理：响应路由器的邻居请求 (解决入站路由和 &lt;code&gt;connection timed out&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：当外部服务器响应你的请求时，你的服务商路由器需要通过邻居发现协议(NDP)找到你那个随机 IP 对应的 MAC 地址。默认情况下，你的服务器不会对未明确绑定的 IP 进行应答。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：使用 &lt;code&gt;ndppd&lt;/code&gt; (NDP Proxy Daemon) 自动应答所有来自你子网的 NDP 请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 安装 ndppd
sudo apt install -y ndppd

# 编辑配置文件
sudo nano /etc/ndppd.conf

# 清空文件内容，然后粘贴以下为你定制的配置
# 同样，替换 &amp;lt;your_ipv6_cidr&amp;gt; 和 &amp;lt;interface&amp;gt;
# --- ndppd.conf start ---
route-ttl 30000

proxy &amp;lt;interface&amp;gt; {
    router no
    timeout 500
    ttl 30000
    rule &amp;lt;your_ipv6_cidr&amp;gt; {
        static
    }
}
# --- ndppd.conf end ---

# 重启并设置开机自启
sudo systemctl restart ndppd
sudo systemctl enable ndppd

# 检查服务状态，确保 active (running)
sudo systemctl status ndppd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;第三步：启动代理并进行最终测试&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;所有配置完成后，我们可以启动代理并验证它是否按预期工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 启动代理服务&lt;/strong&gt;
在 &lt;code&gt;go-proxy-ipv6-pool&lt;/code&gt; 目录下执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 假设你的 HTTP 代理端口是 52122，SOCKS5 是 52123
# CIDR 替换成你自己的
./run.sh --port 52122 --cidr &amp;lt;your_ipv6_cidr&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. 循环测试&lt;/strong&gt;
打开一个新的终端窗口，运行下面的命令来持续测试代理，并观察出口 IP 是否在变化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 将端口号 52122 替换为你自己设置的 HTTP 端口
while true; do
    curl -s \
         -w &quot;\n--- Timing Info ---\nHttpCode: %{http_code}\nTotal Time: %{time_total}s\n---------------------\n&quot; \
         -x http://localhost:52122 \
         --connect-timeout 5 \
         ipv6.ip.sb
    sleep 0.5
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果一切正常，你将看到屏幕上不断打印出不同的、来自你 &lt;code&gt;/64&lt;/code&gt; 地址池的 IPv6 地址，并且没有任何报错。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;结论&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;部署一个功能完备的 IPv6 代理池，关键不在于应用本身，而在于对底层网络环境的深刻理解和正确配置。通过 &lt;code&gt;ip_nonlocal_bind&lt;/code&gt;、&lt;code&gt;local route&lt;/code&gt; 和 &lt;code&gt;ndppd&lt;/code&gt; 这“三板斧”，我们成功地解决了从内核到网络的各层障碍，实现了应用的预期功能。希望这篇指南能帮助到遇到同样问题的朋友们。&lt;/p&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Gemini自动创建APIKEY</title><link>https://blog.useforall.com/posts/17/</link><guid isPermaLink="true">https://blog.useforall.com/posts/17/</guid><description>利用Google Cloud Shell自动创建Gemini APIKEY。手动是及其不健康的</description><pubDate>Mon, 21 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;相关信息&quot;}
利用Google Cloud Shell自动创建Gemini APIKEY。手动是及其不健康的
:::&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2025-08-05 20:58：创建密钥脚本添加睡眠时间防止429错误&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;前往&lt;a href=&quot;https://console.cloud.google.com/cloud-resource-manager&quot;&gt;Google Cloud&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;点击右上角的shell图标，并打开editor
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/07/21/687e3a4278c04.png&quot; alt=&quot;CleanShot 2025-07-21 at 21.01.24@2x.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;创建&lt;code&gt;a.sh&lt;/code&gt;文件，粘贴下面内容：&lt;pre&gt;&lt;code&gt;#!/bin/bash

# 设置要创建的项目数量为12
PROJECT_COUNT=20
# 获取当前账号的邮箱
current_email=$(gcloud config get-value account)
echo &quot;当前账号的邮箱是: $current_email&quot;

# 项目前缀 - 确保以字母开头
EMAIL_PREFIX=${current_email%%@*}
# 替换prrfix里的`.`为`-`
EMAIL_PREFIX=${EMAIL_PREFIX//\./-g}
echo &quot;生成的项目前缀为: $EMAIL_PREFIX&quot;

# 如果前缀以数字开头，添加字母p前缀
if [[ $EMAIL_PREFIX =~ ^[0-9] ]]; then
  PROJECT_ID_PREFIX=&quot;p$EMAIL_PREFIX&quot;
else
  PROJECT_ID_PREFIX=&quot;$EMAIL_PREFIX&quot;
fi
echo &quot;生成的项目前缀为: $PROJECT_ID_PREFIX&quot;

# 存储项目ID的数组
PROJECT_IDS=()

# 检查已有项目
EXISTING_PROJECTS=$(gcloud projects list --format=&quot;value(projectId)&quot;)

# 如果存在项目,添加到数组中
if [ ! -z &quot;$EXISTING_PROJECTS&quot; ]; then
  for PROJECT_ID in $EXISTING_PROJECTS; do
    PROJECT_IDS+=($PROJECT_ID)
  done
  echo &quot;已将${#PROJECT_IDS[@]}个已有项目添加到数组中&quot;
else
  echo &quot;未发现已有项目&quot;
fi

# 创建指定数量的项目
for i in $(seq 1 $PROJECT_COUNT); do
  PROJECT_ID=&quot;$PROJECT_ID_PREFIX-$i&quot;
  echo &quot;正在创建项目: $PROJECT_ID&quot;

  # 创建新项目，不使用 --no-enable-billing 参数
  gcloud projects create $PROJECT_ID --name=&quot;API Project $i&quot; &amp;amp;
  # 等待后台任务完成
  wait
  # 将项目ID添加到数组中
  PROJECT_IDS+=($PROJECT_ID)
  echo &quot;项目 $PROJECT_ID 创建完成&quot;
  sleep 2
done

# 输出API密钥列表
API_KEYS=&quot;&quot;

# 循环处理每个项目ID
for PROJECT_ID in &quot;${PROJECT_IDS[@]}&quot;; do
  echo &quot;正在处理项目: $PROJECT_ID&quot;

  # 启用 Generative Language API
  gcloud services enable generativelanguage.googleapis.com --project=$PROJECT_ID &amp;amp;

  # 生成新的API密钥
  KEY_NAME=&quot;gl-api-key-$(date +%Y%m%d-%H%M%S)&quot;
  KEY_RESPONSE=$(gcloud beta services api-keys create \
    --display-name=&quot;Generative Language API Key&quot; \
    --api-target=service=generativelanguage.googleapis.com \
    --project=$PROJECT_ID \
    --format=&quot;json&quot; &amp;amp;)

  # 等待所有后台任务完成
  wait

  # 检查返回结果中是否包含keyString
  if echo &quot;$KEY_RESPONSE&quot; | grep -q &quot;keyString&quot;; then
    # 直接从返回结果中提取keyString
    API_KEY=$(echo &quot;$KEY_RESPONSE&quot; | grep -o &apos;&quot;keyString&quot;: &quot;[^&quot;]*&apos; | cut -d&apos;&quot;&apos; -f4)
  else
    # 如果返回结果中没有keyString,则使用原来的方式获取
    echo &quot;返回结果中没有keyString,使用备用方式获取&quot;

    # 获取新创建的API密钥
    KEY_NAME=$(gcloud beta services api-keys list \
      --project=$PROJECT_ID \
      --filter=&quot;displayName:Generative Language API Key&quot; \
      --format=&quot;value(name)&quot; \
      --limit=1 &amp;amp;)

    # 等待密钥列表返回
    wait

    # 获取API密钥字符串
    API_KEY=$(gcloud beta services api-keys get-key-string $KEY_NAME \
      --project=$PROJECT_ID \
      --format=&quot;get(keyString)&quot; &amp;amp;)
  fi

  # 等待 API 密钥获取完成
  wait

  # 添加到API密钥列表
  API_KEYS=&quot;${API_KEYS}${API_KEY}\n&quot;

  echo &quot;已在项目 $PROJECT_ID 中创建API密钥&quot;
  sleep 2
done

echo &quot;密钥生成完成&quot;

# 获取当前用户邮箱并输出结果
USER_EMAIL=$(gcloud config get-value account)
KEY_COUNT=$(echo -e &quot;$API_KEYS&quot; | grep -c .)

echo &quot;API密钥列表(共${KEY_COUNT}个):&quot;
echo &quot;$USER_EMAIL&quot;
echo -e &quot;$API_KEYS&quot;

# 删除当前目录下的a.sh和b.sh文件
rm -f a.sh b.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;等待即可。最后会输出apikey在终端&lt;/li&gt;
&lt;li&gt;删除所有项目，创建&lt;code&gt;b.sh&lt;/code&gt;，粘贴以下内容&lt;pre&gt;&lt;code&gt;#!/bin/bash

# 获取当前账号的邮箱
current_email=$(gcloud config get-value account)
echo &quot;当前账号的邮箱是: $current_email&quot;

# 获取当前账号下的所有项目
echo &quot;正在获取当前账号下的所有项目...&quot;
PROJECT_IDS_TO_DELETE=($(gcloud projects list --format=&quot;value(projectId)&quot;))

if [ ${#PROJECT_IDS_TO_DELETE[@]} -eq 0 ]; then
  echo &quot;当前账号下没有发现任何项目&quot;
  exit 0
fi

echo &quot;发现 ${#PROJECT_IDS_TO_DELETE[@]} 个项目:&quot;
for PROJECT_ID in &quot;${PROJECT_IDS_TO_DELETE[@]}&quot;; do
  echo &quot;  - $PROJECT_ID&quot;
done

# 直接开始删除，无需确认

echo &quot;&quot;
echo &quot;开始删除项目...&quot;

# 删除计数器
deleted_count=0
failed_count=0
failed_projects=()

# 逐个删除项目
for PROJECT_ID in &quot;${PROJECT_IDS_TO_DELETE[@]}&quot;; do
  echo &quot;正在处理项目: $PROJECT_ID&quot;

  # 检查项目是否存在
  if gcloud projects describe $PROJECT_ID &amp;amp;&amp;gt;/dev/null; then
    # 获取项目状态
    PROJECT_STATE=$(gcloud projects describe $PROJECT_ID --format=&quot;value(lifecycleState)&quot; 2&amp;gt;/dev/null)
    echo &quot;项目状态: $PROJECT_STATE&quot;

    # 如果项目处于DELETE_REQUESTED状态，先恢复
    if [ &quot;$PROJECT_STATE&quot; = &quot;DELETE_REQUESTED&quot; ]; then
      echo &quot;项目处于删除请求状态，正在恢复...&quot;
      if gcloud projects undelete $PROJECT_ID --quiet; then
        echo &quot;项目 $PROJECT_ID 恢复成功，等待3秒...&quot;
        sleep 3
      else
        echo &quot;项目 $PROJECT_ID 恢复失败&quot;
        ((failed_count++))
        failed_projects+=($PROJECT_ID)
        continue
      fi
    fi

    # 删除项目
    echo &quot;正在删除项目: $PROJECT_ID&quot;
    if gcloud projects delete $PROJECT_ID --quiet; then
      echo &quot;项目 $PROJECT_ID 删除成功&quot;
      ((deleted_count++))
    else
      echo &quot;项目 $PROJECT_ID 删除失败&quot;
      ((failed_count++))
      failed_projects+=($PROJECT_ID)
    fi
  else
    echo &quot;项目 $PROJECT_ID 不存在，跳过&quot;
  fi
done

echo &quot;&quot;
echo &quot;删除操作完成!&quot;
echo &quot;成功删除: $deleted_count 个项目&quot;
echo &quot;删除失败: $failed_count 个项目&quot;

if [ $failed_count -gt 0 ]; then
  echo &quot;删除失败的项目:&quot;
  for PROJECT_ID in &quot;${failed_projects[@]}&quot;; do
    echo &quot;  - $PROJECT_ID&quot;
  done
fi

echo &quot;&quot;
echo &quot;注意: 删除的项目会进入30天恢复期，期间不会产生费用&quot;
在此处键入或粘贴代码
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>B站视频转移</title><link>https://blog.useforall.com/posts/16/</link><guid isPermaLink="true">https://blog.useforall.com/posts/16/</guid><description>利用BBDown和biliup-rs搬运某个B账号的视频到另外一个B站账号</description><pubDate>Sun, 13 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
利用&lt;code&gt;BBDown&lt;/code&gt;和&lt;code&gt;biliup-rs&lt;/code&gt;搬运某个B账号的视频到另外一个B站账号
:::&lt;/p&gt;
&lt;p&gt;因为一直有台VDS闲置着，六核心7900x服务器，想着闲着也是闲着，就去做录播了，想着薅一点叔叔的创作激励填补下服务器费用，但是可能因为录的太多了，导致创作激励被扬了，然后创建了个新账号继续做，想着把老账号视频搬过来，因此就有个这个文章。&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python&lt;/strong&gt;：代码运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Openlist&lt;/strong&gt;：上传视频到Openlist对应的网盘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BBDown&lt;/strong&gt;：用于B站视频下载和合并。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;biliup-rs&lt;/strong&gt;：上传视频到B站账号。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;基本配置&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;创建对于文件夹&lt;pre&gt;&lt;code&gt;mkdir -p docker_data/bilisync/data &amp;amp;&amp;amp; cd docker_data/bilisync/data
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;下载&lt;a href=&quot;https://github.com/nilaoda/BBDown/releases/latest&quot;&gt;&lt;code&gt;BBDown&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://github.com/biliup/biliup-rs/releases/latest&quot;&gt;&lt;code&gt;biliup-rs&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://github.com/BtbN/FFmpeg-Builds/releases/latest&quot;&gt;&lt;code&gt;ffmpeg&lt;/code&gt;&lt;/a&gt;二进制文件。&lt;pre&gt;&lt;code&gt;# 这里都以linux x86系统为例
wget https://github.com/nilaoda/BBDown/releases/download/1.6.3/BBDown_1.6.3_20240814_linux-x64.zip
unzip BBDown_1.6.3_20240814_linux-x64.zip &amp;amp;&amp;amp; chmod +x BBDown &amp;amp;&amp;amp; rm BBDown_1.6.3_20240814_linux-x64.zip

wget https://github.com/biliup/biliup-rs/releases/download/v0.2.3/biliupR-v0.2.3-x86_64-linux.tar.xz
tar -xf biliupR-v0.2.3-x86_64-linux.tar.xz &amp;amp;&amp;amp; mv biliupR-v0.2.3-x86_64-linux/biliup . &amp;amp;&amp;amp; chmod +x biliup &amp;amp;&amp;amp; rm biliupR-v0.2.3-x86_64-linux.tar.xz &amp;amp;&amp;amp; rm -rf biliupR-v0.2.3-x86_64-linux

wget https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz
tar -xf ffmpeg-master-latest-linux64-gpl.tar.xz &amp;amp;&amp;amp; mv ffmpeg-master-latest-linux64-gpl/bin/ffmpeg . &amp;amp;&amp;amp; chmod +x ffmpeg &amp;amp;&amp;amp; rm ffmpeg-master-latest-linux64-gpl.tar.xz &amp;amp;&amp;amp; rm -rf ffmpeg-master-latest-linux64-gpl
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BBDown&lt;/code&gt;登录，设置配置文件&lt;pre&gt;&lt;code&gt;./BBDown login
nano BBDown.config
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#本文件是BBDown程序的配置文件
#以#开头的都会被程序忽略
#然后剩余非空白内容程序逐行读取，对于一个选项，其参数应当在下一行出现

# 单个视频非分P的情况的文件下载后存储规则
--file-pattern
&amp;lt;videoTitle&amp;gt;/&amp;lt;videoTitle&amp;gt;

# 单个视频多分P的情况的文件下载后存储规则
--multi-file-pattern
&amp;lt;videoTitle&amp;gt;/&amp;lt;pageTitle&amp;gt;

# 下面设置下载多个分P时，每个分P的下载间隔为2秒
--delay-per-page
2

# ffmpeg二进制文件路径
--ffmpeg-path
/app/data/ffmpeg

# 如果你的机器下载很慢，可以尝试更改这个host，地址可以参考https://github.com/the1812/Bilibili-Evolved/issues/3234#issuecomment-1504764774
#--upos-host
#upos-sz-mirrorcosov.bilivideo.com

# 跳过封面下载
--skip-cover

# 跳过字幕下载
--skip-subtitle

# 使用aria2下载引擎，不需要请注释
-aria2

# 额外的aira2下载参数
--aria2c-args
-j48 # 48线程
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;biliup-rs&lt;/code&gt;登录&lt;pre&gt;&lt;code&gt;./biliup login
&lt;/code&gt;&lt;/pre&gt;
按照提示登录（这个账号是你希望把视频上传到哪个B站账号，和下载账号区分开来）
:::warning{title=&quot;安全警告&quot;}
运行登录命令后，会在 &lt;code&gt;data&lt;/code&gt; 目录下生成包含您B站账户凭证的敏感文件（如 &lt;code&gt;BBDown.data&lt;/code&gt; 和 &lt;code&gt;cookies.json&lt;/code&gt;）。&lt;strong&gt;请妥善保管，切勿在任何地方（如GitHub、论坛）分享这些文件的内容！&lt;/strong&gt;
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;代码配置&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建&lt;code&gt;bilivideosync.py&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nano bilivideosync.py
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
import json
import os
import subprocess
import sys
import re
import shutil
import time
import threading
from alist import AlistUploader

# --- 配置区 ---
# 您的视频数据
# ❗只要保证每个元素有对应的bvid和title即可。获取方法非常多，说两个我用的
# 1. 去主页-&amp;gt;投稿Tab，找到以`https://api.bilibili.com/x/space/wbi/arc/search?pn=2&amp;amp;ps=40&amp;amp;tid=0&amp;amp;special_type=`开头的请求，复制preview-&amp;gt;data-&amp;gt;list-&amp;gt;vlist数据
# 2. 打开某一个合集，找到以`https://api.bilibili.com/x/polymer/web-space/seasons_archives_list?mid=`开头的请求，复制preview-&amp;gt;data-&amp;gt;archives数据
JSON_DATA = &quot;&quot;&quot;
[
  {
    &quot;aid&quot;: xxxxx,
    &quot;bvid&quot;: &quot;xxxxx&quot;,
    &quot;ctime&quot;: xxxxx,
    &quot;duration&quot;: xxxx,
    &quot;enable_vt&quot;: false,
    &quot;interactive_video&quot;: false,
    &quot;pic&quot;: &quot;http://xxxxx&quot;,
    &quot;playback_position&quot;: 0,
    &quot;pubdate&quot;: xxxxx,
    &quot;stat&quot;: {
      &quot;view&quot;: xxxxx,
      &quot;vt&quot;: 0
    },
    &quot;state&quot;: 0,
    &quot;title&quot;: &quot;xxxxxx&quot;,
    &quot;ugc_pay&quot;: 0,
    &quot;vt_display&quot;: &quot;&quot;,
    &quot;is_lesson_video&quot;: 0
  }
]
&quot;&quot;&quot;
# 日志文件名
SUCCESS_LOG_FILE = &quot;success.log&quot;
FAIL_LOG_FILE = &quot;fail.log&quot;

# Alist配置
ALIST_BASE_URL = &quot;http://172.17.0.1:65007&quot;  # 修改为你的Alist服务器地址
ALIST_USERNAME = &quot;xxxxxx&quot;                   # ❗修改为你的Alist用户名
ALIST_PASSWORD = &quot;xxxxxx&quot;                   # ❗修改为你的Alist密码

# --- 配置区结束 ---

# 全局变量，用于存储一次性输入的用户信息
upload_details = {}

# 初始化Alist上传器
alist_uploader = AlistUploader(ALIST_BASE_URL, ALIST_USERNAME, ALIST_PASSWORD)

def check_executables():
    &quot;&quot;&quot;检查依赖的可执行文件是否存在且可执行&quot;&quot;&quot;
    if not (os.path.exists(&apos;./BBDown&apos;) and os.access(&apos;./BBDown&apos;, os.X_OK)):
        print(&quot;❌ 错误: &apos;./BBDown&apos; 不存在或没有执行权限。&quot;)
        sys.exit(1)
    if not (os.path.exists(&apos;./biliup&apos;) and os.access(&apos;./biliup&apos;, os.X_OK)):
        print(&quot;❌ 错误: &apos;./biliup&apos; 不存在或没有执行权限。&quot;)
        sys.exit(1)
    print(&quot;✅ 依赖检查通过 (BBDown, biliup)。&quot;)

def sanitize_filename(filename):
    &quot;&quot;&quot;移除Windows和Linux文件名中的非法字符以匹配BBDown的行为&quot;&quot;&quot;
    return re.sub(r&apos;[\\/:*?&quot;&amp;lt;&amp;gt;|]&apos;, &apos;_&apos;, filename)

def find_video_files(directory):
    &quot;&quot;&quot;在指定目录中查找所有视频文件，并返回一个路径列表。&quot;&quot;&quot;
    video_paths = []
    if not os.path.isdir(directory):
        return video_paths # 返回空列表

    # 排序以保证P序稳定
    file_list = sorted(os.listdir(directory))

    for file in file_list:
        if file.lower().endswith((&apos;.mp4&apos;, &apos;.flv&apos;, &apos;.mkv&apos;, &apos;.webm&apos;)):
            video_paths.append(os.path.join(directory, file))
    return video_paths

def read_log_file(filepath):
    &quot;&quot;&quot;读取日志文件内容到集合中&quot;&quot;&quot;
    if not os.path.exists(filepath):
        return set()
    with open(filepath, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
        return set(line.strip() for line in f if line.strip())

def update_log_files(bvid, status):
    &quot;&quot;&quot;更新成功或失败日志&quot;&quot;&quot;
    success_bvids = read_log_file(SUCCESS_LOG_FILE)
    fail_bvids = read_log_file(FAIL_LOG_FILE)
    if status == &apos;success&apos;:
        success_bvids.add(bvid)
        if bvid in fail_bvids:
            fail_bvids.remove(bvid)
    else: # status == &apos;fail&apos;
        fail_bvids.add(bvid)
        if bvid in success_bvids:
            success_bvids.remove(bvid)
    with open(SUCCESS_LOG_FILE, &apos;w&apos;, encoding=&apos;utf-8&apos;) as f:
        f.write(&apos;\n&apos;.join(sorted(list(success_bvids))) + &apos;\n&apos;)
    with open(FAIL_LOG_FILE, &apos;w&apos;, encoding=&apos;utf-8&apos;) as f:
        f.write(&apos;\n&apos;.join(sorted(list(fail_bvids))) + &apos;\n&apos;)

def get_upload_details_from_user():
    &quot;&quot;&quot;从用户处获取上传所需信息，仅执行一次&quot;&quot;&quot;
    if not upload_details:
        print(&quot;\n--- 首次上传，请输入投稿信息 (后续将自动复用) ---&quot;)
        upload_details[&apos;tid&apos;] = input(&quot;请输入投稿分区TID (例如: 171代表单机游戏): &quot;)
        upload_details[&apos;desc&apos;] = input(&quot;请输入视频简介: &quot;)
        upload_details[&apos;tags&apos;] = input(&quot;请输入视频标签 (用英文逗号 &apos;,&apos; 分隔): &quot;)
def download_video(bvid, title):
    &quot;&quot;&quot;
    下载指定BVID的视频，并实时显示下载进度。
    &quot;&quot;&quot;
    print(f&quot;\n[📥] 正在下载: {title} (BVID: {bvid})&quot;)
    print(&quot;-&quot; * 40)
    try:
        command = [&apos;./BBDown&apos;, bvid]
        subprocess.run(command, check=True, text=True)
        print(&quot;-&quot; * 40)
        print(f&quot;✅ 下载成功: {title}&quot;)
        return True
    except subprocess.CalledProcessError:
        print(&quot;-&quot; * 40)
        print(f&quot;❌ 下载失败: {title} (BVID: {bvid})&quot;)
        return False
    except FileNotFoundError:
        print(&quot;-&quot; * 40)
        print(f&quot;❌ 命令执行失败: 找不到 &apos;./BBDown&apos; 可执行文件。&quot;)
        return False

def upload_to_alist(title, video_paths):
    &quot;&quot;&quot;
    上传视频文件到Alist指定路径

    参数:
        title: 视频标题，用于生成Alist目录路径
        video_paths: 视频文件路径列表

    返回:
        成功上传所有文件返回True，否则返回False
    &quot;&quot;&quot;
    if not video_paths:
        print(&quot;⚠️ 没有找到需要上传到Alist的视频文件&quot;)
        return False

    # 生成Alist目标路径
    remote_dir_path = alist_uploader.generate_folder_path(title)
    print(f&quot;[🗂️] 创建Alist目录: {remote_dir_path}&quot;)

    # 创建目标文件夹
    if not alist_uploader.create_new_folder(remote_dir_path):
        print(f&quot;❌ 创建Alist目录失败: {remote_dir_path}&quot;)
        return False

    # 开始上传文件
    print(f&quot;[☁️] 开始上传 {len(video_paths)} 个文件到Alist...&quot;)
    upload_complete_event = alist_uploader.upload_files(video_paths, remote_dir_path)

    # 等待所有上传完成
    print(&quot;等待所有文件上传完成...&quot;)
    upload_complete_event.wait()

    # 检查是否所有文件都上传成功
    success = alist_uploader.all_uploads_successful()
    if success:
        print(f&quot;✅ 所有文件都成功上传到Alist: {remote_dir_path}&quot;)
    else:
        print(f&quot;❌ 部分文件上传Alist失败，请检查日志&quot;)

    return success

def upload_video(bvid, title):
    &quot;&quot;&quot;上传指定视频并返回成功与否&quot;&quot;&quot;
    sanitized_folder = sanitize_filename(title)
    video_paths = find_video_files(sanitized_folder)
    if not video_paths:
        print(f&quot;❌ 上传预处理失败: 在 &apos;{sanitized_folder}&apos; 目录中找不到视频文件。&quot;)
        return False

    get_upload_details_from_user()

    print(f&quot;\n[🚀] 正在上传: {title} (共 {len(video_paths)} 个分P)&quot;)
    command = [
        &apos;./biliup&apos;, &apos;upload&apos;,
        &apos;--copyright&apos;, &apos;1&apos;,
        &apos;--line&apos;, &apos;txa&apos;,
        &apos;--limit&apos;, &apos;20&apos;,
        &apos;--title&apos;, title,
        &apos;--tid&apos;, upload_details[&apos;tid&apos;],
        &apos;--desc&apos;, upload_details[&apos;desc&apos;],
        &apos;--tag&apos;, upload_details[&apos;tags&apos;],
    ]

    command.extend(video_paths)
    try:
        subprocess.run(command, check=True, text=True)
        print(f&quot;✅ 上传成功: {title}&quot;)
        return True
    except subprocess.CalledProcessError:
        print(f&quot;❌ 上传失败: {title}&quot;)
        return False
    except FileNotFoundError:
        print(f&quot;❌ 命令执行失败: 找不到 &apos;./biliup&apos; 可执行文件。&quot;)
        return False

def process_video(bvid, title, download_first=True):
    &quot;&quot;&quot;完整的处理流程：下载 -&amp;gt; 上传到Alist -&amp;gt; 上传到B站 -&amp;gt; 清理 -&amp;gt; 记录&quot;&quot;&quot;
    if download_first:
        if not download_video(bvid, title):
            update_log_files(bvid, &apos;fail&apos;)
            return

    # 获取下载的视频文件路径
    sanitized_folder = sanitize_filename(title)
    video_paths = find_video_files(sanitized_folder)

    # 上传到Alist（可选步骤，失败不影响后续B站上传）
    # ❗如果不需要上传到openlist，请把下面三行注释掉
    print(&quot;\n[☁️] 开始上传到Alist...&quot;)
    alist_success = upload_to_alist(title, video_paths)
    print(f&quot;[☁️] Alist上传{&apos;成功&apos; if alist_success else &apos;失败&apos;}&quot;)

    # 上传到B站
    if upload_video(bvid, title):
        update_log_files(bvid, &apos;success&apos;)
        # 上传成功后，删除文件夹
        try:
            shutil.rmtree(sanitized_folder)
            print(f&quot;🗑️ 已清理文件夹: {sanitized_folder}&quot;)
        except OSError as e:
            print(f&quot;⚠️ 清理文件夹失败: {sanitized_folder}, 错误: {e}&quot;)
    else:
        update_log_files(bvid, &apos;fail&apos;)

def main():
    &quot;&quot;&quot;主执行函数&quot;&quot;&quot;
    check_executables()

    try:
        videos_data = json.loads(JSON_DATA)
        video_map = {v[&apos;bvid&apos;]: v for v in videos_data}
    except json.JSONDecodeError:
        print(&quot;❌ 错误: JSON 数据格式不正确。&quot;)
        sys.exit(1)

    successful_bvids = read_log_file(SUCCESS_LOG_FILE)
    failed_bvids = read_log_file(FAIL_LOG_FILE)
    processed_bvids = set()

    if failed_bvids:
        print(&quot;\n--- 检查上次失败的任务 ---&quot;)
        for bvid in list(failed_bvids):
            if bvid not in video_map:
                print(f&quot;ℹ️ 失败列表中的 {bvid} 在当前JSON数据中不存在，已跳过。&quot;)
                continue
            video_info = video_map[bvid]
            title = video_info[&apos;title&apos;]
            sanitized_folder = sanitize_filename(title)

            print(f&quot;\n[🔄] 正在重试失败任务: {title} (BVID: {bvid})&quot;)
            if os.path.isdir(sanitized_folder):
                print(&quot;   └── 检测到已下载的文件夹，将直接尝试上传。&quot;)
                process_video(bvid, title, download_first=False)
            else:
                print(&quot;   └── 未检测到文件夹，将重新下载并上传。&quot;)
                process_video(bvid, title, download_first=True)

            processed_bvids.add(bvid)

    print(&quot;\n--- 开始处理新的任务列表 ---&quot;)
    for video in videos_data:
        bvid = video.get(&quot;bvid&quot;)
        title = video.get(&quot;title&quot;)
        if not bvid or not title:
            continue

        if bvid in processed_bvids:
            print(f&quot;ℹ️ {bvid} 已在失败重试阶段处理过，跳过。&quot;)
            continue

        if bvid in successful_bvids:
            print(f&quot;ℹ️ {title} (BVID: {bvid}) 已在之前成功上传，跳过。&quot;)
            continue

        process_video(bvid, title, download_first=True)

    print(&quot;\n\n🎉🎉🎉 所有任务处理完毕！🎉🎉🎉&quot;)
    final_fails = read_log_file(FAIL_LOG_FILE)
    if final_fails:
        print(f&quot;\n⚠️ 注意: 仍有 {len(final_fails)} 个任务失败，请检查 &apos;{FAIL_LOG_FILE}&apos; 文件。&quot;)

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note{title=&quot;注&quot;}
注意代码里的❗符号，需要修改或确认自己需要不需要
:::&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建&lt;code&gt;alist.py&lt;/code&gt;文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nano alist.py
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import requests
import hashlib
import json
import urllib.parse
import os
import time
import threading
import queue
import re
from datetime import datetime
from tqdm import tqdm
import subprocess

class AlistUploader:
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        self.token = None
        self.upload_queue = queue.Queue()
        self.upload_threads = []
        self.max_threads = 6  # 最大并发上传线程数
        self.upload_results = {}  # 存储上传结果
        self.upload_complete_event = threading.Event()

    def get_token(self):
        &quot;&quot;&quot;获取 Alist 的 token&quot;&quot;&quot;
        if self.token:
            return self.token

        url = f&quot;{self.base_url}/api/auth/login/hash&quot;

        password_with_suffix = f&quot;{self.password}-https://github.com/alist-org/alist&quot;
        hashed_password = hashlib.sha256(password_with_suffix.encode()).hexdigest()

        payload = {
            &quot;username&quot;: self.username,
            &quot;password&quot;: hashed_password
        }

        headers = {&quot;Content-Type&quot;: &quot;application/json&quot;}
        response = requests.post(url, headers=headers, data=json.dumps(payload))

        if response.status_code == 200:
            result = response.json()
            if result[&quot;code&quot;] == 200 and result[&quot;message&quot;] == &quot;success&quot;:
                self.token = result[&quot;data&quot;][&quot;token&quot;]
                return self.token

        # print(f&quot;获取 token 失败: {response.text}&quot;)
        tqdm.write(f&quot;获取 token 失败: {response.text}&quot;)
        return None

    def create_new_folder(self, folder_path):
        &quot;&quot;&quot;创建文件夹&quot;&quot;&quot;
        url = f&quot;{self.base_url}/api/fs/mkdir&quot;
        token = self.get_token()
        if not token:
            return False

        headers = {
            &quot;Authorization&quot;: token,
            &quot;Content-Type&quot;: &quot;application/json&quot;
        }
        payload = {
            &quot;path&quot;: folder_path
        }

        response = requests.post(url, headers=headers, json=payload)
        if response.status_code == 200:
            result = response.json()
            if result[&quot;code&quot;] == 200 and result[&quot;message&quot;] == &quot;success&quot;:
                # print(f&quot;新建文件夹成功: {folder_path}&quot;)
                tqdm.write(f&quot;新建文件夹成功: {folder_path}&quot;)
                return True
            else:
                # print(f&quot;新建文件夹失败: {result[&apos;message&apos;]}&quot;)
                tqdm.write(f&quot;新建文件夹失败: {result[&apos;message&apos;]}&quot;)
                return False
        else:
            # print(f&quot;新建文件夹请求失败，状态码: {response.status_code}, 响应内容: {response.text}&quot;)
            tqdm.write(f&quot;新建文件夹请求失败，状态码: {response.status_code}, 响应内容: {response.text}&quot;)
            return False

    def extract_date_user_from_title(self, title):
        &quot;&quot;&quot;从标题中提取日期和用户名&quot;&quot;&quot;
        # 提取用户名
        user_match = re.search(r&apos;【弹幕】(.*?)直播回放&apos;, title)
        user = user_match.group(1) if user_match else &quot;未知用户&quot;

        # 提取日期
        date_match = re.search(r&apos;(\d{4})年(\d{2})月(\d{2})日&apos;, title)
        if date_match:
            year = date_match.group(1)
            month = date_match.group(2)
            day = date_match.group(3)
        else:
            # 尝试其他日期格式
            date_match = re.search(r&apos;(\d{4})[-.年](\d{1,2})[-.月](\d{1,2})&apos;, title)
            if date_match:
                year = date_match.group(1)
                month = date_match.group(2).zfill(2)
                day = date_match.group(3).zfill(2)
            else:
                # 使用当前日期
                now = datetime.now()
                year = str(now.year)
                month = str(now.month).zfill(2)
                day = str(now.day).zfill(2)

        return user, year, month, day

    def generate_folder_path(self, title):
        &quot;&quot;&quot;生成文件夹路径: /115/录播/{{user}}/{{yyyy}}年/{{MM}}-{{dd}}&quot;&quot;&quot;
        user, year, month, day = self.extract_date_user_from_title(title)
        return f&quot;/115/录播/{user}/{year}年/{month}-{day}&quot;


    def upload_file(self, local_file_path, remote_dir_path, as_task=True, retry=0):
        &quot;&quot;&quot;
        上传单个文件到 Alist，通过调用系统 curl 命令实现。
        避免 Python 内存占用，并为并行上传做准备（并行性由调用方管理）。
        &quot;&quot;&quot;
        token = self.get_token()
        if not token:
            tqdm.write(&quot;无法获取 AList 访问令牌。&quot;)
            return False

        file_name = os.path.basename(local_file_path)
        # 确保远程目录路径以 / 结尾，或者不以 / 结尾，然后拼接文件名为 /
        # Alist 的 File-Path 头部需要一个完整的远程文件路径，例如 /path/to/dir/file.txt
        remote_file_path = f&quot;{remote_dir_path.rstrip(&apos;/&apos;)}/{file_name}&quot;

        url = f&quot;{self.base_url}/api/fs/form&quot;

        # 构建 curl 命令
        cmd_parts = [
            &quot;curl&quot;,
            &quot;-X&quot;, &quot;PUT&quot;,  # HTTP 方法为 PUT
            &quot;--http1.1&quot;,
            &quot;--retry&quot;, &quot;3&quot;,               # 失败自动重试3次
            &quot;--retry-delay&quot;, &quot;5&quot;,          # 重试间隔5秒
            &quot;-H&quot;, f&quot;Authorization: {token}&quot;,
            # File-Path 头部的值需要 URL 编码
            &quot;-H&quot;, f&quot;File-Path: {urllib.parse.quote(remote_file_path)}&quot;,
        ]

        if as_task:
            cmd_parts.extend([&quot;-H&quot;, &quot;As-Task: true&quot;])

        cmd_parts.extend([&quot;-s&quot;, &quot;--show-error&quot;, &quot;--fail&quot;])
        cmd_parts.extend([&quot;-F&quot;, f&quot;file=@{local_file_path}&quot;])

        # 添加 URL
        cmd_parts.append(url)

        # 可选：为了调试，你可以添加 -v (verbose) 或 --progress-bar
        # cmd_parts.append(&quot;-v&quot;) # 输出更详细的请求/响应过程
        # cmd_parts.append(&quot;--progress-bar&quot;) # 显示进度条

        try:
            curl_response = subprocess.run(
                cmd_parts,
                capture_output=True, # 捕获 stdout 和 stderr
                text=True,
                check=False, # 不在非零退出码时抛出 CalledProcessError
                encoding=&apos;utf-8&apos;,
            )

            # 优先解析 AList 的 JSON 响应
            result = None
            try:
                if curl_response.stdout: # 只有当 stdout 不为空时才尝试解析
                    result = json.loads(curl_response.stdout)
            except json.JSONDecodeError:
                # 如果 JSON 解析失败，则这是一个错误情况
                error_message = f&quot;无法解析 AList 响应 JSON: {curl_response.stdout.strip() if curl_response.stdout else &apos;无输出&apos;}&quot;
                if curl_response.stderr:
                    error_message += f&quot;\ncURL 错误输出: {curl_response.stderr.strip()}&quot;

                if retry &amp;lt; 3:
                    tqdm.write(f&quot;上传 {file_name} 失败: {error_message}，正在重试...&quot;)
                    return self.upload_file(local_file_path, remote_dir_path, as_task, retry + 1)
                else:
                    tqdm.write(f&quot;上传 {file_name} 最终失败: {error_message}&quot;)
                    return False

            # 根据 AList 的 JSON 响应判断成功或失败
            if result and result.get(&quot;code&quot;) == 200 and result.get(&quot;message&quot;) == &quot;success&quot;:
                # AList 明确表示成功，即使 curl 退出码非零 (如 92)，也视为成功
                if as_task and &apos;task&apos; in result.get(&quot;data&quot;, {}):
                    task_id = result[&quot;data&quot;][&quot;task&quot;][&quot;id&quot;]
                    # tqdm.write(f&quot;文件 {file_name} 上传任务已创建，任务ID: {task_id}&quot;)
                    return self.monitor_upload_task(task_id, local_file_path, remote_dir_path)

                # tqdm.write(f&quot;文件 {file_name} 上传成功。&quot;)
                return True
            else:
                # AList 返回了错误代码或消息，或者没有返回有效的 JSON (已在上层捕获)
                alist_error_message = result.get(&quot;message&quot;, &quot;未知错误或无AList响应&quot;) if result else &quot;无AList响应或解析失败&quot;

                # 组合 AList 的错误信息和 curl 的退出码/错误输出，用于日志
                full_error_detail = f&quot;AList错误: {alist_error_message}. &quot;
                if curl_response.returncode != 0:
                    full_error_detail += f&quot;curl退出码: {curl_response.returncode}. &quot;
                if curl_response.stderr:
                    full_error_detail += f&quot;cURL错误输出: {curl_response.stderr.strip()}.&quot;

                if retry &amp;lt; 3:
                    tqdm.write(f&quot;上传 {file_name} 失败: {full_error_detail}，正在重试...&quot;)
                    return self.upload_file(local_file_path, remote_dir_path, as_task, retry + 1)
                else:
                    tqdm.write(f&quot;上传 {file_name} 最终失败: {full_error_detail}&quot;)
                    return False

        except FileNotFoundError:
            # curl 命令本身没有找到
            tqdm.write(f&quot;错误: curl 命令未找到。请确保 curl 已安装并配置在系统 PATH 中。&quot;)
            return False
        except Exception as e:
            # 捕获其他任何意外的Python异常
            if retry &amp;lt; 3:
                tqdm.write(f&quot;上传文件 {file_name} 时出错: {str(e)}，正在重试...&quot;)
                return self.upload_file(local_file_path, remote_dir_path, as_task, retry + 1)
            else:
                tqdm.write(f&quot;上传文件 {file_name} 时出错: {str(e)}&quot;)
                return False

    def get_task_info(self, task_id):
        &quot;&quot;&quot;获取指定任务的信息&quot;&quot;&quot;
        token = self.get_token()
        if not token:
            return None

        url = f&quot;{self.base_url}/api/task/upload/info&quot;

        params = {&quot;tid&quot;: task_id}
        headers = {&quot;Authorization&quot;: token}

        response = requests.post(url, headers=headers, params=params)

        if response.status_code == 200:
            result = response.json()
            if result[&quot;code&quot;] == 200 and result[&quot;message&quot;] == &quot;success&quot;:
                return result[&quot;data&quot;]
            else:
                # print(f&quot;获取任务信息失败: {result[&apos;message&apos;]}&quot;)
                tqdm.write(f&quot;获取任务信息失败: {result[&apos;message&apos;]}&quot;)
                return None
        else:
            tqdm.write(f&quot;获取任务信息失败，状态码: {response.status_code}, 响应内容: {response.text}&quot;)
            return None

    def monitor_upload_task(self, task_id, local_file_path=None, remote_dir_path=None, poll_interval=2, timeout=7200, retry=0):
        &quot;&quot;&quot;
        监控任务进度，直到任务完成，失败时支持重试

        参数:
            task_id: 任务ID
            local_file_path: 本地文件路径，用于重试
            remote_dir_path: 远程目录路径，用于重试
            poll_interval: 轮询间隔（秒）
            timeout: 超时时间（秒）
            retry: 剩余重试次数

        返回:
            成功时返回任务信息，失败且重试次数用完时返回False
        &quot;&quot;&quot;
        from tqdm import tqdm

        start_time = time.time()
        pbar = None

        try:
            while time.time() - start_time &amp;lt; timeout:
                task_info = self.get_task_info(task_id)

                if not task_info:
                    time.sleep(poll_interval)
                    continue

                # 获取进度
                progress = task_info.get(&apos;progress&apos;, 0)
                state = task_info.get(&apos;state&apos;, &apos;&apos;)

                # 初始化进度条
                if pbar is None:
                    # pbar = tqdm(total=100, desc=f&quot;上传任务 {task_id}&quot;, unit=&quot;%&quot;)
                    pbar = tqdm(total=100, desc=f&quot;{local_file_path.split(&apos;/&apos;)[-1]} 上传中&quot;, unit=&quot;%&quot;)

                # 更新进度条
                pbar.n = int(progress * 100)
                pbar.refresh()

                # 检查任务是否完成
                state = task_info.get(&apos;state&apos;, &apos;&apos;)
                error = task_info.get(&apos;error&apos;, &apos;&apos;)

                if state == 2 and progress &amp;gt;= 1.0:
                    # tqdm.write(f&quot;任务完成！总共传输: {task_info.get(&apos;total_bytes&apos;, 0)} 字节&quot;)
                    return True
                elif state == 2 and progress == 0:
                    # print(&quot;任务已完成，秒传成功&quot;)
                    # tqdm.write(&quot;任务已完成，秒传成功&quot;)
                    return True
                elif error:
                    retry += 1
                    # 如果有错误且有重试次数，尝试重新上传
                    if retry &amp;lt; 4 and local_file_path and remote_dir_path and os.path.exists(local_file_path):
                        pbar.close()
                        pbar = None
                        # print(f&quot;任务失败: {error}，剩余重试次数: {4 - retry}&quot;)
                        tqdm.write(f&quot;任务失败: {error}，剩余重试次数: {4 - retry}&quot;)

                        # 重新上传文件
                        file_name = os.path.basename(local_file_path)
                        remote_file_path = f&quot;{remote_dir_path}/{file_name}&quot;

                        url = f&quot;{self.base_url}/api/fs/form&quot;
                        headers = {
                            &quot;Authorization&quot;: self.get_token(),
                            &quot;File-Path&quot;: urllib.parse.quote(remote_file_path),
                            &quot;As-Task&quot;: &quot;true&quot;,
                            &quot;Content-Length&quot;: str(os.path.getsize(local_file_path))
                        }

                        with open(local_file_path, &apos;rb&apos;) as file:
                            # 核心修改：重试时也使用 data 参数进行流式上传
                            response = requests.put(url, headers=headers, data=file)

                        if response.status_code == 200:
                            result = response.json()
                            if result[&quot;code&quot;] == 200 and result[&quot;message&quot;] == &quot;success&quot; and &apos;task&apos; in result[&quot;data&quot;]:
                                new_task_id = result[&quot;data&quot;][&quot;task&quot;][&quot;id&quot;]
                                tqdm.write(f&quot;重新创建上传任务，新任务ID: {new_task_id}&quot;)
                                # 递归调用自身监控新任务，减少重试次数
                                return self.monitor_upload_task(
                                    new_task_id,
                                    local_file_path,
                                    remote_dir_path,
                                    poll_interval,
                                    timeout - (time.time() - start_time),  # 剩余超时时间
                                    retry - 1
                                )

                    elif retry &amp;gt;= 4:
                        tqdm.write(f&quot;任务失败: {error}，所有重试次数已用完&quot;)
                        return False
                    else:
                        tqdm.write(f&quot;任务失败: {error}，无法重试&quot;)
                        return False

                time.sleep(poll_interval)

            tqdm.write(f&quot;\n监控超时，已经等待 {timeout} 秒&quot;)
            return False
        finally:
            # 确保进度条被关闭
            if pbar is not None:
                pbar.close()

    def _worker(self):
        &quot;&quot;&quot;工作线程函数，处理上传队列中的任务&quot;&quot;&quot;
        while True:
            try:
                file_info = self.upload_queue.get(block=False)
                if file_info is None:  # 结束信号
                    self.upload_queue.task_done()
                    break

                local_file_path, remote_dir_path = file_info
                file_name = os.path.basename(local_file_path)

                # tqdm.write(f&quot;开始上传文件: {file_name} 到 {remote_dir_path}&quot;)
                success = self.upload_file(local_file_path, remote_dir_path)

                # 记录上传结果
                self.upload_results[local_file_path] = success

                # tqdm.write(f&quot;文件 {file_name} {&apos;上传成功&apos; if success else &apos;上传失败&apos;}&quot;)
                self.upload_queue.task_done()
            except queue.Empty:
                # 队列为空，检查是否所有任务已完成
                if self.upload_queue.unfinished_tasks == 0:
                    break
                time.sleep(0.5)
            except Exception as e:
                tqdm.write(f&quot;上传线程出错: {str(e)}&quot;)
                self.upload_queue.task_done()

    def upload_files(self, file_paths, remote_dir_path):
        &quot;&quot;&quot;
        异步上传多个文件到 Alist 的指定目录

        参数:
            file_paths: 本地文件路径列表
            remote_dir_path: 远程目录路径

        返回:
            一个事件对象，可以用于等待上传完成
        &quot;&quot;&quot;
        # 清空之前的结果和事件状态
        self.upload_results = {}
        self.upload_complete_event.clear()

        # 将所有文件加入上传队列
        for file_path in file_paths:
            self.upload_queue.put((file_path, remote_dir_path))

        # 启动工作线程
        self.upload_threads = []
        for _ in range(min(self.max_threads, len(file_paths))):
            thread = threading.Thread(target=self._worker)
            thread.daemon = True
            thread.start()
            self.upload_threads.append(thread)

        # 启动监控线程，等待所有上传完成
        monitor_thread = threading.Thread(target=self._monitor_upload_completion)
        monitor_thread.daemon = True
        monitor_thread.start()

        return self.upload_complete_event

    def _monitor_upload_completion(self):
        &quot;&quot;&quot;监控线程，等待所有上传任务完成&quot;&quot;&quot;
        for thread in self.upload_threads:
            thread.join()
        self.upload_complete_event.set()

        # 汇总上传结果
        total_files = len(self.upload_results)
        success_count = sum(1 for success in self.upload_results.values() if success)
        tqdm.write(f&quot;\n上传完成: {success_count}/{total_files} 文件上传成功&quot;)

    def all_uploads_successful(self):
        &quot;&quot;&quot;检查所有文件是否都上传成功&quot;&quot;&quot;
        if not self.upload_results:
            return False
        return all(self.upload_results.values())

# 测试代码
if __name__ == &quot;__main__&quot;:
    uploader = AlistUploader(
        base_url=&quot;http://xxxxx&quot;,
        username=&quot;xxxxx&quot;,
        password=&quot;xxxxx&quot;
    )

    # 测试创建文件夹
    test_folder = &quot;/115/录播/测试用户/2025年/05-31&quot;
    uploader.create_new_folder(test_folder)

    # 测试上传文件
    test_files = [&quot;/config/Desktop/bilibili.tar.gz&quot;]
    event = uploader.upload_files(test_files, test_folder)

    # 等待上传完成
    event.wait()
    tqdm.write(f&quot;所有文件上传完成，全部成功: {uploader.all_uploads_successful()}&quot;)
    ```

&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;配置Docker容器&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;创建&lt;code&gt;Dockerfile&lt;/code&gt;和&lt;code&gt;docker-compose.yaml&lt;/code&gt;&lt;pre&gt;&lt;code&gt;cd .. &amp;amp;&amp;amp; nano Dockerfile &amp;amp;&amp;amp; nano docker-compose.yaml`
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;FROM python:3.10-bookworm

WORKDIR /app

# 安装aria2和对应python包
RUN apt update &amp;amp;&amp;amp; \
    apt install -y \
    aria2 &amp;amp;&amp;amp; \
    apt clean &amp;amp;&amp;amp; \
    rm -rf /var/lib/apt/lists/* &amp;amp;&amp;amp; \
    pip install --no-cache-dir tqdm requests


CMD [&quot;python3&quot;, &quot;bilivideosync.py&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;services:
  bilivideosync:
    build: .
    volumes:
      - ./data:/app
    tty: true # 必须保留，否则第一个视频下载并同步到网盘结束后，无法输入所需的上传到B站的分区ID，视频简介和视频标签
    stdin_open: true # 必须保留，否则第一个视频下载并同步到网盘结束后，无法输入所需的上传到B站的分区ID，视频简介和视频标签
    network_mode: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;启动容器&lt;code&gt;docker compose up -d&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;启动容器后，输入&lt;code&gt;docker compose attach --sig-proxy=false bilivideosync&lt;/code&gt;进入容器内交互式终端，等待输入&lt;code&gt;分区ID&lt;/code&gt;的提示出现，分别填写完分区ID、视频简介和视频Tags后，即可退出，后续会复用第一次填写的信息&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>使用Moviepilot实现115网盘+Emby Strm播放</title><link>https://blog.useforall.com/posts/15/</link><guid isPermaLink="true">https://blog.useforall.com/posts/15/</guid><description>使用Docker部署Moviepilot，并通过其中的115网盘STRM助手实现strm整理及刮削、MediaWrap插件进行302重定向播放。（注意，此教程更加适用于PT站已经毕业了的人使用）</description><pubDate>Wed, 11 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
使用Docker部署Moviepilot，并通过其中的115网盘STRM助手实现strm整理及刮削、MediaWrap插件进行302重定向播放。（注意，此教程更加适用于PT站已经毕业了的人使用）
:::&lt;/p&gt;
&lt;p&gt;前段时间购买了115永V，加上家里NAS内存实际上其实不够（本人有点点仓鼠症），想着与其买那么多硬盘不如直接上网盘了。本来想着白嫖OneDrive的E5账号，奈何直连效果确实不佳，且不能配合MP自动下资源（本人懒，追更找资源太麻烦）。我整套流程均部署在腾讯云香港服务器上（2C8G），性能完全够用，本文也会提供海外VPS被115封控导致连接&lt;code&gt;webapi.115.com&lt;/code&gt;报错&lt;code&gt;403&lt;/code&gt;的问题。废话不多说，直接开始。&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Moviepilot（MP）&lt;/strong&gt;：搜索PT站资源下载，自动追更剧集电影等。详细请看官方&lt;a href=&quot;https://wiki.movie-pilot.org/&quot;&gt;WIKI&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;qBittorrent（Qbit）&lt;/strong&gt;：种子下载客户端。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Emby&lt;/strong&gt;：家庭媒体服务器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nginx Proxy Manager&lt;/strong&gt;：反向代理 MP、Qbit、Emby。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clash + proxychains-ng&lt;/strong&gt;：解决海外 VPS 直连 115 网盘 Web API 被风控导致的 403 问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;配置容器&lt;/h2&gt;
&lt;h3&gt;基本配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;创建对应文件夹和文件&lt;pre&gt;&lt;code&gt;mkdir -p /docker_data/media &amp;amp;&amp;amp; cd docker_data/media &amp;amp;&amp;amp; mkdir video
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;编辑&lt;code&gt;docker-compose.yaml&lt;/code&gt;&lt;pre&gt;&lt;code&gt;nano docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;services:
  qbit:
    image: linuxserver/qbittorrent:4.6.5
    container_name: qbit
    network_mode: bridge
    environment:
      - PUID=0
      - PGID=0
      - TZ=Asia/Shanghai
      - WEBUI_PORT=8000 # WEBUI端口号，改成你想要的。同下。
      - TORRENTING_PORT=32156 # 做种端口号，改成你需要的，例如改成8080，那么ports mapping部分要改成xxxx:8080
      - QB_PASSWORD=xxxxxx # WEBUI登录密码
      - QB_USERNAME=xxxxxx # WEBUI登录用户名
    volumes:
      - ./qbit:/config
      - ./video:/video # 媒体目录。如果Map别的本地的目录，下面所有媒体目录部分也要跟着一起改。建议别动
    ports:
      - 65036:8000
      - 32156:32156
    restart: unless-stopped

  emby:
    image: amilys/embyserver:latest
    container_name: emby
    network_mode: bridge
    restart: unless-stopped
    ports:
      - 65037:8096
      - 65038:8920
      - 65039:7359/udp
      - 65040:1900/udp
    volumes:
      - ./emby:/config
      - ./video:/video # 同Qbit媒体目录。若上面修改，这里请同步修改
    environment:
      - PUID=0
      - PGID=0
      - GIDLIST=0
      - TZ=Asia/Shanghai
      - EMBY_PublishedServerUrl=xxx.xxx.xxx.xxx # 修改你成本地IPV4
    privileged: true

  moviepilotv2:
    #image: jxxghp/moviepilot-v2:latest
    build:
      context: .
      dockerfile: Dockerfile.mp # 解决海外VPS无法访问webapi.115.com问题，重新打包一下镜像
    restart: always
    stdin_open: true
    tty: true
    container_name: moviepilot-v2
    hostname: moviepilot-v2
    network_mode: bridge
    ports:
      - 65041:3000
      - 65042:65042 # ！！如果稍后在MP的MediaWarp环节设置的端口不是65042，这里请记得修改！！
    volumes:
      - ./video:/video # 同Qbit媒体目录。若上面修改，这里请同步修改
      - ./moviepilot:/config
      - ./mp-core:/moviepilot/.cache/ms-playwright
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./proxychains.conf:/etc/proxychains.conf:ro # 使用proxychains4强制访问国内的代理绕过海外VPS封控限制
    environment:
      - NGINX_PORT=3000
      - PORT=3001
      - PUID=0
      - PGID=0
      - UMASK=000
      - SUPERUSER=改成你的用户名
      - TZ=Asia/Shanghai
      - AUTH_SITE=leaves # 参考MP WIKI设置认证站点，否则无法使用
      - LEAVES_UID=xxxxxxx #UID
      - LEAVES_PASSKEY=xxxxxxx #passkey
      - GITHUB_TOKEN=xxxxx # 设置Github Token防止获取插件过于频繁导致风控
    depends_on:
      - clash

  clash:
    image: dreamacro/clash
    network_mode: bridge
    restart: always
    volumes:
      - ./clash:/root/.config/clash
    container_name: clash
    ports:
      # 主机http代理端口:容器http代理端口，默认7890
      # 主机sock5代理端口:容器sock5代理端口，默认7890
      # 主机REST API端口:容器REST API端口，默认9090
      - 7890:7890
      - 7891:9090
&lt;/code&gt;&lt;/pre&gt;
:::note{title=&quot;为什么要有Clash还要proxychains4？&quot;}
尽管你可以给&lt;code&gt;MP&lt;/code&gt;容器配置http_proxy、https_proxy等环境变量来一定程度上接管流量，但是实测，只有&lt;code&gt;MP&lt;/code&gt;自己的&lt;code&gt;115 OpenAPI&lt;/code&gt;会遵循环境代理，而&lt;code&gt;MP&lt;/code&gt;里的&lt;code&gt;115网盘STRM助手&lt;/code&gt;这个插件（里面的&lt;a href=&quot;https://github.com/ChenyangGao/p115client&quot;&gt;p115client&lt;/a&gt;这个python依赖）不honor环境变量，导致请求&lt;code&gt;webapi.115.com&lt;/code&gt;依然不走回国代理。所以出此下策使用proxychains4强行接管&lt;code&gt;MP&lt;/code&gt;容器流量到&lt;code&gt;Clash&lt;/code&gt;容器。
:::&lt;/li&gt;
&lt;li&gt;编辑&lt;code&gt;Dockerfile.mp&lt;/code&gt;、&lt;code&gt;entrypoint-wrapper.sh&lt;/code&gt;、&lt;code&gt;proxychains.conf&lt;/code&gt;&lt;pre&gt;&lt;code&gt;nano Dockerfile.mp
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;FROM jxxghp/moviepilot-v2:latest

# 切换到 root 用户以安装软件包
USER root

# 更新包列表并安装 proxychains-ng
# jxxghp/moviepilot-v2 通常基于 Debian/Ubuntu
RUN apt-get update &amp;amp;&amp;amp; \
    apt-get install -y proxychains-ng &amp;amp;&amp;amp; \
    rm -rf /var/lib/apt/lists/*

# 复制包装器脚本到镜像中
COPY entrypoint-wrapper.sh /entrypoint-wrapper.sh
RUN chmod +x /entrypoint-wrapper.sh

# 使用包装器脚本作为新的入口点
# 原镜像的 ENTRYPOINT 是 [&quot;/entrypoint.sh&quot;]
# 我们用 proxychains4 来执行它
ENTRYPOINT [&quot;/entrypoint-wrapper.sh&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;nano entrypoint-wrapper.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh
# 使用 proxychains4 执行原始的 /entrypoint.sh 脚本，并传递所有参数
exec proxychains4 /entrypoint.sh &quot;$@&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;nano proxychains.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# /etc/proxychains.conf
strict_chain
proxy_dns

localnet 127.0.0.0/8
localnet 172.16.0.0/12
localnet 10.0.0.0/8 # 改为你本地的IPV4 CIDR

[ProxyList]
http 172.17.0.1 7890
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;配置&lt;code&gt;clash&lt;/code&gt;&lt;pre&gt;&lt;code&gt;mkdir clash &amp;amp;&amp;amp; nano ./clash/config.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;mixed-port: 7890
allow-lan: true
mode: Rule
log-level: info
#external-ui: dashboard
external-controller: &quot;0.0.0.0:9090&quot;
proxies:
  - {name: test, server: xxxxx.com, port: 2054, type: ss, cipher: aes-256-gcm, password: xxxx} # 替换成你自己的代理，这里只是举例
proxy-groups:
  - name: PROXY
    type: select
    proxies:
      - test
rules:
  - DOMAIN-KEYWORD,115.com,PROXY
  - MATCH,DIRECT
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;启动服务&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;配置&lt;code&gt;Qbit&lt;/code&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;进入WEBUI界面，如图进行配置
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/12/6849b0afbaa1a.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;配置&lt;code&gt;MP&lt;/code&gt;&lt;/h2&gt;
&lt;h3&gt;基本配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;配置下载器。请一定记得打开&lt;code&gt;自动分类管理&lt;/code&gt;和&lt;code&gt;顺序下载&lt;/code&gt;。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/12/684abb66aee0b.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;配置媒体服务器，存储&amp;amp;目录
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/12/684abd1dda437.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/12/684ac4747bb6c.png&quot; alt=&quot;image.png&quot; /&gt;
登录完成显示容量后再进行下一步目录配置。自动整理请一定记得改为&lt;code&gt;下载器监控&lt;/code&gt;模式。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/12/684ac53778f66.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;插件配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;配置插件源。如图复制粘贴。确认后，等待一会，然后搜索插件&lt;code&gt;115网盘STRM助手&lt;/code&gt;和&lt;code&gt;MediaWarp&lt;/code&gt;这两个插件，安装即可。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/12/684ace64695a1.png&quot; alt=&quot;image.png&quot; /&gt;&lt;pre&gt;&lt;code&gt;https://github.com/jxxghp/MoviePilot-Plugins/
https://github.com/thsrite/MoviePilot-Plugins/
https://github.com/honue/MoviePilot-Plugins/
https://github.com/InfinityPacer/MoviePilot-Plugins/
https://github.com/dandkong/MoviePilot-Plugins/
https://github.com/Aqr-K/MoviePilot-Plugins/
https://github.com/AnjoyLi/MoviePilot-Plugins/
https://github.com/WithdewHua/MoviePilot-Plugins/
https://github.com/HankunYu/MoviePilot-Plugins/
https://github.com/baozaodetudou/MoviePilot-Plugins/
https://github.com/almus2zhang/MoviePilot-Plugins/
https://github.com/Pixel-LH/MoviePilot-Plugins/
https://github.com/lightolly/MoviePilot-Plugins/
https://github.com/suraxiuxiu/MoviePilot-Plugins/
https://github.com/gxterry/MoviePilot-Plugins/
https://github.com/hotlcc/MoviePilot-Plugins-Third/
https://github.com/boeto/MoviePilot-Plugins/
https://github.com/xiangt920/MoviePilot-Plugins/
https://github.com/yubanmeiqin9048/MoviePilot-Plugins/
https://github.com/loongcheung/MoviePilot-Plugins/
https://github.com/xcehnz/MoviePilot-Plugins/
https://github.com/imaliang/MoviePilot-Plugins/
https://github.com/wikrin/MoviePilot-Plugins/
https://github.com/DDS-Derek/MoviePilot-Plugins/
https://github.com/KoWming/MoviePilot-Plugins
https://github.com/madrays/MoviePilot-Plugins
https://github.com/aClarkChen/MoviePilot-Plugins
https://github.com/justzerock/MoviePilot-Plugins
https://github.com/Seed680/MoviePilot-Plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;配置&lt;code&gt;115网盘STRM助手&lt;/code&gt;。记得选上媒体服务器。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/12/684ac5ff0719c.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/12/684ac6c26e856.png&quot; alt=&quot;image.png&quot; /&gt;
:::note{title=&quot;注&quot;}
如果你没改过&lt;code&gt;docker-composeyaml&lt;/code&gt;里的volume mapping的话，这里不需要填写&lt;code&gt;媒体库服务器映射目录&lt;/code&gt;# &lt;code&gt;MP映射目录&lt;/code&gt;。这里具体什么意思呢？如果你把本地的&lt;code&gt;./video&lt;/code&gt;映射为了&lt;code&gt;MP&lt;/code&gt;容器里的&lt;code&gt;/video&lt;/code&gt;，但是在&lt;code&gt;Emby&lt;/code&gt;容器里是例如：&lt;code&gt;./video:/media&lt;/code&gt;，那么这里就需要填写了。&lt;code&gt;媒体库服务器映射目录&lt;/code&gt;填写&lt;code&gt;/media&lt;/code&gt;，&lt;code&gt;MP映射目录&lt;/code&gt;填写&lt;code&gt;/video&lt;/code&gt;。否则会导致插件刷新媒体库失败。
:::&lt;/li&gt;
&lt;li&gt;配置&lt;code&gt;MediaWarp&lt;/code&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/12/684ac8fb12858.png&quot; alt=&quot;image.png&quot; /&gt;
:::note{title=&quot;注&quot;}
注意，这里的端口的意思是你后期进入Emby的实际端口，设置后，访问&lt;code&gt;http://xxx.xxx.xxx:65042&lt;/code&gt;即可进入&lt;code&gt;Emby&lt;/code&gt;界面。如果你这里修改为了别的端口，记得同步修改&lt;code&gt;docker-compose.yaml&lt;/code&gt;里&lt;code&gt;MP&lt;/code&gt;容器部分的port mapping设置。
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;NPM反代（应该不需要介绍了吧🙃，有点懒，写不动了）&lt;/h2&gt;
&lt;h2&gt;后续使用&lt;/h2&gt;
&lt;p&gt;至此，配置全部完成。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 MP 中搜索资源，自动下载至本地 &lt;code&gt;./video/downloads&lt;/code&gt;，&lt;code&gt;MP&lt;/code&gt;检测下载器下载完成，自动推送视频到115网盘，并根据分类自动在网盘创建对应文件夹。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;115网盘STRM助手&lt;/code&gt;会在本地&lt;code&gt;./video/strm&lt;/code&gt;自动创建对应目录，生成strm文件和刮削文件。&lt;/li&gt;
&lt;li&gt;至于&lt;code&gt;Emby&lt;/code&gt;添加媒体库乱七八糟的就不过多赘述了，&lt;code&gt;docker-compose.yaml&lt;/code&gt;里的&lt;code&gt;Emby&lt;/code&gt;镜像默认是开心版，自带神医插件。（因为我没怎么使用过神医插件，也就不介绍了）&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>获取onedrive token替换alist闭源api获取的token</title><link>https://blog.useforall.com/posts/14/</link><guid isPermaLink="true">https://blog.useforall.com/posts/14/</guid><description>众所周知，alist悄咪咪把项目卖了半年，今天才发消息通知用户。为了不用alist不开源的api（因为不知道那个api都会干什么），用gpt写了个python脚本</description><pubDate>Wed, 11 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;众所周知，alist悄咪咪把项目卖了半年，今天才发消息通知用户。为了不用alist不开源的api（因为不知道那个api都会干什么），用gpt写了个python脚本&lt;/p&gt;
&lt;p&gt;运行方法是你要有python环境。然后&lt;code&gt;python alist-onedrive.py&lt;/code&gt;一下了。
感谢&lt;a href=&quot;https://www.nodeseek.com/space/139#/general&quot;&gt;Falling42&lt;/a&gt;提供的办法，原贴：&lt;a href=&quot;https://www.nodeseek.com/post-361455-1&quot;&gt;自动获取onedrive配置并添加alist存储&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;p&gt;去 https://portal.azure.com 用你想要挂载onedrive的E5或者个人账户登录&lt;/p&gt;
&lt;p&gt;然后去 https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade 创建应用程序&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/68497c548e103.png&quot; alt=&quot;1749646418249.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意
受支持的账户类型选 &lt;code&gt;任何组织目录(任何 Microsoft Entra ID 租户 - 多租户)中的帐户和个人 Microsoft 帐户(例如 Skype、Xbox) &lt;/code&gt;
重定向URI 选 Web 是 http://localhost:53682/&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/68497eeb85cf5.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后记住这个 是 &lt;code&gt;client_id&lt;/code&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/68497f6807544.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后添加客户端密码&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/6849803978955.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;记住这个东西是 &lt;code&gt;client_secret&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/684980b6767d6.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后去添加API权限 &lt;code&gt;Files.ReadWrite.All&lt;/code&gt; &lt;code&gt;offline_access &lt;/code&gt; &lt;code&gt;Sites.Read.All&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/68498277b17b1.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/6849829c1865b.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/684982efc5d66.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/68498339293bf.png&quot; alt=&quot;image.png&quot; /&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/68498376a6e7a.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后运行py脚本就行了&lt;/p&gt;
&lt;p&gt;输入区可以写在&lt;code&gt;or&lt;/code&gt;后面 也可以按照命令行提示输入&lt;/p&gt;
&lt;p&gt;脚本会打开默认浏览器 接受权限&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/68497d578cf73.png&quot; alt=&quot;1749646677213.png&quot; /&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/68497d6ccca2b.png&quot; alt=&quot;1749646699378.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后就成功了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/68497d7ab42ae.png&quot; alt=&quot;1749646712824.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;遇到这个问题把代理全退出就好了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/06/11/68497d8e12907.png&quot; alt=&quot;1749646732337.png&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import http.server
import webbrowser
import urllib.parse
import requests
import json
import os

# ========== 输入区 ==========
CLIENT_ID = input(&quot;🔑 请输入 Azure client_id: &quot;).strip() or &quot; client_id&quot;
CLIENT_SECRET = input(&quot;🔒 请输入 Azure client_secret: &quot;).strip() or &quot;client_secret&quot;
ALIST_REMARK = input(&quot;📝 Alist 挂载备注（如 OneDrive）: &quot;).strip() or &quot;My OneDrive&quot;
ALIST_PATH = input(&quot;📂 Alist 显示路径（如 /onedrive）: &quot;).strip() or &quot;/onedrive&quot;
ONEDRIVE_ROOT = input(&quot;📁 OneDrive 根路径（如 / 或 /某个子目录）: &quot;).strip() or &quot;/&quot;
ALIST_URL = input(&quot;📁  Alist 服务地址: &quot;).strip() or &quot;http://localhost:5244&quot;
ALIST_USER = input(&quot;👤 Alist 管理员用户名: &quot;).strip() or &quot;admin&quot;
ALIST_PASS = input(&quot;🔑 Alist 管理员密码: &quot;).strip() or &quot;123456&quot;

# ========== 常量区 ==========
REDIRECT_URI = &quot;http://localhost:53682/&quot;
SCOPES = &quot;Files.ReadWrite.All offline_access User.Read Sites.Read.All&quot;
TOKEN_URL = &quot;https://login.microsoftonline.com/common/oauth2/v2.0/token&quot;
CONFIG_OUTPUT_PATH = &quot;alist_onedrive_config.json&quot;

auth_code = None

class OAuthHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        global auth_code
        params = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
        if &quot;code&quot; in params:
            auth_code = params[&quot;code&quot;][0]
            self.send_response(200)
            self.send_header(&quot;Content-type&quot;, &quot;text/html; charset=utf-8&quot;)
            self.end_headers()
            self.wfile.write(&quot;&amp;lt;h1&amp;gt;✅ 授权成功！请返回终端。&amp;lt;/h1&amp;gt;&quot;.encode(&quot;utf-8&quot;))
        else:
            self.send_response(400)
            self.send_header(&quot;Content-type&quot;, &quot;text/html; charset=utf-8&quot;)
            self.end_headers()
            self.wfile.write(&quot;&amp;lt;h1&amp;gt;❌ 授权失败，未获取到 code。&amp;lt;/h1&amp;gt;&quot;.encode(&quot;utf-8&quot;))


def start_server():
    server = http.server.HTTPServer((&apos;localhost&apos;, 53682), OAuthHandler)
    server.handle_request()

def get_alist_token(alist_url: str, username: str, password: str, otp: str = None) -&amp;gt; str:
    login_url = f&quot;{alist_url}/api/auth/login&quot;
    payload = {
        &quot;username&quot;: username,
        &quot;password&quot;: password
    }
    if otp:
        payload[&quot;otp_code&quot;] = otp

    res = requests.post(login_url, json=payload)
    if res.status_code != 200:
        raise Exception(&quot;❌ 无法连接 Alist 登录接口&quot;)
    data = res.json()
    if data.get(&quot;code&quot;) != 200:
        raise Exception(f&quot;❌ 登录失败：{data.get(&apos;message&apos;)}&quot;)
    return data[&quot;data&quot;][&quot;token&quot;]

def main():
    print(&quot;🌐 打开浏览器进行 OneDrive 授权...&quot;)
    auth_url = (
        f&quot;https://login.microsoftonline.com/common/oauth2/v2.0/authorize&quot;
        f&quot;?client_id={CLIENT_ID}&quot;
        f&quot;&amp;amp;response_type=code&quot;
        f&quot;&amp;amp;redirect_uri={urllib.parse.quote(REDIRECT_URI)}&quot;
        f&quot;&amp;amp;scope={urllib.parse.quote(SCOPES)}&quot;
    )
    webbrowser.open(auth_url)
    start_server()

    if not auth_code:
        print(&quot;❌ 未获取到授权码。&quot;)
        return

    print(&quot;🔄 获取 refresh_token 中...&quot;)
    data = {
        &quot;client_id&quot;: CLIENT_ID,
        &quot;client_secret&quot;: CLIENT_SECRET,
        &quot;code&quot;: auth_code,
        &quot;redirect_uri&quot;: REDIRECT_URI,
        &quot;grant_type&quot;: &quot;authorization_code&quot;
    }
    response = requests.post(TOKEN_URL, data=data)
    if response.status_code != 200:
        print(&quot;❌ 获取 token 失败：&quot;)
        print(response.text)
        return

    tokens = response.json()
    refresh_token = tokens.get(&quot;refresh_token&quot;)
    if not refresh_token:
        print(&quot;❌ token 响应中未找到 refresh_token。&quot;)
        return

    # 获取 Alist 管理 token
    TWO_FA_CODE = input(&quot;📲 请输入两步验证码（如启用）[可留空]: &quot;).strip()
    print(&quot;🔐 正在登录 Alist...&quot;)
    alist_token = get_alist_token(ALIST_URL, ALIST_USER, ALIST_PASS, TWO_FA_CODE)

    # 构造符合 Alist API 要求格式的配置
    payload = {
        &quot;mount_path&quot;: ALIST_PATH,
        &quot;order&quot;: 0,
        &quot;remark&quot;: ALIST_REMARK,
        &quot;cache_expiration&quot;: 30,
        &quot;web_proxy&quot;: False,
        &quot;webdav_policy&quot;: &quot;302_redirect&quot;,
        &quot;down_proxy_url&quot;: &quot;&quot;,
        &quot;order_by&quot;: &quot;&quot;,
        &quot;order_direction&quot;: &quot;&quot;,
        &quot;extract_folder&quot;: &quot;&quot;,
        &quot;enable_sign&quot;: False,
        &quot;driver&quot;: &quot;Onedrive&quot;,
        &quot;addition&quot;: json.dumps({
            &quot;root_folder_path&quot;: ONEDRIVE_ROOT,
            &quot;region&quot;: &quot;global&quot;,
            &quot;is_sharepoint&quot;: False,
            &quot;client_id&quot;: CLIENT_ID,
            &quot;client_secret&quot;: CLIENT_SECRET,
            &quot;redirect_uri&quot;: REDIRECT_URI,
            &quot;refresh_token&quot;: refresh_token,
            &quot;site_id&quot;: &quot;&quot;,
            &quot;chunk_size&quot;: 5,
            &quot;custom_host&quot;: &quot;&quot;
        }, ensure_ascii=False)
    }

    print(&quot;🚀 向 Alist 推送挂载配置...&quot;)
    api_url = f&quot;{ALIST_URL}/api/admin/storage/create&quot;
    headers = {
        &quot;Content-Type&quot;: &quot;application/json&quot;,
        &quot;Authorization&quot;: alist_token
    }
    res = requests.post(api_url, json=payload, headers=headers)

    if res.status_code == 200 and res.json().get(&quot;code&quot;) == 200:
        print(&quot;✅ 成功添加 OneDrive 挂载！请在 Alist 后台查看。&quot;)
    else:
        print(&quot;❌ 推送失败：&quot;)
        print(res.text)

    # 同时写入配置文件备份
    print(f&quot;💾 正在写入配置到 {CONFIG_OUTPUT_PATH}...&quot;)
    with open(CONFIG_OUTPUT_PATH, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
        json.dump(payload, f, ensure_ascii=False, indent=2)


if __name__ == &quot;__main__&quot;:
    main()

&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Docker搭建Node Foward Bot(NFD) TG机器人</title><link>https://blog.useforall.com/posts/12/</link><guid isPermaLink="true">https://blog.useforall.com/posts/12/</guid><description>使用Docker本地搭建NFD Telegram机器人，替代原始的Cloudflare方法。</description><pubDate>Sat, 15 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
使用Docker本地搭建NFD Telegram机器人，替代原始的Cloudflare方法。
:::&lt;/p&gt;
&lt;p&gt;原始&lt;a href=&quot;https://github.com/LloydAsp/nfd&quot;&gt;NFD&lt;/a&gt;项目是通过Cloudflare Worker进行搭建的，使用了&lt;a href=&quot;https://developers.cloudflare.com/kv/&quot;&gt;Cloudflare kv&lt;/a&gt;作为数据存储方案，而kv有每天1000条写入的&lt;a href=&quot;https://developers.cloudflare.com/kv/platform/limits/&quot;&gt;配额限制&lt;/a&gt;。此本地搭建方式使用sqlite代替kv数据库，以避免每日限额问题。&lt;/p&gt;
&lt;p&gt;为什么不使用Livegram Bot？因为这个平台最近鬼迷心窍，开始用Livegram Bot给用户群发广告，几乎所有使用者受到影响，取消广告需要购买付费套餐，不买的话，你的机器人就变成tg的发广告工具了，着实抽象。&lt;/p&gt;
&lt;p&gt;部署这个项目请确保你的VPS服务器可以正常访问Telegram服务器。&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NFD&lt;/strong&gt;：主要的机器人组件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nginx Proxy Manager&lt;/strong&gt;：反代机器人组件的TG Webhook&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;配置NFD机器人&lt;/h2&gt;
&lt;h3&gt;基本配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;创建对应文件夹，拉取项目&lt;pre&gt;&lt;code&gt;mkdir -p /docker_data/nfd_bot &amp;amp;&amp;amp; cd docker_data/nfd_bot &amp;amp;&amp;amp; git clone https://github.com/LloydAsp/nfd.git src
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;添加&lt;code&gt;pachage.json&lt;/code&gt;文件&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt; EOF &amp;gt; package.json
{
  &quot;name&quot;: &quot;nfd-bot&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;Telegram message forwarding bot with anti-fraud features&quot;,
  &quot;main&quot;: &quot;src/index.js&quot;,
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;node src/index.js&quot;,
    &quot;dev&quot;: &quot;nodemon src/index.js&quot;
  },
  &quot;dependencies&quot;: {
    &quot;axios&quot;: &quot;^1.4.0&quot;,
    &quot;body-parser&quot;: &quot;^1.20.2&quot;,
    &quot;express&quot;: &quot;^4.18.2&quot;,
    &quot;sqlite3&quot;: &quot;^5.1.6&quot;,
    &quot;sqlite&quot;: &quot;^4.2.1&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;nodemon&quot;: &quot;^2.0.22&quot;
  }
}
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;创建&lt;code&gt;Dockerfile&lt;/code&gt;&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt; EOF &amp;gt; Dockerfile
FROM node:16-alpine

WORKDIR /app

COPY package.json ./
RUN npm install

COPY . .

EXPOSE 3000

CMD [&quot;npm&quot;, &quot;start&quot;]
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;创建&lt;code&gt;docker-compose.yaml&lt;/code&gt;和&lt;code&gt;.env&lt;/code&gt;文件&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt; EOF &amp;gt; Dockerfile
services:
  nfd-bot:
    build: .
    container_name: nfd-bot
    restart: always
    ports:
      - &quot;${PORT:-3000}:3000&quot;
    environment:
      - BOT_TOKEN=${BOT_TOKEN}
      - BOT_SECRET=${BOT_SECRET}
      - ADMIN_UID=${ADMIN_UID}
      - PORT=3000
      - BASE_URL=${BASE_URL}
    volumes:
      - ./src/data:/app/src/data
    network_mode: bridge
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;BOT_TOKEN=&apos;替换为你自己的Bot Token （Bot Father获取）&apos;
BOT_SECRET=&apos;UUID生成或替换为你自己的强密码&apos;
ADMIN_UID=访问https://t.me/myidbot获取你自己账户的id
BASE_URL=https://example.com/tg # 注意最后不要加&apos;/&apos;
PORT=65035 # 替换你自己的端口
&lt;/code&gt;&lt;/pre&gt;
:::note{title=&quot;UUID生成&quot;}
&amp;lt;iframe width=&apos;100%&apos; height=&apos;300px&apos; src=&apos;https://blog.useforall.com/c/tools/page/random-generation.html&apos;/&amp;gt;
:::&lt;/li&gt;
&lt;li&gt;创建&lt;code&gt;src/index.js&lt;/code&gt;，&lt;code&gt;nano src/index.js&lt;/code&gt;&lt;pre&gt;&lt;code&gt;const express = require(&apos;express&apos;);
const bodyParser = require(&apos;body-parser&apos;);
const axios = require(&apos;axios&apos;);
const fs = require(&apos;fs&apos;);
const path = require(&apos;path&apos;);
const sqlite3 = require(&apos;sqlite3&apos;);
const { open } = require(&apos;sqlite&apos;);

const app = express();
app.use(bodyParser.json());

// 从环境变量获取配置
const TOKEN = process.env.BOT_TOKEN;
const SECRET = process.env.BOT_SECRET;
const ADMIN_UID = process.env.ADMIN_UID;
const PORT = process.env.PORT || 3000;
const WEBHOOK_PATH = process.env.WEBHOOK_PATH || &apos;/endpoint&apos;;
const NOTIFY_INTERVAL = 3600 * 1000;
const enable_notification = true;

// 数据文件路径
const FRAUD_DB_PATH = path.join(__dirname, &apos;data/fraud.db&apos;);
const NOTIFICATION_PATH = path.join(__dirname, &apos;data/notification.txt&apos;);
const START_MSG_PATH = path.join(__dirname, &apos;data/startMessage.md&apos;);
const DB_PATH = path.join(__dirname, &apos;data/nfd.sqlite&apos;);

// SQLite 数据库连接
let db;

// 初始化数据库
async function initDatabase() {
  db = await open({
    filename: DB_PATH,
    driver: sqlite3.Database
  });

  // 创建表
  await db.exec(`
    CREATE TABLE IF NOT EXISTS kv_store (
      key TEXT PRIMARY KEY,
      value TEXT,
      timestamp INTEGER
    )
  `);

  console.log(&apos;Database initialized&apos;);
}

// 封装 KV 操作的函数
async function kvGet(key) {
  const row = await db.get(&apos;SELECT value FROM kv_store WHERE key = ?&apos;, [key]);
  return row ? JSON.parse(row.value) : null;
}

async function kvPut(key, value) {
  const jsonValue = JSON.stringify(value);
  await db.run(
    &apos;INSERT OR REPLACE INTO kv_store (key, value, timestamp) VALUES (?, ?, ?)&apos;,
    [key, jsonValue, Date.now()]
  );
}

/**
 * 返回 Telegram API URL
 */
function apiUrl(methodName, params = null) {
  let query = &apos;&apos;;
  if (params) {
    const searchParams = new URLSearchParams();
    Object.entries(params).forEach(([key, value]) =&amp;gt; {
      searchParams.append(key, value);
    });
    query = &apos;?&apos; + searchParams.toString();
  }
  return `https://api.telegram.org/bot${TOKEN}/${methodName}${query}`;
}

/**
 * 向 Telegram API 发送请求
 */
async function requestTelegram(methodName, body, params = null) {
  try {
    const response = await axios.post(apiUrl(methodName, params), body);
    return response.data;
  } catch (error) {
    console.error(`Error calling Telegram API ${methodName}:`, error.message);
    return { ok: false, error: error.message };
  }
}

function makeReqBody(body) {
  return body;
}

function sendMessage(msg = {}) {
  return requestTelegram(&apos;sendMessage&apos;, makeReqBody(msg));
}

function copyMessage(msg = {}) {
  return requestTelegram(&apos;copyMessage&apos;, makeReqBody(msg));
}

function forwardMessage(msg) {
  return requestTelegram(&apos;forwardMessage&apos;, makeReqBody(msg));
}

/**
 * 处理 webhook 请求
 */
app.post(WEBHOOK_PATH, async (req, res) =&amp;gt; {
  // 验证 secret token
  if (req.headers[&apos;x-telegram-bot-api-secret-token&apos;] !== SECRET) {
    return res.status(403).send(&apos;Unauthorized&apos;);
  }

  const update = req.body;

  // 异步处理 update
  onUpdate(update).catch(err =&amp;gt; {
    console.error(&apos;Error processing update:&apos;, err);
  });

  return res.status(200).send(&apos;Ok&apos;);
});

/**
 * 处理传入的 Update
 */
async function onUpdate(update) {
  if (&apos;message&apos; in update) {
    await onMessage(update.message);
  }
}

/**
 * 处理传入的 Message
 */
async function onMessage(message) {
  if (message.text === &apos;/start&apos;) {
    let startMsg = await readFile(START_MSG_PATH);
    return sendMessage({
      chat_id: message.chat.id,
      text: startMsg,
    });
  }

  if (message.chat.id.toString() === ADMIN_UID) {
    if (!message?.reply_to_message?.chat) {
      return sendMessage({
        chat_id: ADMIN_UID,
        text: &apos;使用方法，回复转发的消息，并发送回复消息，或者`/block`、`/unblock`、`/checkblock`等指令&apos;
      });
    }

    if (/^\/block$/.exec(message.text)) {
      return handleBlock(message);
    }

    if (/^\/unblock$/.exec(message.text)) {
      return handleUnBlock(message);
    }

    if (/^\/checkblock$/.exec(message.text)) {
      return checkBlock(message);
    }

    let guestChantId = await kvGet(`msg-map-${message?.reply_to_message.message_id}`);
    return copyMessage({
      chat_id: guestChantId,
      from_chat_id: message.chat.id,
      message_id: message.message_id,
    });
  }

  return handleGuestMessage(message);
}

async function handleGuestMessage(message) {
  let chatId = message.chat.id;
  let isblocked = await kvGet(`isblocked-${chatId}`);

  if (isblocked) {
    return sendMessage({
      chat_id: chatId,
      text: &apos;Your are blocked&apos;
    });
  }

  let forwardReq = await forwardMessage({
    chat_id: ADMIN_UID,
    from_chat_id: message.chat.id,
    message_id: message.message_id
  });

  console.log(JSON.stringify(forwardReq));

  if (forwardReq.ok) {
    await kvPut(`msg-map-${forwardReq.result.message_id}`, chatId);
  }

  return handleNotify(message);
}

async function handleNotify(message) {
  // 先判断是否是诈骗人员，如果是，则直接提醒
  // 如果不是，则根据时间间隔提醒：用户id，交易注意点等
  let chatId = message.chat.id;

  if (await isFraud(chatId)) {
    return sendMessage({
      chat_id: ADMIN_UID,
      text: `检测到骗子，UID${chatId}`
    });
  }

  if (enable_notification) {
    let lastMsgTime = await kvGet(`lastmsg-${chatId}`);

    if (!lastMsgTime || Date.now() - lastMsgTime &amp;gt; NOTIFY_INTERVAL) {
      await kvPut(`lastmsg-${chatId}`, Date.now());

      return sendMessage({
        chat_id: ADMIN_UID,
        text: await readFile(NOTIFICATION_PATH)
      });
    }
  }
}

async function handleBlock(message) {
  let guestChantId = await kvGet(`msg-map-${message.reply_to_message.message_id}`);

  if (guestChantId === ADMIN_UID) {
    return sendMessage({
      chat_id: ADMIN_UID,
      text: &apos;不能屏蔽自己&apos;
    });
  }

  await kvPut(`isblocked-${guestChantId}`, true);

  return sendMessage({
    chat_id: ADMIN_UID,
    text: `UID:${guestChantId}屏蔽成功`,
  });
}

async function handleUnBlock(message) {
  let guestChantId = await kvGet(`msg-map-${message.reply_to_message.message_id}`);
  await kvPut(`isblocked-${guestChantId}`, false);

  return sendMessage({
    chat_id: ADMIN_UID,
    text: `UID:${guestChantId}解除屏蔽成功`,
  });
}

async function checkBlock(message) {
  let guestChantId = await kvGet(`msg-map-${message.reply_to_message.message_id}`);
  let blocked = await kvGet(`isblocked-${guestChantId}`);

  return sendMessage({
    chat_id: ADMIN_UID,
    text: `UID:${guestChantId}` + (blocked ? &apos;被屏蔽&apos; : &apos;没有被屏蔽&apos;)
  });
}

/**
 * 发送纯文本消息
 */
async function sendPlainText(chatId, text) {
  return sendMessage({
    chat_id: chatId,
    text
  });
}

/**
 * 设置 webhook
 */
app.get(&apos;/registerWebhook&apos;, async (req, res) =&amp;gt; {
  const BASE_URL = process.env.BASE_URL || `http://localhost:${PORT}`;
  const webhookUrl = `${BASE_URL}${WEBHOOK_PATH}`;

  try {
    const r = await axios.get(apiUrl(&apos;setWebhook&apos;, {
      url: webhookUrl,
      secret_token: SECRET
    }));

    if (r.data.ok) {
      return res.send(&apos;Webhook registered successfully&apos;);
    } else {
      return res.status(400).send(JSON.stringify(r.data, null, 2));
    }
  } catch (error) {
    return res.status(500).send(`Error: ${error.message}`);
  }
});

/**
 * 删除 webhook
 */
app.get(&apos;/unRegisterWebhook&apos;, async (req, res) =&amp;gt; {
  try {
    const r = await axios.get(apiUrl(&apos;setWebhook&apos;, { url: &apos;&apos; }));

    if (r.data.ok) {
      return res.send(&apos;Webhook unregistered successfully&apos;);
    } else {
      return res.status(400).send(JSON.stringify(r.data, null, 2));
    }
  } catch (error) {
    return res.status(500).send(`Error: ${error.message}`);
  }
});

/**
 * 检查是否是骗子
 */
async function isFraud(id) {
  id = id.toString();
  const fraudDb = await readFile(FRAUD_DB_PATH);
  let arr = fraudDb.split(&apos;\n&apos;).filter(v =&amp;gt; v);
  console.log(JSON.stringify(arr));
  let flag = arr.filter(v =&amp;gt; v === id).length !== 0;
  console.log(flag);
  return flag;
}

/**
 * 读取文件辅助函数
 */
function readFile(filePath) {
  return new Promise((resolve, reject) =&amp;gt; {
    fs.readFile(filePath, &apos;utf8&apos;, (err, data) =&amp;gt; {
      if (err) {
        console.error(`Error reading file ${filePath}:`, err);
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

// 启动服务器
async function startServer() {
  // 初始化数据库
  await initDatabase();

  // 启动 Express 服务器
  app.listen(PORT, () =&amp;gt; {
    console.log(`NFD Bot server running on port ${PORT}`);
    console.log(`Webhook path: ${WEBHOOK_PATH}`);
  });
}

// 启动应用
startServer().catch(err =&amp;gt; {
  console.error(&apos;Failed to start server:&apos;, err);
  process.exit(1);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;启动服务&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h2&gt;配置Nginx Proxy Manager&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;随便找一个已经反代了的服务(这里假设是&lt;code&gt;https://example.com&lt;/code&gt;服务)，前往&lt;code&gt;Custom locations&lt;/code&gt;，如图添加一个&lt;code&gt;location&lt;/code&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/03/15/67d52ece06a1a.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;访问&lt;code&gt;&apos;https://example.com/tg/registerWebhook&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;其他&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;验证是否成功
&lt;ol&gt;
&lt;li&gt;如果Bot发送消息给你了，那就代表成功了。&lt;/li&gt;
&lt;li&gt;或者运行&lt;code&gt;curl &quot;https://api.telegram.org/bot替换为你的BotToken/getWebhookInfo&quot;&lt;/code&gt;，返回如下结果就算成功。&lt;pre&gt;&lt;code&gt;{&quot;ok&quot;:true,&quot;result&quot;:{&quot;url&quot;:&quot;https://example/tg/endpoint&quot;,&quot;has_custom_certificate&quot;:false,&quot;pending_update_count&quot;:0,&quot;max_connections&quot;:40,&quot;ip_address&quot;:&quot;xx.xx.xx.xx&quot;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>使用Nginx Proxy Manager搭建自己的Huggingface镜像站</title><link>https://blog.useforall.com/posts/11/</link><guid isPermaLink="true">https://blog.useforall.com/posts/11/</guid><description>使用NPM来创建一个自己的Huggingface镜像站，并设置URL Prefix来防止被自动采集后被被人使用。</description><pubDate>Sat, 01 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
使用NPM来创建一个自己的Huggingface镜像站，并设置URL Prefix来防止被自动采集后被被人使用。
:::&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2025-09-22: 添加xethub链接反代&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;众所周知，Huggingface不能国内直接访问，同时镜像站又存在速度限制，例如&lt;code&gt;hf-mirror&lt;/code&gt;最高我这只有&lt;code&gt;6MB/s&lt;/code&gt;。&lt;code&gt;hf-mirror&lt;/code&gt;虽然开源了镜像站代码，但是实测无法运行（主要由于本人不会使用Caddy）。此外使用VPN虽然可以解决无法访问的情况，但是一直存在一个问题就是模型会越下越慢，哪怕开启TUN模式也没用，目前也没找到解决办法，所以至此萌生了使用NPM反代一个自己的镜像站。目前实测下载模型不会越下越慢，同时所有网页功能均可使用，包括登录注册等。&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nginx Proxy Manager&lt;/strong&gt;: 提供反向代理功能。搭建参考：&lt;a href=&quot;https://blog.useforall.com/post/3#%E9%85%8D%E7%BD%AE%20Nginx%20Proxy%20Manager&quot;&gt;搭建 Nginx Proxy Manager&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;反代Huggingface主站和LFS CDN下载节点&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;如图配置，这里假设你用的域名是&lt;code&gt;example.com&lt;/code&gt;：
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/03/01/67c2e3a81cd93.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;前往Custom Location设置直接访问&lt;code&gt;https://example.com/&lt;/code&gt;时判断是否有我们自己设置的&lt;code&gt;cookie&lt;/code&gt;，没有的话，不允许访问：&lt;pre&gt;&lt;code&gt;# 检查 Cookie 是否存在
if ($http_cookie !~* &quot;access_granted=true&quot;) { # key-value pair请一定跟后续配置的一样
    return 403; # 或者 401, 502，根据你的需求选择合适的错误码
}

# 如果 Cookie 存在，并且 URI 不是以 /jZPU8pzjCweCDUiXxGB9B/ 开头，则内部重写。
# 请注意替换为`设置实际访问的反代链接`部分设置的location，这里只是以我自己的location举例
if ($request_uri !~ &quot;^/jZPU8pzjCweCDUiXxGB9B/&quot;) {
    rewrite ^/(.*)$ /jZPU8pzjCweCDUiXxGB9B/$1 last;
    # 使用 &apos;last&apos; 指令进行内部重写，并重新查找 location
}
&lt;/code&gt;&lt;/pre&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/03/01/67c2e48492279.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;反代如下下载节点，请自行修改你需要的&lt;code&gt;location&lt;/code&gt;：
&lt;ol&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; location /b3k3EnxpRbqvuomRASVE3/ {
     proxy_pass https://cdn-lfs-us-1.hf.co/;

     proxy_ssl_server_name on;
     proxy_ssl_name cdn-lfs-us-1.hf.co;
     proxy_set_header Host cdn-lfs-us-1.hf.co;
     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_redirect off;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; location /3z7eaA2TmkKAfVdKGHMqzuGc5RdiPHBb/ {
     proxy_pass https://transfer.xethub.hf.co/;

     proxy_ssl_server_name on;
     proxy_ssl_name transfer.xethub.hf.co;
     proxy_set_header Host transfer.xethub.hf.co;
     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_redirect off;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; location /BxZtnutU8K7si8kDtNpGJfpMNFCF3xHu/ {
     proxy_pass https://cas-server.xethub.hf.co/;

     proxy_ssl_server_name on;
     proxy_ssl_name cas-server.xethub.hf.co;
     proxy_set_header Host cas-server.xethub.hf.co;
     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_redirect off;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt; location /SyiDVN7og9c65PiesJj6ArNthsDjXnGz/ {
     proxy_pass https://cas-bridge.xethub.hf.co/;

     proxy_ssl_server_name on;
     proxy_ssl_name cas-bridge.xethub.hf.co;
     proxy_set_header Host cas-bridge.xethub.hf.co;
     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_redirect off;
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
:::note{title=&quot;注&quot;}
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;location&lt;/code&gt;可以自行设置&lt;/li&gt;
&lt;li&gt;这里代表的含义是当你访问&lt;code&gt;https://example.com/b3k3EnxpRbqvuomRASVE3/repos/***.bin&lt;/code&gt;下载LFS文件的时候，实际上访问的是&lt;code&gt;https://cdn-lfs-us-1.hf.co/repos/***.bin&lt;/code&gt;，但是这不代表你是通过原Huggingface CDN连接下载，而是你是通过你的VPS-&amp;gt;VPS访问文件-&amp;gt;VPS传输回来给你的。
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;设置实际访问的反代链接&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;还是在Custom Location，填入如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;location /jZPU8pzjCweCDUiXxGB9B/ {
    proxy_pass https://huggingface.co/;
    proxy_ssl_server_name on;
    proxy_ssl_name huggingface.co;
    proxy_set_header Host huggingface.co;
    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_redirect https://cdn-lfs-us-1.hf.co https://exmaple.com/b3k3EnxpRbqvuomRASVE3;
    proxy_redirect https://transfer.xethub.hf.co https://example.com/3z7eaA2TmkKAfVdKGHMqzuGc5RdiPHBb;
    proxy_redirect https://cas-server.xethub.hf.co https://example.com/BxZtnutU8K7si8kDtNpGJfpMNFCF3xHu;
    proxy_redirect https://cas-bridge.xethub.hf.co https://example.com/SyiDVN7og9c65PiesJj6ArNthsDjXnGz;
    proxy_redirect https://huggingface.co https://example.com/jZPU8pzjCweCDUiXxGB9B/;

    add_header Set-Cookie &quot;access_granted=true; Path=/;&quot;; # Path=/ 表示 Cookie 对整个域名有效
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note{title=&quot;注&quot;}&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;proxy_redirect https://cdn-lfs-us-1.hf.co https://example.com/b3k3EnxpRbqvuomRASVE3;&lt;/code&gt;：修改&lt;code&gt;exmaple.com&lt;/code&gt;为你自己设置域名，修改&lt;code&gt;/b3k3EnxpRbqvuomRASVE3&lt;/code&gt;为&lt;code&gt;上一部分第三步&lt;/code&gt;设置的location，记得不要最后的&lt;code&gt;/&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;proxy_redirect https://huggingface.co https://example.com/jZPU8pzjCweCDUiXxGB9B/;&lt;/code&gt;：修改&lt;code&gt;exmaple.com&lt;/code&gt;为你自己设置域名，修改&lt;code&gt;/jZPU8pzjCweCDUiXxGB9B/&lt;/code&gt;为&lt;code&gt;此步骤&lt;/code&gt;设置的location。
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;结束。至此，当正确访问&lt;code&gt;https://example.com/jZPU8pzjCweCDUiXxGB9B/&lt;/code&gt;后，浏览器会设置一个&lt;code&gt;cookie&lt;/code&gt;，也就是&lt;code&gt;access_granted=true&lt;/code&gt;，此后哪怕没有输入正确的location，也可以正常访问，因为判断到浏览器有对应的&lt;code&gt;cookie&lt;/code&gt;。注意这个cookie是会话级的，关闭窗口一段时间后会失效，要重新访问正确的连接才可以。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>OPNSense添加WebDAV备份插件</title><link>https://blog.useforall.com/posts/9/</link><guid isPermaLink="true">https://blog.useforall.com/posts/9/</guid><description>由于OPNsense 24.1默认配置备份只支持本地和Goolge Drive两种备份方式，本文主要是给OPNSense添加WebDAV备份功能。</description><pubDate>Wed, 26 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
由于&lt;code&gt;OPNsense 24.1&lt;/code&gt;默认配置备份只支持本地和Goolge Drive两种备份方式，本文主要是给&lt;code&gt;OPNSense&lt;/code&gt;添加&lt;code&gt;WebDAV&lt;/code&gt;备份功能。
:::&lt;/p&gt;
&lt;h1&gt;配置流程&lt;/h1&gt;
&lt;h2&gt;添加WebDAV备份插件&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;进入&lt;code&gt;OPNSense&lt;/code&gt;，打开&lt;code&gt;安全shell&lt;/code&gt;设置，ssh连接进入系统shell。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/26/67be9e56928a1.png&quot; alt=&quot;CleanShot 2025-02-26 at 12.53.16@2x.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;创建文件夹 &lt;code&gt;mkdir -p /usr/local/opnsense/mvc/app/models/OPNsense/Backup/&lt;/code&gt; 后，添加核心实现逻辑代码。&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt; &apos;EOF&apos; &amp;gt; /usr/local/opnsense/mvc/app/library/OPNsense/Backup/WebDAV.php
&amp;lt;?php
///usr/local/opnsense/mvc/app/library/OPNsense/Backup/WebDAV.php
/*
 * Copyright (C) 2018 Deciso B.V.
 * Copyright (C) 2018 Fabian Franz
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS&apos;&apos; AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

namespace OPNsense\Backup;

use OPNsense\Core\Config;

/**
 * Class WebDAV backup
 * @package OPNsense\Backup
 */
class WebDAV extends Base implements IBackupProvider
{
    /**
     * get required (user interface) fields for backup connector
     * @return array configuration fields, types and description
     */
    public function getConfigurationFields()
    {
        $fields = array(
            array(
                &quot;name&quot; =&amp;gt; &quot;enabled&quot;,
                &quot;type&quot; =&amp;gt; &quot;checkbox&quot;,
                &quot;label&quot; =&amp;gt; gettext(&quot;Enable&quot;),
                &quot;value&quot; =&amp;gt; null
            ),
            array(
                &quot;name&quot; =&amp;gt; &quot;url&quot;,
                &quot;type&quot; =&amp;gt; &quot;text&quot;,
                &quot;label&quot; =&amp;gt; gettext(&quot;URL&quot;),
                &quot;help&quot; =&amp;gt; gettext(&quot;The Base URL to WebDAV without trailing slash. For example: https://dav.example.com&quot;),
                &quot;value&quot; =&amp;gt; null
            ),
            array(
                &quot;name&quot; =&amp;gt; &quot;user&quot;,
                &quot;type&quot; =&amp;gt; &quot;text&quot;,
                &quot;label&quot; =&amp;gt; gettext(&quot;User Name&quot;),
                &quot;help&quot; =&amp;gt; gettext(&quot;The name you use for logging into your WebDAV account&quot;),
                &quot;value&quot; =&amp;gt; null
            ),
            array(
                &quot;name&quot; =&amp;gt; &quot;password&quot;,
                &quot;type&quot; =&amp;gt; &quot;password&quot;,
                &quot;label&quot; =&amp;gt; gettext(&quot;Password&quot;),
                &quot;help&quot; =&amp;gt; gettext(&quot;The password you use for logging into your WebDAV account&quot;),
                &quot;value&quot; =&amp;gt; null
            ),
            array(
                &quot;name&quot; =&amp;gt; &quot;password_encryption&quot;,
                &quot;type&quot; =&amp;gt; &quot;password&quot;,
                &quot;label&quot; =&amp;gt; gettext(&quot;Encryption Password (Optional)&quot;),
                &quot;help&quot; =&amp;gt; gettext(&quot;A password to encrypt your configuration&quot;),
                &quot;value&quot; =&amp;gt; null
            ),
            array(
                &quot;name&quot; =&amp;gt; &quot;backupdir&quot;,
                &quot;type&quot; =&amp;gt; &quot;text&quot;,
                &quot;label&quot; =&amp;gt; gettext(&quot;Directory Name without leading slash, starting from user&apos;s root&quot;),
                &quot;value&quot; =&amp;gt; &apos;OPNsense-Backup&apos;
            )
        );
        $webdav = new WebDAVSettings();
        foreach ($fields as &amp;amp;$field) {
            $field[&apos;value&apos;] = (string)$webdav-&amp;gt;getNodeByReference($field[&apos;name&apos;]);
        }
        return $fields;
    }

    /**
     * backup provider name
     * @return string user friendly name
     */
    public function getName()
    {
        return gettext(&quot;WebDAV&quot;);
    }

    /**
     * validate and set configuration
     * @param array $conf configuration array
     * @return array of validation errors when not saved
     * @throws \OPNsense\Base\ModelException
     * @throws \ReflectionException
     */
    public function setConfiguration($conf)
    {
        $webdav = new WebDAVSettings();
        $this-&amp;gt;setModelProperties($webdav, $conf);
        $validation_messages = $this-&amp;gt;validateModel($webdav);
        if (empty($validation_messages)) {
            $webdav-&amp;gt;serializeToConfig();
            Config::getInstance()-&amp;gt;save();
        }
        return $validation_messages;
    }

    /**
     * perform backup
     * @return array filelist
     * @throws \OPNsense\Base\ModelException
     * @throws \ReflectionException
     */
    public function backup()
    {
        $cnf = Config::getInstance();
        $webdav = new WebDAVSettings();
        if ($cnf-&amp;gt;isValid() &amp;amp;&amp;amp; !empty((string)$webdav-&amp;gt;enabled)) {
            $config = $cnf-&amp;gt;object();
            $url = (string)$webdav-&amp;gt;url;
            $username = (string)$webdav-&amp;gt;user;
            $password = (string)$webdav-&amp;gt;password;
            $backupdir = (string)$webdav-&amp;gt;backupdir;
            $crypto_password = (string)$webdav-&amp;gt;password_encryption;
            $hostname = $config-&amp;gt;system-&amp;gt;hostname . &apos;.&apos; . $config-&amp;gt;system-&amp;gt;domain;
            $configname = &apos;config-&apos; . $hostname . &apos;-&apos; .  date(&apos;Y-m-d_H_i_s&apos;) . &apos;.xml&apos;;
            // backup source data to local strings (plain/encrypted)
            $confdata = file_get_contents(&apos;/conf/config.xml&apos;);
            if (!empty($crypto_password)) {
                $confdata = $this-&amp;gt;encrypt($confdata, $crypto_password);
            }
            // Check if destination directory exists, create (full path) if not
            try {
                $this-&amp;gt;create_directory($url, $username, $password, $backupdir);
            } catch (\Exception $e) {
                return array();
            }
            try {
                $this-&amp;gt;upload_file_content(
                    $url,
                    $username,
                    $password,
                    $backupdir,
                    $configname,
                    $confdata
                );
                // do not list directories
                return array_filter(
                    $this-&amp;gt;listFiles($url, $username, $password, &quot;/$backupdir/&quot;, false),
                    function ($filename) {
                        return (substr($filename, -1) !== &apos;/&apos;);
                    }
                );
            } catch (\Exception $e) {
                return array();
            }
        }
    }

    /**
     * dir listing
     * @param string $url remote location
     * @param string $username username
     * @param string $password password to use
     * @param string $directory location to list
     * @param bool $only_dirs only list directories
     * @return array
     * @throws \Exception
     */
    public function listFiles($url, $username, $password, $directory = &apos;/&apos;, $only_dirs = true)
    {
        $result = $this-&amp;gt;curl_request(
            &quot;$url$directory&quot;,
            $username,
            $password,
            &apos;PROPFIND&apos;,
            &quot;Error while fetching filelist from WebDAV &apos;{$directory}&apos; path&quot;
        );
        //remove line breaks from xml string
        $xml = preg_replace(&quot;/\r?\n/&quot;, &apos;&apos;, $result[&apos;response&apos;]);

        // workaround - simplexml seems to be broken when using namespaces - remove them.
        //$xml = str_replace([&apos;&amp;lt;D:&apos;, &apos;&amp;lt;/D:&apos;, &apos;&amp;lt;d:&apos;, &apos;&amp;lt;/d:&apos;, &apos;&amp;lt;lp1:&apos;, &apos;&amp;lt;/lp1:&apos;], [&apos;&amp;lt;&apos;, &apos;&amp;lt;/&apos;, &apos;&amp;lt;&apos;, &apos;&amp;lt;/&apos;, &apos;&amp;lt;&apos;, &apos;&amp;lt;/&apos;], $xml);
        // better workaround: remove any namespace
        $xml = preg_replace(&apos;/\s+xmlns:[^=]+=&quot;[^&quot;]*&quot;/&apos;, &apos;&apos;, $xml);
        $xml = preg_replace(&apos;/\b(\w+)\:/i&apos;, &apos;&apos;, $xml);

        $xml = simplexml_load_string($xml);
        $ret = array();

        //parse URL for a check if path exists
        $parsedUrl = parse_url($url);

        foreach ($xml-&amp;gt;children() as $response) {
            // d:response
            if ($response-&amp;gt;getName() == &apos;response&apos;) {

                $fileurl = (string)$response-&amp;gt;href;

                //check if URL has a path
                if (isset($parsedUrl[&apos;path&apos;])) {
                    //URL DOES have a path - extracting path from fileurl
                    $dirname = explode($parsedUrl[&apos;path&apos;], $fileurl, 2)[1];
                }
                else {
                    //URL does NOT have a path
                    $dirname = $fileurl;
                }

                if (
                    $response-&amp;gt;propstat-&amp;gt;prop-&amp;gt;resourcetype-&amp;gt;children()-&amp;gt;count() &amp;gt; 0 &amp;amp;&amp;amp;
                    $response-&amp;gt;propstat-&amp;gt;prop-&amp;gt;resourcetype-&amp;gt;children()[0]-&amp;gt;getName() == &apos;collection&apos; &amp;amp;&amp;amp;
                    $only_dirs
                ) {

                    $ret[] = $dirname;

                } elseif (!$only_dirs) {

                    $ret[] = $dirname;

                }
            }
        }
        return $ret;
    }

    /**
     * upload file
     * @param string $url remote location
     * @param string $username remote user
     * @param string $password password to use
     * @param string $backupdir remote directory
     * @param string $filename filename to use
     * @param string $local_file_content contents to save
     * @throws \Exception when upload fails
     */
    public function upload_file_content($url, $username, $password, $backupdir, $filename, $local_file_content)
    {
        $this-&amp;gt;curl_request(
            $url . &quot;/$backupdir/$filename&quot;,
            $username,
            $password,
            &apos;PUT&apos;,
            &apos;cannot execute PUT&apos;,
            $local_file_content
        );
    }

    /**
     * create new remote directory if doesn&apos;t exist
     * @param string $url remote location
     * @param string $username remote user
     * @param string $password password to use
     * @param string $backupdir remote directory
     * @throws \Exception when create dir fails
     */
    public function create_directory($url, $username, $password, $backupdir)
    {
        $parent_path = dirname($backupdir);

        try {
            $directories = $this-&amp;gt;listFiles($url, $username, $password, &quot;/{$parent_path}&quot;);
        } catch (\Exception $e) {
            if ($backupdir == &quot;.&quot;) {
                // We cannot create root, if we reached here there&apos;s some other problem
                syslog(LOG_ERR, &quot;Check WebDAV configuration parameters&quot;);
                return false;
            }
            // If error assume dir doesn&apos;t exist. Create parent folder
            if ($this-&amp;gt;create_directory($url, $username, $password, $parent_path) === false) {
                throw new \Exception();
            }
        }

        // if path exists ok
        if (in_array(&quot;/{$backupdir}/&quot;, $directories)) {
            return;
        }

        // create backupdir, because path does not exist
        $this-&amp;gt;curl_request(
            $url . &quot;/{$backupdir}&quot;,
            $username,
            $password,
            &apos;MKCOL&apos;,
            &apos;cannot execute MKCOL&apos;
        );
    }

    /**
     * @param string $url remote location
     * @param string $username remote user
     * @param string $password password to use
     * @param string $method http method, PUT, GET, ...
     * @param string $error_message message to log on failure
     * @param null|string $postdata http body
     * @param array $headers HTTP headers
     * @return array response status
     * @throws \Exception when request fails
     */
    public function curl_request(
        $url,
        $username,
        $password,
        $method,
        $error_message,
        $postdata = null,
        $headers = array(&apos;User-Agent: OPNsense Firewall&apos;)
    ) {
        //workaround for Hetzner Storagebox
        if ($method == &quot;PROPFIND&quot;) {
        array_push($headers, &apos;Depth: 1&apos;);
        }
    $curl = curl_init();
        curl_setopt_array($curl, array(
            CURLOPT_URL =&amp;gt; $url,
            CURLOPT_CUSTOMREQUEST =&amp;gt; $method, // Create a file in WebDAV is PUT
            CURLOPT_RETURNTRANSFER =&amp;gt; true, // Do not output the data to STDOUT
            CURLOPT_VERBOSE =&amp;gt; 0,           // same here
            CURLOPT_MAXREDIRS =&amp;gt; 0,         // no redirects
            CURLOPT_TIMEOUT =&amp;gt; 60,          // maximum time: 1 min
            CURLOPT_HTTP_VERSION =&amp;gt; CURL_HTTP_VERSION_1_1,
            CURLOPT_USERPWD =&amp;gt; $username . &quot;:&quot; . $password,
            CURLOPT_HTTPHEADER =&amp;gt; $headers
        ));
        if ($postdata != null) {
            curl_setopt($curl, CURLOPT_POSTFIELDS, $postdata);
        }
        $response = curl_exec($curl);
        $err = curl_error($curl);
        $info = curl_getinfo($curl);
        if (!($info[&apos;http_code&apos;] == 200 || $info[&apos;http_code&apos;] == 207 || $info[&apos;http_code&apos;] == 201) || $err) {
            syslog(LOG_ERR, $error_message);
            syslog(LOG_ERR, json_encode($info));
            throw new \Exception();
        }
        curl_close($curl);
        return array(&apos;response&apos; =&amp;gt; $response, &apos;info&apos; =&amp;gt; $info);
    }

    /**
     * Is this provider enabled
     * @return boolean enabled status
     * @throws \OPNsense\Base\ModelException
     * @throws \ReflectionException
     */
    public function isEnabled()
    {
        $webdav = new WebDAVSettings();
        return (string)$webdav-&amp;gt;enabled === &quot;1&quot;;
    }
}
&apos;EOF&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt; &apos;EOF&apos; &amp;gt; /usr/local/opnsense/mvc/app/models/OPNsense/Backup/WebDAVSettings.php
&amp;lt;?php
///usr/local/opnsense/mvc/app/models/OPNsense/Backup/WebDAVSettings.php
/**
 *    Copyright (C) 2018 Fabian Franz
 *
 *    All rights reserved.
 *
 *    Redistribution and use in source and binary forms, with or without
 *    modification, are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *
 *    THIS SOFTWARE IS PROVIDED ``AS IS&apos;&apos; AND ANY EXPRESS OR IMPLIED WARRANTIES,
 *    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 *    AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 *    AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 *    OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *    POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace OPNsense\Backup;

use OPNsense\Base\BaseModel;

/**
 * Class WebDAV
 * @package Backup
 */
class WebDAVSettings extends BaseModel
{
}
&apos;EOF&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;添加&lt;code&gt;WebDAVSettings.xml&lt;/code&gt;，这一步让在&lt;code&gt;OPNSense&lt;/code&gt;备份中显示&lt;code&gt;WebDAV&lt;/code&gt;配置界面。&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt; &apos;EOF&apos; &amp;gt; /usr/local/opnsense/mvc/app/models/OPNsense/Backup/WebDAVSettings.xml
&amp;lt;model&amp;gt;
    &amp;lt;mount&amp;gt;//system/backup/webdav&amp;lt;/mount&amp;gt;
    &amp;lt;version&amp;gt;1.0.0&amp;lt;/version&amp;gt;
    &amp;lt;description&amp;gt;OPNsense WebDAV Backup Settings&amp;lt;/description&amp;gt;
    &amp;lt;items&amp;gt;
        &amp;lt;enabled type=&quot;BooleanField&quot;&amp;gt;
            &amp;lt;default&amp;gt;0&amp;lt;/default&amp;gt;
            &amp;lt;Required&amp;gt;Y&amp;lt;/Required&amp;gt;
        &amp;lt;/enabled&amp;gt;
        &amp;lt;url type=&quot;TextField&quot;&amp;gt;
            &amp;lt;Required&amp;gt;N&amp;lt;/Required&amp;gt;
            &amp;lt;mask&amp;gt;/^https?:\/\/.*[^\/]$/&amp;lt;/mask&amp;gt;
            &amp;lt;ValidationMessage&amp;gt;The URL must be valid without a trailing slash. For example: https://dav.example.com&amp;lt;/ValidationMessage&amp;gt;
            &amp;lt;Constraints&amp;gt;
                &amp;lt;check001&amp;gt;
                    &amp;lt;ValidationMessage&amp;gt;A URL for the WebDAV server must be set.&amp;lt;/ValidationMessage&amp;gt;
                    &amp;lt;type&amp;gt;DependConstraint&amp;lt;/type&amp;gt;
                    &amp;lt;addFields&amp;gt;
                        &amp;lt;field1&amp;gt;enabled&amp;lt;/field1&amp;gt;
                    &amp;lt;/addFields&amp;gt;
                &amp;lt;/check001&amp;gt;
            &amp;lt;/Constraints&amp;gt;
        &amp;lt;/url&amp;gt;
        &amp;lt;user type=&quot;TextField&quot;&amp;gt;
            &amp;lt;Constraints&amp;gt;
                &amp;lt;check001&amp;gt;
                    &amp;lt;ValidationMessage&amp;gt;A user for the WebDAV server must be set.&amp;lt;/ValidationMessage&amp;gt;
                    &amp;lt;type&amp;gt;DependConstraint&amp;lt;/type&amp;gt;
                    &amp;lt;addFields&amp;gt;
                        &amp;lt;field1&amp;gt;enabled&amp;lt;/field1&amp;gt;
                    &amp;lt;/addFields&amp;gt;
                &amp;lt;/check001&amp;gt;
            &amp;lt;/Constraints&amp;gt;
        &amp;lt;/user&amp;gt;
        &amp;lt;password type=&quot;TextField&quot;&amp;gt;
            &amp;lt;Constraints&amp;gt;
                &amp;lt;check001&amp;gt;
                    &amp;lt;ValidationMessage&amp;gt;A password for the WebDAV server must be set.&amp;lt;/ValidationMessage&amp;gt;
                    &amp;lt;type&amp;gt;DependConstraint&amp;lt;/type&amp;gt;
                    &amp;lt;addFields&amp;gt;
                        &amp;lt;field1&amp;gt;enabled&amp;lt;/field1&amp;gt;
                    &amp;lt;/addFields&amp;gt;
                &amp;lt;/check001&amp;gt;
            &amp;lt;/Constraints&amp;gt;
        &amp;lt;/password&amp;gt;
        &amp;lt;password_encryption type=&quot;TextField&quot;&amp;gt;
            &amp;lt;Required&amp;gt;N&amp;lt;/Required&amp;gt;
        &amp;lt;/password_encryption&amp;gt;
        &amp;lt;backupdir type=&quot;TextField&quot;&amp;gt;
            &amp;lt;Required&amp;gt;Y&amp;lt;/Required&amp;gt;
            &amp;lt;mask&amp;gt;/^([\w%+\-]+\/)*[\w+%\-]+$/&amp;lt;/mask&amp;gt;
            &amp;lt;default&amp;gt;OPNsense-Backup&amp;lt;/default&amp;gt;
            &amp;lt;ValidationMessage&amp;gt;The Backup Directory can only consist of alphanumeric characters, dash, underscores and slash. No leading or trailing slash.&amp;lt;/ValidationMessage&amp;gt;
        &amp;lt;/backupdir&amp;gt;
    &amp;lt;/items&amp;gt;
&amp;lt;/model&amp;gt;
&apos;EOF&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;进入&lt;code&gt;OPNSense&lt;/code&gt;，&lt;code&gt;系统 -&amp;gt; 配置 -&amp;gt; 备份 -&amp;gt; 滑到最后&lt;/code&gt;。按需配置即可。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/26/67be9ffc9e125.png&quot; alt=&quot;CleanShot 2025-02-26 at 12.59.24@2x.png&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Docker搭建GitLab并使用Nginx Proxy Manager反代</title><link>https://blog.useforall.com/posts/8/</link><guid isPermaLink="true">https://blog.useforall.com/posts/8/</guid><description>本文主要介绍了如何使用Docker搭建GitLab代码服务器，并且解决无法使用标准端口（443）可能存在的问题，最后用Nginx Proxy Manager进行反向代理。</description><pubDate>Tue, 25 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
本文主要介绍了如何使用Docker搭建GitLab代码服务器，并且解决无法使用标准端口（443）可能存在的问题，最后用Nginx Proxy Manager进行反向代理。
:::&lt;/p&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitLab&lt;/strong&gt;：代码托管平台，可以自部署。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nginx Proxy Manager&lt;/strong&gt;: 提供反向代理功能&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;配置GitLab&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;mkdir docker_data &amp;amp;&amp;amp; cd docker_data &amp;amp;&amp;amp; mkdir gitlab &amp;amp;&amp;amp; cd gitlab &amp;amp;&amp;amp; nano docker-compose.yaml&lt;/code&gt;
添加以下内容：&lt;pre&gt;&lt;code&gt;services:
  gitlab:
    image: gitlab/gitlab-ee:17.4.2-ee.0
    container_name: gitlab
    restart: always
    ports:
      - 65007:80
      - 65008:443
      - 65009:22
    volumes:
      - ./config:/etc/gitlab
      - ./logs:/var/log/gitlab
      - ./data:/var/opt/gitlab
    shm_size: &apos;256m&apos;
    network_mode: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;启动服务：&lt;pre&gt;&lt;code&gt;sudo docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;修改配置。&lt;code&gt;nano config/gitlab.rb&lt;/code&gt;&lt;pre&gt;&lt;code&gt;# 替换eternal_url为域名（非标端口，443端口未开放的情况）
sudo sed -i &quot;s|# external_url &apos;GENERATED_EXTERNAL_URL&apos;|external_url &apos;https://example.com:4433&apos;|&quot; config/gitlab.rb
sudo sed -i &quot;s|# letsencrypt\[&apos;enable&apos;\] = nil|letsencrypt[&apos;enable&apos;] = false|&quot; config/gitlab.rb
sudo sed -i &quot;s|# nginx\[&apos;listen_port&apos;\] = nil|nginx\[&apos;listen_port&apos;\] = 80|&quot; config/gitlab.rb
sudo sed -i &quot;s|# nginx\[&apos;listen_https&apos;\] = nil|nginx\[&apos;listen_https&apos;\] = false|&quot; config/gitlab.rb
sudo docker compose up -d --force-recreate &amp;amp;&amp;amp; sudo docker compose exec -it gitlab grep &apos;Password:&apos; /etc/gitlab/initial_root_password
&lt;/code&gt;&lt;/pre&gt;
:::note{title=&quot;注&quot;}
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;external_url&lt;/code&gt; 为&lt;code&gt;http&lt;/code&gt;时，默认监听80。如果需要使用非标端口，例192.168.1.67:8008，那么&lt;code&gt;docker-compose.yaml&lt;/code&gt;里端口映射改为65007:8008&lt;/li&gt;
&lt;li&gt;如果为&lt;code&gt;https&lt;/code&gt;，经过测试，默认只能是443，例如上面指定4433端口但实际还是监听443。同时当为https时，没有提供有效的证书它甚至会去申请Let’s Encrypt的证书，这一定是会失败的（因为根本没配置&lt;code&gt;acme&lt;/code&gt;）。此时就需要修改&lt;code&gt;nginx[&apos;listen_port&apos;]&lt;/code&gt;为&lt;code&gt;80&lt;/code&gt;，同时关闭https监听（因为我们要自己使用nginx反代）（相当于只保留了http服务）
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;开心版（网络收集，不确认是否有效）
&lt;ol&gt;
&lt;li&gt;先创建一个&lt;code&gt;license.rb&lt;/code&gt; 。&lt;code&gt;mkdir crack &amp;amp;&amp;amp; cd crack &amp;amp;&amp;amp; nano license.rb&lt;/code&gt;&lt;pre&gt;&lt;code&gt;require &quot;openssl&quot;
require &quot;gitlab/license&quot;

key_pair = OpenSSL::PKey::RSA.generate(2048)
File.open(&quot;license_key&quot;, &quot;w&quot;) { |f| f.write(key_pair.to_pem) }

public_key = key_pair.public_key
File.open(&quot;license_key.pub&quot;, &quot;w&quot;) { |f| f.write(public_key.to_pem) }

private_key = OpenSSL::PKey::RSA.new File.read(&quot;license_key&quot;)
Gitlab::License.encryption_key = private_key

license = Gitlab::License.new
license.licensee = {
  &quot;Name&quot; =&amp;gt; &quot;修改为你想叫的名字&quot;,
  &quot;Company&quot; =&amp;gt; &quot;修改为你想叫的名字&quot;,
  &quot;Email&quot; =&amp;gt; &quot;修改为你想要的邮箱@example.com&quot;,
}
license.starts_at = Date.new(2024, 1, 1) # 开始时间
license.expires_at = Date.new(2050, 12, 31) # 结束时间
license.notify_admins_at = Date.new(2049, 12, 31)
license.notify_users_at = Date.new(2049, 12, 31)
license.block_changes_at = Date.new(2050, 12, 1)
license.restrictions = {
  active_user_count: 100000,
  plan: &quot;ultimate&quot;,
  id: 1,
  subscription_id: 1,
}

puts &quot;License:&quot;
puts license

data = license.export
puts &quot;Exported license:&quot;
puts data
File.open(&quot;GitLabBV.gitlab-license&quot;, &quot;w&quot;) { |f| f.write(data) }

public_key = OpenSSL::PKey::RSA.new File.read(&quot;license_key.pub&quot;)
Gitlab::License.encryption_key = public_key

data = File.read(&quot;GitLabBV.gitlab-license&quot;)
$license = Gitlab::License.import(data)

puts &quot;Imported license:&quot;
puts $license

unless $license
  raise &quot;The license is invalid.&quot;
end

if $license.restricted?(:active_user_count)
  active_user_count = 10000
  if active_user_count &amp;gt; $license.restrictions[:active_user_count]
    raise &quot;The active user count exceeds the allowed amount!&quot;
  end
end

if $license.notify_admins?
  puts &quot;The license is due to expire on #{$license.expires_at}.&quot;
end

if $license.notify_users?
  puts &quot;The license is due to expire on #{$license.expires_at}.&quot;
end

module Gitlab
  class GitAccess
    def check(cmd, changes = nil)
      if $license.block_changes?
        return build_status_object(false, &quot;License expired&quot;)
      end
    end
  end
end

puts &quot;This instance of GitLab Enterprise Edition is licensed to:&quot;
$license.licensee.each do |key, value|
  puts &quot;#{key}: #{value}&quot;
end

if $license.expired?
  puts &quot;The license expired on #{$license.expires_at}&quot;
elsif $license.will_expire?
  puts &quot;The license will expire on #{$license.expires_at}&quot;
else
  puts &quot;The license will never expire.&quot;
end
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;运行ruby镜像生成证书。&lt;code&gt;sudo docker run -it --rm -v ./crack:/crack ruby:latest bash&lt;/code&gt;。会进入到&lt;strong&gt;容器内的bash界面&lt;/strong&gt;，输入以下指令：&lt;pre&gt;&lt;code&gt;cd /crack &amp;amp;&amp;amp; gem install gitlab-license &amp;amp;&amp;amp; ruby license.rb
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;会额外生成三个文件&lt;code&gt;GitLabBV.gitlab-license&lt;/code&gt;, &lt;code&gt;license_key&lt;/code&gt;, &lt;code&gt;license_key.pub&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;退出容器，进入crack文件夹，把&lt;code&gt;license_key.pub&lt;/code&gt;拷贝到容器内。&lt;pre&gt;&lt;code&gt;sudo docker compose cp license_key.pub gitlab:/opt/gitlab/embedded/service/gitlab-rails/.license_encryption_key.pub
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;按图片操作。&lt;code&gt;cat crack/GitLabBV.gitlab-license&lt;/code&gt;。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd817440bed.png&quot; alt=&quot;CleanShot 2024-10-25 at 13.46.47@2x.png&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;配置Nginx Proxy Manager&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;没什么难度，就反代80端口就行。不再赘述了这里。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;其他&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;非标端口进行ssh推送&lt;pre&gt;&lt;code&gt;git clone ssh://git@exmaple.com:65009/develop/test.git
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>PVE旁路由透明代理</title><link>https://blog.useforall.com/posts/7/</link><guid isPermaLink="true">https://blog.useforall.com/posts/7/</guid><description>在PVE里，使用两种方法实现透明代理。通过创建CT虚拟机或者创建OpenWRT虚拟机（笔者目前使用的方法）。涉及到的文件都是免费的，请联系笔者获取。</description><pubDate>Tue, 25 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
在PVE里，使用两种方法实现透明代理。通过创建CT虚拟机或者创建OpenWRT虚拟机（笔者目前使用的方法）。涉及到的文件都是免费的，请联系笔者获取。
:::&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2025-3-1：添加OPNSense UDP说明。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;CT虚拟机方式&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;下载debian/ubuntu CT template。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd6daca2ba9.png&quot; alt=&quot;CleanShot 2024-09-23 at 21.55.09@2x.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;创建完成后，开始直通设备。请确认自己PVE安装的版本，PVE8.2版本开始，tun设备直通和之前版本不一样了。
:::note{title=&quot;PVE &amp;lt; 8.2&quot;}
确认新创建的CT机器的ID，打开PVE自身的Shell，输入&lt;code&gt;nano /etc/pve/lxc/容器id.conf&lt;/code&gt;&lt;pre&gt;&lt;code&gt;# 添加最后
lxc.apparmor.profile: unconfined
lxc.cgroup.devices.allow: a
lxc.cap.drop:
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
&lt;/code&gt;&lt;/pre&gt;
:::
:::note{title=&quot;PVE ≥ 8.2&quot;}
添加一个Device Passthrough
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd6fd2089d0.png&quot; alt=&quot;CleanShot 2024-09-30 at 17.38.22@2x.png&quot; /&gt;
:::&lt;/li&gt;
&lt;li&gt;完成后，启动CT虚拟机，从PVE进入虚拟机的Console，手动开启root登录权限&lt;pre&gt;&lt;code&gt;sed -i &apos;s/^#\?PermitRootLogin.*/PermitRootLogin yes/&apos; /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alist.hk.tencent.useforall.com/OneDrive%E5%A4%87%E4%BB%BD/Home/%E8%BD%AF%E4%BB%B6%E5%8C%85/TPCLbuild.tar.gz&quot;&gt;下载Clash文件&lt;/a&gt;，并执行下列指令&lt;pre&gt;&lt;code&gt;tar -zxf TPCLbuild.tar.gz &amp;amp;&amp;amp; cd TPCLbuild
mv tpclash-meta-linux-amd64 /usr/local/bin/tpclash &amp;amp;&amp;amp; chmod +x /usr/local/bin/tpclash # 自行根据机器架构选择文件
mv tpclash.service /etc/systemd/system
mv clash.yaml /etc/clash.yaml
sudo systemctl enable tpclash.service --now &amp;amp;&amp;amp; systemctl start tpclash.service
&lt;/code&gt;&lt;/pre&gt;
自行替换clash.yaml的内容&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;OpenWRT虚拟机方式&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;下载&lt;a href=&quot;https://alist.hk.tencent.useforall.com/OneDrive%E5%A4%87%E4%BB%BD/Home/%E8%BD%AF%E4%BB%B6%E5%8C%85/openwrt-192.168.100.4.gz&quot;&gt;镜像包&lt;/a&gt;并解压后上传至&lt;code&gt;PVE-&amp;gt;local(PVE)-&amp;gt;ISO Image&lt;/code&gt;（或者可以自行去&lt;a href=&quot;https://openwrt.ai/&quot;&gt;OpenWRT.ai&lt;/a&gt;创建自己的镜像）。&lt;/li&gt;
&lt;li&gt;创建虚拟机（看图）
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd7309c3767.png&quot; alt=&quot;CleanShot 2024-10-26 at 01.13.12@2x.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;创建完成后（不要开机虚拟机），进去PVE自身的Shell，import上传的镜像到虚拟机
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd744274c08.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;此时回到创建好的虚拟机，点击&lt;code&gt;Hardware&lt;/code&gt; ，会出现一个未分配磁盘。
&lt;ol&gt;
&lt;li&gt;双击磁盘
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd74fe0fa74.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;点击添加
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd74fbbe340.png&quot; alt=&quot;image2.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;扩大空间(不做也可以,不重要)。此时也可以删除&lt;code&gt;CD/DVD Drive&lt;/code&gt; （不做也可以,不重要）
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd74f5682d7.png&quot; alt=&quot;image3.png&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;修改引导顺序，把刚刚设置的硬盘调整到第一位，取消其他的引导盘。记得打开开机自启。最后启动虚拟机。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd756eebab4.png&quot; alt=&quot;image4.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;进入OpenWRT（我的镜像默认为192.168.100.4）添加DNS转发指向Clash fakeip端口。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd75cf64c9e.png&quot; alt=&quot;CleanShot 2024-10-26 at 01.24.12@2x.png&quot; /&gt;&lt;/li&gt;
&lt;li&gt;下载&lt;a href=&quot;https://alist.hk.tencent.useforall.com/OneDrive%E5%A4%87%E4%BB%BD/Home/%E8%BD%AF%E4%BB%B6%E5%8C%85/Backup-OpenClash-Kwrt-x86_64-2024-10-26-00-35-33.tar.gz&quot;&gt;OpenClash配置备份&lt;/a&gt;，在图片位置上传备份。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd7659401be.png&quot; alt=&quot;cleanshot&quot; /&gt;&lt;/li&gt;
&lt;li&gt;最后回到&lt;code&gt;运行状态&lt;/code&gt;，往下滑点击&lt;code&gt;启动OpenClash&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;软理由代理设置（这里使用的OPNSense防火墙）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建网关
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd76fbc7609.png&quot; alt=&quot;OPNSense创建网关&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;防火墙 → 规则 → LAN&lt;/code&gt;按如图创建规则。（如果需要UDP转发，记得创建第二条的时候把UDP也选上）
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd7762d8080.png&quot; alt=&quot;CleanShot 2024-10-02 at 12.15.30@2x.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::note{title=&quot;Docker MACVlan创建&quot;}
返回&lt;code&gt;OPNSense → Firewall → Aliases&lt;/code&gt;创建一个别名。可以放上家里其他设备的内网IP在&lt;code&gt;Content&lt;/code&gt;里。这里只是拿Docker的MacVlan举例。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd786413204.png&quot; alt=&quot;CleanShot 2024-10-02 at 12.12.49@2x.png&quot; /&gt;
还没写，见到请催更😋
:::&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;服务 → ISC DHCPv4&lt;/code&gt;。设置分发网关为主路由IP和DNS服务器为OpenWRT IP
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/02/25/67bd77b6d8764.png&quot; alt=&quot;CleanShot 2024-10-02 at 12.09.51@2x.png&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>VPS融合怪测试合集</title><link>https://blog.useforall.com/posts/6/</link><guid isPermaLink="true">https://blog.useforall.com/posts/6/</guid><description>目前已测试：腾讯云、ZgoCloud、Claw、Oracle部分机型&apos;&apos;</description><pubDate>Wed, 05 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import ShowMoreCollapse from &quot;../../components/custom/showMoreCollapse/main.tsx&quot;;&lt;/p&gt;
&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
目前已测试：腾讯云、ZgoCloud、Claw、Oracle部分机型
:::&lt;/p&gt;
&lt;p&gt;测试脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -it --rm --privileged --network host spiritlhl/goecs:latest -menu=true
curl -sL https://yabs.sh | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;腾讯云&lt;/h1&gt;
&lt;h2&gt;轻量服务器镜驰型 - 香港（2C8G200Mbps - ¥120/月）&lt;/h2&gt;
&lt;p&gt;&amp;lt;ShowMoreCollapse collapsedHeight={&apos;800px&apos;} client:visible&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--------------------------------------系统基础信息--------------------------------------
 CPU 型号            : AMD EPYC 7K62 48-Core Processor @2595.122 MHz
 CPU 数量            : 2 Virtual CPU(s)
 CPU 缓存            : 512 KB
 AES-NI              : ✔️ Enabled
 VM-x/AMD-V/Hyper-V  : ✔️ Enabled
 内存                : 3.20 GB / 7.63 GB
 气球驱动            : ✔️ Enabled
 虚拟内存 Swap       : [ no swap partition or swap file detected ]
 硬盘空间            : 31.75 GB / 78.63 GB
 系统                : alpine 3.21.3 [x86_64]
 内核                : 6.1.0-28-amd64
 系统在线时间        : 24 days, 00 hours, 56 minutes
 时区                : UTC
 负载                : 0.11 / 0.06 / 0.01
 虚拟化架构          : Dedicated (No visible signage)
 NAT类型             : Port Restricted Cone
 TCP加速方式         : cubic
 IPV4 ASN            : AS132203 Tencent Building, Kejizhongyi Avenue
 IPV4 Location       : Hong Kong / Singapore / Hong Kong
 IPV6 ASN            : AS132203 Tencent Building, Kejizhongyi Avenue
 IPV6 Location       : Singapore / Singapore / Singapore
--------------------------------CPU测试-通过sysbench测试--------------------------------
1 线程测试(单核)得分:   1461.81
2 线程测试(多核)得分:   2836.27
--------------------------------内存测试-通过sysbench测试---------------------------------
单线程顺序写速度: 10862.67 MB/s(11.39K IOPS, 5s)
单线程顺序读速度: 22816.06 MB/s(23.92K IOPS, 5s)
-----------------------------------硬盘测试-通过fio测试-----------------------------------
测试路径      块大小   读测试(IOPS)            写测试(IOPS)            总和(IOPS)
/root         4k       100.30 MB/s(25.1k)      100.56 MB/s(25.1k)      200.87 MB/s(50.2k)
/root         64k      282.05 MB/s(4406)       283.53 MB/s(4430)       565.57 MB/s(8836)
/root         512k     261.24 MB/s(510)        275.12 MB/s(537)        536.36 MB/s(1047)
/root         1m       257.75 MB/s(251)        274.92 MB/s(268)        532.68 MB/s(519)
-------------------------------------御三家流媒体解锁-------------------------------------
----------------Netflix-----------------
[IPV4]
您的出口IP可以使用Netflix，但仅可看Netflix自制剧
NF所识别的IP地域信息：中国香港
[IPV6]
您的网络可能没有正常配置IPv6，或者没有IPv6网络接入
----------------Youtube-----------------
[IPV4]
连接方式: Youtube Video Server
视频缓存节点地域: 中国香港(HKG07S42)
Youtube识别地域: 中国香港(HK)
[IPV6]
Youtube在您的出口IP所在的国家不提供服务
---------------DisneyPlus---------------
[IPV4]
当前出口所在地区解锁DisneyPlus
区域：HK 区
[IPV6]
DisneyPlus在您的出口IP所在的国家不提供服务
-------------------------------------跨国流媒体解锁--------------------------------------
IPV4:
============[ 跨国平台 ]============
Dazn                      Banned
Disney+                   NO (forbidden-location)
Netflix                   Restricted (Originals Only)
Netflix CDN               JP
YouTube Region            YES (Region: HK)
YouTube CDN               HKG
Amazon Prime Video        YES (Region: HK)
Paramount+                YES
TVBAnywhere+              YES (Region: HK)
IQiYi                     YES (Region: HK)
Viu.com                   YES
Spotify Registration      YES (Region: HK)
Steam Store               YES (Community Available) (Region: HK)
ChatGPT                   YES (Only Available with Mobile APP)
Sora                      Banned (VPN Blocked)
Claude                    YES
Gemini                    NO
MetaAI                    NO (AbraGeoBlocked)
Apple                     YES (Region: HKG)
Wikipedia Editability     NO
Reddit                    NO
TikTok                    NO
BingSearch                YES (Region: HK)
Instagram Licensed Audio  YES
KOCOWA                    NO
SonyLiv                   NO (Proxy Detected) (Region: HK)
OneTrust                  YES (Region: HK)
GoogleSearch              YES
--------------------------------------IP质量检测--------------------------------------
以下为各数据库编号，输出结果后将自带数据库来源对应的编号
ipinfo数据库  [0] | scamalytics数据库 [1] | virustotal数据库   [2] | abuseipdb数据库   [3] | ip2location数据库    [4]
ip-api数据库  [5] | ipwhois数据库     [6] | ipregistry数据库   [7] | ipdata数据库      [8] | db-ip数据库          [9]
ipapiis数据库 [A] | ipapicom数据库    [B] | bigdatacloud数据库 [C] | cheervision数据库 [D] | ipqualityscore数据库 [E]
IPV4:
安全得分:
声誉(越高越好): 0 [2]
信任得分(越高越好): 0 [8]
VPN得分(越低越好): 100 [8]
代理得分(越低越好): 100 [8]
社区投票-无害: 0 [2]
社区投票-恶意: 0 [2]
威胁得分(越低越好): 100 [8]
欺诈得分(越低越好): 0 [1] 65 [E]
滥用得分(越低越好): 0 [3]
ASN滥用得分(越低越好): 0.0028 (Low) [A]
公司滥用得分(越低越好): 0 (Very Low) [A]
威胁级别: low [9 B]
黑名单记录统计:(有多少黑名单网站有记录):
无害记录数: 0 [2]  恶意记录数: 0 [2]  可疑记录数: 0 [2]  无记录数: 94 [2]
安全信息:
使用类型: DataCenter/WebHosting/Transit [3] business [8] hosting [0 7 9 A] unknown [C]
公司类型: hosting [0 7] business [A]
是否云提供商: Yes [7 D]
是否数据中心: Yes [0 1 5 6 A] No [8 C]
是否移动设备: No [5 A C] Yes [E]
是否代理: Yes [E] No [0 1 4 5 6 7 8 9 A B C D]
是否VPN: Yes [A E] No [0 1 6 7 C D]
是否Tor: No [0 1 3 6 7 8 A B C D E]
是否Tor出口: No [1 7 D]
是否网络爬虫: No [9 A B E]
是否匿名: No [1 6 7 8 D]
是否攻击者: No [7 8 D]
是否滥用者: No [7 8 A C D E]
是否威胁: No [7 8 C D]
是否中继: No [0 7 8 C D]
是否Bogon: No [7 8 A C D]
是否机器人: No [E]
DNS-黑名单: 313(Total_Check) 0(Clean) 5(Blacklisted) 23(Other)
IPV6:
安全得分:
欺诈得分(越低越好): 0 [1]
滥用得分(越低越好): 0 [3]
ASN滥用得分(越低越好): 0.0028 (Low) [A]
公司滥用得分(越低越好): 0 (Very Low) [A]
威胁级别: low [B]
安全信息:
使用类型: DataCenter/WebHosting/Transit [3] hosting [A]
公司类型: business [A]
是否云提供商: Yes [D]
是否数据中心: No [A] Yes [1]
是否移动设备: No [A]
是否代理: No [1 A B D]
是否VPN: No [1 A D]
是否TorExit: No [1 D]
是否Tor出口: No [1 D]
是否网络爬虫: No [A B]
是否匿名: No [1 D]
是否攻击者: No [D]
是否滥用者: No [A D]
是否威胁: No [D]
是否中继: No [D]
是否Bogon: No [A D]
DNS-黑名单: 313(Total_Check) 0(Clean) 0(Blacklisted) 313(Other)
--------------------------------------邮件端口检测--------------------------------------
Platform  SMTP  SMTPS POP3  POP3S IMAP  IMAPS
LocalPort ✔     ✔     ✔     ✔     ✔     ✔
QQ        ✘     ✔     ✔     ✘     ✔     ✘
163       ✘     ✔     ✔     ✘     ✔     ✘
Sohu      ✘     ✔     ✔     ✘     ✔     ✘
Yandex    ✘     ✔     ✔     ✘     ✔     ✘
Gmail     ✘     ✔     ✘     ✘     ✘     ✘
Outlook   ✘     ✘     ✔     ✘     ✔     ✘
Office365 ✘     ✘     ✔     ✘     ✔     ✘
Yahoo     ✘     ✔     ✘     ✘     ✘     ✘
MailCOM   ✘     ✔     ✔     ✘     ✔     ✘
MailRU    ✘     ✔     ✘     ✘     ✔     ✘
AOL       ✘     ✔     ✘     ✘     ✘     ✘
GMX       ✘     ✘     ✔     ✘     ✔     ✘
Sina      ✘     ✔     ✔     ✘     ✔     ✘
Apple     ✘     ✘     ✘     ✘     ✘     ✘
FastMail  ✘     ✔     ✘     ✘     ✘     ✘
ProtonMail✘     ✘     ✘     ✘     ✘     ✘
MXRoute   ✘     ✘     ✔     ✘     ✔     ✘
Namecrane ✘     ✔     ✔     ✘     ✔     ✘
XYAMail   ✘     ✘     ✘     ✘     ✘     ✘
ZohoMail  ✘     ✔     ✘     ✘     ✘     ✘
Inbox_eu  ✘     ✔     ✔     ✘     ✘     ✘
Free_fr   ✘     ✔     ✔     ✘     ✔     ✘
-------------------------------------三网回程线路检测-------------------------------------
北京电信 219.141.140.10  电信163    [普通线路]
北京联通 202.106.195.68  联通4837   [普通线路]
北京移动 221.179.155.161 移动CMI    [普通线路]
上海电信 202.96.209.133  电信163    [普通线路]
上海联通 210.22.97.1     联通4837   [普通线路]
上海移动 211.136.112.200 电信163    [普通线路]
广州电信 58.60.188.222   电信163    [普通线路]
广州联通 210.21.196.6    联通4837   [普通线路]
广州移动 120.196.165.24  电信CN2GT  [优质线路] 电信163    [普通线路]
成都电信 61.139.2.69     电信163    [普通线路]
成都联通 119.6.6.6       联通4837   [普通线路]
成都移动 211.137.96.205  移动CMI    [普通线路]
-------------------------------------三网回程路由检测-------------------------------------
[NextTrace API] preferred API IP - 172.67.69.163 - 32.10ms - Misaka.HKG
广州电信 - ICMP v4 - traceroute to 58.60.188.222, 30 hops max, 52 byte packets
*
*
1.77 ms      *
1.84 ms      *
1.69 ms      *
2.16 ms      AS4134     [CHINANET-FJ]      中国, 香港, www.chinatelecom.com.cn  电信
19.76 ms     AS4134     [CHINANET-BB]      中国, 广东, 广州, www.chinatelecom.com.cn
6.80 ms      AS4134     [CHINANET-BB]      中国, 广东, 广州, www.chinatelecom.com.cn
*
13.62 ms     AS4134     [CHINANET-GD]      中国, 广东, 深圳, www.chinatelecom.com.cn  电信
*
10.13 ms     AS4134                        中国, 广东, 深圳, www.chinatelecom.com.cn  电信
广州联通 - ICMP v4 - traceroute to 210.21.196.6, 30 hops max, 52 byte packets
1.12 ms      *
*
*
1.57 ms      *
1.49 ms      *
3.47 ms      AS10099    [CUG-BACKBONE]     中国, 香港, chinaunicomglobal.com  联通
3.52 ms      *                             中国, 香港
*
2.70 ms      AS10099                       中国, 香港, chinaunicomglobal.com
6.95 ms      AS4837     [CU169-BACKBONE]   中国, 广东, 广州, chinaunicom.cn
6.90 ms      AS4837     [CU169-BACKBONE]   中国, 广东, 广州, chinaunicom.cn  联通
*
10.05 ms     AS17816    [APNIC-AP]         中国, 广东, 深圳, chinaunicom.cn  联通
16.77 ms     AS17623    [APNIC-AP]         中国, 广东, 深圳, chinaunicom.cn  联通
10.64 ms     AS17623                       中国, 广东, 深圳, chinaunicom.cn  联通
广州移动 - ICMP v4 - traceroute to 120.196.165.24, 30 hops max, 52 byte packets
*
*
*
1.60 ms      *
2.01 ms      *
1.33 ms      *
24.95 ms     AS23764    [CTHKI-HK]         中国, 香港, chinatelecomglobal.com  电信
*
2.08 ms      *                             中国, 香港, 电信/CTGNet
*
6.85 ms      *          [CN2-BackBone]     中国, 广东, 广州, chinatelecom.cn  电信
57.42 ms     AS4134     [CHINANET-BB]      中国, 广东, 广州, www.chinatelecom.com.cn  电信
65.38 ms     AS4134     [CHINANET-BB]      中国, 广东, 广州, www.chinatelecom.com.cn  电信
*
107.81 ms    AS9808     [CMNET]            中国, 北京, 北京, chinamobileltd.com
*
89.45 ms     AS9808     [CMNET]            中国, 广东, 广州, chinamobileltd.com  移动
65.42 ms     AS9808     [CMNET]            中国, 广东, 广州, chinamobileltd.com  移动
72.82 ms     AS56040    [APNIC-AP]         中国, 广东, 深圳, gd.10086.cn  移动
--------------------------------------就近节点测速--------------------------------------
位置            上传速度        下载速度        延迟            丢包率
Speedtest.net   118.38 Mbps     353.83 Mbps     1.28 ms         Not available.
中国香港        191.02 Mbps     332.93 Mbps     3.11 ms         Not available.
新加坡          198.79 Mbps     522.36 Mbps     32.93 ms        0.0%
联通上海5G      193.16 Mbps     566.52 Mbps     32.52 ms        0.0%
电信Zhenjiang5G 192.45 Mbps     889.17 Mbps     35.10 ms        Not available.
电信Suzhou5G    190.59 Mbps     603.24 Mbps     31.51 ms        Not available.
移动Chengdu     186.63 Mbps     664.38 Mbps     96.74 ms        Not available.
----------------------------------------------------------------------------------
花费          : 9 分 22 秒
时间          : Tue Feb 25 06:48:31 UTC 2025
----------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;
:::note{title=&quot;注&quot;}
有人反馈存在限速问题，本人目前暂时没有遇到。相比阿里云的200mbps的机器，限速策略相对较好。阿里云一天使用60GB会限速到20Mbps，解封时间一般是第二天。
:::&lt;/p&gt;
&lt;h1&gt;ZgoCloud&lt;/h1&gt;
&lt;h2&gt;Ryzen9 Performance VPS - 洛杉矶（2C2G500Mbps - $58/年)&lt;/h2&gt;
&lt;p&gt;&amp;lt;ShowMoreCollapse collapsedHeight={&apos;800px&apos;} client:visible&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--------------------------------------系统基础信息--------------------------------------
 CPU 型号            : AMD Ryzen 9 7950X 16-Core Processor @4491.556 MHz
 CPU 数量            : 2 Virtual CPU(s)
 CPU 缓存            : 1024 KB
 AES-NI              : ✔️ Enabled
 VM-x/AMD-V/Hyper-V  : ✔️ Enabled
 内存                : 653.32 MB / 1.89 GB
 气球驱动            : ✔️ Enabled
 虚拟内存 Swap       : [ no swap partition or swap file detected ]
 硬盘空间            : 8.93 GB / 39.82 GB
 系统                : alpine 3.21.2 [x86_64]
 内核                : 6.1.0-30-amd64
 系统在线时间        : 2 days, 16 hours, 16 minutes
 时区                : UTC
 负载                : 0.01 / 0.01 / 0.00
 虚拟化架构          : Dedicated (No visible signage)
 NAT类型             : Full Cone
 TCP加速方式         : bbr
 IPV4 ASN            : AS8796 FD-298-8796
 IPV4 Location       : Frankfort / Kentucky / United States
 IPV6 ASN            : AS8796 FD-298-8796
 IPV6 Location       : Frankfort / Kentucky / United States
 IPv6 子网掩码       : /64
--------------------------------CPU测试-通过sysbench测试--------------------------------
1 线程测试(单核)得分:   5096.14
2 线程测试(多核)得分:  10156.51
--------------------------------内存测试-通过sysbench测试---------------------------------
单线程顺序写速度: 24542.72 MB/s(25.73K IOPS, 5s)
单线程顺序读速度: 32140.93 MB/s(33.70K IOPS, 5s)
-----------------------------------硬盘测试-通过fio测试-----------------------------------
测试路径      块大小   读测试(IOPS)            写测试(IOPS)            总和(IOPS)
/root         4k       246.86 MB/s(61.7k)      247.51 MB/s(61.9k)      494.38 MB/s(123.6k)
/root         64k      1.94 GB/s(30.3k)        1.95 GB/s(30.5k)        3.89 GB/s(60.9k)
/root         512k     1.70 GB/s(3325)         1.79 GB/s(3501)         3.50 GB/s(6826)
/root         1m       1.24 GB/s(1214)         1.33 GB/s(1295)         2.57 GB/s(2509)
-------------------------------------御三家流媒体解锁-------------------------------------
----------------Netflix-----------------
[IPV4]
您的出口IP完整解锁Netflix，支持非自制剧的观看
NF所识别的IP地域信息：美国
[IPV6]
您的出口IP完整解锁Netflix，支持非自制剧的观看
NF所识别的IP地域信息：美国
----------------Youtube-----------------
[IPV4]
连接方式: Youtube Video Server
视频缓存节点地域: 美国  洛杉机(LAX31S13)
[IPV6]
连接方式: Youtube Video Server
视频缓存节点地域: 美国  洛杉机(LAX17S56)
---------------DisneyPlus---------------
[IPV4]
当前出口所在地区解锁DisneyPlus
区域：US 区
[IPV6]
当前出口所在地区解锁DisneyPlus
区域：US 区
-------------------------------------跨国流媒体解锁--------------------------------------
IPV4:
============[ 跨国平台 ]============
Dazn                      YES (Region: US)
Disney+                   YES (Region: US)
Netflix                   YES (Region: US)
Netflix CDN               US
YouTube Region            YES (Region: US)
YouTube CDN               LAX
Amazon Prime Video        YES (Region: US)
Paramount+                YES
TVBAnywhere+              YES (Region: US)
IQiYi                     YES (Region: US)
Viu.com                   YES
Spotify Registration      YES (Region: US)
Steam Store               YES (Community Available) (Region: US)
ChatGPT                   YES (Region: US)
Claude                    YES
Sora                      YES (Region: US)
Gemini                    YES (Region: USA)
MetaAI                    NO (AbraGeoBlocked)
Wikipedia Editability     YES
Reddit                    YES
TikTok                    YES (Region: US)
BingSearch                YES (Region: US)
Instagram Licensed Audio  YES
KOCOWA                    YES
SonyLiv                   YES (Region: US)
OneTrust                  YES (Region: US)
GoogleSearch              YES
--------------------------------------IP质量检测--------------------------------------
以下为各数据库编号，输出结果后将自带数据库来源对应的编号
ipinfo数据库  [0] | scamalytics数据库 [1] | virustotal数据库   [2] | abuseipdb数据库   [3] | ip2location数据库    [4]
ip-api数据库  [5] | ipwhois数据库     [6] | ipregistry数据库   [7] | ipdata数据库      [8] | db-ip数据库          [9]
ipapiis数据库 [A] | ipapicom数据库    [B] | bigdatacloud数据库 [C] | cheervision数据库 [D] | ipqualityscore数据库 [E]
IPV4:
安全得分:
声誉(越高越好): 0 [2]
信任得分(越高越好): 1 [8]
VPN得分(越低越好): 97 [8]
代理得分(越低越好): 100 [8]
社区投票-无害: 0 [2]
社区投票-恶意: 0 [2]
威胁得分(越低越好): 100 [8]
欺诈得分(越低越好): 0 [1 E]
滥用得分(越低越好): 0 [3]
ASN滥用得分(越低越好): 0.0004 (Very Low) [A]
公司滥用得分(越低越好): 0.0039 (Low) [A]
威胁级别: low [9 B]
黑名单记录统计:(有多少黑名单网站有记录):
无害记录数: 0 [2]  恶意记录数: 0 [2]  可疑记录数: 0 [2]  无记录数: 94 [2]
安全信息:
使用类型: FixedLineISP [3] hosting - moderate probability [C] isp [0 7] corporate [9] business [8]
公司类型: business [0 A] isp [7]
是否云提供商: No [7 D]
是否数据中心: No [0 5 6 8 A C] Yes [1]
是否移动设备: No [5 A C] Yes [E]
是否代理: No [0 1 4 5 6 7 8 9 A B C D E]
是否VPN: No [0 1 6 7 A C D E]
是否TorExit: No [1 7 D]
是否Tor出口: No [1 7 D]
是否网络爬虫: No [9 A B E]
是否匿名: No [1 6 7 8 D]
是否攻击者: No [7 8 D]
是否滥用者: No [7 8 A C D E]
是否威胁: No [7 8 C D]
是否中继: No [0 7 8 C D]
是否Bogon: No [7 8 A C D]
是否机器人: No [E]
DNS-黑名单: 313(Total_Check) 0(Clean) 5(Blacklisted) 23(Other)
IPV6:
安全得分:
欺诈得分(越低越好): 0 [1]
滥用得分(越低越好): 0 [3]
ASN滥用得分(越低越好): 0.0004 (Very Low) [A]
公司滥用得分(越低越好): 0 (Very Low) [A]
威胁级别: low [B]
安全信息:
使用类型: FixedLineISP [3]
公司类型: hosting [A]
是否云提供商: No [D]
是否数据中心: Yes [1 A]
是否移动设备: No [A]
是否代理: No [1 A B D]
是否VPN: No [1 A D]
是否TorExit: No [1 D]
是否Tor出口: No [1 D]
是否网络爬虫: No [A B]
是否匿名: No [1 D]
是否攻击者: No [D]
是否滥用者: No [A D]
是否威胁: No [D]
是否中继: No [D]
是否Bogon: No [A D]
DNS-黑名单: 313(Total_Check) 0(Clean) 0(Blacklisted) 313(Other)
--------------------------------------邮件端口检测--------------------------------------
Platform  SMTP  SMTPS POP3  POP3S IMAP  IMAPS
LocalPort ✔     ✔     ✔     ✔     ✔     ✔
QQ        ✔     ✔     ✔     ✘     ✔     ✘
163       ✔     ✔     ✔     ✘     ✔     ✘
Sohu      ✔     ✔     ✔     ✘     ✔     ✘
Yandex    ✔     ✔     ✔     ✘     ✔     ✘
Gmail     ✔     ✔     ✘     ✘     ✘     ✘
Outlook   ✔     ✘     ✔     ✘     ✔     ✘
Office365 ✔     ✘     ✔     ✘     ✔     ✘
Yahoo     ✔     ✔     ✘     ✘     ✘     ✘
MailCOM   ✔     ✔     ✔     ✘     ✔     ✘
MailRU    ✔     ✔     ✘     ✘     ✔     ✘
AOL       ✔     ✔     ✘     ✘     ✘     ✘
GMX       ✔     ✘     ✔     ✘     ✔     ✘
Sina      ✔     ✔     ✔     ✘     ✔     ✘
Apple     ✘     ✘     ✘     ✘     ✘     ✘
FastMail  ✘     ✔     ✘     ✘     ✘     ✘
ProtonMail✘     ✘     ✘     ✘     ✘     ✘
MXRoute   ✔     ✘     ✔     ✘     ✔     ✘
Namecrane ✔     ✔     ✔     ✘     ✔     ✘
XYAMail   ✘     ✘     ✘     ✘     ✘     ✘
ZohoMail  ✘     ✔     ✘     ✘     ✘     ✘
Inbox_eu  ✔     ✔     ✔     ✘     ✘     ✘
Free_fr   ✘     ✔     ✔     ✘     ✔     ✘
-------------------------------------三网回程线路检测-------------------------------------
北京电信 219.141.140.10  电信CN2GIA [精品线路]
北京联通 202.106.195.68  联通9929   [优质线路] 联通4837   [普通线路]
北京移动 221.179.155.161 移动CMI    [普通线路] 移动CMIN2  [精品线路]
上海电信 202.96.209.133  电信CN2GIA [精品线路]
上海联通 210.22.97.1     联通9929   [优质线路] 联通4837   [普通线路]
上海移动 211.136.112.200 移动CMI    [普通线路] 移动CMIN2  [精品线路]
广州电信 58.60.188.222   电信CN2GIA [精品线路]
广州联通 210.21.196.6    联通9929   [优质线路] 联通4837   [普通线路]
广州移动 120.196.165.24  移动CMI    [普通线路] 移动CMIN2  [精品线路]
成都电信 61.139.2.69     电信CN2GIA [精品线路]
成都联通 119.6.6.6       联通4837   [普通线路]
成都移动 211.137.96.205  移动CMI    [普通线路] 移动CMIN2  [精品线路]
-------------------------------------三网回程路由检测-------------------------------------
[NextTrace API] preferred API IP - 104.26.13.151 - 36.41ms - Misaka.LAX
广州电信 - ICMP v4 - traceroute to 58.60.188.222, 30 hops max, 52 byte packets
2.36 ms      AS8796                        美国, 加利福尼亚, 洛杉矶, kurun.com
*
0.77 ms      *
1.09 ms      AS4134     [CHINANET-US]      美国, 加利福尼亚, 洛杉矶, www.chinatelecom.com.cn  电信
150.95 ms    *          [CN2-BackBone]     中国, 广东, 广州, chinatelecom.cn  电信
255.25 ms    *          [CN2-Global]       中国, 广东, 广州, chinatelecom.cn  电信
157.06 ms    *          [CN2-BackBone]     中国, 广东, 广州, chinatelecom.cn  电信
160.22 ms    *          [CN2-BackBone]     中国, 广东, 广州, chinatelecom.cn  电信
*
*
160.10 ms    AS4134                        中国, 广东, 深圳, www.chinatelecom.com.cn  电信
广州联通 - ICMP v4 - traceroute to 210.21.196.6, 30 hops max, 52 byte packets
2.32 ms      AS8796                        美国, 加利福尼亚, 洛杉矶, kurun.com
*
0.46 ms      *
0.73 ms      AS10099    [CUG-BACKBONE]     美国, 加利福尼亚, 洛杉矶, chinaunicomglobal.com  联通
139.73 ms    AS10099    [CUG-BACKBONE]     美国, 加利福尼亚, 洛杉矶, chinaunicomglobal.com  联通
139.78 ms    AS9929     [CNC-BACKBONE]     中国, 上海, chinaunicom.cn  联通 CUII
165.99 ms    AS9929     [CNC-BACKBONE]     中国, 广东, 广州, chinaunicom.cn  联通 CUII
166.77 ms    *          [CNC-BACKBONE]     中国, 广东, 广州
*
*
166.99 ms    AS17816    [APNIC-AP]         中国, 广东, 深圳, chinaunicom.cn  联通
172.49 ms    AS17623    [APNIC-AP]         中国, 广东, 深圳, chinaunicom.cn  联通
171.72 ms    AS17623                       中国, 广东, 深圳, chinaunicom.cn  联通
广州移动 - ICMP v4 - traceroute to 120.196.165.24, 30 hops max, 52 byte packets
1.68 ms      AS8796                        美国, 加利福尼亚, 洛杉矶, kurun.com
*
3.48 ms      *
0.46 ms      *
0.73 ms      AS58807    [CMIN2-NET]        美国, 加利福尼亚, 洛杉矶, cmi.chinamobile.com  移动
127.14 ms    AS58807    [CMIN2-NET]        美国, 加利福尼亚, 洛杉矶, cmi.chinamobile.com  移动
126.21 ms    AS58807    [CMIN2-NET]        中国, 上海, cmi.chinamobile.com  移动
128.37 ms    AS9808     [CMNET]            中国, 上海, chinamobileltd.com  移动
127.70 ms    AS9808     [CMNET]            中国, 上海, chinamobileltd.com  移动
128.45 ms    AS9808     [CMNET]            中国, 上海, chinamobileltd.com
151.58 ms    AS9808     [CMNET]            中国, 北京, chinamobileltd.com  移动
151.93 ms    AS9808     [CMNET]            中国, 北京, chinamobileltd.com  移动
156.50 ms    AS9808     [CMNET]            中国, 北京, chinamobileltd.com  移动
154.20 ms    AS56040    [APNIC-AP]         中国, 广东, 深圳, gd.10086.cn  移动
--------------------------------------就近节点测速--------------------------------------
位置            上传速度        下载速度        延迟            丢包率
Speedtest.net   364.31 Mbps     209.50 Mbps     41.56 ms        8.3%
洛杉矶          304.36 Mbps     296.37 Mbps     4.22 ms         0.0%
日本东京        300.63 Mbps     204.13 Mbps     111.13 ms       0.0%
电信浙江        265.21 Mbps     204.96 Mbps     138.05 ms       Not available.
电信浙江        319.26 Mbps     242.33 Mbps     130.61 ms       Not available.
移动Chengdu     267.53 Mbps     345.72 Mbps     168.61 ms       Not available.
----------------------------------------------------------------------------------
花费          : 8 分 1 秒
时间          : Tue Feb 25 06:49:04 UTC 2025
----------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;
:::note{title=&quot;注&quot;}
长时间占用CPU会封禁机器（具体多长未注明），商家信誉有待考察。
:::&lt;/p&gt;
&lt;h1&gt;Oracle&lt;/h1&gt;
&lt;h1&gt;Claw&lt;/h1&gt;
&lt;h2&gt;VDS - 香港（2C2G1Gbps - $10/月）&lt;/h2&gt;
&lt;p&gt;&amp;lt;ShowMoreCollapse collapsedHeight={&apos;800px&apos;} client:visible&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--------------------------------------系统基础信息--------------------------------------
 CPU 型号            : Intel(R) Xeon(R) Platinum @ 2500.000 MHz
 CPU 数量            : 2 Virtual CPU(s)
 CPU 缓存            : 33792 KB
 AES-NI              : ✔️ Enabled
 VM-x/AMD-V/Hyper-V  : ✔️ Enabled
 内存                : 683.59 MB / 1.84 GB
 虚拟内存 Swap       : [ no swap partition or swap file detected ]
 硬盘空间            : 7.59 GB / 38.58 GB
 系统                : alpine 3.21.0 [x86_64]
 内核                : 5.15.0-131-generic
 系统在线时间        : 6 days, 16 hours, 34 minutes
 时区                : UTC
 负载                : 0.00 / 0.00 / 0.00
 虚拟化架构          : Dedicated (No visible signage)
 NAT类型             : Port Restricted Cone
 TCP加速方式         : cubic
 IPV4 ASN            : AS45102 Alibaba US Technology Co., Ltd.
 IPV4 Location       : Hong Kong / Hong Kong
 IPV6 ASN            : AS45102 Alibaba US Technology Co., Ltd.
 IPV6 Location       : Hong Kong / Hong Kong
--------------------------------CPU测试-通过sysbench测试--------------------------------
1 线程测试(单核)得分:    970.76
2 线程测试(多核)得分:   1552.59
--------------------------------内存测试-通过sysbench测试---------------------------------
单线程顺序写速度: 11142.64 MB/s(11.68K IOPS, 5s)
单线程顺序读速度: 20580.47 MB/s(21.58K IOPS, 5s)
-----------------------------------硬盘测试-通过fio测试-----------------------------------
测试路径      块大小   读测试(IOPS)            写测试(IOPS)            总和(IOPS)
/root         4k       20.67 MB/s(5166)        20.68 MB/s(5170)        41.35 MB/s(10.3k)
/root         64k      65.91 MB/s(1029)        66.33 MB/s(1036)        132.25 MB/s(2065)
/root         512k     63.45 MB/s(123)         66.65 MB/s(130)         130.10 MB/s(253)
/root         1m       62.70 MB/s(61)          67.17 MB/s(65)          129.87 MB/s(126)
-------------------------------------御三家流媒体解锁-------------------------------------
----------------Netflix-----------------
[IPV4]
您的出口IP可以使用Netflix，但仅可看Netflix自制剧
NF所识别的IP地域信息：新加坡
[IPV6]
您的网络可能没有正常配置IPv6，或者没有IPv6网络接入
----------------Youtube-----------------
[IPV4]
Youtube在您的出口IP所在的国家不提供服务
[IPV6]
Youtube在您的出口IP所在的国家不提供服务
---------------DisneyPlus---------------
[IPV4]
当前出口所在地区解锁DisneyPlus
区域：HK 区
[IPV6]
DisneyPlus在您的出口IP所在的国家不提供服务
-------------------------------------跨国流媒体解锁--------------------------------------
IPV4:
============[ 跨国平台 ]============
Dazn                      Banned
Disney+                   NO (forbidden-location)
Netflix                   Restricted (Originals Only)
Netflix CDN               HK
YouTube Region            YES (Region: HK) [Native]
YouTube CDN               NO (Network Err)
Amazon Prime Video        YES (Region: HK) [Native]
Paramount+                YES [Native]
TVBAnywhere+              NO
IQiYi                     YES (Region: HK) [Native]
Viu.com                   YES [Native]
Spotify Registration      YES (Region: HK) [Native]
Steam Store               YES (Community Available) (Region: HK)
ChatGPT                   YES (Only Available with Mobile APP) [Via DNS]
Sora                      Banned (VPN Blocked)
Gemini                    NO
MetaAI                    YES (Region: GB) [Native]
Wikipedia Editability     NO
Reddit                    NO
TikTok                    NO
Bing Region               YES (Risky) (Region: HK)
Instagram Licensed Audio  YES [Native]
KOCOWA                    NO
SonyLiv                   YES (Region: HK) [Via DNS]
OneTrust                  YES (Region: HK) [Via DNS]
GoogleSearch              YES
--------------------------------------IP质量检测--------------------------------------
以下为各数据库编号，输出结果后将自带数据库来源对应的编号
ipinfo数据库  [0] | scamalytics数据库 [1] | virustotal数据库   [2] | abuseipdb数据库   [3] | ip2location数据库    [4]
ip-api数据库  [5] | ipwhois数据库     [6] | ipregistry数据库   [7] | ipdata数据库      [8] | db-ip数据库          [9]
ipapiis数据库 [A] | ipapicom数据库    [B] | bigdatacloud数据库 [C] | cheervision数据库 [D] | ipqualityscore数据库 [E]
IPV4:
安全得分:
声誉(越高越好): 0 [2]
信任得分(越高越好): 0 [8]
VPN得分(越低越好): 100 [8]
代理得分(越低越好): 100 [8]
社区投票-无害: 0 [2]
社区投票-恶意: 0 [2]
威胁得分(越低越好): 100 [8]
欺诈得分(越低越好): 6 [1] 65 [E]
滥用得分(越低越好): 0 [3]
ASN滥用得分(越低越好): 0.0012 (Low) [A]
公司滥用得分(越低越好): 0.0047 (Low) [A]
威胁级别: low [9 B]
黑名单记录统计:(有多少黑名单网站有记录):
无害记录数: 0 [2] 恶意记录数: 0 [2]  可疑记录数: 0 [2]  无记录数: 94 [2]
安全信息:
使用类型: hosting [0 7 9 A] business [8] hosting - high probability [C] DataCenter/WebHosting/Transit [3]
公司类型: hosting [0 7 A]
是否云提供商: Yes [7 D]
是否数据中心: Yes [0 1 5 6 A] No [8 C]
是否移动设备: No [5 A C] Yes [E]
是否代理: No [0 1 4 5 6 7 8 9 A B C D] Yes [E]
是否VPN: Yes [A E] No [0 1 6 7 C D]
是否Tor: No [0 1 3 6 7 8 A B C D E]
是否Tor出口: No [1 7 D]
是否网络爬虫: No [9 A B E]
是否匿名: No [1 6 7 8 D]
是否攻击者: No [7 8 D]
是否滥用者: No [7 8 A C D E]
是否威胁: No [7 8 C D]
是否中继: No [0 7 8 C D]
是否Bogon: No [7 8 A C D]
是否机器人: No [E]
DNS-黑名单: 313(Total_Check) 0(Clean) 6(Blacklisted) 18(Other)
IPV6:
安全得分:
欺诈得分(越低越好): 15 [1]
滥用得分(越低越好): 0 [3]
ASN滥用得分(越低越好): 0.0012 (Low) [A]
公司滥用得分(越低越好): 0 (Very Low) [A]
威胁级别: low [B]
安全信息:
使用类型: DataCenter/WebHosting/Transit [3] hosting [A]
公司类型: hosting [A]
是否云提供商: Yes [D]
是否数据中心: Yes [1 A]
是否移动设备: No [A]
是否代理: No [1 A B D]
是否VPN: No [1 A D]
是否TorExit: No [1 D]
是否Tor出口: No [1 D]
是否网络爬虫: No [A B]
是否匿名: No [1 D]
是否攻击者: No [D]
是否滥用者: No [A D]
是否威胁: No [D]
是否中继: No [D]
是否Bogon: No [A D]
DNS-黑名单: 313(Total_Check) 0(Clean) 0(Blacklisted) 313(Other)
--------------------------------------邮件端口检测--------------------------------------
Platform  SMTP  SMTPS POP3  POP3S IMAP  IMAPS
LocalPort ✔     ✔     ✔     ✔     ✔     ✔
QQ        ✘     ✔     ✔     ✘     ✔     ✘
163       ✘     ✔     ✔     ✘     ✔     ✘
Sohu      ✘     ✔     ✘     ✘     ✔     ✘
Yandex    ✘     ✔     ✔     ✘     ✔     ✘
Gmail     ✘     ✔     ✘     ✘     ✘     ✘
Outlook   ✘     ✘     ✔     ✘     ✔     ✘
Office365 ✘     ✘     ✔     ✘     ✔     ✘
Yahoo     ✘     ✔     ✘     ✘     ✘     ✘
MailCOM   ✘     ✔     ✔     ✘     ✔     ✘
MailRU    ✘     ✔     ✘     ✘     ✔     ✘
AOL       ✘     ✔     ✘     ✘     ✘     ✘
GMX       ✘     ✘     ✔     ✘     ✔     ✘
Sina      ✘     ✔     ✔     ✘     ✔     ✘
-------------------------------------三网回程线路检测-------------------------------------
北京电信 219.141.140.10  电信163    [普通线路]
北京联通 202.106.195.68  联通4837   [普通线路]
北京移动 221.179.155.161 移动CMI    [普通线路]
上海电信 202.96.209.133  电信163    [普通线路]
上海联通 210.22.97.1     联通4837   [普通线路]
上海移动 211.136.112.200 移动CMI    [普通线路]
广州电信 58.60.188.222   联通4837   [普通线路]
广州联通 210.21.196.6    联通4837   [普通线路]
广州移动 120.196.165.24  移动CMI    [普通线路]
成都电信 61.139.2.69     联通4837   [普通线路]
成都联通 119.6.6.6       联通4837   [普通线路]
成都移动 211.137.96.205  移动CMI    [普通线路]
-------------------------------------三网回程路由检测-------------------------------------
[NextTrace API] preferred API IP - 172.67.69.163 - 30.78ms - Misaka.HKG
广州电信 - ICMP v4 - traceroute to 58.60.188.222, 30 hops max, 52 byte packets
*
*
1.61 ms      *
1.91 ms      *
258.98 ms    *
2.98 ms      *                             中国, 香港
2.62 ms      *          [CUG-ASIA]         中国, 香港
*
56.58 ms     AS10099                       中国, 香港, chinaunicomglobal.com
11.41 ms     AS4837     [CU169-BACKBONE]   中国, 广东, 广州, chinaunicom.cn
8.56 ms      AS4837     [CU169-BACKBONE]   中国, 广东, 广州, chinaunicom.cn  联通
9.53 ms      AS4837     [CU169-BACKBONE]   中国, 广东, 广州, chinaunicom.cn  联通
*
143.01 ms    AS4837     [CU169-BACKBONE]   中国, 北京, chinaunicom.cn  联通
*
*
*
40.57 ms     AS134774   [CHINANET-GD]      中国, 广东, 深圳, chinatelecom.cn  电信
*
40.65 ms     AS4134                        中国, 广东, 深圳, www.chinatelecom.com.cn  电信
广州联通 - ICMP v4 - traceroute to 210.21.196.6, 30 hops max, 52 byte packets
*
1.71 ms      *
1.50 ms      *
*
2.74 ms      *
2.37 ms      *                             中国, 香港
2.61 ms      *          [CUG-ASIA]         中国, 香港
*
5.89 ms      AS10099                       中国, 香港, chinaunicomglobal.com
13.26 ms     AS4837     [CU169-BACKBONE]   中国, 广东, 广州, chinaunicom.cn
7.74 ms      AS4837     [CU169-BACKBONE]   中国, 广东, 广州, chinaunicom.cn  联通
*
11.91 ms     AS17816    [UNICOM-GD]        中国, 广东, 深圳, chinaunicom.cn  联通
14.53 ms     AS17623    [APNIC-AP]         中国, 广东, 深圳, chinaunicom.cn  联通
12.75 ms     AS17623                       中国, 广东, 深圳, chinaunicom.cn  联通
广州移动 - ICMP v4 - traceroute to 120.196.165.24, 30 hops max, 52 byte packets
*
*
1.27 ms      *
*
2.70 ms      *
3.13 ms      AS58453    [CMI-INT]          中国, 香港, cmi.chinamobile.com  移动
54.18 ms     AS58453    [CMI-INT]          中国, 香港, cmi.chinamobile.com  移动
7.96 ms      AS58453    [CMI-INT]          中国, 广东, 广州, cmi.chinamobile.com  移动
8.95 ms      AS9808     [CMNET]            中国, 广东, 广州, chinamobileltd.com  移动
8.99 ms      AS9808     [CMNET]            中国, 广东, 广州, chinamobileltd.com  移动
11.14 ms     AS9808     [CMNET]            中国, 广东, 广州, chinamobileltd.com  移动
11.56 ms     AS9808     [CMNET]            中国, 广东, 广州, chinamobileltd.com  移动
64.44 ms     AS9808     [CMNET]            中国, 广东, 广州, chinamobileltd.com  移动
12.64 ms     AS56040    [APNIC-AP]         中国, 广东, 深圳, gd.10086.cn  移动
--------------------------------------就近节点测速--------------------------------------
位置            上传速度        下载速度        延迟            丢包率
Speedtest.net   811.09 Mbps     1012.66 Mbps    1.98 ms         0.0%
中国香港        718.26 Mbps     409.21 Mbps     3.13 ms         Not available.
新加坡          1051.51 Mbps    906.43 Mbps     40.85 ms        0.0%
联通上海5G      1050.44 Mbps    1017.85 Mbps    34.28 ms        39.2%
电信浙江        1051.64 Mbps    998.91 Mbps     28.90 ms        Not available.
电信浙江        1051.88 Mbps    911.83 Mbps     37.06 ms        Not available.
移动Chengdu     1051.75 Mbps    1039.15 Mbps    55.97 ms        Not available.
----------------------------------------------------------------------------------
花费          : 9 分 4 秒
时间          : Tue Feb 25 06:50:19 UTC 2025
----------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;
:::note{title=&quot;注&quot;}
存在限速情况。看IPTV半小时，上下行都限速到2mbps。
:::&lt;/p&gt;
&lt;h1&gt;Netcup&lt;/h1&gt;
&lt;h2&gt;RS 8000 G12 Pro - 德国（20C64GB - €47.83/月）&lt;/h2&gt;
&lt;h3&gt;2025-11-28&lt;/h3&gt;
&lt;p&gt;&amp;lt;ShowMoreCollapse collapsedHeight={&apos;800px&apos;} client:visible&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## #
#              Yet-Another-Bench-Script              #
#                     v2025-04-20                    #
# https://github.com/masonr/yet-another-bench-script #
# ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## #

Fri Nov 28 10:08:01 PM CST 2025

Basic System Information:
---------------------------------
Uptime     : 0 days, 0 hours, 9 minutes
Processor  : AMD EPYC 9645 96-Core Processor
CPU cores  : 20 @ 2296.548 MHz
AES-NI     : ✔ Enabled
VM-x/AMD-V : ❌ Disabled
RAM        : 62.8 GiB
Swap       : 0.0 KiB
Disk       : 3.0 TiB
Distro     : Debian GNU/Linux 12 (bookworm)
Kernel     : 6.1.0-41-amd64
VM Type    : KVM
IPv4/IPv6  : ✔ Online / ✔ Online

IPv6 Network Information:
---------------------------------
ISP        : netcup GmbH
ASN        : AS197540 netcup GmbH
Host       : Netcup KVM VIE
Location   : Vienna, Vienna (9)
Country    : Austria

fio Disk Speed Tests (Mixed R/W 50/50) (Partition /dev/vda4):
---------------------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ----
Read       | 240.34 MB/s  (60.0k) | 1.54 GB/s    (24.1k)
Write      | 240.98 MB/s  (60.2k) | 1.55 GB/s    (24.2k)
Total      | 481.32 MB/s (120.3k) | 3.09 GB/s    (48.3k)
           |                      |
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ----
Read       | 1.46 GB/s     (2.8k) | 2.00 GB/s     (1.9k)
Write      | 1.54 GB/s     (3.0k) | 2.13 GB/s     (2.0k)
Total      | 3.00 GB/s     (5.8k) | 4.14 GB/s     (4.0k)

iperf3 Network Speed Tests (IPv4):
---------------------------------
Provider        | Location (Link)           | Send Speed      | Recv Speed      | Ping
-----           | -----                     | ----            | ----            | ----
Clouvider       | London, UK (10G)          | 2.69 Gbits/sec  | 2.35 Gbits/sec  | 14.1 ms
Eranium         | Amsterdam, NL (100G)      | 854 Mbits/sec   | 2.35 Gbits/sec  | 15.5 ms
Uztelecom       | Tashkent, UZ (10G)        | 21.7 Mbits/sec  | busy            | 88.4 ms
Leaseweb        | Singapore, SG (10G)       | 617 Mbits/sec   | 1.26 Gbits/sec  | 157 ms
Clouvider       | Los Angeles, CA, US (10G) | 84.2 Mbits/sec  | 1.03 Gbits/sec  | 154 ms
Leaseweb        | NYC, NY, US (10G)         | 1.04 Gbits/sec  | 2.11 Gbits/sec  | 94.8 ms
Edgoo           | Sao Paulo, BR (1G)        | 429 Mbits/sec   | 348 Mbits/sec   | 209 ms

iperf3 Network Speed Tests (IPv6):
---------------------------------
Provider        | Location (Link)           | Send Speed      | Recv Speed      | Ping
-----           | -----                     | ----            | ----            | ----
Clouvider       | London, UK (10G)          | 2.71 Gbits/sec  | 2.32 Gbits/sec  | 14.1 ms
Eranium         | Amsterdam, NL (100G)      | 2.72 Gbits/sec  | 2.32 Gbits/sec  | 15.5 ms
Uztelecom       | Tashkent, UZ (10G)        | 2.43 Gbits/sec  | busy            | 88.4 ms
Leaseweb        | Singapore, SG (10G)       | 2.25 Gbits/sec  | 1.60 Gbits/sec  | 157 ms
Clouvider       | Los Angeles, CA, US (10G) | 586 Mbits/sec   | 934 Mbits/sec   | 154 ms
Leaseweb        | NYC, NY, US (10G)         | 2.11 Gbits/sec  | 1.93 Gbits/sec  | 95.5 ms
Edgoo           | Sao Paulo, BR (1G)        | 2.12 Gbits/sec  | 1.19 Gbits/sec  | 209 ms

Geekbench 5 Benchmark Test:
---------------------------------
Test            | Value
                |
Single Core     | 1763
Multi Core      | 27920
Full Test       | https://browser.geekbench.com/v5/cpu/23933743

Geekbench 6 Benchmark Test:
---------------------------------
Test            | Value
                |
Single Core     | 2249
Multi Core      | 17988
Full Test       | https://browser.geekbench.com/v6/cpu/15279542

YABS completed in 11 min 13 sec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/p&gt;
&lt;h2&gt;RS 2000 G12 - 德国（8C16G - €69.83/月）&lt;/h2&gt;
&lt;h3&gt;2025-11-28&lt;/h3&gt;
&lt;p&gt;&amp;lt;ShowMoreCollapse collapsedHeight={&apos;800px&apos;} client:visible&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## #
#              Yet-Another-Bench-Script              #
#                     v2025-04-20                    #
# https://github.com/masonr/yet-another-bench-script #
# ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## #

Fri Nov 28 11:42:06 PM CST 2025

Basic System Information:
---------------------------------
Uptime     : 7 days, 3 hours, 54 minutes
Processor  : AMD EPYC 9645 96-Core Processor
CPU cores  : 8 @ 2296.548 MHz
AES-NI     : ✔ Enabled
VM-x/AMD-V : ❌ Disabled
RAM        : 15.6 GiB
Swap       : 0.0 KiB
Disk       : 1007.9 GiB
Distro     : Debian GNU/Linux 12 (bookworm)
Kernel     : 6.1.0-40-amd64
VM Type    : KVM
IPv4/IPv6  : ✔ Online / ✔ Online

IPv4 Network Information:
---------------------------------
ISP        : netcup GmbH
ASN        : AS197540 netcup GmbH
Host       : netcup GmbH
Location   : Karlsruhe, Baden-Wurttemberg (BW)
Country    : Germany

fio Disk Speed Tests (Mixed R/W 50/50) (Partition /dev/vda4):
---------------------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ----
Read       | 201.67 MB/s  (50.4k) | 1.30 GB/s    (20.3k)
Write      | 202.20 MB/s  (50.5k) | 1.30 GB/s    (20.4k)
Total      | 403.88 MB/s (100.9k) | 2.60 GB/s    (40.7k)
           |                      |
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ----
Read       | 1.31 GB/s     (2.5k) | 1.73 GB/s     (1.6k)
Write      | 1.38 GB/s     (2.7k) | 1.85 GB/s     (1.8k)
Total      | 2.70 GB/s     (5.2k) | 3.59 GB/s     (3.5k)

iperf3 Network Speed Tests (IPv4):
---------------------------------
Provider        | Location (Link)           | Send Speed      | Recv Speed      | Ping
-----           | -----                     | ----            | ----            | ----
Clouvider       | London, UK (10G)          | 2.69 Gbits/sec  | 2.35 Gbits/sec  | 14.8 ms
Eranium         | Amsterdam, NL (100G)      | 2.72 Gbits/sec  | 2.36 Gbits/sec  | 12.9 ms
Uztelecom       | Tashkent, UZ (10G)        | 2.13 Gbits/sec  | busy            | 105 ms
Leaseweb        | Singapore, SG (10G)       | 2.30 Gbits/sec  | busy            | 161 ms
Clouvider       | Los Angeles, CA, US (10G) | 1.09 Gbits/sec  | 861 Mbits/sec   | 151 ms
Leaseweb        | NYC, NY, US (10G)         | busy            | 1.90 Gbits/sec  | 94.7 ms
Edgoo           | Sao Paulo, BR (1G)        | 1.66 Gbits/sec  | 654 Mbits/sec   | 190 ms

iperf3 Network Speed Tests (IPv6):
---------------------------------
Provider        | Location (Link)           | Send Speed      | Recv Speed      | Ping
-----           | -----                     | ----            | ----            | ----
Clouvider       | London, UK (10G)          | 2.65 Gbits/sec  | busy            | 16.3 ms
Eranium         | Amsterdam, NL (100G)      | 2.70 Gbits/sec  | 2.33 Gbits/sec  | 12.9 ms
Uztelecom       | Tashkent, UZ (10G)        | 2.52 Gbits/sec  | 1.71 Gbits/sec  | 103 ms
Leaseweb        | Singapore, SG (10G)       | 2.33 Gbits/sec  | busy            | 161 ms
Clouvider       | Los Angeles, CA, US (10G) | 1.09 Gbits/sec  | 891 Mbits/sec   | 151 ms
Leaseweb        | NYC, NY, US (10G)         | 2.47 Gbits/sec  | 1.85 Gbits/sec  | 94.7 ms
Edgoo           | Sao Paulo, BR (1G)        | 1.72 Gbits/sec  | 475 Mbits/sec   | 186 ms

Geekbench 5 Benchmark Test:
---------------------------------
Test            | Value
                |
Single Core     | 1578
Multi Core      | 10661
Full Test       | https://browser.geekbench.com/v5/cpu/23933979

Geekbench 6 Benchmark Test:
---------------------------------
Test            | Value
                |
Single Core     | 2170
Multi Core      | 11174
Full Test       | https://browser.geekbench.com/v6/cpu/15281926

YABS completed in 7 min 10 sec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/ShowMoreCollapse&amp;gt;&lt;/p&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Docker部署肥羊IPTV直播源并使用Nginx Proxy Manager反代</title><link>https://blog.useforall.com/posts/5/</link><guid isPermaLink="true">https://blog.useforall.com/posts/5/</guid><description>本文介绍如何搭建肥羊IPTV直播源，并使用NPM进行HTTPS反代</description><pubDate>Sun, 19 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
本文介绍如何搭建肥羊IPTV直播源，并使用NPM进行HTTPS反代
:::&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;2025-2-26：docker-compose.yaml添加watchtower自动更新镜像。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IPTV&lt;/strong&gt;: 肥羊直播源主要组件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPTV-Formatter&lt;/strong&gt;: 对 youshandefeiyang/allinone 的 /tv.m3u、/tptv.m3u、/migu.m3u 进行聚合 &amp;amp; 重新分组&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nginx Proxy Manager&lt;/strong&gt;: 提供反向代理功能&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;配置肥羊IPTV ALL-IN-ONE&lt;/h2&gt;
&lt;h3&gt;基本配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;访问&lt;a href=&quot;https://imgtool.v1.mk/allinone.html&quot;&gt; ALLINONE部署指令生成器 &lt;/a&gt;获取AES密钥
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/19/678d18d409ab3.png&quot; alt=&quot;获取AES密钥&quot; /&gt;&lt;/li&gt;
&lt;li&gt;访问 &lt;a href=&quot;https://t.me/newallinone_bot&quot;&gt; ALLINONE机器人 &lt;/a&gt;。输入 &lt;code&gt;/generate&lt;/code&gt; 生成 &lt;code&gt;UserID&lt;/code&gt; 和 &lt;code&gt;token&lt;/code&gt;。把获取的内容填入上面的网页中，点击 &lt;code&gt;生成部署ALLINONE命令&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;创建并编辑 &lt;code&gt;docker-compose.yaml&lt;/code&gt;：&lt;pre&gt;&lt;code&gt;mkdir -p ~/docker_data/iptv &amp;amp;&amp;amp; cd ~/docker_data/iptv &amp;amp;&amp;amp; nano docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;
添加以下内容：&lt;pre&gt;&lt;code&gt;services:
  iptv:
   image: youshandefeiyang/allinone
   container_name: iptv_allinone
   privileged: false # 不清楚为什么生成的命令默认是true，经测试，改成false不影响使用。为了安全起见，建议改为false
   restart: always
   pull_policy: always
   ports:
     - 65025:35455 # 修改为你自己的端口
   network_mode: bridge
   command: # 把生成的指令的command部分替换到这里即可，其他的不需要

  iptv_format:
   image: yuexuangu/allinone_format:latest
   container_name: iptv_format
   restart: unless-stopped
   pull_policy: always
   network_mode: bridge # openwrt 等系统可能需要此参数
   ports:
     - 65026:35456
   volumes:
     - ./config/:/app/config/ # 请将 ./config/ 改为你的配置文件存放目录

  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --schedule &quot;0 2 * * *&quot; --cleanup iptv_allinone iptv_format
    network_mode: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;启动服务：&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt; &lt;/p&gt;
&lt;h3&gt;配置Nginx Proxy Manager反代&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;如图配置（SSL证书获取不赘述）：
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/19/678d1af627bda.png&quot; alt=&quot;NPM配置1&quot; /&gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/19/678d1b75cc52a.png&quot; alt=&quot;NPM配置2&quot; /&gt;&lt;pre&gt;&lt;code&gt;rewrite ^/iptv/(.*) /$1 break;
&lt;/code&gt;&lt;/pre&gt;
可以把 &lt;code&gt;/iptv&lt;/code&gt; 修改为别的，但是注意要对应的修改 &lt;code&gt;rewrite&lt;/code&gt; 部分。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;配置IPTV-Formatter&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;访问设置的反代域名&lt;/li&gt;
&lt;li&gt;按照图片内容进行设置，然后点击&quot;保存配置&quot;：
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/19/678d1d4a41a4c.png&quot; alt=&quot;IPTV-Formatter配置&quot; /&gt;&lt;/li&gt;
&lt;li&gt;点击查看订阅源，随后随便选择一个使用即可。可以选择复制第一个，然后粘贴到 PotPlayer 进行测试&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Docker部署Lsky图床并配合Openlist网盘存储</title><link>https://blog.useforall.com/posts/4/</link><guid isPermaLink="true">https://blog.useforall.com/posts/4/</guid><description>本文介绍如何搭建基于Openlist+Lsky的图床系统，包括：1. 使用Docker部署Openlist作为存储后端 2. 使用Docker部署Lsky作为图床前端 3. Nginx反向代理配置与防盗链设置</description><pubDate>Tue, 07 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
本文介绍如何搭建基于Openlist+Lsky的图床系统，包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用Docker部署Openlist作为存储后端&lt;/li&gt;
&lt;li&gt;使用Docker部署Lsky作为图床前端&lt;/li&gt;
&lt;li&gt;Nginx反向代理配置与防盗链设置
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;2025-8-29：使用Openlist代替Alist&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;组件说明&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Openlist: 作为存储后端，用于对接各类网盘&lt;/li&gt;
&lt;li&gt;Lsky: 作为图床前端，提供图片上传和管理功能&lt;/li&gt;
&lt;li&gt;Nginx: 提供反向代理和防盗链功能&lt;/li&gt;
&lt;li&gt;PostgreSQL: 为Lsky提供数据存储（&lt;a href=&quot;https://blog.useforall.com/post/3#%E9%85%8D%E7%BD%AE%20PostgreSQL%20%E6%95%B0%E6%8D%AE%E5%BA%93&quot;&gt;配置参考&lt;/a&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;配置Openlist&lt;/h2&gt;
&lt;h3&gt;基本配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;创建并编辑&lt;code&gt;docker-compose.yaml&lt;/code&gt;&lt;pre&gt;&lt;code&gt;mkdir -p ~/docker_data/openlist &amp;amp;&amp;amp; cd ~/docker_data/openlist &amp;amp;&amp;amp; nano docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;
添加以下内容：&lt;pre&gt;&lt;code&gt;services:
   openlist:
     image: &apos;openlistteam/openlist:latest-aio&apos;
     networks:
       - network
     container_name: openlist
     volumes:
         - &apos;./openlist:/opt/openlist/data&apos;
     ports:
         - &apos;65023:5244&apos;
     environment:
         - PUID=0
         - PGID=0
         - UMASK=022
     restart: unless-stopped

networks:
  network:
    driver: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;启动Openlist服务&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;访问&lt;code&gt;http://your-ip:65023&lt;/code&gt;，按照下面的流程进行配置。
&lt;ul&gt;
&lt;li&gt;配置&lt;code&gt;Aria2&lt;/code&gt;。填入地址和密钥。
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/07/677d4ce02465d.png&quot; alt=&quot;image.png&quot; /&gt;
:::note{title=&quot;注意&quot;}
&lt;code&gt;Aria2地址&lt;/code&gt;中的host为你的aria2容器名称。这里为&lt;code&gt;Aria2-Pro&lt;/code&gt;
:::&lt;/li&gt;
&lt;li&gt;配置&lt;code&gt;存储&lt;/code&gt;。这里以OneDrive为例（具体配置流程请参考&lt;a href=&quot;https://doc.oplist.org/guide/drivers/onedrive_app&quot;&gt;Openlist文档&lt;/a&gt;）
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/08/677d595e45218.png&quot; alt=&quot;image.png&quot; /&gt;
:::warning{title=&quot;注意&quot;}
如果要开启Lsky的原图保护和动态水印的话，那么对应的存储的&lt;code&gt;WebDAV策略&lt;/code&gt;必须是&lt;code&gt;本地代理&lt;/code&gt;！！！！否则访问Lsky生成的带水印的图片链接会造成404（直接访问&lt;code&gt;/Blog/图片路径&lt;/code&gt;没问题）
:::&lt;/li&gt;
&lt;li&gt;创建一个用于访问图床的账号。&amp;lt;span style=&quot;color: #db6363&quot;&amp;gt;注意这里的&amp;lt;code&amp;gt;基本路径&amp;lt;/code&amp;gt;需要和上面存储里的&amp;lt;code&amp;gt;挂载路径&amp;lt;/code&amp;gt;一致&amp;lt;/span&amp;gt;
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/07/677d4d97a1118.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;配置Nginx Proxy Manager反代&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;如图配置（SSL证书获取不赘述）
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/07/677d475dc3a8c.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;配置Lsky&lt;/h2&gt;
&lt;h3&gt;基本配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;创建并编辑&lt;code&gt;docker-compose.yaml&lt;/code&gt;&lt;pre&gt;&lt;code&gt;mkdir -p ~/docker_data/lskypro &amp;amp;&amp;amp; cd ~/docker_data/lskypro &amp;amp;&amp;amp; nano docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;
添加以下内容：&lt;pre&gt;&lt;code&gt;services:
   lskypro:
     image: halcyonazure/lsky-pro-docker:latest
     restart: unless-stopped
     hostname: lskypro
     container_name: lskypro
     volumes:
       - ./web:/var/www/html/
     ports:
       - 65022:8089
     networks:
       - pgsql_network

networks:
  pgsql_network:
    external: true
&lt;/code&gt;&lt;/pre&gt;
:::note{title=&quot;注&quot;}
&lt;code&gt;networks&lt;/code&gt;是用外部数据库网络配置是为了在不暴露数据库端口的情况下，使得在后续网页配置数据库阶段可以直接使用PostgreSQL容器名（参考教程容器名为&lt;code&gt;db&lt;/code&gt;）作为数据库连接host。
:::&lt;/li&gt;
&lt;li&gt;启动LskyPro服务&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;在PostgreSQL数据库中创建一个Database。根据上面教程部署PostgreSQL数据库后，复制下面脚本内容到&lt;code&gt;~/docker_data/pgsql&lt;/code&gt;下，然后写入你想要的Database名称到&lt;code&gt;databases&lt;/code&gt;内。这里以&lt;code&gt;lskypro&lt;/code&gt;为例。&lt;pre&gt;&lt;code&gt;#!/bin/bash
set -e

# List of databases to create or ensure exist
databases=(&quot;lskypro&quot; &quot;typecho-blog&quot;)

# 定义 PostgreSQL 环境变量
DB_USER=&quot;typecho_lim&quot;         # 替换为您的 Postgres 用户名
DB_NAME=&quot;postgres&quot;         # 替换为默认的初始数据库名称

# 遍历数据库列表并创建数据库
for db in &quot;${databases[@]}&quot;; do
    # 执行 SELECT 命令并将结果传递给 grep
    if ! docker compose exec db psql -v ON_ERROR_STOP=1 --username &quot;$DB_USER&quot; --dbname &quot;$DB_NAME&quot; -c &quot;SELECT 1 FROM pg_database WHERE datname=&apos;$db&apos;&quot; | grep -q &quot;1&quot;; then
        echo &quot;Database &apos;$db&apos; does not exist, creating it...&quot;
        docker compose exec db psql -v ON_ERROR_STOP=1 --username &quot;$DB_USER&quot; --dbname &quot;$DB_NAME&quot; -c &quot;CREATE DATABASE $db&quot;
    else
        echo &quot;Database &apos;$db&apos; already exists.&quot;
    fi
done

echo &quot;All databases have been created or verified.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;访问&lt;code&gt;http://your-ip:65022&lt;/code&gt;，如图完成配置。
&amp;lt;p&amp;gt;
&amp;lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/07/677d43b2f1c61.png&quot; alt=&quot;image.png&quot; width=&quot;45%&quot; style=&quot;display: inline-block;&quot; /&amp;gt;
&amp;lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/07/677d459f63055.png&quot; alt=&quot;image.png&quot; width=&quot;45%&quot; style=&quot;display: inline-block;&quot; /&amp;gt;
&amp;lt;/p&amp;gt;&lt;/li&gt;
&lt;li&gt;添加一个新存储策略。&lt;code&gt;存储策略 -&amp;gt; 创建存储策略&lt;/code&gt;。Openlist&lt;code&gt;图床&lt;/code&gt;文件夹里里必须要对应有&lt;code&gt;Blog&lt;/code&gt;这个对于文件夹（可以自行替换，只要保证图床访问域名最后部分在Openlist里有对应名称文件夹即可）
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/08/677e7ac1d8865.png&quot; alt=&quot;image.png&quot; /&gt;
:::note{title=&quot;注&quot;}
上文在Openlist搭建部分创建的账号的&lt;code&gt;基本路径&lt;/code&gt;和Openlist的&lt;code&gt;存储&lt;/code&gt;的&lt;code&gt;挂载路径&lt;/code&gt;一致（这里是&lt;code&gt;图床&lt;/code&gt;），意味着通过此账号的&lt;code&gt;/dav&lt;/code&gt;接口访问时，直是返回的&lt;code&gt;/图床/*&lt;/code&gt;文件夹内的文件（不包括&lt;code&gt;/图床&lt;/code&gt;自己）。（例如访问&lt;code&gt;/dav/Blog/1.png&lt;/code&gt;对应到Openlist里是&lt;code&gt;/图床/Blog/1.png&lt;/code&gt;）因此上图里的&lt;code&gt;路径前缀&lt;/code&gt;实际访问的是&lt;code&gt;/图床/Blog&lt;/code&gt;。
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;配置Nginx Proxy Manager&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;如图配置（SSL证书获取不赘述）
&lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/08/677e804f6eb2a.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置Custom Location
&amp;lt;p style=&quot;display: flex; flex-direction: row; gap: 10px&quot;&amp;gt;
&amp;lt;span style=&quot;width: 50%;&quot;&amp;gt;
- 这一步主要是为了解决在设置防盗链后，Lsky自己的&amp;lt;code&amp;gt;thumbnails&amp;lt;/code&amp;gt;失效无法访问的问题
&amp;lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/08/677e819067be0.png&quot; alt=&quot;image.png&quot; style=&quot;display: inline-block;&quot; /&amp;gt;
&amp;lt;/span&amp;gt;
&amp;lt;span style=&quot;width: 50%; text-wrap: pre&quot;&amp;gt;
- 配置防盗链。注意这里需要指向到Openlist的服务，不是Lksy服务！！
&amp;lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/08/677e83f59471c.png&quot; alt=&quot;CleanShot 2025-01-08 at 21.54.57@2x.png&quot; style=&quot;display: inline-block;&quot; /&amp;gt;
&amp;lt;/span&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;localtion ~ ^/(.*\.(jpeg|jpg|png|gif|tif|bmp|ico|psd|webp))$ {
    proxy_cache public-cache; # 设置缓存区域。进入NPM docker容器，cat /etc/nginx/nginx.conf | grep proxy_cache_path，可以看到有public-cach和private-cache两个选择
    proxy_cache_valid 200 301 302 1d; # 当后端返回状态码为 200,301,302时，将该响应缓存 1 天。
    proxy_cache_valid 404 1m; # 404 页面缓存 1 分钟
    proxy_ignore_headers Cache-Control Set-Cookie; # 由于上有服务器有Cache-Control，会导致前端访问图片返回x-cache:CONFIG_NOCACHE，因此需要ignore上游的Cache-Control
    add_header X-Cache-Status $upstream_cache_status; # 监控缓存状态，并查看哪些请求被缓存，哪些没有被缓存。HIT：命中缓存，MISS：未命中，可能是第一次请求，BYPASS：缓存被绕过，可能是因为配置或缓存已过期。
    proxy_pass http://172.17.0.1:65023/dav/$1
    proxy_set_header Authorization &quot;Basic xxxxx&quot;;
    valid_referers none blocked server_names
                *.useforall.com ~\.google\. ~\.bing\. ~\.baidu\. ;
    if ($invalid_referer) {
        rewrite ^/ http://img.buo.us.kg/i/2025/01/05/zgqtqm.webp;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::info{title=&quot;讲解&quot;}&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果访问链接内包含&lt;code&gt;thumbnails&lt;/code&gt;，那么还是指向Lsky服务自己；&lt;/li&gt;
&lt;li&gt;其次，如果访问的是&lt;code&gt;jpeg|jpg|png|gif|tif|bmp|ico|psd|webp&lt;/code&gt;这些文件，那么赋值到$1然后在proxy_pass调用；&lt;/li&gt;
&lt;li&gt;把创建的Openlist账户的用户名和密码做base64加密（输入&lt;code&gt;用户名:密码&lt;/code&gt;)，然后替换&lt;code&gt;proxy_set_header Authorization&lt;/code&gt;里的&lt;code&gt;xxxxx&lt;/code&gt;为加密后字符串；
&amp;lt;iframe src=&quot;https://blog.useforall.com/c/tools/page/base64-encode-decode.html&quot; width=&quot;100%&quot; style=&quot;height: 500px; border-radius: 4px;&quot;/&amp;gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;valid_referers&lt;/code&gt; 相当于一个白名单列表，里面配置的都是放行名单，支持正则表达式；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;none&lt;/code&gt; 表示允许请求报文中没有 &lt;code&gt;referer&lt;/code&gt; 的情况，目的是为了让图片能直接通过浏览器地址栏打开；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;blocked&lt;/code&gt; 表示允许请求报文中有 &lt;code&gt;referer&lt;/code&gt;，但值为空或无效的情况，主要是为了应对请求通过防火墙或代理服务器时，referer 的值被删除或伪装的情形；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*.useforall.com&lt;/code&gt; 是允许的所有子域名；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~\.google\.&lt;/code&gt; &lt;code&gt;~\.bing\.&lt;/code&gt; &lt;code&gt;~\.baidu\&lt;/code&gt;. 等是为了不影响各大搜索引擎收录的，当然，如果还想放行其它的网站也可以继续追加，如github等。
:::&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item><item><title>Docker部署Typecho并使用Nginx Proxy Manager反代</title><link>https://blog.useforall.com/posts/3/</link><guid isPermaLink="true">https://blog.useforall.com/posts/3/</guid><description>本文介绍了如何使用 Docker 部署 Typecho，特别是解决官方 Docker 镜像导致部分主题不可使用的问题，如 `Cuckoo` 主题。</description><pubDate>Sat, 04 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::info{title=&quot;本文主要内容&quot;}
本文介绍了如何使用 Docker 部署 Typecho，特别是解决官方 Docker 镜像导致部分主题不可使用的问题，如 &lt;code&gt;Cuckoo&lt;/code&gt; 主题。
:::&lt;/p&gt;
&lt;h1&gt;拉取官方代码&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;克隆仓库并进入目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/typecho/Dockerfile &amp;amp;&amp;amp; cd Dockerfile
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 Dockerfile 和初始化脚本：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;打开 &lt;code&gt;Dockerfile.base&lt;/code&gt; 并将最后几行改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VOLUME /typoche
WORKDIR /typoche
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改 &lt;code&gt;scripts/init.sh&lt;/code&gt; 文件，替换所有 &lt;code&gt;/app&lt;/code&gt; 关键字为 &lt;code&gt;typecho&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改构建脚本：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编辑 &lt;code&gt;build.sh&lt;/code&gt;，设置 Docker Hub 用户名：&lt;pre&gt;&lt;code&gt;dockerhub_username=${dockerhub_username:-your_username}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;执行构建命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./build.sh -v 1.2.1 -p 8.0 -o alpine fpm
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;v&lt;/code&gt;: Typecho 版本，参考官方 &lt;a href=&quot;https://github.com/typecho/typecho/tags&quot;&gt;Release Tag&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p&lt;/code&gt;: PHP 版本&lt;/li&gt;
&lt;li&gt;&lt;code&gt;o&lt;/code&gt;: 镜像平台，这里使用 &lt;code&gt;alpine&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fpm&lt;/code&gt;: 使用 &lt;code&gt;fpm&lt;/code&gt;，如需 apache 版本，改为 &lt;code&gt;php&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;部署流程&lt;/h1&gt;
&lt;h2&gt;配置 PostgreSQL 数据库&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建并编辑 &lt;code&gt;docker-compose.yaml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd .. &amp;amp;&amp;amp; mkdir pgsql &amp;amp;&amp;amp; cd pgsql &amp;amp;&amp;amp; nano docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  db:
    image: postgres:17-alpine
    restart: unless-stopped
    networks:
      - network
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - ./data:/var/lib/postgresql/data
      - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    healthcheck:
      test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U ${DB_USER} -d ${DB_NAME}&quot;]
      interval: 5s
      timeout: 5s
      retries: 5
networks:
  network:
    driver: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建环境变量文件 &lt;code&gt;.env&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DB_USER=xxxx
DB_PASSWORD=&apos;xxxxxx&apos;
DB_NAME=typecho-blog
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动数据库服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;配置 Typecho&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;设置 Typecho 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd .. &amp;amp;&amp;amp; mkdir typecho &amp;amp;&amp;amp; cd typecho &amp;amp;&amp;amp; nano docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  typecho:
    image: your_dockerhub_username/typecho:1.2.1-php8.0-fpm-alpine
    restart: unless-stopped
    networks:
      - pgsql_network
    ports:
      - 65021:9000
    environment:
      TYPECHO_SITE_URL: https://blog.useforall.com
      TYPECHO_DB_ADAPTER: Pdo_Pgsql
      TYPECHO_DB_HOST: db
      TYPECHO_DB_PORT: 5432
      TYPECHO_DB_USER: ${DB_USER}
      TYPECHO_DB_PASSWORD: ${DB_PASSWORD}
      TYPECHO_DB_DATABASE: ${DB_NAME}
      TYPECHO_USER_NAME: ${ADMIN_USER}
      TYPECHO_USER_PASSWORD: ${ADMIN_PASSWORD}
      TYPECHO_USER_MAIL: ${ADMIN_MAIL}
      TYPECHO_INSTALL: 1
      TIMEZONE: Asia/Shanghai
      MEMORY_LIMIT: 200M
    volumes:
      - ./typecho:/typecho
networks:
  pgsql_network:
    external: true
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建 &lt;code&gt;.env&lt;/code&gt; 文件以设定环境变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加以下信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DB_USER=xxxx
DB_PASSWORD=&apos;xxxx&apos;
DB_NAME=typecho-blog
ADMIN_USER=lim
ADMIN_PASSWORD=&apos;xxxx&apos;
ADMIN_MAIL=blog@awebapp.useforall.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning{title=&quot;注意&quot;}
DB_* 配置项必须与 PostgreSQL 数据库中的一致，否则无法连接数据库。
:::&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动 Typecho 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;配置 Nginx Proxy Manager&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;设置 Nginx 代理服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd .. &amp;amp;&amp;amp; mkdir npm &amp;amp;&amp;amp; nano docker-compose.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加如下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  app:
    network_mode: bridge
    image: &apos;jc21/nginx-proxy-manager:latest&apos;
    restart: unless-stopped
    ports:
      - 80:80
      - 65020:81
      - 443:443
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
      - ../typecho/typecho:/typecho:ro
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动 Nginx Proxy Manager：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 Nginx 使用 SSL 证书：
&amp;lt;p&amp;gt;
&amp;lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/04/6779579813f6d.png&quot; alt=&quot;image.png&quot; width=&quot;45%&quot; style=&quot;display: inline-block;&quot; /&amp;gt;
&amp;lt;img src=&quot;https://lsky.useforall.com/Blog/2025/01/04/6779583498bbc.png&quot; alt=&quot;clipboard-2025-01-04.png&quot; width=&quot;45%&quot; style=&quot;display: inline-block;&quot; /&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;Nginx 配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root /typecho;
index index.php;
location / {
    try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php(.*)$ {
    fastcgi_pass   172.17.0.1:65021;
    fastcgi_index  index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Lim&apos;s Blog</author></item></channel></rss>