一些有关OpenList与Hugging Face的小巧思

本文介绍基于 Hugging Face 部署 OpenList 挂载网盘的实操经验,重点解决 HF 免费空间自动休眠导致的配置丢失问题。通过 Dockerfile 脚本实现配置自动注入,并对比了 Public 与 Private 空间在 WebDAV 挂载中的权衡

正如文章标题所述,本文的核心在于“小巧思”。它并不打算做一个全能的避风港,而是更适合那些对 OpenList 需求不大、但又希望有一个轻量云端方案的用户。如果你也想在不折腾 VPS 的前提下,优雅地解决云端部署问题,或许这篇文章可以给你一些启发。

OpenList

OpenList

对 AList 有过一定了解,但没太关注社区消息的朋友,可能会好奇为什么会有 OpenList

其实起因很简单:自从前年 AList 项目被整体出售,随后又陷入了涉嫌投毒的风波,整个社区的信任基础受到了极大的冲击。

而 OpenList 由 AList 的原贡献者基于原项目 fork 开发而成,作为一个有韧性、长期治理、社区驱动的分支,旨在防御基于信任的开源攻击。

介绍以及具体的来龙去脉就不多说了,总之,如果我们仍然希望寻找一个纯粹、可靠、且更具社区属性的挂载方案,由衷建议原 AList 用户转向 OpenList

为什么需要云端部署OpenList

说实话,在我的影音娱乐体系里,本地下载始终是绝对的主力。天翼云盘和夸克网盘的下载速度已经足够满足我的需求,这导致不管是 AList 时期,还是后来的 OpenList,这些软件对我而言只是个“备选项”。

我习惯于在 Windows 上挂着 Alist Helper,在 Android 上开着 AListLite,即用即走,轻量且本分。顺带一提,虽然这两款软件的名字仍然还带着“AList”,但现在都已经转向支持更纯粹的 OpenList 了

既然本地已经很香了,为什么突然想折腾一下云端部署?

起因是前段时间我闲来无事,重新捡起了 网易爆米花,这是一款类似 Infuse、VidHub 的播放软件,主打极其优雅的海报墙和刮削能力,旨在帮助用户打造一个完美的私人影视库。

但在使用过程中,我发现了一个尴尬的痛点:网易爆米花原生支持天翼云盘,却偏偏把夸克网盘挡在了门外。

如果想在软件里调取夸克的资源,目前唯一的办法就是通过 WebDAV 挂载。

于是我开始琢磨,要不要整一个 24 小时在线、且不占用本地资源 的 WebDAV 服务端呢?

Hugging Face

Hugging Face

我对 Hugging Face(以下简称 HF) 其实没什么了解,我单纯将其当作了一个可以免费部署的容器化环境云平台。以下是 Gemini 对 HF 的简单介绍:

  • HF 是目前全球最活跃的 AI 开源社区,被誉为“AI 界的 GitHub”。除了托管模型和数据集,它提供的 Spaces 功能允许用户通过 Docker、Streamlit 或 Gradio 快速部署 Web 应用。对于开发者而言,这本质上是一个免费且高性能的容器运行环境。

撇开这些 AI 头衔不谈,我看中的其实是它背后的“硬条件”。相比于折腾 VPS 的系统维护或是各种云服务器的计费陷阱,HF 提供的免费服务,显然足够满足我部署 OpenList 的需求了。

我不关心它有多少模型,也不关心它在社区里多有名,我只看中一点:它能让我把 OpenList 简单地“搬”到云端,作为一个 24 小时待命的 WebDAV 服务中心,随时响应网易爆米花或者其他端的调用。

部署 OpenList 到 Hugging Face

部署 OpenList 到 HF 其实也比较简单。

你可以参照我在 HF Space 里的项目LogLInk1K/openlist

不需要研究复杂的代码,只需要点击页面右上角三个点里的 “Duplicate this Space”,直接复制我的空间环境,就能将 OpenList 服务一键部署到你自己的 HF 账号下了。

在复制(Duplicate)时,你会看到这个项目目前预设的几个环境变量:

  • OPENLIST_ADMIN_PASSWORD(建议填写):通过环境变量直接指定 OpenList 的管理员密码

  • STORAGE_JSON_1(选填):填入你从 OpenList 后台导出的第一个存储配置 JSON 字符串

  • STORAGE_JSON_2(选填):如果有第二个网盘,填入此处(脚本目前支持到 STORAGE_JSON_10)

如果你没有如接下来的章节里的特殊需求,那么你甚至可以不填写任何环境变量。

使用 Hugging Face 部署 OpenList 会有什么问题?

既然是“白嫖” HF 的资源,自然会有一些条条框框的限制。在实际把 HF 里的 OpenList 当作 WebDAV 服务端使用的过程中,我遇到了几个比较棘手的问题:

  1. 无法自定义域名

HF 的免费 Space 是不支持自定义域名的。这意味着你只能使用类似 user-repo.hf.space 这种长串的二级域名。如果你想用自己的顶级域名来访问,官方给出的唯一方案是:升级到 PRO 订阅。

对于我这种追求“白嫖”到极致的人来说,为了一个 WebDAV 服务去按月交钱,显然不符合折腾的初衷。

  1. 自动休眠与配置归零

这是最头疼的一点。HF 的免费实例有一个“休眠机制”:如果 48 小时内没有流量访问,容器就会自动关机。而一旦它睡着,非持久化的文件系统就会重置。

于是,一个尴尬的死循环就出现了:

  • 为了省心,我把 OpenList 丢在云端
  • 结果我两天没看电影,容器休眠了
  • 等我第三天想看时,好不容易唤醒了它,却发现之前挂载的网盘账号、系统设置全丢了。

如果按照常规思路,每次看电影前都得去重配一遍,那这方案就不是“巧思”而是“受罪”了。

虽然 OpenList 后台自带备份与恢复功能,但每次唤醒都要登录后台、手动上传备份、等待恢复……这种割裂感完全违背了我的初衷:

“我只是想看场电影,我有什么错!”(哈哈哈哈🤣)

我要的不是一个能“手动抢救”的备份,而是一个“随时可用、无需重复配置”的影视库。

解决配置归零

我们先解决最重要的问题:伴随自动休眠所导致的配置归零。

针对我的需求来说,我基本不需要频繁调整 OpenList 的配置。通常在首次启动后,我只需要设置好密码并挂载上网盘,剩下的就是等很久以后网盘 Cookie 过期了才去更新一下。

所以我并没有选择使用监控(如 UptimeRobot 等)每隔一段时间去“戳”一下容器以维持运行。这种保活手段在互联网上虽然很常用,但对我来说更偏向“治标不治本”的外部手段。

与其费劲心思不让它“入睡”,我更倾向于让 OpenList “忘记”它曾经“初始化”过,从而实现逻辑上的“持久化配置”。

我选择了一个更原生的方案:将核心的存储配置以 JSON 格式保存在 HF 的 Secrets 中。每次容器从休眠中被唤醒启动时,通过 Dockerfile 里的逻辑,全自动地把这些配置“喂”给 OpenList。

这意味着:即便容器文件系统重置了,只要它再次启动,就会瞬间从 Secrets 中读回所有网盘挂载信息,实现“原地复活”。

如果你仔细查看了我 HF Space 里的 LogLInk1K/openlist 项目,你会发现我在 Dockerfile 编写了一套严谨的启动脚本:

CMD sh -c "\
    # 1. 启动服务
    ./openlist server & \
    PID=\$!; \
    \
    # 2. 等待并检查服务是否真的启动成功
    echo '⏳ 正在等待 OpenList 服务启动...'; \
    echo ''; \
    sleep 5; \
    \
    MAX_RETRIES=10; \
    while ! wget -q --spider http://127.0.0.1:5244/api/public/settings; do \
        sleep 2; \
        MAX_RETRIES=\$((MAX_RETRIES - 1)); \
        if [ \$MAX_RETRIES -le 0 ]; then \
            echo ''; \
            echo '❌ 错误: 服务启动超时,请检查日志。'; \
            exit 1; \
        fi; \
    done; \
    echo ''; \
    echo '✅ 服务已就绪!'; \
    echo ''; \
    \
    # 3. 环境变量及密码有效性强校验
    if [ -z \"\$OPENLIST_ADMIN_PASSWORD\" ]; then \
        echo '❌ 错误: 未检测到 OPENLIST_ADMIN_PASSWORD。请在 Secrets 中配置!'; \
        wait \$PID; exit 1; \
    fi; \
    \
    # 4. 获取 Token 
    echo '🔑 正在获取 Token...'; \
    RAW_TOKEN=\$(wget -qO- --timeout=5 --post-data=\"{\\\"username\\\":\\\"admin\\\",\\\"password\\\":\\\"\$OPENLIST_ADMIN_PASSWORD\\\"}\" \
          --header='Content-Type: application/json' \
          http://127.0.0.1:5244/api/auth/login); \
    \
    TOKEN=\$(echo \$RAW_TOKEN | grep -o '\"token\":\"[^\"]*\"' | cut -d'\"' -f4); \
    \
    # 5. 执行注入逻辑
    if [ -n \"\$TOKEN\" ] && [ \"\${#TOKEN}\" -gt 20 ]; then \
        echo '🚀 登录成功,正在解析配置...'; \
        echo ''; \
        FOUND_VALID=0; \
        for i in 1 2 3 4 5 6 7 8 9 10; do \
            eval JSON=\\\$STORAGE_JSON_\$i; \
            \
            if [ -z \"\$JSON\" ]; then \
                continue; \
            fi; \
            \
            FOUND_VALID=1; \
            if echo \"\$JSON\" | grep -qv '\"mount_path\"'; then \
                echo \"⚠️ 错误: STORAGE_JSON_\$i 已检测到但内容格式错误(缺少 mount_path),已跳过。\"; \
                echo \"💡 提示: 请检查是否填入了正确的 JSON 字符串。\"; \
                continue; \
            fi; \
            \
            echo \"📦 正在添加第 \$i 个存储配置...\"; \
            RES=\$(wget -qO- --timeout=5 --post-data=\"\$JSON\" \
                 --header=\"Content-Type: application/json\" \
                 --header=\"Authorization: \$TOKEN\" \
                 http://127.0.0.1:5244/api/admin/storage/create); \
            echo \"\$RES\"; \
            echo ''; \
        done; \
        \
        if [ \"\$FOUND_VALID\" -eq 0 ]; then \
            echo 'ℹ️ 提示: 未发现有效的存储配置。'; \
            echo '👉 请检查 Secrets 中的 STORAGE_JSON_x 是否已填入内容,且变量名拼写正确。'; \
        fi; \
        echo '✅ 所有存储处理完成!'; \
    else \
        echo '❌ 登录失败!'; \
        echo \"💡 响应内容: \${RAW_TOKEN:-'请求超时或无返回'}\"; \
    fi; \
    \
    # 维持主进程
    wait \$PID"

为什么说这套启动脚本是严谨的?

  • 分层解耦:我把 STORAGE_JSON_1 到 STORAGE_JSON_10 设置为环境变量。这意味着我们只需要在 HF Space 后台把网盘导出的存储配置 JSON 字符串往里一贴,容器每次启动都会自动去调 API 把网盘重新挂载好。

  • 强校验机制:脚本里包含了对服务启动状态、管理员密码有效性、JSON 格式完整性的多重检查。它不是盲目地执行,而是确保 OpenList 后台真的“活了”才开始灌注配置。

  • 安全性与私密性:因为存储的配置都在 HF 的 Secrets 里,即便 Space 仓库设为 Public(公开),外人也只能看到脚本,看不到具体的网盘 Cookie、Token、账户和密码。

为什么选择这样的方式?

正如上面提到的安全性与私密性,原因在于当我尝试把 Space 设为 Private(私有),想直接将敏感信息存在仓库中时,发现这会带来一个尴尬的问题:访问权限的死锁。

在 HF 的机制下,Private Space 的访问是有门槛的:

  • 浏览器访问受限:如果你换个浏览器,或者在没登录 HF 账号的设备上打开,你会直接看到 401 错误或登录提醒,根本进不去 OpenList 的界面。

  • WebDAV 彻底瘫痪:这是最致命的。网易爆米花、Infuse 或者播放器是通过 WebDAV 协议去“撞”地址的。如果 Space 是私有的,HF 会在最外层加一把鉴权大锁,你的播放器根本拿不到数据,除非你在播放器端处理复杂的 HF 登录态(这几乎不可能)。

所以,为了让 WebDAV 能 24 小时随时随地调取资源,Space 必须设为 Public。

但 Public 意味着“裸奔”,仓库里的文件谁都能看。于是,我选择了“代码公开 + 存储配置存入 Secrets + 启动动态注入”这样的方案,来当成我唯一的标准答案:

  • 对外:它是 Public 的。播放器和 WebDAV 客户端可以顺着地址直接访问到 OpenList 的服务,无需通过 HF 的账号登录拦截。

  • 对内:它是加密的。即便别人顺着地址摸到我的代码仓库,他也只能看到一堆处理逻辑。核心的网盘 Token、账号和密码全部锁在只有我能看到的 Secrets 保险箱里。

通过 Cloudflare Workers 反代实现自定义域名

正如前面所述,HF 的免费 Space 是不支持自定义域名的。

如果你和我一样,希望在不额外支付 PRO 订阅的前提下,给 OpenList 套上一个体面的顶级域名(比如 dav.yourdomain.com)。

那么,就让我们祭出另一件“白嫖界”的神器:Cloudflare Workers。

通过 CF Workers,我们不仅能绕过 HF 的域名限制,给 OpenList 套上自己的顶级域名,还能顺便处理掉 WebDAV 常见的跨域和握手问题。

为什么要折腾这一步?

  • 摆脱长尾域名(最主要的原因):那一串 *.hf.space 不仅难记,在某些网络环境下访问也不够顺畅。

  • 解决 WebDAV 握手难题:避免播放器(如网易爆米花、Infuse)在连接 WebDAV 时对跨域(CORS)和请求头有严格要求,从而导致直接直连 HF 有时会因为网关限制而挂载失败的问题。

  • 隐藏上游地址:所有的流量和请求都通过 CF 的边缘节点中转,既能加速,又能隐藏真实的 Space 运行地址。

核心代码实现

我在 CF Worker 中实现了一套逻辑,它不仅负责域名的“换壳”,更重要的是它会伪装请求头(让 HF 以为是直连访问),并注入全量的 WebDAV 跨域头:

export default {
  /**
   * @param {{ url: string | URL; headers: HeadersInit; method: any; body: any; }} request
   * @param {any} env
   */
  async fetch(request, env) {
    const upstream = 'loglink1k-openlist.hf.space'; // 你的 HF 域名
    const url = new URL(request.url);
    
    // 替换域名
    url.host = upstream;
    url.protocol = 'https:'; // 强制 HTTPS

    // 复制原始请求的 Headers,防止只读限制
    const newHeaders = new Headers(request.headers);
    
    // 注入关键 Header,让 HF 以为是直连访问
    newHeaders.set('Host', upstream);
    newHeaders.set('Origin', `https://${upstream}`);
    newHeaders.set('Referer', `https://${upstream}`);
    
    // 构造新的请求
    const newRequest = new Request(url.toString(), {
      method: request.method,
      headers: newHeaders,
      body: request.body,
      redirect: 'follow'
    });

    try {
      const response = await fetch(newRequest);
      
      // 处理跨域,方便 Webdav 客户端握手
      const newResponseHeaders = new Headers(response.headers);
      newResponseHeaders.set('Access-Control-Allow-Origin', '*');
      newResponseHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK');
      newResponseHeaders.set('Access-Control-Allow-Headers', '*');

      return new Response(response.body, {
        status: response.status,
        statusText: response.statusText,
        headers: newResponseHeaders
      });
    } catch (e) {
      return new Response('Error: Upstream connection failed. ' + e.message, { status: 502 });
    }
  }
};

“小巧思”的价值

至此,整套方案终于严丝合缝地闭合了:

  • 成本 0 元:全程利用 HF 的容器额度和 CF Workers 的每日免费 10 万次请求额度,个人观影完全溢出。

  • 原地复活:通过环境变量注入,解决了 HF 自动休眠导致的“失忆”问题。

  • 访问体面:拥有了自定义域名,播放器刮削顺滑,海报墙秒开。

正如我在前言里说的,这套方案确实只是“小巧思”。它更适合那些对 OpenList 需求不大、但又希望有一个轻量云端方案的用户。如果你也想在不折腾 VPS 的前提下,优雅地解决云端部署问题,希望我的这段折腾经历能给你一点启发。

评论区