Spaces:
Paused
Paused
yun tianming commited on
Upload 9 files
Browse files- .github/workflows/deploy.yml +88 -0
- .gitignore +137 -0
- Dockerfile +61 -0
- LICENSE +21 -0
- README.md +213 -12
- entrypoint.sh +70 -0
- requirements.txt +24 -0
- src/__init__.py +0 -0
- src/data_sync.py +279 -0
.github/workflows/deploy.yml
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Build and Deploy to Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [ main ]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [ main ]
|
| 8 |
+
|
| 9 |
+
env:
|
| 10 |
+
SPACE_REPOSITORY: ${{ secrets.HF_SPACE_REPO }}
|
| 11 |
+
|
| 12 |
+
jobs:
|
| 13 |
+
build:
|
| 14 |
+
runs-on: ubuntu-latest
|
| 15 |
+
steps:
|
| 16 |
+
- name: Checkout code
|
| 17 |
+
uses: actions/checkout@v4
|
| 18 |
+
|
| 19 |
+
- name: Set up Docker Buildx
|
| 20 |
+
uses: docker/setup-buildx-action@v3
|
| 21 |
+
|
| 22 |
+
- name: Build Docker image
|
| 23 |
+
uses: docker/build-push-action@v5
|
| 24 |
+
with:
|
| 25 |
+
context: .
|
| 26 |
+
push: false
|
| 27 |
+
tags: hermes-spaces:test
|
| 28 |
+
cache-from: type=gha
|
| 29 |
+
cache-to: type=gha,mode=max
|
| 30 |
+
|
| 31 |
+
deploy:
|
| 32 |
+
needs: build
|
| 33 |
+
runs-on: ubuntu-latest
|
| 34 |
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
| 35 |
+
|
| 36 |
+
steps:
|
| 37 |
+
- name: Checkout code
|
| 38 |
+
uses: actions/checkout@v4
|
| 39 |
+
with:
|
| 40 |
+
fetch-depth: 0
|
| 41 |
+
|
| 42 |
+
- name: Configure Git
|
| 43 |
+
run: |
|
| 44 |
+
git config --global user.name "GitHub Actions"
|
| 45 |
+
git config --global user.email "actions@github.com"
|
| 46 |
+
|
| 47 |
+
- name: Deploy to Hugging Face Spaces
|
| 48 |
+
env:
|
| 49 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 50 |
+
run: |
|
| 51 |
+
# Install Hugging Face CLI
|
| 52 |
+
pip install huggingface-hub
|
| 53 |
+
|
| 54 |
+
# Clone the Space repository
|
| 55 |
+
git clone https://user:$HF_TOKEN@huggingface.co/spaces/${{ secrets.HF_SPACE_REPO }} space-repo
|
| 56 |
+
|
| 57 |
+
# Copy files to Space repository
|
| 58 |
+
cp -r . space-repo/
|
| 59 |
+
cd space-repo
|
| 60 |
+
|
| 61 |
+
# Remove .git directory to avoid conflicts
|
| 62 |
+
rm -rf .git
|
| 63 |
+
|
| 64 |
+
# Initialize git and push
|
| 65 |
+
git init
|
| 66 |
+
git add .
|
| 67 |
+
git commit -m "Deploy from GitHub Actions - $(date)"
|
| 68 |
+
git push --force https://user:$HF_TOKEN@huggingface.co/spaces/${{ secrets.HF_SPACE_REPO }} main
|
| 69 |
+
|
| 70 |
+
- name: Wait for Space to be ready
|
| 71 |
+
env:
|
| 72 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 73 |
+
run: |
|
| 74 |
+
echo "Waiting for Space to be ready..."
|
| 75 |
+
sleep 30
|
| 76 |
+
|
| 77 |
+
# Check if Space is running
|
| 78 |
+
SPACE_URL="https://${{ secrets.HF_SPACE_REPO }}.hf.space/health"
|
| 79 |
+
for i in {1..10}; do
|
| 80 |
+
if curl -f $SPACE_URL > /dev/null 2>&1; then
|
| 81 |
+
echo "✅ Space is ready!"
|
| 82 |
+
exit 0
|
| 83 |
+
fi
|
| 84 |
+
echo "⏳ Waiting... ($i/10)"
|
| 85 |
+
sleep 10
|
| 86 |
+
done
|
| 87 |
+
|
| 88 |
+
echo "⚠️ Space may not be fully ready yet, but deployment was successful"
|
.gitignore
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
*.egg-info/
|
| 24 |
+
.installed.cfg
|
| 25 |
+
*.egg
|
| 26 |
+
|
| 27 |
+
# PyInstaller
|
| 28 |
+
*.manifest
|
| 29 |
+
*.spec
|
| 30 |
+
|
| 31 |
+
# Installer logs
|
| 32 |
+
pip-log.txt
|
| 33 |
+
pip-delete-this-directory.txt
|
| 34 |
+
|
| 35 |
+
# Unit test / coverage reports
|
| 36 |
+
htmlcov/
|
| 37 |
+
.tox/
|
| 38 |
+
.nox/
|
| 39 |
+
.coverage
|
| 40 |
+
.coverage.*
|
| 41 |
+
.cache
|
| 42 |
+
nosetests.xml
|
| 43 |
+
coverage.xml
|
| 44 |
+
*.cover
|
| 45 |
+
.hypothesis/
|
| 46 |
+
.pytest_cache/
|
| 47 |
+
|
| 48 |
+
# Translations
|
| 49 |
+
*.mo
|
| 50 |
+
*.pot
|
| 51 |
+
|
| 52 |
+
# Django stuff:
|
| 53 |
+
*.log
|
| 54 |
+
local_settings.py
|
| 55 |
+
db.sqlite3
|
| 56 |
+
|
| 57 |
+
# Flask stuff:
|
| 58 |
+
instance/
|
| 59 |
+
.webassets-cache
|
| 60 |
+
|
| 61 |
+
# Scrapy stuff:
|
| 62 |
+
.scrapy
|
| 63 |
+
|
| 64 |
+
# Sphinx documentation
|
| 65 |
+
docs/_build/
|
| 66 |
+
|
| 67 |
+
# PyBuilder
|
| 68 |
+
target/
|
| 69 |
+
|
| 70 |
+
# Jupyter Notebook
|
| 71 |
+
.ipynb_checkpoints
|
| 72 |
+
|
| 73 |
+
# IPython
|
| 74 |
+
profile_default/
|
| 75 |
+
ipython_config.py
|
| 76 |
+
|
| 77 |
+
# pyenv
|
| 78 |
+
.python-version
|
| 79 |
+
|
| 80 |
+
# celery beat schedule file
|
| 81 |
+
celerybeat-schedule
|
| 82 |
+
|
| 83 |
+
# SageMath parsed files
|
| 84 |
+
*.sage.py
|
| 85 |
+
|
| 86 |
+
# Environments
|
| 87 |
+
.env
|
| 88 |
+
.venv
|
| 89 |
+
env/
|
| 90 |
+
venv/
|
| 91 |
+
ENV/
|
| 92 |
+
env.bak/
|
| 93 |
+
venv.bak/
|
| 94 |
+
|
| 95 |
+
# Spyder project settings
|
| 96 |
+
.spyderproject
|
| 97 |
+
.spyproject
|
| 98 |
+
|
| 99 |
+
# Rope project settings
|
| 100 |
+
.ropeproject
|
| 101 |
+
|
| 102 |
+
# mkdocs documentation
|
| 103 |
+
/site
|
| 104 |
+
|
| 105 |
+
# mypy
|
| 106 |
+
.mypy_cache/
|
| 107 |
+
.dmypy.json
|
| 108 |
+
dmypy.json
|
| 109 |
+
|
| 110 |
+
# Pyre type checker
|
| 111 |
+
.pyre/
|
| 112 |
+
|
| 113 |
+
# Hermes specific
|
| 114 |
+
.hermes/
|
| 115 |
+
/data/
|
| 116 |
+
/tmp/hermes_*/
|
| 117 |
+
|
| 118 |
+
# Logs
|
| 119 |
+
logs/
|
| 120 |
+
*.log
|
| 121 |
+
|
| 122 |
+
# IDE
|
| 123 |
+
.vscode/
|
| 124 |
+
.idea/
|
| 125 |
+
*.swp
|
| 126 |
+
*.swo
|
| 127 |
+
*~
|
| 128 |
+
|
| 129 |
+
# OS
|
| 130 |
+
.DS_Store
|
| 131 |
+
Thumbs.db
|
| 132 |
+
|
| 133 |
+
# Docker
|
| 134 |
+
.dockerignore
|
| 135 |
+
|
| 136 |
+
# Hugging Face
|
| 137 |
+
.hf_token
|
Dockerfile
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
LABEL maintainer="Hermes Agent Community"
|
| 4 |
+
LABEL version="0.9.0"
|
| 5 |
+
LABEL description="Hermes Agent v0.9.0 on Hugging Face Spaces with persistent storage"
|
| 6 |
+
|
| 7 |
+
# 设置环境变量
|
| 8 |
+
ENV PYTHONUNBUFFERED=1
|
| 9 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 10 |
+
ENV HERMES_HOME=/data/.hermes
|
| 11 |
+
ENV PYTHONPATH=/app
|
| 12 |
+
|
| 13 |
+
# 安装系统依赖
|
| 14 |
+
RUN apt-get update && apt-get install -y \
|
| 15 |
+
build-essential \
|
| 16 |
+
nodejs \
|
| 17 |
+
npm \
|
| 18 |
+
ffmpeg \
|
| 19 |
+
git \
|
| 20 |
+
curl \
|
| 21 |
+
unzip \
|
| 22 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 23 |
+
|
| 24 |
+
# 安装 Python 依赖
|
| 25 |
+
COPY requirements.txt /tmp/
|
| 26 |
+
RUN pip install --no-cache-dir -r /tmp/requirements.txt
|
| 27 |
+
|
| 28 |
+
# 安装 Hermes Agent [web] 包含 Dashboard 依赖
|
| 29 |
+
RUN pip install --no-cache-dir hermes-agent[all,web]==0.9.0
|
| 30 |
+
|
| 31 |
+
# 安装 Playwright(使用精简版以减小镜像体积)
|
| 32 |
+
RUN npx playwright install chromium --with-deps --only-shell
|
| 33 |
+
|
| 34 |
+
# 创建应用目录
|
| 35 |
+
WORKDIR /app
|
| 36 |
+
|
| 37 |
+
# 复制应用代码
|
| 38 |
+
COPY src/ /app/src/
|
| 39 |
+
COPY entrypoint.sh /app/
|
| 40 |
+
|
| 41 |
+
# 创建数据目录
|
| 42 |
+
RUN mkdir -p /data/.hermes && \
|
| 43 |
+
mkdir -p /app/logs && \
|
| 44 |
+
chmod +x /app/entrypoint.sh
|
| 45 |
+
|
| 46 |
+
# 设置非 root 用户(Hugging Face Spaces 要求)
|
| 47 |
+
RUN useradd -m -u 1000 appuser && \
|
| 48 |
+
chown -R appuser:appuser /data && \
|
| 49 |
+
chown -R appuser:appuser /app
|
| 50 |
+
|
| 51 |
+
USER appuser
|
| 52 |
+
|
| 53 |
+
# 暴露端口(Hugging Face Spaces 默认 7860)
|
| 54 |
+
EXPOSE 7860
|
| 55 |
+
|
| 56 |
+
# 健康检查
|
| 57 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
| 58 |
+
CMD curl -f http://localhost:7860/health || exit 1
|
| 59 |
+
|
| 60 |
+
# 启动脚本
|
| 61 |
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 Hermes Agent Community
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,12 +1,213 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hermes Agent v0.9.0 on Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
智能AI代理,支持16个消息平台,持久化记忆,和Web管理界面。
|
| 4 |
+
|
| 5 |
+
## 功能
|
| 6 |
+
|
| 7 |
+
- 🤖 AI 对话与工具调用
|
| 8 |
+
- 🌐 Web Dashboard 管理
|
| 9 |
+
- 💾 数据持久化到 Hugging Face Dataset
|
| 10 |
+
- 🔔 支持消息网关(Telegram/Discord/Slack等)
|
| 11 |
+
- ⚡ 自动唤醒保持在线
|
| 12 |
+
|
| 13 |
+
## 技术栈
|
| 14 |
+
|
| 15 |
+
- Hermes Agent v0.9.0
|
| 16 |
+
- FastAPI Web Dashboard
|
| 17 |
+
- Hugging Face Datasets 持久化
|
| 18 |
+
- Docker Spaces
|
| 19 |
+
|
| 20 |
+
## 数据目录映射
|
| 21 |
+
|
| 22 |
+
| Hermes 目录 | Dataset 路径 | 说明 |
|
| 23 |
+
|------------|-------------|------|
|
| 24 |
+
| `~/.hermes/config.yaml` | `/config/config.yaml` | 核心配置 |
|
| 25 |
+
| `~/.hermes/.env` | `/config/.env` | 环境变量 |
|
| 26 |
+
| `~/.hermes/auth.json` | `/config/auth.json` | OAuth认证 |
|
| 27 |
+
| `~/.hermes/SOUL.md` | `/personality/SOUL.md` | 代理人格 |
|
| 28 |
+
| `~/.hermes/memories/` | `/memories/` | 持久记忆 |
|
| 29 |
+
| `~/.hermes/skills/` | `/skills/` | 自定义技能 |
|
| 30 |
+
| `~/.hermes/sessions/` | `/sessions/` | 会话历史 |
|
| 31 |
+
| `~/.hermes/state.db` | `/state/state.db` | SQLite数据库 |
|
| 32 |
+
| `~/.hermes/logs/` | `/logs/` | 日志文件 |
|
| 33 |
+
| `~/.hermes/cron/` | `/cron/` | 定时任务 |
|
| 34 |
+
|
| 35 |
+
## 环境变量
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
# 必需
|
| 39 |
+
HF_DATASET_REPO=your-username/hermes-data
|
| 40 |
+
|
| 41 |
+
# 可选
|
| 42 |
+
HERMES_HOME=/data/.hermes
|
| 43 |
+
SYNC_INTERVAL=300 # 同步间隔(秒)
|
| 44 |
+
HF_TOKEN=your-huggingface-token # 用于访问私有 dataset
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
## 快速开始
|
| 48 |
+
|
| 49 |
+
### 1. 创建 Hugging Face Dataset
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
# 创建一个新的 dataset 用于存储数据
|
| 53 |
+
huggingface-cli repo create hermes-data --type dataset
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
### 2. 设置环境变量
|
| 57 |
+
|
| 58 |
+
在 Hugging Face Spaces 的 Settings 中设置:
|
| 59 |
+
- `HF_DATASET_REPO`: 你的 dataset 名称 (例如: username/hermes-data)
|
| 60 |
+
- `HF_TOKEN`: Hugging Face access token
|
| 61 |
+
|
| 62 |
+
### 3. 配置唤醒服务
|
| 63 |
+
|
| 64 |
+
使用 UptimeRobot 或 Cron-job.org 定期访问 Space URL 以保持在线。
|
| 65 |
+
|
| 66 |
+
## 部署步骤
|
| 67 |
+
|
| 68 |
+
### 部署到 Hugging Face Spaces
|
| 69 |
+
|
| 70 |
+
#### 方式一:通过 Web 界面部署
|
| 71 |
+
|
| 72 |
+
1. **创建 Space**: 访问 https://huggingface.co/new-space
|
| 73 |
+
- 选择 "Docker" SDK
|
| 74 |
+
- 设置 Space 名称
|
| 75 |
+
- 选择硬件配置(免费版即可开始)
|
| 76 |
+
|
| 77 |
+
2. **克隆 Space 仓库**:
|
| 78 |
+
```bash
|
| 79 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
|
| 80 |
+
cd YOUR_SPACE_NAME
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
3. **复制项目文件**:
|
| 84 |
+
```bash
|
| 85 |
+
# 复制所有项目文件到 Space 目录
|
| 86 |
+
cp -r /path/to/hermes-spaces/* .
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
4. **配置环境变量**:
|
| 90 |
+
- 在 Space Settings → Variables 中设置:
|
| 91 |
+
- `HF_DATASET_REPO`: `your-username/hermes-data`
|
| 92 |
+
- `HF_TOKEN`: `your-huggingface-token`
|
| 93 |
+
|
| 94 |
+
5. **提交并推送**:
|
| 95 |
+
```bash
|
| 96 |
+
git add .
|
| 97 |
+
git commit -m "Initial deployment"
|
| 98 |
+
git push
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
6. **等待部署完成**:
|
| 102 |
+
- 访问 `https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME`
|
| 103 |
+
- 查看 Build 日志,等待部署完成
|
| 104 |
+
|
| 105 |
+
#### 方式二:通过 GitHub Actions 自动部署
|
| 106 |
+
|
| 107 |
+
1. **Fork 本仓库** 到你的 GitHub 账号
|
| 108 |
+
|
| 109 |
+
2. **在 Hugging Face 创建 Space**:
|
| 110 |
+
- 访问 https://huggingface.co/new-space
|
| 111 |
+
- 选择 Docker SDK
|
| 112 |
+
- 记下 Space 名称
|
| 113 |
+
|
| 114 |
+
3. **配置 GitHub Secrets**:
|
| 115 |
+
- 进入 GitHub 仓库 → Settings → Secrets and variables → Actions
|
| 116 |
+
- 添加以下 secrets:
|
| 117 |
+
- `HF_TOKEN`: 你的 Hugging Face access token
|
| 118 |
+
- `HF_SPACE_REPO`: `YOUR_USERNAME/YOUR_SPACE_NAME`
|
| 119 |
+
|
| 120 |
+
4. **推送代码到 main 分支**:
|
| 121 |
+
```bash
|
| 122 |
+
git push origin main
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
5. **GitHub Actions 会自动部署**:
|
| 126 |
+
- 查看 Actions 标签页查看部署进度
|
| 127 |
+
- 部署完成后访问 Space URL
|
| 128 |
+
|
| 129 |
+
## 配置唤醒服务
|
| 130 |
+
|
| 131 |
+
由于 Hugging Face Spaces 免费版会在 48 小时无活动后休眠,建议配置外部唤醒服务:
|
| 132 |
+
|
| 133 |
+
### 使用 UptimeRobot(推荐)
|
| 134 |
+
|
| 135 |
+
1. 访问 https://uptimerobot.com 并注册账号
|
| 136 |
+
2. 点击 "Add New Monitor"
|
| 137 |
+
3. 配置如下:
|
| 138 |
+
- **Monitor Type**: HTTP(s)
|
| 139 |
+
- **Friendly Name**: Hermes Agent
|
| 140 |
+
- **URL**: `https://YOUR_USERNAME-YOUR_SPACE_NAME.hf.space/health`
|
| 141 |
+
- **Monitoring Interval**: 5 minutes
|
| 142 |
+
4. 点击 "Create Monitor"
|
| 143 |
+
|
| 144 |
+
### 使用 Cron-job.org(备选)
|
| 145 |
+
|
| 146 |
+
1. 访问 https://cron-job.org
|
| 147 |
+
2. 创建新的 cron job
|
| 148 |
+
3. 配置 URL: `https://YOUR_USERNAME-YOUR_SPACE_NAME.hf.space/health`
|
| 149 |
+
4. 设置每 5 分钟执行一次
|
| 150 |
+
|
| 151 |
+
## 首次使用
|
| 152 |
+
|
| 153 |
+
1. 访问 Space URL: `https://YOUR_USERNAME-YOUR_SPACE_NAME.hf.space`
|
| 154 |
+
2. 首次启动会创建默认配置,从 Dataset 恢复数据(如果存在)
|
| 155 |
+
3. 在 Web Dashboard 中配置:
|
| 156 |
+
- API Keys(OpenAI, OpenRouter, etc.)
|
| 157 |
+
- 消息网关(Telegram, Discord, etc.)
|
| 158 |
+
- 代理人格(SOUL.md)
|
| 159 |
+
|
| 160 |
+
## 故障排除
|
| 161 |
+
|
| 162 |
+
### Space 无法启动
|
| 163 |
+
|
| 164 |
+
1. **检查构建日志**:
|
| 165 |
+
- 访问 Space 页面 → Files → "Build logs"
|
| 166 |
+
- 查看错误信息
|
| 167 |
+
|
| 168 |
+
2. **常见问题**:
|
| 169 |
+
- **内存不足**: 升级到付费硬件(CPU Upgrade)
|
| 170 |
+
- **依赖安装失败**: 检查 requirements.txt 格式
|
| 171 |
+
- **权限错误**: 确保 Dockerfile 中设置了 USER appuser
|
| 172 |
+
|
| 173 |
+
### 数据未持久化
|
| 174 |
+
|
| 175 |
+
1. **检查环境变量**:
|
| 176 |
+
- 确认 `HF_DATASET_REPO` 已正确设置
|
| 177 |
+
- 确认 `HF_TOKEN` 有写入 Dataset 的权限
|
| 178 |
+
|
| 179 |
+
2. **检查 Dataset 权限**:
|
| 180 |
+
- 确保 Dataset 是私有的(推荐)
|
| 181 |
+
- 确保 Token 有 `write` 权限
|
| 182 |
+
|
| 183 |
+
3. **查看同步日志**:
|
| 184 |
+
- 在 Space 的 Logs 中查看 data-sync 相关日志
|
| 185 |
+
|
| 186 |
+
### 消息网关断开
|
| 187 |
+
|
| 188 |
+
1. **这是正常现象**: Spaces 休眠后所有连接会断开
|
| 189 |
+
2. **解决方案**:
|
| 190 |
+
- 升级到付费版保持持续运行
|
| 191 |
+
- 或配置网关自动重连(在 Hermes 配置中设置)
|
| 192 |
+
|
| 193 |
+
## 文件说明
|
| 194 |
+
|
| 195 |
+
- `Dockerfile` - Docker 镜像定义
|
| 196 |
+
- `entrypoint.sh` - 容器启动脚本
|
| 197 |
+
- `requirements.txt` - Python 依赖
|
| 198 |
+
- `src/data_sync.py` - 数据同步服务
|
| 199 |
+
- `.github/workflows/deploy.yml` - GitHub Actions 自动部署
|
| 200 |
+
|
| 201 |
+
## 许可证
|
| 202 |
+
|
| 203 |
+
MIT License - 详见 LICENSE 文件
|
| 204 |
+
|
| 205 |
+
## 贡献
|
| 206 |
+
|
| 207 |
+
欢迎提交 Issue 和 Pull Request!
|
| 208 |
+
|
| 209 |
+
## 相关链接
|
| 210 |
+
|
| 211 |
+
- [Hermes Agent 官方仓库](https://github.com/NousResearch/hermes-agent)
|
| 212 |
+
- [Hugging Face Spaces 文档](https://huggingface.co/docs/hub/spaces-overview)
|
| 213 |
+
- [Hermes Agent 文档](https://hermes-agent.com)
|
entrypoint.sh
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Entrypoint script for Hermes Agent on Hugging Face Spaces
|
| 3 |
+
|
| 4 |
+
set -e
|
| 5 |
+
|
| 6 |
+
echo "🚀 Hermes Agent v0.9.0 - Hugging Face Spaces"
|
| 7 |
+
echo "=============================================="
|
| 8 |
+
|
| 9 |
+
# 检查必要的环境变量
|
| 10 |
+
if [ -z "$HF_DATASET_REPO" ]; then
|
| 11 |
+
echo "⚠️ 警告: HF_DATASET_REPO 未设置,数据将不会持久化到 Dataset"
|
| 12 |
+
echo " 设置方式: export HF_DATASET_REPO=your-username/hermes-data"
|
| 13 |
+
fi
|
| 14 |
+
|
| 15 |
+
# 创建必要的目录
|
| 16 |
+
echo "📁 初始化目录结构..."
|
| 17 |
+
mkdir -p /data/.hermes/{cron,sessions,logs,memories,skills,pairing,hooks,image_cache,audio_cache,whatsapp/session}
|
| 18 |
+
mkdir -p /app/logs
|
| 19 |
+
|
| 20 |
+
# 从 Dataset 恢复数据(如果配置了)
|
| 21 |
+
if [ -n "$HF_DATASET_REPO" ]; then
|
| 22 |
+
echo "📥 从 Dataset 恢复数据..."
|
| 23 |
+
python -m src.data_sync restore || {
|
| 24 |
+
echo "⚠️ 数据恢复失败,使用空配置启动"
|
| 25 |
+
echo " 首次运行时会创建新的配置文件"
|
| 26 |
+
}
|
| 27 |
+
fi
|
| 28 |
+
|
| 29 |
+
# 设置同步间隔(默认5分钟)
|
| 30 |
+
SYNC_INTERVAL=${SYNC_INTERVAL:-300}
|
| 31 |
+
echo "🔄 数据同步间隔: ${SYNC_INTERVAL}秒"
|
| 32 |
+
|
| 33 |
+
# 启动后台数据同步服务(守护进程模式)
|
| 34 |
+
echo "🔄 启动数据同步服务..."
|
| 35 |
+
python -m src.data_sync daemon &
|
| 36 |
+
SYNC_PID=$!
|
| 37 |
+
echo " 同步服务 PID: $SYNC_PID"
|
| 38 |
+
|
| 39 |
+
# 注册优雅关闭处理函数
|
| 40 |
+
cleanup() {
|
| 41 |
+
echo ""
|
| 42 |
+
echo "🛑 收到关闭信号,执行清理..."
|
| 43 |
+
|
| 44 |
+
# 停止数据同步服务
|
| 45 |
+
if kill -0 $SYNC_PID 2>/dev/null; then
|
| 46 |
+
echo " 停止数据同步服务..."
|
| 47 |
+
kill $SYNC_PID 2>/dev/null || true
|
| 48 |
+
wait $SYNC_PID 2>/dev/null || true
|
| 49 |
+
fi
|
| 50 |
+
|
| 51 |
+
# 执行最终备份
|
| 52 |
+
if [ -n "$HF_DATASET_REPO" ]; then
|
| 53 |
+
echo " 执行最终数据备份..."
|
| 54 |
+
python -m src.data_sync backup --force || echo " 备份失败"
|
| 55 |
+
fi
|
| 56 |
+
|
| 57 |
+
echo "👋 再见!"
|
| 58 |
+
exit 0
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
trap cleanup SIGTERM SIGINT
|
| 62 |
+
|
| 63 |
+
echo ""
|
| 64 |
+
echo "🌐 启动 Hermes Web Dashboard..."
|
| 65 |
+
echo " 访问地址: http://localhost:7860"
|
| 66 |
+
echo " 健康检查: http://localhost:7860/health"
|
| 67 |
+
echo ""
|
| 68 |
+
|
| 69 |
+
# 启动 Hermes Web Dashboard
|
| 70 |
+
exec hermes web --host 0.0.0.0 --port 7860 --no-open
|
requirements.txt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hermes Agent on Hugging Face Spaces
|
| 2 |
+
# Python 依赖
|
| 3 |
+
|
| 4 |
+
# 核心依赖
|
| 5 |
+
huggingface-hub>=0.20.0
|
| 6 |
+
datasets>=2.14.0
|
| 7 |
+
requests>=2.31.0
|
| 8 |
+
pydantic>=2.0.0
|
| 9 |
+
python-dotenv>=1.0.0
|
| 10 |
+
|
| 11 |
+
# 数据处理和加密
|
| 12 |
+
cryptography>=41.0.0
|
| 13 |
+
|
| 14 |
+
# 日志和监控
|
| 15 |
+
loguru>=0.7.0
|
| 16 |
+
psutil>=5.9.0
|
| 17 |
+
|
| 18 |
+
# 异步支持
|
| 19 |
+
aiohttp>=3.9.0
|
| 20 |
+
aiocron>=1.8
|
| 21 |
+
|
| 22 |
+
# 工具
|
| 23 |
+
click>=8.1.0
|
| 24 |
+
rich>=13.0.0
|
src/__init__.py
ADDED
|
File without changes
|
src/data_sync.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Hermes Agent Data Sync Service
|
| 4 |
+
Handles data persistence to/from Hugging Face Dataset
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import time
|
| 10 |
+
import json
|
| 11 |
+
import shutil
|
| 12 |
+
import tarfile
|
| 13 |
+
import argparse
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
from typing import Optional, Dict, List
|
| 17 |
+
|
| 18 |
+
from huggingface_hub import HfApi, hf_hub_download, upload_folder
|
| 19 |
+
from loguru import logger
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class DatasetManager:
|
| 23 |
+
"""Manages data synchronization with Hugging Face Dataset"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, dataset_repo: Optional[str] = None, token: Optional[str] = None):
|
| 26 |
+
self.dataset_repo = dataset_repo or os.environ.get('HF_DATASET_REPO')
|
| 27 |
+
self.token = token or os.environ.get('HF_TOKEN') or os.environ.get('HUGGING_FACE_HUB_TOKEN')
|
| 28 |
+
self.api = HfApi(token=self.token)
|
| 29 |
+
self.hermes_home = Path(os.environ.get('HERMES_HOME', '/data/.hermes'))
|
| 30 |
+
self.temp_dir = Path('/tmp/hermes_sync')
|
| 31 |
+
|
| 32 |
+
# 数据路径映射
|
| 33 |
+
self.path_mapping = {
|
| 34 |
+
'config': self.hermes_home / 'config.yaml',
|
| 35 |
+
'env': self.hermes_home / '.env',
|
| 36 |
+
'auth': self.hermes_home / 'auth.json',
|
| 37 |
+
'soul': self.hermes_home / 'SOUL.md',
|
| 38 |
+
'memories': self.hermes_home / 'memories',
|
| 39 |
+
'skills': self.hermes_home / 'skills',
|
| 40 |
+
'sessions': self.hermes_home / 'sessions',
|
| 41 |
+
'state_db': self.hermes_home / 'state.db',
|
| 42 |
+
'logs': self.hermes_home / 'logs',
|
| 43 |
+
'cron': self.hermes_home / 'cron',
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
def validate(self) -> bool:
|
| 47 |
+
"""验证配置是否正确"""
|
| 48 |
+
if not self.dataset_repo:
|
| 49 |
+
logger.error("HF_DATASET_REPO not set")
|
| 50 |
+
return False
|
| 51 |
+
|
| 52 |
+
if not self.token:
|
| 53 |
+
logger.warning("HF_TOKEN not set, will try public dataset")
|
| 54 |
+
|
| 55 |
+
return True
|
| 56 |
+
|
| 57 |
+
def prepare_backup_data(self) -> Path:
|
| 58 |
+
"""准备备份数据到临时目录"""
|
| 59 |
+
logger.info("Preparing backup data...")
|
| 60 |
+
|
| 61 |
+
# 清理并创建临时目录
|
| 62 |
+
if self.temp_dir.exists():
|
| 63 |
+
shutil.rmtree(self.temp_dir)
|
| 64 |
+
self.temp_dir.mkdir(parents=True)
|
| 65 |
+
|
| 66 |
+
# 创建目录结构
|
| 67 |
+
(self.temp_dir / 'config').mkdir()
|
| 68 |
+
(self.temp_dir / 'personality').mkdir()
|
| 69 |
+
(self.temp_dir / 'memories').mkdir()
|
| 70 |
+
(self.temp_dir / 'skills').mkdir()
|
| 71 |
+
(self.temp_dir / 'sessions').mkdir()
|
| 72 |
+
(self.temp_dir / 'state').mkdir()
|
| 73 |
+
(self.temp_dir / 'logs').mkdir()
|
| 74 |
+
(self.temp_dir / 'cron').mkdir()
|
| 75 |
+
|
| 76 |
+
# 复制文件
|
| 77 |
+
try:
|
| 78 |
+
# 配置文件
|
| 79 |
+
if self.path_mapping['config'].exists():
|
| 80 |
+
shutil.copy2(self.path_mapping['config'], self.temp_dir / 'config' / 'config.yaml')
|
| 81 |
+
|
| 82 |
+
# 环境变量(敏感信息)
|
| 83 |
+
if self.path_mapping['env'].exists():
|
| 84 |
+
shutil.copy2(self.path_mapping['env'], self.temp_dir / 'config' / '.env')
|
| 85 |
+
|
| 86 |
+
# OAuth 认证
|
| 87 |
+
if self.path_mapping['auth'].exists():
|
| 88 |
+
shutil.copy2(self.path_mapping['auth'], self.temp_dir / 'config' / 'auth.json')
|
| 89 |
+
|
| 90 |
+
# 人格定义
|
| 91 |
+
if self.path_mapping['soul'].exists():
|
| 92 |
+
shutil.copy2(self.path_mapping['soul'], self.temp_dir / 'personality' / 'SOUL.md')
|
| 93 |
+
|
| 94 |
+
# 记忆
|
| 95 |
+
if self.path_mapping['memories'].exists():
|
| 96 |
+
shutil.copytree(self.path_mapping['memories'], self.temp_dir / 'memories', dirs_exist_ok=True)
|
| 97 |
+
|
| 98 |
+
# 技能
|
| 99 |
+
if self.path_mapping['skills'].exists():
|
| 100 |
+
shutil.copytree(self.path_mapping['skills'], self.temp_dir / 'skills', dirs_exist_ok=True)
|
| 101 |
+
|
| 102 |
+
# 会话
|
| 103 |
+
if self.path_mapping['sessions'].exists():
|
| 104 |
+
shutil.copytree(self.path_mapping['sessions'], self.temp_dir / 'sessions', dirs_exist_ok=True)
|
| 105 |
+
|
| 106 |
+
# 数据库
|
| 107 |
+
if self.path_mapping['state_db'].exists():
|
| 108 |
+
shutil.copy2(self.path_mapping['state_db'], self.temp_dir / 'state' / 'state.db')
|
| 109 |
+
|
| 110 |
+
# 日志
|
| 111 |
+
if self.path_mapping['logs'].exists():
|
| 112 |
+
shutil.copytree(self.path_mapping['logs'], self.temp_dir / 'logs', dirs_exist_ok=True)
|
| 113 |
+
|
| 114 |
+
# 定时任务
|
| 115 |
+
if self.path_mapping['cron'].exists():
|
| 116 |
+
shutil.copytree(self.path_mapping['cron'], self.temp_dir / 'cron', dirs_exist_ok=True)
|
| 117 |
+
|
| 118 |
+
# 添加元数据
|
| 119 |
+
metadata = {
|
| 120 |
+
'timestamp': datetime.now().isoformat(),
|
| 121 |
+
'version': '0.9.0',
|
| 122 |
+
'hermes_home': str(self.hermes_home)
|
| 123 |
+
}
|
| 124 |
+
with open(self.temp_dir / 'metadata.json', 'w') as f:
|
| 125 |
+
json.dump(metadata, f, indent=2)
|
| 126 |
+
|
| 127 |
+
logger.success(f"Backup prepared at {self.temp_dir}")
|
| 128 |
+
return self.temp_dir
|
| 129 |
+
|
| 130 |
+
except Exception as e:
|
| 131 |
+
logger.error(f"Failed to prepare backup: {e}")
|
| 132 |
+
raise
|
| 133 |
+
|
| 134 |
+
def upload_to_dataset(self, force: bool = False) -> bool:
|
| 135 |
+
"""上传数据到 Hugging Face Dataset"""
|
| 136 |
+
try:
|
| 137 |
+
backup_dir = self.prepare_backup_data()
|
| 138 |
+
|
| 139 |
+
logger.info(f"Uploading to dataset: {self.dataset_repo}")
|
| 140 |
+
|
| 141 |
+
# 上传文件夹到 dataset
|
| 142 |
+
self.api.upload_folder(
|
| 143 |
+
folder_path=str(backup_dir),
|
| 144 |
+
repo_id=self.dataset_repo,
|
| 145 |
+
repo_type="dataset",
|
| 146 |
+
commit_message=f"Hermes Agent backup - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
logger.success("Backup uploaded successfully")
|
| 150 |
+
return True
|
| 151 |
+
|
| 152 |
+
except Exception as e:
|
| 153 |
+
logger.error(f"Failed to upload to dataset: {e}")
|
| 154 |
+
return False
|
| 155 |
+
|
| 156 |
+
def download_from_dataset(self) -> bool:
|
| 157 |
+
"""从 Hugging Face Dataset 下载数据"""
|
| 158 |
+
try:
|
| 159 |
+
logger.info(f"Downloading from dataset: {self.dataset_repo}")
|
| 160 |
+
|
| 161 |
+
# 创建临时下载目录
|
| 162 |
+
download_dir = Path('/tmp/hermes_download')
|
| 163 |
+
if download_dir.exists():
|
| 164 |
+
shutil.rmtree(download_dir)
|
| 165 |
+
download_dir.mkdir(parents=True)
|
| 166 |
+
|
| 167 |
+
# 下载所有文件
|
| 168 |
+
self.api.snapshot_download(
|
| 169 |
+
repo_id=self.dataset_repo,
|
| 170 |
+
repo_type="dataset",
|
| 171 |
+
local_dir=str(download_dir)
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
logger.success("Download completed")
|
| 175 |
+
|
| 176 |
+
# 恢复数据到 Hermes 目录
|
| 177 |
+
self.restore_from_download(download_dir)
|
| 178 |
+
return True
|
| 179 |
+
|
| 180 |
+
except Exception as e:
|
| 181 |
+
logger.error(f"Failed to download from dataset: {e}")
|
| 182 |
+
return False
|
| 183 |
+
|
| 184 |
+
def restore_from_download(self, download_dir: Path):
|
| 185 |
+
"""从下载的目录恢复数据"""
|
| 186 |
+
logger.info("Restoring data to Hermes home...")
|
| 187 |
+
|
| 188 |
+
# 确保目标目录存在
|
| 189 |
+
self.hermes_home.mkdir(parents=True, exist_ok=True)
|
| 190 |
+
|
| 191 |
+
# 恢复各个部分
|
| 192 |
+
restore_mapping = {
|
| 193 |
+
'config/config.yaml': self.path_mapping['config'],
|
| 194 |
+
'config/.env': self.path_mapping['env'],
|
| 195 |
+
'config/auth.json': self.path_mapping['auth'],
|
| 196 |
+
'personality/SOUL.md': self.path_mapping['soul'],
|
| 197 |
+
'memories': self.path_mapping['memories'],
|
| 198 |
+
'skills': self.path_mapping['skills'],
|
| 199 |
+
'sessions': self.path_mapping['sessions'],
|
| 200 |
+
'state/state.db': self.path_mapping['state_db'],
|
| 201 |
+
'logs': self.path_mapping['logs'],
|
| 202 |
+
'cron': self.path_mapping['cron'],
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
for src_rel, dst in restore_mapping.items():
|
| 206 |
+
src = download_dir / src_rel
|
| 207 |
+
if src.exists():
|
| 208 |
+
try:
|
| 209 |
+
if src.is_file():
|
| 210 |
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
| 211 |
+
shutil.copy2(src, dst)
|
| 212 |
+
logger.info(f"Restored: {src_rel}")
|
| 213 |
+
elif src.is_dir():
|
| 214 |
+
if dst.exists():
|
| 215 |
+
shutil.rmtree(dst)
|
| 216 |
+
shutil.copytree(src, dst)
|
| 217 |
+
logger.info(f"Restored directory: {src_rel}")
|
| 218 |
+
except Exception as e:
|
| 219 |
+
logger.error(f"Failed to restore {src_rel}: {e}")
|
| 220 |
+
else:
|
| 221 |
+
logger.warning(f"Not found in backup: {src_rel}")
|
| 222 |
+
|
| 223 |
+
logger.success("Data restoration completed")
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def run_daemon():
|
| 227 |
+
"""后台守护进程模式 - 定期同步"""
|
| 228 |
+
logger.info("Starting data sync daemon...")
|
| 229 |
+
|
| 230 |
+
sync_interval = int(os.environ.get('SYNC_INTERVAL', '300')) # 默认5分钟
|
| 231 |
+
manager = DatasetManager()
|
| 232 |
+
|
| 233 |
+
if not manager.validate():
|
| 234 |
+
logger.error("Configuration invalid, exiting")
|
| 235 |
+
sys.exit(1)
|
| 236 |
+
|
| 237 |
+
logger.info(f"Sync interval: {sync_interval} seconds")
|
| 238 |
+
|
| 239 |
+
while True:
|
| 240 |
+
try:
|
| 241 |
+
time.sleep(sync_interval)
|
| 242 |
+
logger.info("Performing scheduled backup...")
|
| 243 |
+
manager.upload_to_dataset()
|
| 244 |
+
except KeyboardInterrupt:
|
| 245 |
+
logger.info("Daemon stopped")
|
| 246 |
+
break
|
| 247 |
+
except Exception as e:
|
| 248 |
+
logger.error(f"Sync error: {e}")
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def main():
|
| 252 |
+
parser = argparse.ArgumentParser(description='Hermes Agent Data Sync')
|
| 253 |
+
parser.add_argument('action', choices=['backup', 'restore', 'daemon'],
|
| 254 |
+
help='Action to perform')
|
| 255 |
+
parser.add_argument('--force', '-f', action='store_true',
|
| 256 |
+
help='Force backup even if no changes')
|
| 257 |
+
|
| 258 |
+
args = parser.parse_args()
|
| 259 |
+
|
| 260 |
+
manager = DatasetManager()
|
| 261 |
+
|
| 262 |
+
if not manager.validate():
|
| 263 |
+
logger.error("Configuration invalid")
|
| 264 |
+
sys.exit(1)
|
| 265 |
+
|
| 266 |
+
if args.action == 'backup':
|
| 267 |
+
success = manager.upload_to_dataset(force=args.force)
|
| 268 |
+
sys.exit(0 if success else 1)
|
| 269 |
+
|
| 270 |
+
elif args.action == 'restore':
|
| 271 |
+
success = manager.download_from_dataset()
|
| 272 |
+
sys.exit(0 if success else 1)
|
| 273 |
+
|
| 274 |
+
elif args.action == 'daemon':
|
| 275 |
+
run_daemon()
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
if __name__ == '__main__':
|
| 279 |
+
main()
|