Skip to content

Demo 站 VPS 部署手册 ​

把 Familiars 的在线演示站(demo.familiars.cn)部署到一台 CPU-only VPS 的完整手册。 演示站跑完整的 Electron 渲染 SPA 的 VITE_DEMO_MODE web 构建 + 只读的 CPU-only bridge(不加载本地模型),通过 nginx 同源 /api 反代到 bridge(proxy_buffering off 保 SSE 聊天流)。


架构 ​

浏览器 → Cloudflare → Caddy/nginx :443 (TLS)
 └── / → 静态 SPA(electron/dist,`VITE_DEMO_MODE=1` 构建)
 └── /api/* → 反代到 127.0.0.1:18711(bridge,FAMILIARS_HTTP_INSECURE=1)
 proxy_buffering off(SSE 聊天/状态流)
  • 演示 bridge 是独立进程,专用 WORKSPACE_ROUTE + FAMILIARS_DB_PATH(与生产/你本机完全隔离)。
  • 只开 FAMILIARS_APPROVAL_DISABLED=1 仅当需要跳过工具审批(演示默认开着审批,展示真实流程)。
  • 绝不暴露 POST /api/demo/token 给非演示环境 —— 该端点会返回当前 workspace 的 bridge token。

前置 ​

  • 一台 VPS(2C4G 即可;无需 GPU),Ubuntu 22.04/24.04。
  • 域名:demo.familiars.cn(加一条 A 记录到 VPS IP;若用 Cloudflare 则橙色云 + Caddy)。
  • 本机仓库里有 electron/dist(VITE_DEMO_MODE=1 构建)与 src/。

第 1 步:构建演示 SPA ​

在本机(仓库根):

bash
cd electron
VITE_DEMO_MODE=1 npm run build # Windows: set VITE_DEMO_MODE=1 && npm run build

产物:electron/dist/(纯静态 Vue SPA,base: './',无 electronAPI 时自动走 window.location.origin, 启动时 POST /api/demo/token 拿公共演示 token)。

第 2 步:准备服务器 ​

bash
# 系统依赖(sqlite 由 Python 内置,主要是 python3.11 + venv)
apt update && apt install -y python3.11 python3.11-venv nginx

创建目录并拷贝:

bash
mkdir -p /opt/familiars-demo/{app,spa,data}
# 从本机把 src/ 与 requirements.txt 复制过去
scp -r src/ root@<VPS>:/opt/familiars-demo/app/
scp requirements.txt root@<VPS>:/opt/familiars-demo/
scp -r electron/dist root@<VPS>:/opt/familiars-demo/spa/

第 3 步:装依赖 + 写环境 ​

bash
cd /opt/familiars-demo
python3.11 -m venv .venv
.venv/bin/pip install -r requirements.txt

.env(演示专用;LLM 端点按你熟悉的配):

bash
# LLM(演示也要用到 —— agent 的对话/生成都走这个 OpenAI 兼容端点)
BASE_URL=https://你的-llm-网关/v1
API_KEY=sk-xxxx
DEFAULT_MODEL=你的模型名
MANAGER_MODEL=你的模型名

# 演示隔离(关键)
FAMILIARS_DB_PATH=/opt/familiars-demo/data/demo.db
WORKSPACE_ROUTE=/opt/familiars-demo/data/workspace

# 桥接回退到明文 HTTP(nginx 在同一台机器上做 /api 反代,不需要 HTTPS 动机)
FAMILIARS_HTTP_INSECURE=1

# 每租户:官方桥把 WORKSPACE_ROUTE/FAMILIARS_DB_PATH 按用户 $HOME 分开即可
# (见本手册末尾“每租户隔离”)

WORKSPACE_ROUTE 必须与 src.base.config 的默认 workspace 布局兼容 —— 它会在此目录下生成 agents/、familiars.db(若未指定 FAMILIARS_DB_PATH)等。Demo 各用户分开则另起进程。

第 4 步:起 bridge(CPU-only) ​

bash
cd /opt/familiars-demo
WORKSPACE_ROUTE=/opt/familiars-demo/data/workspace \
FAMILIARS_DB_PATH=/opt/familiars-demo/data/demo.db \
FAMILIARS_HTTP_INSECURE=1 \
FAMILIARS_BROKER_BIND=0.0.0.0 \
FAMILIARS_APPROVAL_DISABLED=0 \
.venv/bin/python -m src.bridge.run

FAMILIARS_BROKER_BIND=0.0.0.0 必须设。bridge 默认只监听 127.0.0.1 (src/bridge/run.py:_bind_host),而 nginx/Caddy 是另一个容器,它只能 通过宿主机网桥(172.18.0.1 / host.docker.internal)回连 —— 若 bridge 绑在 loopback,反代必然 502。绑 0.0.0.0 后请务必确保防火墙 / 安全组不把 18711 暴露到公网 (POST /api/demo/token 会返回该 workspace 的 bridge token,公网可达等于 完全接管演示数据的权限)。

系统服务(systemd,/etc/systemd/system/familiars-demo.service):

ini
[Unit]
Description=Familiars Demo Bridge
After=network.target

[Service]
User=root
WorkingDirectory=/opt/familiars-demo
EnvironmentFile=/opt/familiars-demo/.env
Environment=FAMILIARS_BROKER_BIND=0.0.0.0
ExecStart=/opt/familiars-demo/.venv/bin/python -m src.bridge.run
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
bash
systemctl daemon-reload && systemctl enable --now familiars-demo
curl http://127.0.0.1:18711/api/demo/status # 期望: {"ok":true,...}
curl http://172.18.0.1:18711/api/demo/status # 容器侧验证(与 Caddy 反代同路径)

第 5 步:nginx 同源反代 + 静态站点 ​

/etc/nginx/sites-available/familiars-demo:

nginx
server {
 listen 80;
 server_name demo.familiars.cn;
 root /opt/familiars-demo/spa;

 # —— SPA ——
 location / {
 try_files $uri $uri/ /index.html;
 }

 # —— /api 反代到 bridge(SSE 必须关闭 buffering)——
 location /api/ {
 proxy_pass http://127.0.0.1:18711;
 proxy_http_version 1.1;
 proxy_buffering off;
 proxy_cache off;
 proxy_set_header Host $host;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_read_timeout 300s;
 proxy_send_timeout 300s;
 }

 client_max_body_size 50m;
}
bash
ln -s /etc/nginx/sites-available/familiars-demo /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

若套 Cloudflare 且想上 HTTPS,把 listen 80 加证书即可;或加一层 Caddy。核心不变: 同源 /api 反代 + proxy_buffering off。

验证(期望看到的直播状态) ​

浏览器打开 https://demo.familiars.cn/ 应看到 SPA:

  1. 顶部横幅提示“共享云端演示实例 · 只读沙箱”,第二行说明这里只有一个共享的云上 agent、 不提供 apps / 汇报 / 数字人 / 本机文件;
  2. 首屏 POST /api/demo/token → 拿到 demo_session_* token → X-Bridge-Token 装好;

演示 token 只在 /api/demo/* 与 /api/chat/* 放行,其余 /api/* 一律 401 —— 公共 token 拿不到 workspace 里任何其他数据(不会被当成 bridge 全局 token 用)。

  1. 给 agent 0 发一条消息 → SSE /api/chat/stream 逐字回复(观察 nginx 日志无 buffering 卡顿);
  2. 点一个写型工具(如 write/exec)→ 拿到的是一条明确的「云端演示沙箱:只读演示,不会真实写入」 的模拟返回,而不是卡死或报错。

会话隔离("一个共享工作区 + 独立 token") ​

演示采用 shared-workspace + per-session token 隔离,而不是每用户一进程:

  • 所有访问者连到同一个演示 bridge 进程、同一个只读工作区(WORKSPACE_ROUTE)。
  • 每人调用 /token 领到独立的 demo_session_* token(6 小时过期); 这个 token 只放行 /api/demo/*(状态/反馈)与 /api/chat/*(聊天 SSE)。
  • 聊天、待办都是真实后端在各自的沙箱工作区里跑 —— 每个访问者有独立的沙箱目录 (sessions/<账户>/workspace),文件写入互不干扰;记忆在演示中整体关闭;危险写型工具 被 gate 拦截,不会执行。

权衡:solo-agent 聊天用 DemoWorklogScope 是纯样本的;如果未来要「每个用户真有自己的记忆空间」, 就得回到每租户进程(独立 WORKSPACE_ROUTE + FAMILIARS_DB_PATH),把第 4 步包进 run-user.sh 按用户分开 —— 见下一节。

计费与并发(demo_seats 固定席位池) ​

演示桥会真实消耗云端 LLM 费用,因此有两道代码级限额(src/bridge/demo_seats.py 固定席位池 + src/bridge/demo_accounts.py 每账户额度):

限额默认值环境变量说明
每账户成本¥20 (2000 分)MAX_COST_CENTS_PER_ACCOUNT(代码常量)按浏览器指纹累计 LLM 成本,跨会话保留
并发席位15DEMO_MAX_SEATS同一时间最多活跃会话数,满席返回 503
席位闲置回收1800sDEMO_SEAT_IDLE_TTL闲置超时自动释放席位
每 IP 席位上限2DEMO_MAX_SEATS_PER_IP防一人独占全部席位

工作方式:

  • POST /api/demo/token 先过席位门(lease_seat):有席位才签发 token;满席 → 503 + Retry-After: 30; 每 IP 超限 → 429。
  • 每次 demo 请求由中间件 touch_seat(account_id) 保活,活跃会话不丢席位;touch 是 no-op, 不会为一个没有席位的请求新造席位。
  • 席位是内存态(_seats dict),bridge 重启即全部释放 —— 对并发池这是正确语义。
  • GET /api/demo/status 返回 seats: {total, leased, available, per_ip},前端状态卡据此显示 “余 N/15 席”。

客户端 IP 判定(XFF 防伪造) ​

demo.py:_client_ip 的信任链是 Cloudflare → Caddy → socat → bridge,因此判定顺序为:

  1. CF-Connecting-IP(Cloudflare 边缘写入,客户端无法伪造)→ 优先信任;
  2. 仅当 socket 对端是私有/回环地址(即我们自己的反代)才信任 X-Forwarded-For,且取最后一位 (反代追加的那一跳)—— 客户端直连时伪造 XFF 首位不会生效;
  3. 否则回退 socket 对端地址。

部署注意:X-Forwarded-For 只在私有代理对端下被信任。若你的反向代理不在本机 (socket 对端是公网),请改用 CF-Connecting-IP 或给 Caddy/nginx 配 trusted_proxies, 否则 per-IP 限流会被伪造 XFF 绕过。

验证满席 ​

bash
# 用 20 个不同指纹(≥32 字符)请求 token,前 15 个成功、第 16 个起 503 满席:
for i in $(seq 1 20); do
  FP="fp_test_$(printf '%030d' $i)"
  curl -s -X POST http://127.0.0.1:18711/api/demo/token \
    -H "Content-Type: application/json" \
    -d "{\"fingerprint\":\"$FP\"}" | head -c 200; echo
done
# 同一 IP 下第 3 个起返回 429 per_ip_cap(若 MAX_SEATS_PER_IP=2)
# 状态端点应显示余席:
curl -s http://127.0.0.1:18711/api/demo/status | python -m json.tool | grep -A 5 seats

若从 127.0.0.1 发起(socket 对端是回环=受信代理),可用 X-Forwarded-For: 10.$i.0.1 模拟不同 IP 验证全局 15 席上限。

每租户隔离(多用户版) ​

当前演示已经是 shared-workspace + demo token(见上节)—— 两个都想要"每个用户独立的 记忆/任务空间"时,才需要切到这下面的每进程模型。

想要每个演示用户一个独立数据库(避免互相看到对话/任务),把第 4 步的启动命令包进一个 /opt/familiars-demo/run-user.sh,按 $1(用户名)分开:

bash
#!/usr/bin/env bash
USER="$1"
DB="/opt/familiars-demo/data/users/${USER}/demo.db"
WS="/opt/familiars-demo/data/users/${USER}/workspace"
mkdir -p "$(dirname "$DB")" "$WS"
WORKSPACE_ROUTE="$WS" FAMILIARS_DB_PATH="$DB" FAMILIARS_HTTP_INSECURE=1 FAMILIARS_BROKER_BIND=0.0.0.0 \
 /opt/familiars-demo/.venv/bin/python -m src.bridge.run

然后为每个用户起一个 systemd instance 或再套一层进程管理(supervisord 的 program 组、@ service 模板均可)。每个进程独立端口,nginx 用不同 location/子路径(或子域名)分发。

演示数据引导(demo_bootstrap) ​

对话框真正要用的公会屋子需要在演示数据里预置。首次启动后可执行:

bash
WORKSPACE_ROUTE=/opt/familiars-demo/data/workspace FAMILIARS_HTTP_INSECURE=1 \
 FAMILIARS_DB_PATH=/opt/familiars-demo/data/demo.db \
 .venv/bin/python -c "
from src.bridge.deps import get_broker_hub
from src.broker.workshop import repos as wr
conn = get_broker_hub().conn
# 示例:为反馈/演示各建一屋(room_id 固定,`FAMILIAR` 屋作为公开反馈接收处)
wr.create_room(conn, broker_id='<broker_id>', name='Familiar', room_id='FAMILIAR-DEMO',
 created_by_cluster_id='demo')
print('rooms ok')
"

两个固定 room(Familiar=公开反馈/演示;feedback-official=仅官方 bot owner 看到)在部署文档里用 bot 钱包预置 ≥20 VTP(auto_reward_feedback 每任务奖励 20,余额不足会标 reward_pending,不阻断发布)。

运维速查 ​

操作命令
重启 bridgesystemctl restart familiars-demo
看日志journalctl -u familiars-demo -f
更新 SPA本机 VITE_DEMO_MODE=1 npm run build → scp -r electron/dist/* 到 /opt/familiars-demo/spa/
更新 bridge 代码scp -r src/ 到 /opt/familiars-demo/app/ → 重启服务
备份快照 /opt/familiars-demo/data/

相关 ​

Familiars · 多 Agent AI 桌面应用