Avatar
醉后不知天在水

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

4.3K 字
11 分钟
发布于
更新于
格物篇

正如文章标题所述,本文的核心在于“小巧思”。它并不打算做一个全能的避风港,而是更适合那些对 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

以下是 Gemini 对 Hugging Face(以下简称 HF) 的一些简单介绍:

  • HF 是全球顶尖的 AI 协作平台,被誉为 AI 界的 GitHub。它通过开源的 Transformers 库 统一了模型调用标准,并提供 Model Hub(模型库)、Datasets(数据集)和 Spaces(应用演示)三大核心能力。它打破了巨头的技术垄断,让开发者能像搭积木一样,轻松调用、分享和部署最前沿的自然语言处理、计算机视觉及音频模型,是当今生成式 AI 生态赖以生存的基础设施。

说实话,我对 HF 其实没有太深入的了解。最早关注到它,也是因为我在询问 Gemini “有哪些地方能在线免费部署 OpenList”时,Gemini 向我推荐了这里。

虽然如介绍所言,HF 在 AI 领域的名气似乎比较大,但我目前的视角很简单:HF 的 Spaces 能为我提供一个稳定、便捷且免费的容器化托管环境。

而在我实际尝试之后,我发现 HF 的 Spaces 确实已经足够满足我云端部署 OpenList 的需求。它不用我去折腾 VPS,也省去了折腾 VPS 所带来的那些维护问题,更不用我去烦恼各类云服务复杂的计费规则。

这种简单、纯粹的托管方式,能让我把 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 服务启动...'; \
    sleep 8; \
    \
    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 '❌ 错误: 服务启动超时。'; exit 1; \
        fi; \
    done; \
    \
    echo '✅ 服务已就绪!'; \
    \
    # 3. 校验管理员密码
    if [ -z \"\$OPENLIST_ADMIN_PASSWORD\" ]; then \
        echo '❌ 错误: 未配置 OPENLIST_ADMIN_PASSWORD。'; \
        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 '🚀 登录成功,正在处理存储配置...'; \
        FOUND_VALID=0; \
        \
        for i in 1 2 3 4 5 6 7 8 9 10; do \
            # --- 关键:使用 printenv 获取原始 JSON ---
            JSON_BODY=\$(printenv STORAGE_JSON_\$i); \
            \
            if [ -z \"\$JSON_BODY\" ]; then \
                continue; \
            fi; \
            \
            FOUND_VALID=1; \
            # 将原始 JSON 写入临时文件,确保换行和转义原样保留
            echo \"\$JSON_BODY\" > /tmp/payload.json; \
            \
            if ! grep -q '\"mount_path\"' /tmp/payload.json; then \
                echo \"⚠️ STORAGE_JSON_\$i 格式不完整,已跳过。\"; \
                rm /tmp/payload.json; continue; \
            fi; \
            \
            echo \"📦 正在推送第 \$i 个配置到 API...\"; \
            # 使用 --post-file 直接发送文件内容,避开 Shell 转义
            RES=\$(wget -qO- --timeout=10 --post-file=/tmp/payload.json \
                  --header=\"Content-Type: application/json\" \
                  --header=\"Authorization: \$TOKEN\" \
                  http://127.0.0.1:5244/api/admin/storage/create); \
            \
            echo \"响应内容: \$RES\"; \
            rm /tmp/payload.json; \
            echo ''; \
        done; \
        \
        [ \"\$FOUND_VALID\" -eq 0 ] && echo 'ℹ️ 提示: 未发现有效的存储配置。'; \
        echo '✅ 注入流程处理完毕!'; \
    else \
        echo '❌ 登录失败!内容: ' \${RAW_TOKEN:-'无响应'}; \
    fi; \
    \
    # 维持主进程
    wait \$PID"

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

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

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

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

STORAGE_JSON 格式示例

  1. 导出原始配置

在 OpenList 管理后台点击 “备份” 后,你会得到一个 JSON 配置文件。请在该文件中定位到 storages 数组,我们要提取的就是其中每一个独立的存储对象,具体结构类似以下:

{
  "storages": [
  {
    "id": 1,
    "mount_path": "/189", 
    "order": 0,
    "driver": "189CloudPC",
    "cache_expiration": 30,
    "custom_cache_policies": "",
    "status": "work",
    "addition": "{\"login_type\":\"\",\"username\":\"xxx\",\"password\":\"xxx\",\"validate_code\":\"\",\"refresh_token\":\"xxx\",\"root_folder_id\":\"-11\",\"order_by\":\"filename\",\"order_direction\":\"asc\",\"type\":\"personal\",\"family_id\":\"xxx\",\"upload_method\":\"stream\",\"upload_thread\":\"3\",\"family_transfer\":false,\"rapid_upload\":false,\"no_use_ocr\":false}",
    "remark": "",
    "modified": "2026-03-03T12:07:21.141247771Z",
    "disabled": false,
    "disable_index": false,
    "enable_sign": false,
    "order_by": "",
    "order_direction": "",
    "extract_folder": "",
    "web_proxy": false,
    "webdav_policy": "302_redirect",
    "proxy_range": false,
    "down_proxy_url": "",
    "disable_proxy_sign": false
  },
  {
    "id": 2,
    "mount_path": "/quark",
    "order": 0,
    "driver": "Quark",
    "cache_expiration": 30,
    "custom_cache_policies": "",
    "status": "work",
    "addition": "{\"cookie\":\"ctoken\",\"root_folder_id\":\"0\",\"order_by\":\"none\",\"order_direction\":\"asc\",\"use_transcoding_address\":false,\"only_list_video_file\":false,\"AdditionVersion\":2}",
    "remark": "",
    "modified": "2026-03-03T12:07:26.143220307Z",
    "disabled": false,
    "disable_index": false,
    "enable_sign": false,
    "order_by": "",
    "order_direction": "",
    "extract_folder": "",
    "web_proxy": true,
    "webdav_policy": "native_proxy",
    "proxy_range": false,
    "down_proxy_url": "",
    "disable_proxy_sign": false
  }
  ]
}  
  1. 选取注入内容

从 storages 数组中,选择你想要自动挂载的项。

检查步骤:

  • { 开始,用 } 结束

  • 确保 { 之前和 } 之后没有从原数组里带出来的逗号

  • 检查对象内部最后一个字段后面是否有逗号。

  • 请勿完整复制 {"storages": [...]}

  • 推荐手动删除 "id": 1, 这样系统会自动分配 ID,避免冲突。

❌ 错误示例(末尾带有逗号)

{
  "id": 1,  /* 推荐手动删除 */
  "mount_path": "/189",
  /* 其他字段保持不变 */
  "disable_proxy_sign": false
},  /* <-- 这个末尾逗号会导致注入失败!*/

✅ 正确示例(无 ID 字段,干净的闭合)

{
  "mount_path": "/189",
  /* 其他字段保持不变 */
  "disable_proxy_sign": false
}
  1. 环境变量填写

在 HF 的 Settings -> Secrets 中:

变量名:STORAGE_JSON_1(第二个用 STORAGE_JSON_2,依此类推)。

变量值:直接粘贴截取的 { … } 内容。

为什么选择这样的方式?

正如上面提到的安全性与私密性,原因在于当我尝试把 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 域名在哪里查看

你可以在 HF 的 Space 页面里点击右上角三个点里的 “Embed this Space”里,找到你的 Space 域名。

HF 域名查看

为什么要折腾这一步?

  • 摆脱长尾域名(最主要的原因):那一串 *.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 = '*.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 的前提下,优雅地解决云端部署问题,希望我的这段折腾经历能给你一点启发。

评论区