简介
适用对象:希望在低配 Debian VPS 上运行个人微信 AI Agent,并使用远端 OpenAI 兼容模型 API 的用户。
本文目标:不使用 Docker、不在 VPS 编译、不开放 PicoClaw 公网端口,以普通用户运行,并将 Agent 的文件与命令活动限制在专用工作区。
一、最终架构与边界
本文采用的架构如下:
个人微信 iLink
│(长轮询出站连接,无需公网 Webhook)
▼
PicoClaw(systemd)
├─ 普通用户:picoclaw
├─ 白名单:仅自己的 @im.wechat 用户 ID
├─ 工作区:/home/picoclaw/workspace
├─ 本地 Gateway:127.0.0.1:18790 / [::1]:18790
├─ 内存上限:480 MiB
└─ CPU 上限:单核 95%
│
▼
OpenAI 兼容远端模型 API
安全策略
- 不以 root 运行 Agent:仅 root 负责安装、更新、启停 systemd 服务。
- 不开放 PicoClaw 公网端口:微信 iLink 使用出站长轮询;管理仍使用 SSH、Tailscale 或 ZeroTier。
- 单一微信白名单:只有指定
@im.wechatID 可以使用 Agent。 - C-轻量模式:允许 Agent 在工作区内使用 Shell 和用户态工具,但不允许 sudo、apt、systemctl、Docker、网络管理等系统级操作。
- systemd 强制限制:限制权限、设备、临时目录、内核设置、系统目录写入、内存和 CPU。
- 敏感凭证单独保存:模型 API Key 不写进主配置,也不提交 Git。
[!WARNING] 允许远程 Shell 后,即使 Agent 是普通用户,也仍有提示注入、恶意网页、恶意下载脚本等风险。本文的配置能阻止系统级修改,但无法让一个“可以执行任意用户态命令”的进程完全看不到它自身需要使用的凭证。请只允许可信微信账号使用,不要把 Bot 分享给他人或加入群聊。
![在低配VPS 上安全部署 PicoClaw:个人微信 iLink、受限 Shell、systemd 与备份还原、1C 1G 5G配置[picoclaw-debian13-wechat-ilink-deployment]-CXT - Enjoy Life | 生活、技术、交友、分享](https://www.cxthhhhh.com/wp-content/uploads/2026/07/2026071306092851.jpg)
二、资源要求与容量规划
本文实测配置适用于:
| 项目 | 建议值 |
|---|---|
| 系统 | Debian 13(x86_64) |
| CPU | 1 vCPU |
| 内存 | 1 GB |
| Swap | 至少 512 MB,建议 1 GB |
| 磁盘 | 5 GB 起步,建议 10 GB+ 更从容 |
| 模型 | 云端 API;不运行 Ollama 或本地模型 |
低配 VPS 的关键不是 PicoClaw 二进制本身,而是避免同时运行:
- Docker / 多容器沙箱;
- 本地大模型;
- 浏览器自动化;
- 多 Agent 并发;
- 大型向量数据库;
- 大量附件处理、下载和日志积累。
建议首先检查:
uname -m
cat /etc/os-release
free -h
df -h /
三、部署前检查
1. 检查端口和已有服务
ss -lntup
systemctl --type=service --state=running --no-pager
PicoClaw 的本地 Gateway 默认使用 18790。它应只绑定到回环地址,不应监听:
0.0.0.0:18790
[::]:18790
2. 检查微信 iLink 出网
getent hosts ilinkai.weixin.qq.com
curl -I --max-time 10 https://ilinkai.weixin.qq.com
405 Method Not Allowed 对 curl -I 来说通常仍表示网络连接成功:该端点不接受 HEAD 请求,但 HTTPS 已可达。
3. 选择模型 API
本文假设服务商同时支持 OpenAI Chat Completions 风格接口:
https://YOUR-API-HOST/v1/chat/completions
PicoClaw 配置中使用的 Base URL 应为:
https://YOUR-API-HOST/v1
不要在文章、聊天记录、截图或 Git 仓库中粘贴真实 API Key。
四、创建受限运行用户与目录
所有以下系统操作以 root 执行。
if ! id picoclaw >/dev/null 2>&1; then
useradd --create-home --user-group --shell /bin/bash picoclaw
fi
# 锁定密码;root 仍可通过 runuser 切换到该用户。
usermod -L picoclaw
gpasswd -d picoclaw sudo 2>/dev/null || true
install -d -o picoclaw -g picoclaw -m 0700 \
/home/picoclaw/.picoclaw \
/home/picoclaw/workspace \
/home/picoclaw/workspace/bin \
/home/picoclaw/workspace/downloads
id picoclaw
预期:picoclaw 不应属于 sudo 组。
五、安装 PicoClaw 预编译二进制
PicoClaw 是 Go 二进制程序。对 x86_64 Linux,可使用官方 Release 的预编译包;不需要在 VPS 或 WSL 编译。
发布资产名称可能随项目版本变化。以下是当前常见命名方式;若下载出现 404,请先在官方 Release 页面确认资产名。
TMPDIR="$(mktemp -d)"
curl -fL --retry 3 --retry-delay 2 \
-o "${TMPDIR}/picoclaw.tar.gz" \
"https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_Linux_x86_64.tar.gz"
tar -C "${TMPDIR}" -xzf "${TMPDIR}/picoclaw.tar.gz"
BIN_PATH="$(find "${TMPDIR}" -type f -name picoclaw -executable -print -quit)"
test -n "${BIN_PATH}"
install -o root -g root -m 0755 "${BIN_PATH}" /usr/local/bin/picoclaw
rm -rf -- "${TMPDIR}"
/usr/local/bin/picoclaw version
六、写入 PicoClaw 主配置
创建 /home/picoclaw/.picoclaw/config.json:
cat > /home/picoclaw/.picoclaw/config.json <<'EOF'
{
"agents": {
"defaults": {
"workspace": "/home/picoclaw/workspace",
"model_name": "my-openai-compatible-model",
"max_tokens": 12288,
"max_tool_iterations": 5,
"max_parallel_turns": 1,
"restrict_to_workspace": true
}
},
"model_list": [
{
"model_name": "my-openai-compatible-model",
"provider": "openai",
"model": "YOUR_MODEL_NAME",
"api_base": "https://YOUR-API-HOST/v1"
}
],
"channel_list": {
"weixin": {
"enabled": true,
"type": "weixin",
"allow_from": ["blocked-until-owner-id-is-set"]
}
},
"tools": {
"web": {
"duckduckgo": {
"enabled": true,
"max_results": 5
}
},
"exec": {
"allow_remote": false,
"enable_deny_patterns": true,
"custom_deny_patterns": [
"(?i)\\b(sudo|su|apt|apt-get|dpkg|systemctl|service|journalctl|useradd|usermod|passwd|visudo|mount|umount|iptables|nft|ufw|docker|podman|tailscale|zerotier-cli|caddy|reboot|shutdown|poweroff)\\b"
]
},
"cron": {
"enabled": false
}
},
"heartbeat": {
"enabled": false
}
}
EOF
chown picoclaw:picoclaw /home/picoclaw/.picoclaw/config.json
chmod 600 /home/picoclaw/.picoclaw/config.json
配置重点说明
| 字段 | 建议 | 说明 |
|---|---|---|
restrict_to_workspace | true | 让 Agent 的操作范围聚焦在工作区 |
max_tokens | 12288 | 单次模型输出上限,适合代码、长说明和多工具任务 |
max_parallel_turns | 1 | 1C/1G 与单用户微信场景下保持顺序处理 |
max_tool_iterations | 5 | 单次任务最多工具循环次数;复杂任务可拆分消息 |
allow_remote | 初始 false | 白名单确认后再启用 C-轻量 Shell |
allow_from | 初始占位值 | 初始状态拒绝所有微信用户,避免误开放 |
max_tokens 主要限制每次模型输出,不是整个会话总上下文。12288 是远端模型输出上限,主要影响 API 成本、单次响应长度和等待时间,不会让 VPS 本地进行模型推理。
七、私密写入模型 API Key
不要把 API Key 放在 config.json。使用独立安全文件:
runuser -l picoclaw
进入 picoclaw 用户后执行:
umask 077
read -rsp "Paste API key: " PICO_KEY
echo
cat > ~/.picoclaw/.security.yml <<EOF
model_list:
my-openai-compatible-model:
api_keys:
- "${PICO_KEY}"
EOF
unset PICO_KEY
chmod 600 ~/.picoclaw/.security.yml
exit
测试模型连接:
runuser -l picoclaw -c \
'/usr/local/bin/picoclaw agent -m "Reply with: model connection successful"'
模型返回正常后,再继续微信接入。
八、个人微信 iLink 扫码登录
以 root 执行:
runuser -l picoclaw -c '/usr/local/bin/picoclaw auth weixin'
终端会显示二维码。使用微信扫码并确认授权。
成功后,凭证由 picoclaw 用户保存;不要查看、复制、公开或提交这些凭证。
九、安全获取个人微信 User ID 并设置白名单
1. 先启动“拒绝所有人”的服务
本节先使用占位白名单:
blocked-until-owner-id-is-set
这样即使微信 Bot 在线,也不会接受任何用户请求。
2. 向微信 Bot 发送探测消息
在微信中发送:
owner-id-probe
PicoClaw 的微信 context token 通常保存在:
/home/picoclaw/.picoclaw/channels/weixin/context-tokens/
文件名可能是内部哈希,不应直接当作用户 ID。可以只从 context-token 文件中提取形如 @im.wechat 的 ID:
CTX=/home/picoclaw/.picoclaw/channels/weixin/context-tokens
grep -RahoE '[A-Za-z0-9_-]+@im\.wechat' "$CTX" 2>/dev/null | sort -u
如果得到一条唯一输出,例如:
[email protected]
将它作为 allow_from 的白名单值。
如果出现多条,向 Bot 再发送一条唯一探测文本,然后仅检查修改时间最新的 context-token 文件:
find "$CTX" -maxdepth 1 -type f -printf '%T@ %p\n' | sort -n | tail -n 1
3. 用真实 ID 替换占位白名单
read -rp "Enter your own Weixin User ID: " WX_USER_ID
CFG=/home/picoclaw/.picoclaw/config.json
BACKUP="${CFG}.before-owner-allowlist.$(date +%Y%m%d-%H%M%S)"
cp -a -- "$CFG" "$BACKUP"
awk -v id="$WX_USER_ID" '
/"weixin"[[:space:]]*:[[:space:]]*\{/ {
in_weixin = 1
}
in_weixin && /"allow_from"[[:space:]]*:/ {
print " \"allow_from\": [\"" id "\"],"
next
}
in_weixin && /^[[:space:]]*}[[:space:]]*,?[[:space:]]*$/ {
in_weixin = 0
}
{ print }
' "$CFG" > "${CFG}.new"
mv -- "${CFG}.new" "$CFG"
unset WX_USER_ID
chown picoclaw:picoclaw "$CFG"
chmod 600 "$CFG"
白名单验证后,微信中发送:
Please reply: whitelist verified
确认 Bot 正常回复后才继续开启 Shell。
十、英文版 AGENT.md 安全规则
为了避免终端区域设置导致中文乱码,建议 Agent 指令文件使用 ASCII 英文。
cat > /home/picoclaw/workspace/AGENT.md <<'EOF'
---
name: restricted-home-agent
description: Personal WeChat assistant restricted to its workspace.
tools:
- read_file
- write_file
- edit_file
- append_file
- list_dir
- web_search
- web_fetch
- send_file
- exec
---
# Mandatory boundaries
- Work only inside /home/picoclaw/workspace and its subdirectories.
- Never access /root, /etc, /usr, /opt, /var, /boot, or other users' homes.
- Never access or reveal API keys, tokens, credentials, login state, or files
under /home/picoclaw/.picoclaw.
- Never run sudo, su, apt, apt-get, dpkg, systemctl, service, docker, podman,
caddy, tailscale, zerotier-cli, nft, iptables, ufw, reboot, shutdown, or poweroff.
- Keep downloaded files, scripts, and portable user tools inside the workspace.
- Run commands only with /home/picoclaw/workspace as the working directory.
- Read-only web requests are allowed. Before uploading data, submitting forms,
downloading executables, executing downloaded scripts, or contacting an API
other than the configured model provider, explain the impact and ask first.
- Before deleting files, overwriting existing files, changing Agent instructions,
or creating scheduled tasks, explain the impact and ask first.
- Do not create or modify cron jobs, heartbeat tasks, service files, or configs.
- If a request conflicts with these rules, refuse it and explain why.
EOF
chown picoclaw:picoclaw /home/picoclaw/workspace/AGENT.md
chmod 600 /home/picoclaw/workspace/AGENT.md
AGENT.md是行为边界,不是 Linux 沙箱。真正的强制安全边界来自普通用户、白名单、systemd 和文件权限。
十一、创建 systemd 服务
将所有资源限制和安全限制统一写在一个主服务文件中,便于维护。
cat > /etc/systemd/system/picoclaw.service <<'EOF'
[Unit]
Description=PicoClaw WeChat Agent (restricted user service)
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=picoclaw
Group=picoclaw
WorkingDirectory=/home/picoclaw/workspace
Environment=HOME=/home/picoclaw
Environment=PATH=/home/picoclaw/workspace/bin:/usr/local/bin:/usr/bin:/bin
Environment=PICOCLAW_HEARTBEAT_ENABLED=false
ExecStart=/usr/local/bin/picoclaw gateway
Restart=on-failure
RestartSec=8
TimeoutStopSec=30
# Resource limits for 1C / 1GB VPS
MemoryHigh=400M
MemoryMax=480M
CPUQuota=95%
TasksMax=128
LimitNOFILE=1024
# Privilege isolation
UMask=0077
NoNewPrivileges=true
CapabilityBoundingSet=
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectClock=true
ProtectHostname=true
RestrictSUIDSGID=true
LockPersonality=true
RestrictNamespaces=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
# Writable area for PicoClaw only
ReadWritePaths=/home/picoclaw
# Explicitly hide sensitive host locations
InaccessiblePaths=/root
InaccessiblePaths=/etc/caddy
InaccessiblePaths=/var/lib/tailscale
InaccessiblePaths=/var/lib/zerotier-one
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/picoclaw.service
systemctl enable --now picoclaw.service
验证服务
systemctl is-active picoclaw.service
ss -lntp | grep picoclaw || true
systemctl show picoclaw.service \
-p User \
-p MemoryCurrent \
-p MemoryHigh \
-p MemoryMax \
-p CPUQuotaPerSecUSec \
-p NoNewPrivileges \
-p ProtectSystem
预期:
active
127.0.0.1:18790
[::1]:18790
User=picoclaw
MemoryHigh=419430400
MemoryMax=503316480
CPUQuotaPerSecUSec=950ms
NoNewPrivileges=yes
ProtectSystem=strict
若出现:
0.0.0.0:18790
[::]:18790
应立即停止服务并检查配置:
systemctl stop picoclaw.service
十二、确认 Cron 与 Heartbeat 没有实际任务
runuser -l picoclaw -c '/usr/local/bin/picoclaw cron list'
预期:
No scheduled jobs.
日志中即使出现:
Cron service started
Heartbeat service started
也可能只是内部调度组件初始化。只要 Cron 列表为空、PICOCLAW_HEARTBEAT_ENABLED=false 已注入 systemd 环境、且没有创建 HEARTBEAT.md 任务,就不会主动执行计划任务。
十三、开启 C-轻量 Shell 模式
仅在满足以下条件后开启:
- 白名单已经只包含你自己的
@im.wechatID; - 微信回复测试成功;
- Gateway 仅监听回环地址;
cron list没有任务;- Agent 以普通用户运行。
开启 allow_remote
CFG=/home/picoclaw/.picoclaw/config.json
BACKUP="${CFG}.before-c-mode.$(date +%Y%m%d-%H%M%S)"
cp -a -- "$CFG" "$BACKUP"
awk '
/"allow_remote"[[:space:]]*:[[:space:]]*false/ {
sub(/false/, "true")
}
{ print }
' "$CFG" > "${CFG}.new"
mv -- "${CFG}.new" "$CFG"
chown picoclaw:picoclaw "$CFG"
chmod 600 "$CFG"
systemctl restart picoclaw.service
Shell 验证
在微信中发送:
Only run pwd and reply with its output.
预期:
/home/picoclaw/workspace
然后测试工作区写入:
Create c-mode-check.txt in the workspace with the content C-MODE-OK. Reply only with done.
服务器侧验证:
cat /home/picoclaw/workspace/c-mode-check.txt
十四、日常运维与紧急降级
常用命令
# 状态
systemctl status picoclaw.service --no-pager
# 最近日志
journalctl -u picoclaw.service -n 100 --no-pager
# 实时日志
journalctl -u picoclaw.service -f
# 重启 / 停止 / 启动
systemctl restart picoclaw.service
systemctl stop picoclaw.service
systemctl start picoclaw.service
# 资源使用
systemctl show picoclaw.service \
-p MemoryCurrent -p MemoryHigh -p MemoryMax \
-p CPUUsageNSec -p TasksCurrent
# 系统磁盘与内存
free -h
df -h /
# PicoClaw 数据、工作区和日志容量
du -sh /home/picoclaw/.picoclaw /home/picoclaw/workspace
journalctl --disk-usage
紧急关闭 C 模式
若发现异常命令、提示注入或可疑下载,先停止服务:
systemctl stop picoclaw.service
然后关闭远程 Exec:
CFG=/home/picoclaw/.picoclaw/config.json
awk '
/"allow_remote"[[:space:]]*:[[:space:]]*true/ {
sub(/true/, "false")
}
{ print }
' "$CFG" > "${CFG}.new"
mv -- "${CFG}.new" "$CFG"
chown picoclaw:picoclaw "$CFG"
chmod 600 "$CFG"
systemctl start picoclaw.service
此后 PicoClaw 回到 B 模式:微信聊天可用,但远程 Shell 被禁用。
十五、备份与还原
配套脚本:picoclaw-backup.sh。
#!/usr/bin/env bash
# PicoClaw backup / restore helper for a single-host systemd deployment.
# Run this script as root on the PicoClaw server.
set -Eeuo pipefail
shopt -s nullglob
SERVICE_NAME="picoclaw.service"
PICO_USER="picoclaw"
PICO_HOME="/home/${PICO_USER}"
SERVICE_UNIT="/etc/systemd/system/${SERVICE_NAME}"
PICO_BINARY="/usr/local/bin/picoclaw"
DEFAULT_BACKUP_DIR="/root/picoclaw-backups"
BACKUP_DIR="$DEFAULT_BACKUP_DIR"
LAST_ARCHIVE=""
log() { printf '[INFO] %s\n' "$*"; }
warn() { printf '[WARN] %s\n' "$*" >&2; }
die() { printf '[ERROR] %s\n' "$*" >&2; exit 1; }
usage() {
cat <<'EOF'
PicoClaw backup / restore helper
Usage:
picoclaw-backup.sh backup [label]
picoclaw-backup.sh list
picoclaw-backup.sh verify <archive.tar.gz>
picoclaw-backup.sh restore <archive.tar.gz> --yes
picoclaw-backup.sh help
Options:
--backup-dir <path> Override the default backup directory for this invocation.
--yes Required for restore; confirms that files under / will be replaced.
Examples:
sudo ./picoclaw-backup.sh backup before-upgrade
sudo ./picoclaw-backup.sh list
sudo ./picoclaw-backup.sh verify /root/picoclaw-backups/picoclaw-20260713-120000.tar.gz
sudo ./picoclaw-backup.sh restore /root/picoclaw-backups/picoclaw-20260713-120000.tar.gz --yes
Security note:
Archives contain the model API key and WeChat login credentials. Keep them private.
EOF
}
require_root() {
[[ "${EUID}" -eq 0 ]] || die "Run this script as root."
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}
check_dependencies() {
require_command tar
require_command gzip
require_command sha256sum
require_command systemctl
require_command find
}
check_live_paths() {
[[ -d "${PICO_HOME}/.picoclaw" ]] || die "Missing PicoClaw config directory: ${PICO_HOME}/.picoclaw"
[[ -d "${PICO_HOME}/workspace" ]] || die "Missing PicoClaw workspace: ${PICO_HOME}/workspace"
[[ -f "$SERVICE_UNIT" ]] || die "Missing systemd service file: $SERVICE_UNIT"
[[ -x "$PICO_BINARY" ]] || die "Missing PicoClaw binary: $PICO_BINARY"
}
sanitize_label() {
local input="${1:-manual}"
input="${input//[^A-Za-z0-9._-]/-}"
input="${input#-}"
input="${input%-}"
[[ -n "$input" ]] || input="manual"
printf '%s' "$input"
}
service_is_active() {
systemctl is-active --quiet "$SERVICE_NAME"
}
stop_service_if_active() {
SERVICE_WAS_ACTIVE=0
if service_is_active; then
SERVICE_WAS_ACTIVE=1
log "Stopping ${SERVICE_NAME} for a consistent archive..."
systemctl stop "$SERVICE_NAME"
else
log "${SERVICE_NAME} is already inactive."
fi
}
restore_service_state() {
if [[ "${SERVICE_WAS_ACTIVE:-0}" -eq 1 ]]; then
log "Starting ${SERVICE_NAME}..."
systemctl start "$SERVICE_NAME"
fi
}
archive_member_list() {
local -n _members=$1
_members=(
"home/${PICO_USER}/.picoclaw"
"home/${PICO_USER}/workspace/AGENT.md"
"etc/systemd/system/${SERVICE_NAME}"
"usr/local/bin/picoclaw"
)
local rel
for rel in \
"home/${PICO_USER}/workspace/memory" \
"home/${PICO_USER}/workspace/cron" \
"home/${PICO_USER}/workspace/state" \
"home/${PICO_USER}/workspace/sessions"; do
[[ -e "/${rel}" ]] && _members+=("$rel")
done
}
validate_archive_paths() {
local archive="$1"
[[ -f "$archive" ]] || die "Archive not found: $archive"
gzip -t "$archive" || die "gzip integrity check failed: $archive"
local unsafe
unsafe="$(tar -tzf "$archive" | awk '
/^\// || /(^|\/)\.\.($|\/)/ { print; exit }
')"
[[ -z "$unsafe" ]] || die "Unsafe archive path detected: $unsafe"
}
verify_archive() {
local archive="$1"
validate_archive_paths "$archive"
local checksum="${archive}.sha256"
if [[ -f "$checksum" ]]; then
sha256sum -c "$checksum"
else
warn "No checksum sidecar found: $checksum"
log "gzip and archive-path validation passed."
fi
}
create_manifest() {
local archive="$1"
local manifest="${archive%.tar.gz}.manifest.txt"
{
printf 'Created: %s\n' "$(date -Is)"
printf 'Archive: %s\n\n' "$archive"
printf 'PicoClaw version:\n'
"$PICO_BINARY" version 2>/dev/null | tail -n 8 || true
printf '\nService state after backup: '
systemctl is-active "$SERVICE_NAME" || true
printf '\nArchive SHA-256:\n'
cat "${archive}.sha256"
} > "$manifest"
chmod 600 "$manifest"
}
create_backup() {
local label
label="$(sanitize_label "${1:-manual}")"
check_live_paths
install -d -m 700 "$BACKUP_DIR"
local stamp archive partial checksum
stamp="$(date +%Y%m%d-%H%M%S)"
archive="${BACKUP_DIR}/picoclaw-${stamp}-${label}.tar.gz"
partial="${archive}.partial"
checksum="${archive}.sha256"
local -a members=()
archive_member_list members
stop_service_if_active
log "Creating archive: $archive"
if ! tar -C / -czf "$partial" "${members[@]}"; then
warn "Archive creation failed."
rm -f -- "$partial"
restore_service_state || warn "Could not restore service state automatically."
die "Backup failed."
fi
if ! gzip -t "$partial"; then
warn "Archive integrity check failed."
rm -f -- "$partial"
restore_service_state || warn "Could not restore service state automatically."
die "Backup failed."
fi
mv -- "$partial" "$archive"
chmod 600 "$archive"
sha256sum "$archive" > "$checksum"
chmod 600 "$checksum"
restore_service_state || warn "Could not restore service state automatically."
create_manifest "$archive"
LAST_ARCHIVE="$archive"
log "Backup integrity: OK"
printf '\nCreated files:\n'
ls -lh "$archive" "$checksum" "${archive%.tar.gz}.manifest.txt"
}
list_backups() {
[[ -d "$BACKUP_DIR" ]] || {
log "No backup directory exists: $BACKUP_DIR"
return 0
}
local archives=("${BACKUP_DIR}"/*.tar.gz)
if [[ "${#archives[@]}" -eq 0 ]]; then
log "No archives found in: $BACKUP_DIR"
return 0
fi
printf '%-20s %-12s %s\n' 'DATE' 'SIZE' 'ARCHIVE'
for archive in "${archives[@]}"; do
printf '%-20s %-12s %s\n' \
"$(date -r "$archive" '+%Y-%m-%d %H:%M:%S')" \
"$(du -h "$archive" | awk '{print $1}')" \
"$archive"
done
}
restore_archive() {
local archive="$1"
validate_archive_paths "$archive"
local checksum="${archive}.sha256"
if [[ -f "$checksum" ]]; then
sha256sum -c "$checksum" || die "Checksum validation failed. Restore cancelled."
else
warn "No SHA-256 sidecar found; continuing after gzip/path validation only."
fi
log "Creating a pre-restore backup first..."
create_backup "pre-restore"
local pre_restore_archive="$LAST_ARCHIVE"
stop_service_if_active
log "Restoring archive into /: $archive"
if ! tar -C / -xzf "$archive" --numeric-owner; then
warn "Restore failed. Attempting automatic rollback from: $pre_restore_archive"
tar -C / -xzf "$pre_restore_archive" --numeric-owner || warn "Automatic rollback also failed."
systemctl daemon-reload || true
restore_service_state || true
die "Restore failed."
fi
systemctl daemon-reload
# Ensure expected ownership if an archive came from the same host but was unpacked differently.
chown -R "${PICO_USER}:${PICO_USER}" "${PICO_HOME}/.picoclaw" "${PICO_HOME}/workspace"
chmod 700 "${PICO_HOME}/workspace"
chmod 600 "${PICO_HOME}/.picoclaw/config.json" "${PICO_HOME}/.picoclaw/.security.yml" 2>/dev/null || true
chmod 600 "${PICO_HOME}/workspace/AGENT.md" 2>/dev/null || true
restore_service_state || die "Archive restored, but ${SERVICE_NAME} could not be restarted."
log "Restore completed."
printf '\nPost-restore checks:\n'
systemctl is-active "$SERVICE_NAME" || true
"$PICO_BINARY" version 2>/dev/null | tail -n 8 || true
}
parse_global_options() {
while [[ "${1:-}" == "--backup-dir" ]]; do
[[ $# -ge 2 ]] || die "--backup-dir requires a path"
BACKUP_DIR="$2"
shift 2
done
REMAINING_ARGS=("$@")
}
main() {
require_root
check_dependencies
[[ $# -ge 1 ]] || { usage; exit 1; }
parse_global_options "$@"
set -- "${REMAINING_ARGS[@]}"
local command="$1"
shift || true
case "$command" in
backup)
[[ $# -le 1 ]] || die "Usage: backup [label]"
create_backup "${1:-manual}"
;;
list)
[[ $# -eq 0 ]] || die "Usage: list"
list_backups
;;
verify)
[[ $# -eq 1 ]] || die "Usage: verify <archive.tar.gz>"
verify_archive "$1"
;;
restore)
[[ $# -ge 1 ]] || die "Usage: restore <archive.tar.gz> --yes"
local archive="$1"
shift
[[ "${1:-}" == "--yes" && $# -eq 1 ]] || die "Restore requires explicit confirmation: restore <archive.tar.gz> --yes"
restore_archive "$archive"
;;
help|--help|-help|-h)
usage
;;
*)
die "Unknown command: $command"
;;
esac
}
main "$@"
该脚本支持:
- 一致性备份;
- 列出备份;
- gzip 与 SHA-256 校验;
- 还原前自动创建
pre-restore备份; - 还原失败时尝试自动回滚;
- 保留服务原本的运行/停止状态。
上传脚本后先处理换行符
如果脚本从 Windows 上传到 Linux,请确保使用 LF 换行且无 BOM。可在服务器执行:
sed -i 's/\r$//' /root/picoclaw-backup.sh
chmod 700 /root/picoclaw-backup.sh
bash -n /root/picoclaw-backup.sh
使用示例
# 查看帮助
/root/picoclaw-backup.sh help
# 创建备份
/root/picoclaw-backup.sh backup manual
# 升级前备份
/root/picoclaw-backup.sh backup before-upgrade
# 列出备份
/root/picoclaw-backup.sh list
# 校验备份
/root/picoclaw-backup.sh verify \
/root/picoclaw-backups/picoclaw-YYYYMMDD-HHMMSS-manual.tar.gz
# 还原备份(必须显式确认)
/root/picoclaw-backup.sh restore \
/root/picoclaw-backups/picoclaw-YYYYMMDD-HHMMSS-manual.tar.gz \
--yes
[!CAUTION] 备份包含模型 API Key、微信登录凭证、会话和白名单。不要提交到 Git、不要上传到公开网盘、不要发送给他人。建议通过 Tailscale、ZeroTier 或 SSH 下载到本地加密保存。
十六、升级 PicoClaw 的建议
不要在低配生产 VPS 上自动追逐最新版本。推荐流程:
- 确认近期稳定运行;
- 创建
before-upgrade备份; - 下载官方 Release 预编译二进制;
- 先保留旧二进制;
- 替换二进制;
systemctl restart picoclaw.service;- 微信发送一条简单测试消息;
- 若失败,用备份脚本还原,或恢复旧二进制。
十七、上线验收清单
[ ] PicoClaw 以 picoclaw 普通用户运行
[ ] picoclaw 不属于 sudo 组
[ ] config.json 与 .security.yml 权限为 600
[ ] workspace 权限为 700
[ ] 微信 allow_from 只包含自己的 @im.wechat ID
[ ] Gateway 仅监听 127.0.0.1 / ::1
[ ] 没有开放 18790 公网端口
[ ] max_parallel_turns 为 1
[ ] restrict_to_workspace 为 true
[ ] C 模式前 allow_remote 为 false
[ ] C 模式后已用 pwd 验证工作区 Shell
[ ] cron list 没有任务
[ ] systemd MemoryHigh=400M、MemoryMax=480M、CPUQuota=95%
[ ] 已创建至少一份离线备份
[ ] API Key 与微信凭证未出现在博客、Git、截图或聊天记录中
结语
对于 1C1G5G 的 VPS,PicoClaw 的优势在于:单二进制、云端模型、原生微信 iLink、低常驻资源与清晰的 systemd 管理。只要坚持“单用户白名单 + 普通用户 + 工作区限制 + 无公网 Gateway + 有备份”的原则,它可以成为一个足够轻量且可长期维护的个人微信 Agent。