tao-shen Cursor commited on
Commit
4f54dc8
·
1 Parent(s): 6cd5d65

feat: 使用 HF Dataset 做持久化(spaces-storage#dataset-storage)

Browse files
Dockerfile CHANGED
@@ -6,12 +6,15 @@ FROM ubuntu:24.04
6
  # 避免交互式提示
7
  ENV DEBIAN_FRONTEND=noninteractive
8
 
9
- # 安装依赖:安装脚本需要 curltar(Linux 解压)
10
  RUN apt-get update && apt-get install -y --no-install-recommends \
11
  ca-certificates \
12
  curl \
13
  tar \
14
  git \
 
 
 
15
  && rm -rf /var/lib/apt/lists/*
16
 
17
  # 不创建新用户(HF 构建环境可能已有 UID 1000),改为以 root 安装到 /home/user 再 chown
@@ -21,6 +24,10 @@ RUN mkdir -p /home/user/app
21
  # 必须让管道右侧的 bash 也继承 HOME,否则脚本会装到 /root/.opencode/bin
22
  RUN bash -c 'export HOME=/home/user; curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path'
23
 
 
 
 
 
24
  # 将 /home/user 归属给 UID 1000(HF Spaces 运行时使用 UID 1000)
25
  RUN chown -R 1000:1000 /home/user
26
 
@@ -37,6 +44,6 @@ RUN /home/user/.opencode/bin/opencode --version
37
  # Hugging Face Spaces 默认暴露 7860
38
  EXPOSE 7860
39
 
40
- # 启动 headless HTTP server(使用默认数据目录 ~/.local/share/opencode,不指定 /data,避免发消息失败)
41
- # 文档与 API: /doc OpenAPI 3.1 规范
42
- CMD ["/home/user/.opencode/bin/opencode", "serve", "--port", "7860", "--hostname", "0.0.0.0", "--cors", "https://tacits-candy-shop.vercel.app", "--cors", "https://opencode-web-pearl.vercel.app"]
 
6
  # 避免交互式提示
7
  ENV DEBIAN_FRONTEND=noninteractive
8
 
9
+ # 安装依赖:curl/tar/git 用于 opencode,python3/huggingface_hub 用于 Dataset 持久化
10
  RUN apt-get update && apt-get install -y --no-install-recommends \
11
  ca-certificates \
12
  curl \
13
  tar \
14
  git \
15
+ python3 \
16
+ python3-pip \
17
+ && pip3 install --break-system-packages huggingface_hub \
18
  && rm -rf /var/lib/apt/lists/*
19
 
20
  # 不创建新用户(HF 构建环境可能已有 UID 1000),改为以 root 安装到 /home/user 再 chown
 
24
  # 必须让管道右侧的 bash 也继承 HOME,否则脚本会装到 /root/.opencode/bin
25
  RUN bash -c 'export HOME=/home/user; curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path'
26
 
27
+ # Dataset 持久化脚本(见 https://huggingface.co/docs/hub/spaces-storage#dataset-storage)
28
+ COPY scripts/ /home/user/app/scripts/
29
+ RUN chown -R 1000:1000 /home/user/app/scripts && chmod +x /home/user/app/scripts/entrypoint.sh
30
+
31
  # 将 /home/user 归属给 UID 1000(HF Spaces 运行时使用 UID 1000)
32
  RUN chown -R 1000:1000 /home/user
33
 
 
44
  # Hugging Face Spaces 默认暴露 7860
45
  EXPOSE 7860
46
 
47
+ # 使用 entrypoint:若设置 HF_TOKEN + OPENCODE_DATASET_REPO 则从 Dataset 恢复并每 5 分钟保存
48
+ # 否则直接启动 opencode serve(行与原先一致)
49
+ CMD ["/home/user/app/scripts/entrypoint.sh"]
README.md CHANGED
@@ -27,9 +27,21 @@ pinned: false
27
  - `OPENCODE_SERVER_PASSWORD`(必填时启用认证)
28
  - `OPENCODE_SERVER_USERNAME`(可选,默认 `opencode`)
29
 
30
- ## 持久化存储
31
 
32
- 当前 Space 使用容器默认数据目录,**重启后会话等数据会丢失**。请勿在 Settings → Variables 中设置 `XDG_DATA_HOME` / `XDG_CONFIG_HOME` 指向 `/data`否则可能导致无法发送消息。若需长期保留数据,请使外部存储或 Dataset 等方案
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  ## 参考
35
 
 
27
  - `OPENCODE_SERVER_PASSWORD`(必填时启用认证)
28
  - `OPENCODE_SERVER_USERNAME`(可选,默认 `opencode`)
29
 
30
+ ## 持久化存储(Dataset,免费)
31
 
32
+ 采用 [HF 官方推荐的 Dataset 存储](https://huggingface.co/docs/hub/spaces-storage#dataset-storage):会话等数据写入你名下的 **Dataset 仓库**,用免费仓库存储额度,重启后可恢复
33
+
34
+ 1. **新建一个 Dataset 仓库**
35
+ 在 [Hub](https://huggingface.co/new-dataset) 创建,例如 `tao-shen/opencode-data`(可先空着)。
36
+
37
+ 2. **在 Space 里配置**
38
+ - **Settings → Repository secrets**:新增 `HF_TOKEN`,值为你的 [Access Token](https://huggingface.co/settings/tokens)(需 **Write** 权限)。
39
+ - **Settings → Variables**:新增 `OPENCODE_DATASET_REPO`,值为 Dataset 的 `repo_id`,例如 `tao-shen/opencode-data`。
40
+
41
+ 3. **行为**
42
+ - 启动时:从该 Dataset 拉取已有数据到 `~/.local/share/opencode`(若有)。
43
+ - 运行中:每 5 分钟把当前数据上传回该 Dataset。
44
+ - 未设置上述两个变量时,与之前一致:不恢复、不保存,重启后数据丢失。
45
 
46
  ## 参考
47
 
scripts/entrypoint.sh ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ # 若设置了 HF_TOKEN 与 OPENCODE_DATASET_REPO,则从 Dataset 恢复并定期保存到 Dataset;
3
+ # 否则直接启动 opencode serve(与原先行为一致)。
4
+
5
+ set -e
6
+ DATA_DIR="${HOME}/.local/share/opencode"
7
+ OPENCODE_BIN="/home/user/.opencode/bin/opencode"
8
+ SCRIPT_DIR="/home/user/app/scripts"
9
+ SAVE_INTERVAL=300
10
+
11
+ if [ -n "$HF_TOKEN" ] && [ -n "$OPENCODE_DATASET_REPO" ]; then
12
+ # 从 Dataset 恢复
13
+ python3 "$SCRIPT_DIR/restore_from_dataset.py" || true
14
+ # 后台启动 opencode,并定期保存到 Dataset
15
+ trap 'kill -TERM $OPENCODE_PID 2>/dev/null' TERM INT
16
+ $OPENCODE_BIN serve --port 7860 --hostname 0.0.0.0 \
17
+ --cors https://tacits-candy-shop.vercel.app \
18
+ --cors https://opencode-web-pearl.vercel.app &
19
+ OPENCODE_PID=$!
20
+ while kill -0 $OPENCODE_PID 2>/dev/null; do
21
+ sleep $SAVE_INTERVAL
22
+ python3 "$SCRIPT_DIR/save_to_dataset.py" || true
23
+ done
24
+ wait $OPENCODE_PID
25
+ exit $?
26
+ fi
27
+
28
+ # 未配置 Dataset 时直接启动(与原先 CMD 一致)
29
+ exec $OPENCODE_BIN serve --port 7860 --hostname 0.0.0.0 \
30
+ --cors https://tacits-candy-shop.vercel.app \
31
+ --cors https://opencode-web-pearl.vercel.app
scripts/restore_from_dataset.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 从 Hugging Face Dataset 仓库恢复 OpenCode 数据到 ~/.local/share/opencode。
4
+ 需设置环境变量: HF_TOKEN, OPENCODE_DATASET_REPO。
5
+ """
6
+ import os
7
+ import shutil
8
+ import sys
9
+
10
+ def main():
11
+ token = os.environ.get("HF_TOKEN")
12
+ repo_id = os.environ.get("OPENCODE_DATASET_REPO")
13
+ data_dir = os.path.expanduser("~/.local/share/opencode")
14
+
15
+ if not token or not repo_id:
16
+ return 0
17
+
18
+ try:
19
+ from huggingface_hub import HfApi, snapshot_download
20
+ except ImportError:
21
+ print("restore: huggingface_hub not installed, skip restore", file=sys.stderr)
22
+ return 0
23
+
24
+ try:
25
+ api = HfApi(token=token)
26
+ files = api.list_repo_files(repo_id, repo_type="dataset")
27
+ if not files or set(files) <= {".gitattributes"}:
28
+ return 0
29
+ except Exception as e:
30
+ print(f"restore: list repo failed ({e}), skip restore", file=sys.stderr)
31
+ return 0
32
+
33
+ os.makedirs(data_dir, exist_ok=True)
34
+ tmp_dir = data_dir + ".restore_tmp"
35
+ try:
36
+ snapshot_download(
37
+ repo_id=repo_id,
38
+ repo_type="dataset",
39
+ local_dir=tmp_dir,
40
+ token=token,
41
+ )
42
+ for name in os.listdir(tmp_dir):
43
+ if name == ".gitattributes":
44
+ continue
45
+ src = os.path.join(tmp_dir, name)
46
+ dst = os.path.join(data_dir, name)
47
+ if os.path.isdir(src):
48
+ if os.path.exists(dst):
49
+ shutil.rmtree(dst, ignore_errors=True)
50
+ shutil.copytree(src, dst)
51
+ else:
52
+ shutil.copy2(src, dst)
53
+ finally:
54
+ if os.path.isdir(tmp_dir):
55
+ shutil.rmtree(tmp_dir, ignore_errors=True)
56
+ return 0
57
+
58
+ if __name__ == "__main__":
59
+ sys.exit(main())
scripts/save_to_dataset.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 将 ~/.local/share/opencode 上传到 Hugging Face Dataset 仓库。
4
+ 需设置环境变量: HF_TOKEN, OPENCODE_DATASET_REPO。
5
+ """
6
+ import os
7
+ import sys
8
+
9
+ def main():
10
+ token = os.environ.get("HF_TOKEN")
11
+ repo_id = os.environ.get("OPENCODE_DATASET_REPO")
12
+ data_dir = os.path.expanduser("~/.local/share/opencode")
13
+
14
+ if not token or not repo_id:
15
+ return 0
16
+
17
+ if not os.path.isdir(data_dir):
18
+ return 0
19
+
20
+ try:
21
+ from huggingface_hub import HfApi
22
+ except ImportError:
23
+ print("save: huggingface_hub not installed, skip save", file=sys.stderr)
24
+ return 0
25
+
26
+ try:
27
+ api = HfApi()
28
+ api.upload_folder(
29
+ folder_path=data_dir,
30
+ path_in_repo=".",
31
+ repo_id=repo_id,
32
+ repo_type="dataset",
33
+ token=token,
34
+ )
35
+ except Exception as e:
36
+ print(f"save: upload failed ({e})", file=sys.stderr)
37
+ return 1
38
+ return 0
39
+
40
+ if __name__ == "__main__":
41
+ sys.exit(main())