diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..675bb3b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +data/ +desktop-pet/ +dist/ +docs/ +*.pyc +__pycache__/ +.venv/ +uv.lock +.env diff --git a/.env.example b/.env.example index bfb76da..58f8ed7 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,8 @@ ASSET_DRAWER_PASS=replace_with_strong_drawer_password # You can also set these in runtime-config.json via UI GEMINI_API_KEY= GEMINI_MODEL=nanobanana-pro + +# Docker deployment (only needed when using docker-compose) +# Absolute path to your memory directory on the host machine (for 昨日小记) +# Example: /Users/yourname/memory or /home/yourname/memory +MEMORY_HOST_PATH=/absolute/path/to/your/memory diff --git a/.gitignore b/.gitignore index dadad2d..806cc21 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ desktop-pet/src-tauri/icons/*Logo.png electron-shell/node_modules/ electron-shell/release/ join-keys.json + +# Docker persistent data volume (local only) +data/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..94225d1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,135 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Star Office UI is a pixel-art AI office dashboard that visualizes AI agent work states in real time. It supports multi-agent collaboration, trilingual UI (CN/EN/JP), AI-generated backgrounds via Gemini, and an optional Tauri desktop pet mode. + +## Commands + +### Start Backend +```bash +cd backend +python3 app.py +# Serves on http://127.0.0.1:19000 +``` + +### Install Backend Dependencies +```bash +python3 -m pip install -r backend/requirements.txt +# Dependencies: flask==3.0.2, pillow==10.4.0 +``` + +### Set Agent State (CLI) +```bash +python3 set_state.py "" +# States: idle | writing | researching | executing | syncing | error +``` + +### Smoke Test (non-destructive) +```bash +python3 scripts/smoke_test.py --base-url http://127.0.0.1:19000 +``` + +### Security Check +```bash +python3 scripts/security_check.py +``` + +### Docker (recommended for server deployment) +```bash +cp .env.example .env # fill in FLASK_SECRET_KEY, ASSET_DRAWER_PASS, MEMORY_HOST_PATH +docker compose up -d --build # first build and start +docker compose up -d # subsequent starts +docker compose logs -f # follow logs +``` + +Set agent state via API (replaces `python3 set_state.py`): +```bash +curl -X POST http://localhost:19000/set_state \ + -H "Content-Type: application/json" \ + -d '{"state": "writing", "detail": "正在写代码"}' +``` + +Persistent data is stored in `./data/` (Volume mount, gitignored). On first start, `state.json` and `join-keys.json` are auto-initialized from their `.sample.json` counterparts. + +### Desktop Pet (Tauri, optional) +```bash +cd desktop-pet +npm install +npm run dev +# Requires Tauri v2. Starts backend automatically. Points to http://127.0.0.1:19000/?desktop=1 +``` + +## Architecture + +### Backend (`backend/`) +Single Flask app (`app.py`) that serves both the API and the frontend static files. It is split into utility modules: + +- **`app.py`** — All route definitions, state machine logic, asset management endpoints, and background task registry (`_bg_tasks` dict) for async image generation. +- **`store_utils.py`** — JSON load/save helpers for all persistent data files: `state.json`, `agents-state.json`, `join-keys.json`, `asset-positions.json`, `asset-defaults.json`, `runtime-config.json`. +- **`memo_utils.py`** — Reads `../memory/*.md` (relative to project root's parent), extracts and sanitizes a daily work memo for the `/yesterday-memo` API endpoint. +- **`security_utils.py`** — Production-mode detection and validation of `FLASK_SECRET_KEY` and `ASSET_DRAWER_PASS`. + +**State machine:** Six valid agent states (`idle`, `writing`, `researching`, `executing`, `syncing`, `error`) map to three office areas. States in `WORKING_STATES` auto-revert to `idle` after `ttl_seconds` (default 300s) via `load_state()`. + +**Key JSON files** (all at project root): +- `state.json` — Main agent state (copy from `state.sample.json` to initialize) +- `agents-state.json` — Guest agent list (auto-created) +- `join-keys.json` — Guest join keys (auto-created from `join-keys.sample.json`) +- `asset-positions.json`, `asset-defaults.json` — Asset layout overrides +- `runtime-config.json` — Gemini API key and model (chmod 0o600) + +### Frontend (`frontend/`) +Vanilla JS + [Phaser 3](https://phaser.io/) game engine (loaded from `vendor/`). No build step required. + +- **`index.html`** — Main office view +- **`game.js`** — Phaser scene logic: asset loading, sprite animation, state polling, multi-agent rendering, memo card, sidebar, Gemini image generation UI +- **`layout.js`** — All coordinates, z-depths, sprite paths, and canvas dimensions (1280×720). This is the single source of truth for layout — change positions here, not in `game.js`. +- **`join.html` / `invite.html`** — Guest join flow pages + +**Asset format rule:** Transparent assets must use `.png`; non-transparent assets prefer `.webp`. The `getExt()` function in `game.js` applies WebP browser detection fallback. + +**Depth (z-order) convention:** +``` +sofa(10) → starWorking(900) → desk(1000) → flower(1100) +``` + +### Multi-Agent / Guest System +- Host creates join keys in `join-keys.json` (max concurrent agents per key is configurable) +- Guests run `office-agent-push.py` with `JOIN_KEY`, `AGENT_NAME`, and `OFFICE_URL` set +- Guest state uses the `/join-agent` → `/agent-push` → `/leave-agent` API flow +- Concurrent join requests are protected by `threading.Lock()` in `app.py` + +### Desktop Pet (`desktop-pet/`) +Tauri v2 wrapper. `main.js` spawns the Python backend as a child process using `STAR_PROJECT_ROOT` env var (defaults to `..`). `STAR_PYTHON_PATH` can override the Python interpreter. Window loads `electron-standalone.html` (a self-contained copy of the frontend) for offline use. + +## Environment Variables + +| Variable | Purpose | Default | +|----------|---------|---------| +| `FLASK_SECRET_KEY` / `STAR_OFFICE_SECRET` | Session signing | Dev placeholder (must change in prod) | +| `ASSET_DRAWER_PASS` | Sidebar lock password | `1234` (must change in prod) | +| `STAR_OFFICE_ENV` / `FLASK_ENV` | Set to `production` to enable hardening checks | — | +| `GEMINI_API_KEY` / `GOOGLE_API_KEY` | Gemini image generation | — | +| `GEMINI_MODEL` | Gemini model override | `nanobanana-pro` | +| `AUTO_ROTATE_HOME_ON_PAGE_OPEN` | Rotate background on each page load | `0` (off) | +| `STAR_OFFICE_STATE_FILE` | Override `state.json` path | `./state.json` | +| `STAR_OFFICE_DATA_DIR` | Directory for all runtime JSON files (Docker) | `ROOT_DIR` (project root) | +| `STAR_OFFICE_MEMORY_DIR` | Override memory directory path (Docker) | `../memory` relative to project root | + +## Key API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/health` | Health check | +| GET | `/status` | Main agent state | +| POST | `/set_state` | Update main agent state | +| GET | `/agents` | Guest agent list | +| POST | `/join-agent` | Guest joins office | +| POST | `/agent-push` | Guest pushes state update | +| POST | `/leave-agent` | Guest leaves | +| GET | `/yesterday-memo` | Sanitized daily memo | +| GET/POST | `/config/gemini` | Gemini API key/model config | +| GET | `/assets/generate-rpg-background/poll` | Poll async image generation | diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..63ed574 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install dependencies first (layer cache) +COPY backend/requirements.txt ./backend/ +RUN pip install --no-cache-dir -r backend/requirements.txt + +# Copy project files +COPY . . + +EXPOSE 19000 + +ENV STAR_OFFICE_DATA_DIR=/data +ENV STAR_OFFICE_MEMORY_DIR=/memory + +ENTRYPOINT ["sh", "docker-entrypoint.sh"] diff --git a/README.md b/README.md index c736e5c..2df4b99 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,57 @@ python3 scripts/smoke_test.py --base-url http://127.0.0.1:19000 --- +## 🐳 Docker 部署(可选) + +如果你希望用 Docker 运行,无需手动安装 Python 依赖,重启自动恢复。 + +### 1) 准备配置文件 + +```bash +cp .env.example .env +``` + +编辑 `.env`,至少填写 `MEMORY_HOST_PATH`(你本机 memory 目录的绝对路径): + +```bash +FLASK_SECRET_KEY=replace_with_a_long_random_secret +ASSET_DRAWER_PASS=replace_with_strong_drawer_password +MEMORY_HOST_PATH=/absolute/path/to/your/memory +``` + +### 2) 启动 + +```bash +docker compose up -d --build +``` + +打开 `http://127.0.0.1:19000` + +首次启动会自动从 `state.sample.json` 和 `join-keys.sample.json` 初始化数据文件,持久化到 `./data/` 目录。 + +### 3) 切换状态(API 方式) + +Docker 部署时,用 HTTP API 替代 `set_state.py`: + +```bash +curl -X POST http://127.0.0.1:19000/set_state \ + -H "Content-Type: application/json" \ + -d '{"state": "writing", "detail": "正在整理文档"}' +``` + +可用状态:`idle` / `writing` / `researching` / `executing` / `syncing` / `error` + +### 4) 常用命令 + +```bash +docker compose up -d # 启动(已构建过) +docker compose up -d --build # 重新构建并启动 +docker compose logs -f # 查看日志 +docker compose down # 停止 +``` + +--- + ## 🦞 OpenClaw 深度集成 > 以下内容面向 [OpenClaw](https://github.com/openclaw/openclaw) 用户。如果你不使用 OpenClaw,可以跳过这一节。 diff --git a/backend/app.py b/backend/app.py index 77599f4..9373dfa 100644 --- a/backend/app.py +++ b/backend/app.py @@ -35,13 +35,16 @@ # Paths (project-relative, no hardcoded absolute paths) ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -MEMORY_DIR = os.path.join(os.path.dirname(ROOT_DIR), "memory") +# DATA_DIR: override with STAR_OFFICE_DATA_DIR for Docker volume mounts; falls back to ROOT_DIR +DATA_DIR = os.getenv("STAR_OFFICE_DATA_DIR") or ROOT_DIR +# MEMORY_DIR: override with STAR_OFFICE_MEMORY_DIR for Docker volume mounts +MEMORY_DIR = os.getenv("STAR_OFFICE_MEMORY_DIR") or os.path.join(os.path.dirname(ROOT_DIR), "memory") FRONTEND_DIR = os.path.join(ROOT_DIR, "frontend") FRONTEND_INDEX_FILE = os.path.join(FRONTEND_DIR, "index.html") FRONTEND_ELECTRON_STANDALONE_FILE = os.path.join(FRONTEND_DIR, "electron-standalone.html") -STATE_FILE = os.path.join(ROOT_DIR, "state.json") -AGENTS_STATE_FILE = os.path.join(ROOT_DIR, "agents-state.json") -JOIN_KEYS_FILE = os.path.join(ROOT_DIR, "join-keys.json") +STATE_FILE = os.path.join(DATA_DIR, "state.json") +AGENTS_STATE_FILE = os.path.join(DATA_DIR, "agents-state.json") +JOIN_KEYS_FILE = os.path.join(DATA_DIR, "join-keys.json") FRONTEND_PATH = Path(FRONTEND_DIR) ASSET_ALLOWED_EXTS = {".png", ".webp", ".jpg", ".jpeg", ".gif", ".svg", ".avif"} ASSET_TEMPLATE_ZIP = os.path.join(ROOT_DIR, "assets-replace-template.zip") @@ -57,14 +60,14 @@ HOME_FAVORITES_DIR = os.path.join(ROOT_DIR, "assets", "home-favorites") HOME_FAVORITES_INDEX_FILE = os.path.join(HOME_FAVORITES_DIR, "index.json") HOME_FAVORITES_MAX = 30 -ASSET_POSITIONS_FILE = os.path.join(ROOT_DIR, "asset-positions.json") +ASSET_POSITIONS_FILE = os.path.join(DATA_DIR, "asset-positions.json") -# 性能保护:默认关闭“每次打开页面随机换背景”,避免首页首屏被磁盘复制拖慢 +# 性能保护:默认关闭"每次打开页面随机换背景",避免首页首屏被磁盘复制拖慢 AUTO_ROTATE_HOME_ON_PAGE_OPEN = (os.getenv("AUTO_ROTATE_HOME_ON_PAGE_OPEN", "0").strip().lower() in {"1", "true", "yes", "on"}) AUTO_ROTATE_MIN_INTERVAL_SECONDS = int(os.getenv("AUTO_ROTATE_MIN_INTERVAL_SECONDS", "60")) _last_home_rotate_at = 0 -ASSET_DEFAULTS_FILE = os.path.join(ROOT_DIR, "asset-defaults.json") -RUNTIME_CONFIG_FILE = os.path.join(ROOT_DIR, "runtime-config.json") +ASSET_DEFAULTS_FILE = os.path.join(DATA_DIR, "asset-defaults.json") +RUNTIME_CONFIG_FILE = os.path.join(DATA_DIR, "runtime-config.json") # Canonical agent states: single source of truth for validation and mapping VALID_AGENT_STATES = frozenset({"idle", "writing", "researching", "executing", "syncing", "error"}) @@ -976,7 +979,7 @@ def join_agent(): agents = load_agents_state() - # 并发上限:同一个 key “同时在线”最多 3 个。 + # 并发上限:同一个 key "同时在线"最多 3 个。 # 在线判定:lastPushAt/updated_at 在 5 分钟内;否则视为 offline,不计入并发。 now = datetime.now() existing = next((a for a in agents if a.get("name") == name and not a.get("isMain")), None) @@ -1923,7 +1926,7 @@ def assets_upload(): target.parent.mkdir(parents=True, exist_ok=True) - # 首次上传前固化默认资产快照,供“重置为默认资产”使用 + # 首次上传前固化默认资产快照,供"重置为默认资产"使用 default_snap = Path(str(target) + ".default") if not default_snap.exists(): try: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b41344f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + star-office: + build: . + ports: + - "19000:19000" + volumes: + - ./data:/data + - ${MEMORY_HOST_PATH:-../memory}:/memory:ro + environment: + STAR_OFFICE_DATA_DIR: /data + STAR_OFFICE_MEMORY_DIR: /memory + FLASK_SECRET_KEY: ${FLASK_SECRET_KEY:-star-office-dev-secret-change-me} + ASSET_DRAWER_PASS: ${ASSET_DRAWER_PASS:-1234} + restart: unless-stopped diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..e0c37f7 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +DATA_DIR="${STAR_OFFICE_DATA_DIR:-/data}" +mkdir -p "$DATA_DIR" + +# Initialize state files from samples on first run +[ -f "$DATA_DIR/state.json" ] || cp /app/state.sample.json "$DATA_DIR/state.json" +[ -f "$DATA_DIR/join-keys.json" ] || cp /app/join-keys.sample.json "$DATA_DIR/join-keys.json" + +exec python backend/app.py diff --git a/docs/plans/2026-03-09-docker-deployment-design.md b/docs/plans/2026-03-09-docker-deployment-design.md new file mode 100644 index 0000000..0233af5 --- /dev/null +++ b/docs/plans/2026-03-09-docker-deployment-design.md @@ -0,0 +1,163 @@ +# Docker 部署设计方案 + +**日期:** 2026-03-09 +**目标:** 本机个人服务器 Docker 部署,外部脚本通过 HTTP API 转换 Agent 状态 + +--- + +## 架构概览 + +单一容器 + docker-compose,代码打包进镜像,运行时数据通过 Volume 持久化。 + +``` +Host 本机 +├── Star-Office-UI/ +│ ├── data/ ← Volume 挂载点(持久化 JSON) +│ │ ├── state.json +│ │ ├── agents-state.json +│ │ ├── join-keys.json +│ │ └── runtime-config.json +│ └── docker-compose.yml +│ +├── memory/ ← 现有 memory 目录(只读挂载) +│ +└── Docker Container /app + ├── backend/ ← 代码打包进镜像(不可变) + ├── frontend/ ← 静态资源打包进镜像 + ├── /data → ./data/ ← 运行时数据(可写 Volume) + └── /memory → memory/ ← 昨日小记(只读 Volume) +``` + +--- + +## 需要修改的文件 + +### `backend/app.py`(仅改路径常量,8 行) + +新增 `DATA_DIR` 变量,支持通过 `STAR_OFFICE_DATA_DIR` 环境变量覆盖数据文件路径: + +```python +# 修改前 +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MEMORY_DIR = os.path.join(os.path.dirname(ROOT_DIR), "memory") +STATE_FILE = os.path.join(ROOT_DIR, "state.json") +AGENTS_STATE_FILE = os.path.join(ROOT_DIR, "agents-state.json") +JOIN_KEYS_FILE = os.path.join(ROOT_DIR, "join-keys.json") +ASSET_POSITIONS_FILE = os.path.join(ROOT_DIR, "asset-positions.json") +ASSET_DEFAULTS_FILE = os.path.join(ROOT_DIR, "asset-defaults.json") +RUNTIME_CONFIG_FILE = os.path.join(ROOT_DIR, "runtime-config.json") + +# 修改后 +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DATA_DIR = os.getenv("STAR_OFFICE_DATA_DIR") or ROOT_DIR +MEMORY_DIR = os.getenv("STAR_OFFICE_MEMORY_DIR") or os.path.join(os.path.dirname(ROOT_DIR), "memory") +STATE_FILE = os.path.join(DATA_DIR, "state.json") +AGENTS_STATE_FILE = os.path.join(DATA_DIR, "agents-state.json") +JOIN_KEYS_FILE = os.path.join(DATA_DIR, "join-keys.json") +ASSET_POSITIONS_FILE = os.path.join(DATA_DIR, "asset-positions.json") +ASSET_DEFAULTS_FILE = os.path.join(DATA_DIR, "asset-defaults.json") +RUNTIME_CONFIG_FILE = os.path.join(DATA_DIR, "runtime-config.json") +``` + +**向后兼容**:不传环境变量时回退到 `ROOT_DIR`,`python3 app.py` 直接运行行为不变。 + +--- + +## 新增文件 + +### `Dockerfile` + +```dockerfile +FROM python:3.11-slim +WORKDIR /app +COPY backend/requirements.txt ./backend/ +RUN pip install --no-cache-dir -r backend/requirements.txt +COPY . . +EXPOSE 19000 +ENV STAR_OFFICE_DATA_DIR=/data +ENV STAR_OFFICE_MEMORY_DIR=/memory +ENTRYPOINT ["sh", "docker-entrypoint.sh"] +``` + +### `docker-entrypoint.sh` + +首次启动自动从 sample 文件初始化数据目录: + +```bash +#!/bin/sh +mkdir -p "$STAR_OFFICE_DATA_DIR" +[ -f "$STAR_OFFICE_DATA_DIR/state.json" ] || cp /app/state.sample.json "$STAR_OFFICE_DATA_DIR/state.json" +[ -f "$STAR_OFFICE_DATA_DIR/join-keys.json" ] || cp /app/join-keys.sample.json "$STAR_OFFICE_DATA_DIR/join-keys.json" +exec python backend/app.py +``` + +### `docker-compose.yml` + +```yaml +services: + star-office: + build: . + ports: + - "19000:19000" + volumes: + - ./data:/data + - ${MEMORY_HOST_PATH:-../memory}:/memory:ro + environment: + STAR_OFFICE_DATA_DIR: /data + STAR_OFFICE_MEMORY_DIR: /memory + FLASK_SECRET_KEY: ${FLASK_SECRET_KEY:-star-office-dev-secret-change-me} + ASSET_DRAWER_PASS: ${ASSET_DRAWER_PASS:-1234} + restart: unless-stopped +``` + +### `.dockerignore` + +``` +data/ +desktop-pet/ +dist/ +docs/ +*.pyc +__pycache__/ +.venv/ +uv.lock +``` + +### `.env.example` + +```bash +FLASK_SECRET_KEY=your-strong-random-key-here +ASSET_DRAWER_PASS=your-password +MEMORY_HOST_PATH=/absolute/path/to/your/memory +``` + +--- + +## API 使用方式 + +启动容器后,通过 HTTP 调用替代原来的 `python3 set_state.py`: + +```bash +# 切换状态 +curl -X POST http://localhost:19000/set_state \ + -H "Content-Type: application/json" \ + -d '{"state": "writing", "detail": "正在写代码"}' + +# 查询当前状态 +curl http://localhost:19000/status +``` + +可用状态值:`idle` / `writing` / `researching` / `executing` / `syncing` / `error` + +--- + +## 改动清单 + +| 文件 | 动作 | 说明 | +|------|------|------| +| `backend/app.py` | 修改 | 添加 `DATA_DIR` 变量,8 行 | +| `Dockerfile` | 新建 | 镜像构建配置 | +| `docker-entrypoint.sh` | 新建 | 首次启动初始化逻辑 | +| `docker-compose.yml` | 新建 | 容器编排 + Volume + 环境变量 | +| `.dockerignore` | 新建 | 排除不必要文件 | +| `.env.example` | 新建 | 环境变量模板 |