Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
data/
desktop-pet/
dist/
docs/
*.pyc
__pycache__/
.venv/
uv.lock
.env
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
135 changes: 135 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 <state> "<detail>"
# 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 |
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,可以跳过这一节。
Expand Down
23 changes: 13 additions & 10 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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"})
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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
Loading