Repository: ringhyacinth/Star-Office-UI Branch: master Commit: f29c107e9728 Files: 74 Total size: 1.1 MB Directory structure: gitextract_u7eo87gg/ ├── .gitignore ├── LICENSE ├── README.en.md ├── README.ja.md ├── README.md ├── SKILL.md ├── agent-invite-template.txt ├── asset-defaults.json ├── asset-positions.json ├── backend/ │ ├── app.py │ ├── memo_utils.py │ ├── requirements.txt │ ├── run.sh │ ├── security_utils.py │ └── store_utils.py ├── convert_to_webp.py ├── desktop-pet/ │ ├── README.md │ ├── STATE_API.md │ ├── package.json │ ├── src/ │ │ ├── index.html │ │ └── minimized.html │ └── src-tauri/ │ ├── Cargo.toml │ ├── build.rs │ ├── capabilities/ │ │ └── default.json │ ├── gen/ │ │ └── schemas/ │ │ ├── acl-manifests.json │ │ ├── capabilities.json │ │ ├── desktop-schema.json │ │ └── macOS-schema.json │ ├── icons/ │ │ ├── android/ │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ └── ic_launcher.xml │ │ │ └── values/ │ │ │ └── ic_launcher_background.xml │ │ └── icon.icns │ ├── src/ │ │ ├── lib.rs │ │ └── main.rs │ └── tauri.conf.json ├── dist/ │ └── Star-Office-UI-release-20260302/ │ └── RELEASE_NOTES.md ├── docs/ │ ├── CHANGELOG_2026-03.md │ ├── FEATURES_NEW_2026-03-01.md │ ├── OPEN_SOURCE_RELEASE_CHECKLIST.md │ ├── PROJECT_MAINTENANCE_SOP.md │ ├── PROJECT_SUMMARY_2026-03-01.md │ ├── PR_DRAFT_2026-03-refresh.md │ ├── PR_FILELIST_2026-03-refresh.md │ ├── STAR_OFFICE_UI_OVERVIEW.md │ ├── UPDATE_REPORT_2026-03-04_P0_P1.md │ └── UPDATE_REPORT_2026-03-05.md ├── electron-shell/ │ ├── README.md │ ├── main.js │ ├── package.json │ ├── preload.js │ └── standalone-assets/ │ ├── game.js │ └── layout.js ├── frontend/ │ ├── electron-standalone.html │ ├── fonts/ │ │ └── OFL.txt │ ├── game.js │ ├── index.html │ ├── invite.html │ ├── join-office-skill.md │ ├── join.html │ ├── layout.js │ └── office-agent-push.py ├── gif_to_spritesheet.py ├── healthcheck.sh ├── join-keys.sample.json ├── office-agent-push.py ├── pyproject.toml ├── repack_star_working.py ├── resize_map.py ├── runtime-config.sample.json ├── scripts/ │ ├── gemini_image_generate.py │ ├── security_check.py │ └── smoke_test.py ├── set_state.py ├── state.sample.json └── webp_to_spritesheet.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Python environment __pycache__/ *.py[cod] *.pyo *.pyd *.egg-info/ .venv/ venv/ .env # OS/editor .DS_Store # Runtime state (local only) state.json agents-state.json runtime-config.json *.log *.out *.pid *.backup* *.original cloudflared.pid cloudflared.out healthcheck.log backend.log # Generated / mutable assets (local only) assets/bg-history/ assets/home-favorites/ frontend/office_bg.png frontend/*.bak layers/ desktop-pet/src-tauri/icons/*Logo.png # Electron local build artifacts electron-shell/node_modules/ electron-shell/release/ join-keys.json ================================================ FILE: LICENSE ================================================ # Star Office UI — License & Usage Notice This project is a co-created work by **Ring Hyacinth** and **Simon Lee**. ## 1. Code / Logic License (MIT) The code/logic in this repository (the "Software") is licensed under the MIT License: MIT License Copyright (c) 2026 Ring Hyacinth & Simon Lee Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- ## 2. Art Assets License & Disclaimer ### Important: Art Assets are NOT for commercial use All art assets (including but not limited to character sprites, scene backgrounds, posters, furniture, plants, coffee machine, server room, animations, button skins, and rebuilt full asset packs/indexes) are **non-commercial only**. They are for **learning, demonstration, and idea sharing only**. You may NOT use any art assets from this repository for commercial purposes. If you want to use this project commercially, you **must replace all art assets with your own original work**. --- ## 3. Guest Character Asset Attribution Guest character animations use LimeZu’s free assets: - Animated Mini Characters 2 (Platformer) [FREE] - https://limezu.itch.io/animated-mini-characters-2-platform-free Please keep this attribution and follow the original author’s license terms when redistributing or demonstrating. ================================================ FILE: README.en.md ================================================ # Star Office UI 🌐 Language: [中文](./README.md) | **English** | [日本語](./README.ja.md) ![Star Office UI Cover](docs/screenshots/readme-cover-2.jpg) **A pixel-art AI office dashboard** — visualize your AI assistant's work status in real time, so you can see at a glance who's doing what, what they did yesterday, and whether they're online. Supports multi-agent collaboration, trilingual UI (CN/EN/JP), AI-powered room design, and desktop pet mode. Best experienced with [OpenClaw](https://github.com/openclaw/openclaw), but also works standalone as a status dashboard. > This project was co-created by **[Ring Hyacinth](https://x.com/ring_hyacinth)** and **[Simon Lee](https://x.com/simonxxoo)**, and is continuously maintained and improved together with community contributors ([@Zhaohan-Wang](https://github.com/Zhaohan-Wang), [@Jah-yee](https://github.com/Jah-yee), [@liaoandi](https://github.com/liaoandi)). > Issues and PRs are welcome — thank you to everyone who contributes. --- ## ✨ Quick Start ### Option 1: Let your lobster deploy it (recommended for OpenClaw users) If you're using [OpenClaw](https://github.com/openclaw/openclaw), just send this to your lobster: ```text Please follow this SKILL.md to deploy Star Office UI for me: https://github.com/ringhyacinth/Star-Office-UI/blob/master/SKILL.md ``` Your lobster will automatically clone the repo, install dependencies, start the backend, configure status sync, and send you the access URL. ### Option 2: 30-second manual setup > **Requires Python 3.10+** (the codebase uses `X | Y` union type syntax, which is not supported on 3.9 or earlier) ```bash # 1) Clone the repo git clone https://github.com/ringhyacinth/Star-Office-UI.git cd Star-Office-UI # 2) Install dependencies (Python 3.10+ required) python3 -m pip install -r backend/requirements.txt # 3) Initialize state file (first run) cp state.sample.json state.json # 4) Start the backend cd backend python3 app.py ``` Open **http://127.0.0.1:19000** and try switching states: ```bash python3 set_state.py writing "Organizing documents" python3 set_state.py error "Found an issue, debugging" python3 set_state.py idle "Standing by" ``` ![Star Office UI Preview](docs/screenshots/readme-cover-1.jpg) --- ## 🤔 Who is this for? ### Users with OpenClaw / an AI Agent This is the **full experience**. Your agent automatically switches status as it works, and the pixel character walks to the corresponding office area in real time — just open the page and see what your AI is doing right now. ### Users without OpenClaw You can still deploy and use it. You can: - Use `set_state.py` or the API to push status manually or via scripts - Use it as a pixel-art personal status page or remote work dashboard - Connect any system that can send HTTP requests to drive the status --- ## 📋 Features 1. **Status Visualization** — 6 states (`idle` / `writing` / `researching` / `executing` / `syncing` / `error`) mapped to different office areas with animated sprites and speech bubbles 2. **Yesterday Memo** — Automatically reads the latest daily log from `memory/*.md`, sanitizes it, and displays it as a "Yesterday Memo" card 3. **Multi-Agent Collaboration** — Invite other agents to join your office via join keys and see everyone's status in real time 4. **Trilingual UI** — Switch between Chinese, English, and Japanese with one click; all UI text, bubbles, and loading messages update instantly 5. **Custom Art Assets** — Manage characters, scenes, and decorations through the sidebar; dynamic frame sync prevents flickering 6. **AI-Powered Room Design** — Connect your own Gemini API to generate new office backgrounds; core features work fine without an API 7. **Mobile-Friendly** — Open on your phone for a quick status check on the go 8. **Security Hardening** — Sidebar password protection, weak-password blocking in production, hardened session cookies 9. **Flexible Public Access** — Use Cloudflare Tunnel for instant public access, or bring your own domain / reverse proxy 10. **Desktop Pet Mode** — Optional Electron desktop wrapper that turns the office into a transparent desktop widget (see below) --- ## 🚀 Detailed Setup Guide ### 1) Install dependencies ```bash cd Star-Office-UI python3 -m pip install -r backend/requirements.txt ``` ### 2) Initialize state file ```bash cp state.sample.json state.json ``` ### 3) Start the backend ```bash cd backend python3 app.py ``` Open `http://127.0.0.1:19000` > ✅ For local development you can start with the defaults; in production, copy `.env.example` to `.env` and set strong random values for `FLASK_SECRET_KEY` and `ASSET_DRAWER_PASS` to avoid weak passwords and session leaks. ### 4) Switch states ```bash python3 set_state.py writing "Organizing documents" python3 set_state.py syncing "Syncing progress" python3 set_state.py error "Found an issue, debugging" python3 set_state.py idle "Standing by" ``` ### 5) Public access (optional) ```bash cloudflared tunnel --url http://127.0.0.1:19000 ``` Share the `https://xxx.trycloudflare.com` link with anyone. ### 6) Verify your installation (optional) ```bash python3 scripts/smoke_test.py --base-url http://127.0.0.1:19000 ``` If all checks report `OK`, your deployment is good to go. --- ## 🦞 OpenClaw Deep Integration > The following section is for [OpenClaw](https://github.com/openclaw/openclaw) users. If you don't use OpenClaw, feel free to skip this. ### Automatic Status Sync Add the following rule to your `SOUL.md` (or agent config) so your agent updates its status automatically: ```markdown ## Star Office Status Sync Rules - When starting a task: run `python3 set_state.py ""` before beginning work - When finishing a task: run `python3 set_state.py idle "Standing by"` before replying ``` **6 states → 3 office areas:** | State | Office Area | When to use | |-------|-------------|-------------| | `idle` | 🛋 Breakroom (sofa) | Standing by / task complete | | `writing` | 💻 Workspace (desk) | Writing code or docs | | `researching` | 💻 Workspace | Searching / researching | | `executing` | 💻 Workspace | Running commands / tasks | | `syncing` | 💻 Workspace | Syncing data / pushing | | `error` | 🐛 Bug Corner | Error / debugging | ### Invite Other Agents to Your Office **Step 1: Prepare join keys** When you start the backend for the first time, if there is no `join-keys.json` in the project root, the service will automatically create one based on `join-keys.sample.json` (which contains an example key such as `ocj_example_team_01`). You can then edit the generated `join-keys.json` to add, modify, or remove keys; by default each key supports up to 3 concurrent users. **Step 2: Have the guest run the push script** The guest only needs to download `office-agent-push.py` and fill in 3 variables: ```python JOIN_KEY = "ocj_starteam02" # The key you assign AGENT_NAME = "Alice's Lobster" # Display name OFFICE_URL = "https://office.hyacinth.im" # Your office URL ``` ```bash python3 office-agent-push.py ``` The script auto-joins and pushes status every 15 seconds. The guest will appear on the dashboard, moving to the appropriate area based on their state. **Step 3 (optional): Guest installs a Skill** Guests can also use `frontend/join-office-skill.md` as a Skill — their agent will handle setup and pushing automatically. > See [`frontend/join-office-skill.md`](./frontend/join-office-skill.md) for full guest onboarding instructions. --- ## 📡 API Reference | Endpoint | Description | |----------|-------------| | `GET /health` | Health check | | `GET /status` | Get main agent status | | `POST /set_state` | Set main agent status | | `GET /agents` | List all agents | | `POST /join-agent` | Guest joins the office | | `POST /agent-push` | Guest pushes status | | `POST /leave-agent` | Guest leaves | | `GET /yesterday-memo` | Get yesterday's memo | | `GET /config/gemini` | Get Gemini API config | | `POST /config/gemini` | Set Gemini API config | | `GET /assets/generate-rpg-background/poll` | Poll image generation progress | --- ## 🖥 Desktop Pet Mode (Optional) The `desktop-pet/` directory contains a **Electron**-based desktop wrapper that turns the pixel office into a transparent desktop widget. ```bash cd desktop-pet npm install npm run dev ``` - Auto-launches the Python backend on startup - Window points to `http://127.0.0.1:19000/?desktop=1` by default - Customizable via environment variables for project path and Python path > ⚠️ This is an **optional, experimental feature**, primarily developed and tested on macOS. See [`desktop-pet/README.md`](./desktop-pet/README.md) for details. > > 🙏 The desktop pet module was independently developed by [@Zhaohan-Wang](https://github.com/Zhaohan-Wang) — thank you for this contribution! --- ## 🎨 Art Assets & License ### Asset Attribution Guest character animations use free assets by **LimeZu**: - [Animated Mini Characters 2 (Platformer) [FREE]](https://limezu.itch.io/animated-mini-characters-2-platform-free) Please keep attribution when redistributing or demoing, and follow the original license terms. ### License - **Code / Logic: MIT** (see [`LICENSE`](./LICENSE)) - **Art Assets: Non-commercial use only** (learning / demo / sharing) > For commercial use, replace all art assets with your own original artwork. --- ## 📝 Changelog | Date | Summary | Details | |------|---------|---------| | 2026-03-06 | 🔌 Default port updated — backend default port changed from 18791 to 19000 to avoid conflicts with OpenClaw Browser Control; synced scripts, desktop shells, and docs defaults | [`docs/CHANGELOG_2026-03.md`](./docs/CHANGELOG_2026-03.md) | | 2026-03-05 | 📱 Stability fixes — CDN cache fix, async image generation, mobile sidebar UX, join key expiration & concurrency | [`docs/UPDATE_REPORT_2026-03-05.md`](./docs/UPDATE_REPORT_2026-03-05.md) | | 2026-03-04 | 🔒 P0/P1 Security hardening — weak password blocking, backend refactor, stale-state auto-idle, skeleton loading | [`docs/UPDATE_REPORT_2026-03-04_P0_P1.md`](./docs/UPDATE_REPORT_2026-03-04_P0_P1.md) | | 2026-03-03 | 📋 Open-source release checklist completed | [`docs/OPEN_SOURCE_RELEASE_CHECKLIST.md`](./docs/OPEN_SOURCE_RELEASE_CHECKLIST.md) | | 2026-03-01 | 🎉 **v2 Rebuild** — Trilingual support, asset management system, AI room design, full art asset overhaul | [`docs/FEATURES_NEW_2026-03-01.md`](./docs/FEATURES_NEW_2026-03-01.md) | --- ## 📁 Project Structure ```text Star-Office-UI/ ├── backend/ # Flask backend │ ├── app.py │ ├── requirements.txt │ └── run.sh ├── frontend/ # Frontend pages & assets │ ├── index.html │ ├── join.html │ ├── invite.html │ └── layout.js ├── desktop-pet/ # Electron desktop wrapper (optional) ├── docs/ # Documentation & screenshots │ └── screenshots/ ├── office-agent-push.py # Guest push script ├── set_state.py # Status switch script ├── state.sample.json # State file template ├── join-keys.sample.json # Join key template (runtime generates join-keys.json) ├── SKILL.md # OpenClaw Skill └── LICENSE # MIT License ``` --- ## ⭐ Star History [![Star History Chart](https://api.star-history.com/image?repos=ringhyacinth/Star-Office-UI&type=date&legend=top-left)](https://www.star-history.com/?repos=ringhyacinth%2FStar-Office-UI&type=date&legend=top-left) ================================================ FILE: README.ja.md ================================================ # Star Office UI 🌐 Language: [中文](./README.md) | [English](./README.en.md) | **日本語** ![Star Office UI カバー](docs/screenshots/readme-cover-2.jpg) **ピクセルアート風 AI オフィスダッシュボード** —— AI アシスタントの作業状態をリアルタイムで可視化し、「誰が何をしているか」「昨日何をしたか」「今オンラインか」を直感的に把握できます。 マルチ Agent 協調、中英日 3 言語、AI 画像生成による模様替え、デスクトップペットモードに対応。 [OpenClaw](https://github.com/openclaw/openclaw) との統合で最高の体験が得られますが、単体でもステータスダッシュボードとして利用可能です。 > 本プロジェクトは **[Ring Hyacinth](https://x.com/ring_hyacinth)** と **[Simon Lee](https://x.com/simonxxoo)** の共同制作(co-created project)であり、コミュニティの開発者([@Zhaohan-Wang](https://github.com/Zhaohan-Wang)、[@Jah-yee](https://github.com/Jah-yee)、[@liaoandi](https://github.com/liaoandi))とともに継続的にメンテナンス・改善を行っています。 > Issue や PR を歓迎します。貢献してくださるすべての方に感謝いたします。 --- ## ✨ クイックスタート ### 方法 1:ロブスターにデプロイしてもらう(OpenClaw ユーザー向け) [OpenClaw](https://github.com/openclaw/openclaw) をご利用中なら、以下のメッセージをロブスターに送るだけ: ```text この SKILL.md に従って Star Office UI をデプロイしてください: https://github.com/ringhyacinth/Star-Office-UI/blob/master/SKILL.md ``` ロブスターが自動的にリポジトリのクローン、依存関係のインストール、バックエンドの起動、ステータス同期の設定を行い、アクセス URL をお知らせします。 ### 方法 2:30 秒手動セットアップ > **Python 3.10+ が必要です**(コードベースは `X | Y` ユニオン型構文を使用しており、3.9 以前のバージョンではサポートされていません) ```bash # 1) リポジトリをクローン git clone https://github.com/ringhyacinth/Star-Office-UI.git cd Star-Office-UI # 2) 依存関係をインストール(Python 3.10+ が必要) python3 -m pip install -r backend/requirements.txt # 3) 状態ファイルを初期化(初回のみ) cp state.sample.json state.json # 4) バックエンドを起動 cd backend python3 app.py ``` **http://127.0.0.1:19000** を開き、状態を切り替えてみましょう: ```bash python3 set_state.py writing "ドキュメント整理中" python3 set_state.py error "問題を検出、調査中" python3 set_state.py idle "待機中" ``` ![Star Office UI プレビュー](docs/screenshots/readme-cover-1.jpg) --- ## 🤔 誰に向いている? ### OpenClaw / AI Agent をお持ちの方 これが**フル体験**です。Agent が作業中に自動でステータスを切り替え、ピクセルキャラクターがリアルタイムで対応エリアに移動します。ページを開くだけで、AI が今何をしているかがわかります。 ### OpenClaw をお持ちでない方 デプロイして使うことも全く問題ありません: - `set_state.py` や API で手動 / スクリプトからステータスを更新 - ピクセルアート風の個人ステータスページやリモートワークダッシュボードとして利用 - HTTP リクエストを送れるシステムなら何でもステータスを駆動可能 --- ## 📋 機能一覧 1. **ステータス可視化** —— 6 種類の状態(`idle` / `writing` / `researching` / `executing` / `syncing` / `error`)がオフィスの各エリアに自動マッピングされ、アニメーションと吹き出しでリアルタイム表示 2. **昨日メモ** —— `memory/*.md` から直近の作業記録を自動取得し、匿名化して「昨日メモ」カードとして表示 3. **マルチ Agent 協調** —— join key で他の Agent をオフィスに招待し、全員のステータスをリアルタイム確認 4. **中英日 3 言語対応** —— CN / EN / JP をワンクリック切替、UI テキスト・吹き出し・ローディング表示すべてが連動 5. **アート資産カスタマイズ** —— サイドバーからキャラクター / 背景 / 装飾素材を管理、動的フレーム同期でちらつき防止 6. **AI 画像生成による模様替え** —— Gemini API を接続してオフィス背景を AI 生成; API 未接続でもコア機能は利用可能 7. **モバイル対応** —— スマホからそのまま閲覧可能、外出先からのクイックチェックに最適 8. **セキュリティ強化** —— サイドバーのパスワード保護、本番環境での弱パスワード拒否、Session Cookie 強化 9. **柔軟な公開アクセス** —— Cloudflare Tunnel でワンステップ公開、独自ドメイン / リバースプロキシにも対応 10. **デスクトップペット版** —— オプションの Electron デスクトップラッパーで、オフィスを透明ウィンドウのデスクトップペットに(下記参照) --- ## 🚀 詳細セットアップガイド ### 1) 依存関係インストール ```bash cd Star-Office-UI python3 -m pip install -r backend/requirements.txt ``` ### 2) 状態ファイル初期化 ```bash cp state.sample.json state.json ``` ### 3) バックエンド起動 ```bash cd backend python3 app.py ``` `http://127.0.0.1:19000` を開く > ✅ ローカル開発ではデフォルト設定のままで構いませんが、本番環境では `.env.example` を `.env` にコピーし、`FLASK_SECRET_KEY` と `ASSET_DRAWER_PASS` に十分な長さのランダム値を設定してください。 ### 4) ステータス切替 ```bash python3 set_state.py writing "ドキュメント整理中" python3 set_state.py syncing "進捗同期中" python3 set_state.py error "問題を検出、調査中" python3 set_state.py idle "待機中" ``` ### 5) 公開アクセス(任意) ```bash cloudflared tunnel --url http://127.0.0.1:19000 ``` `https://xxx.trycloudflare.com` のリンクを共有するだけで OK。 ### 6) インストール確認(任意) ```bash python3 scripts/smoke_test.py --base-url http://127.0.0.1:19000 ``` すべてのチェックが `OK` と表示されればデプロイ成功です。 --- ## 🦞 OpenClaw 連携 > 以下は [OpenClaw](https://github.com/openclaw/openclaw) ユーザー向けの内容です。OpenClaw を使用していない場合はスキップしてください。 ### ステータス自動同期 `SOUL.md`(またはエージェント設定ファイル)に以下のルールを追加すると、Agent がステータスを自動で更新します: ```markdown ## Star Office ステータス同期ルール - タスク開始時:`python3 set_state.py <状態> "<説明>"` を実行してから作業開始 - タスク完了時:`python3 set_state.py idle "待機中"` を実行してから返答 ``` **6 種類のステータス → 3 つのエリア:** | ステータス | オフィスエリア | 使用場面 | |-----------|--------------|---------| | `idle` | 🛋 休憩エリア(ソファ) | 待機 / タスク完了 | | `writing` | 💻 ワークエリア(デスク) | コーディング / ドキュメント作成 | | `researching` | 💻 ワークエリア | 検索 / リサーチ | | `executing` | 💻 ワークエリア | コマンド実行 / タスク処理 | | `syncing` | 💻 ワークエリア | データ同期 / プッシュ | | `error` | 🐛 バグコーナー | エラー / デバッグ | ### 他の Agent をオフィスに招待 **Step 1:join key を準備** バックエンドを初回起動するとき、カレントディレクトリに `join-keys.json` が存在しない場合は、`join-keys.sample.json` を元にランタイム用の `join-keys.json` が自動生成されます(例として `ocj_example_team_01` などのサンプル key が含まれます)。生成された `join-keys.json` を編集して key を追加・変更・削除できます。各 key はデフォルトで最大 3 名まで同時接続できます。 **Step 2:ゲストにプッシュスクリプトを実行してもらう** ゲストは `office-agent-push.py` をダウンロードし、3 つの変数を入力するだけ: ```python JOIN_KEY = "ocj_starteam02" # あなたが割り当てたキー AGENT_NAME = "太郎のロブスター" # 表示名 OFFICE_URL = "https://office.hyacinth.im" # あなたのオフィス URL ``` ```bash python3 office-agent-push.py ``` スクリプトが自動で参加し、15 秒ごとにステータスをプッシュします。ゲストがダッシュボードに表示され、状態に応じて該当エリアに移動します。 **Step 3(任意):ゲストも Skill をインストール** ゲストは `frontend/join-office-skill.md` を Skill として使うこともできます。Agent が設定とプッシュを自動で行います。 > 詳しいゲスト参加手順は [`frontend/join-office-skill.md`](./frontend/join-office-skill.md) を参照。 --- ## 📡 API リファレンス | エンドポイント | 説明 | |--------------|------| | `GET /health` | ヘルスチェック | | `GET /status` | メイン Agent のステータス取得 | | `POST /set_state` | メイン Agent のステータス設定 | | `GET /agents` | 全 Agent リスト取得 | | `POST /join-agent` | ゲスト参加 | | `POST /agent-push` | ゲストステータスプッシュ | | `POST /leave-agent` | ゲスト退出 | | `GET /yesterday-memo` | 昨日メモ取得 | | `GET /config/gemini` | Gemini API 設定取得 | | `POST /config/gemini` | Gemini API 設定変更 | | `GET /assets/generate-rpg-background/poll` | 画像生成の進捗確認 | --- ## 🖥 デスクトップペット版(任意) `desktop-pet/` ディレクトリには **Electron** ベースのデスクトップラッパーが含まれており、ピクセルオフィスを透明ウィンドウのデスクトップペットにできます。 ```bash cd desktop-pet npm install npm run dev ``` - 起動時に Python バックエンドを自動起動 - デフォルトで `http://127.0.0.1:19000/?desktop=1` を表示 - 環境変数でプロジェクトパスや Python パスをカスタマイズ可能 > ⚠️ これは**オプションの実験的機能**であり、現在は主に macOS で開発・テストされています。詳細は [`desktop-pet/README.md`](./desktop-pet/README.md) を参照。 > > 🙏 デスクトップペット版は [@Zhaohan-Wang](https://github.com/Zhaohan-Wang) が独自に開発しました。貢献に感謝します! --- ## 🎨 アート資産とライセンス ### 資産の出典 ゲストキャラクターのアニメーションには **LimeZu** のフリー素材を使用しています: - [Animated Mini Characters 2 (Platformer) [FREE]](https://limezu.itch.io/animated-mini-characters-2-platform-free) 再配布やデモの際は出典を明記し、原作者のライセンス条項に従ってください。 ### ライセンス - **コード / ロジック:MIT**([`LICENSE`](./LICENSE) を参照) - **アート資産:非商用のみ**(学習 / デモ / 共有用途) > 商用利用の場合は、すべてのアート資産をオリジナル素材に差し替えてください。 --- ## 📝 更新履歴 | 日付 | 概要 | 詳細 | |------|------|------| | 2026-03-06 | 🔌 デフォルトポート変更 — OpenClaw Browser Control との競合を避けるため、バックエンドの既定ポートを 18791 から 19000 に変更。スクリプト、デスクトップシェル、ドキュメントの既定値も同期更新 | [`docs/CHANGELOG_2026-03.md`](./docs/CHANGELOG_2026-03.md) | | 2026-03-05 | 📱 安定性修正 — CDN キャッシュ修正、画像生成非同期化、モバイルサイドバー UX 改善、join key 有効期限・同時接続制御 | [`docs/UPDATE_REPORT_2026-03-05.md`](./docs/UPDATE_REPORT_2026-03-05.md) | | 2026-03-04 | 🔒 P0/P1 セキュリティ強化 — 弱パスワード拒否、バックエンド分割、stale ステータス自動 idle 復帰、スケルトンローディング | [`docs/UPDATE_REPORT_2026-03-04_P0_P1.md`](./docs/UPDATE_REPORT_2026-03-04_P0_P1.md) | | 2026-03-03 | 📋 オープンソース公開チェックリスト完了 | [`docs/OPEN_SOURCE_RELEASE_CHECKLIST.md`](./docs/OPEN_SOURCE_RELEASE_CHECKLIST.md) | | 2026-03-01 | 🎉 **v2 リビルド公開** — 3 言語対応、資産管理システム、AI 画像生成による模様替え、アート資産全面刷新 | [`docs/FEATURES_NEW_2026-03-01.md`](./docs/FEATURES_NEW_2026-03-01.md) | --- ## 📁 プロジェクト構成 ```text Star-Office-UI/ ├── backend/ # Flask バックエンド │ ├── app.py │ ├── requirements.txt │ └── run.sh ├── frontend/ # フロントエンドページ & 資産 │ ├── index.html │ ├── join.html │ ├── invite.html │ └── layout.js ├── desktop-pet/ # Electron デスクトップラッパー(任意) ├── docs/ # ドキュメント & スクリーンショット │ └── screenshots/ ├── office-agent-push.py # ゲストプッシュスクリプト ├── set_state.py # ステータス切替スクリプト ├── state.sample.json # 状態ファイルテンプレート ├── join-keys.sample.json # Join Key テンプレート(起動時に join-keys.json を生成) ├── SKILL.md # OpenClaw Skill └── LICENSE # MIT ライセンス ``` --- ## ⭐ Star History [![Star History Chart](https://api.star-history.com/image?repos=ringhyacinth/Star-Office-UI&type=date&legend=top-left)](https://www.star-history.com/?repos=ringhyacinth%2FStar-Office-UI&type=date&legend=top-left) ================================================ FILE: README.md ================================================ # Star Office UI 🌐 Language: **中文** | [English](./README.en.md) | [日本語](./README.ja.md) ![Star Office UI 封面](docs/screenshots/readme-cover-2.jpg) **一个像素风格的 AI 办公室看板** —— 把 AI 助手的工作状态实时可视化,让你直观看到"谁在做什么、昨天做了什么、现在是否在线"。 支持多 Agent 协作、中英日三语、AI 生图装修、桌面宠物模式。 与 [OpenClaw](https://github.com/openclaw/openclaw) 深度集成时体验最佳,也可以独立部署作为状态看板使用。 > 本项目由 **[Ring Hyacinth](https://x.com/ring_hyacinth)** 与 **[Simon Lee](https://x.com/simonxxoo)** 共同创建(co-created project),并与社区开发者([@Zhaohan-Wang](https://github.com/Zhaohan-Wang)、[@Jah-yee](https://github.com/Jah-yee)、[@liaoandi](https://github.com/liaoandi))一起持续维护和共建。 > 欢迎提交 Issue 和 PR,也感谢每一位贡献者的支持。 --- ## ✨ 快速体验 ### 方式一:让龙虾帮你部署(推荐给 OpenClaw 用户) 如果你正在使用 [OpenClaw](https://github.com/openclaw/openclaw),直接把下面这句话发给你的龙虾: ```text 请按照这个 SKILL.md 帮我完成 Star Office UI 的部署: https://github.com/ringhyacinth/Star-Office-UI/blob/master/SKILL.md ``` 龙虾会自动完成 clone、安装依赖、启动后端、配置状态同步,并把访问地址发给你。 ### 方式二:30 秒手动部署 > **环境要求:Python 3.10+**(代码使用了 `X | Y` union type 语法,不支持 3.9 及更低版本) ```bash # 1) 下载仓库 git clone https://github.com/ringhyacinth/Star-Office-UI.git cd Star-Office-UI # 2) 安装依赖(需要 Python 3.10+) python3 -m pip install -r backend/requirements.txt # 3) 准备状态文件(首次) cp state.sample.json state.json # 4) 启动后端 cd backend python3 app.py ``` 打开 **http://127.0.0.1:19000** 然后试试切状态: ```bash python3 set_state.py writing "正在整理文档" python3 set_state.py error "发现问题,排查中" python3 set_state.py idle "待命中" ``` ![Star Office UI 预览](docs/screenshots/readme-cover-1.jpg) --- ## 🤔 适合谁用? ### 有 OpenClaw / AI Agent 的用户 这是**完整体验**。Agent 在工作时自动切换状态,办公室里的像素角色会实时走到对应区域——你只需要打开网页,就能看到 AI 此刻在做什么。 ### 没有 OpenClaw 的用户 也完全可以部署。你可以: - 用 `set_state.py` 或 API 手动 / 脚本推送状态 - 把它当成一个像素风的个人状态页 / 远程办公看板 - 接入任何能发 HTTP 请求的系统来驱动状态 --- ## 📋 功能一览 1. **状态可视化** —— 6 种状态(`idle` / `writing` / `researching` / `executing` / `syncing` / `error`)自动映射到办公室不同区域,动画 + 气泡实时展示 2. **昨日小记** —— 自动从 `memory/*.md` 读取最近一天的工作记录,脱敏后展示为"昨日小记"卡片 3. **多 Agent 协作** —— 通过 join key 邀请其他 Agent 加入你的办公室,实时查看多人状态 4. **中英日三语** —— CN / EN / JP 一键切换,界面文案、气泡、加载提示全部联动 5. **美术资产自定义** —— 侧边栏管理角色 / 场景 / 装饰素材,支持动态帧同步,避免闪烁 6. **AI 生图装修** —— 接入 Gemini API,用 AI 给办公室换背景;不接入 API 也能正常使用核心功能 7. **移动端适配** —— 手机直接打开即可查看,适合外出时快速瞄一眼 8. **安全加固** —— 侧边栏密码保护、生产环境弱密码拦截、Session Cookie 加固 9. **灵活公网访问** —— 推荐 Cloudflare Tunnel 一键公网化,也可用自有域名 / 反向代理 10. **桌面宠物版** —— 可选的 Electron 桌面封装,把办公室变成透明窗口的桌面宠物(见下方说明) --- ## 🚀 详细部署指南 ### 1) 安装依赖 ```bash cd Star-Office-UI python3 -m pip install -r backend/requirements.txt ``` ### 2) 初始化状态文件 ```bash cp state.sample.json state.json ``` ### 3) 启动后端 ```bash cd backend python3 app.py ``` 打开 `http://127.0.0.1:19000` > ✅ 首次部署可以先保留默认配置;在生产环境中,请复制 `.env.example` 为 `.env` 并设置强随机的 `FLASK_SECRET_KEY` 与 `ASSET_DRAWER_PASS`,避免弱密码和会话泄露。 ### 4) 切换状态 ```bash python3 set_state.py writing "正在整理文档" python3 set_state.py syncing "同步进度中" python3 set_state.py error "发现问题,排查中" python3 set_state.py idle "待命中" ``` ### 5) 公网访问(可选) ```bash cloudflared tunnel --url http://127.0.0.1:19000 ``` 拿到 `https://xxx.trycloudflare.com` 链接即可分享。 ### 6) 验证安装(可选) ```bash python3 scripts/smoke_test.py --base-url http://127.0.0.1:19000 ``` 所有检查显示 `OK` 即表示部署成功。 --- ## 🦞 OpenClaw 深度集成 > 以下内容面向 [OpenClaw](https://github.com/openclaw/openclaw) 用户。如果你不使用 OpenClaw,可以跳过这一节。 ### 状态自动同步 在你的 `SOUL.md`(或 Agent 规则文件)中加入以下规则,让 Agent 自觉维护状态: ```markdown ## Star Office 状态同步规则 - 接到任务时:先执行 `python3 set_state.py <状态> "<描述>"` 再开始工作 - 完成任务后:执行 `python3 set_state.py idle "待命中"` 再回复 ``` **6 种状态 → 3 个区域的映射:** | 状态 | 办公室区域 | 触发场景 | |------|-----------|---------| | `idle` | 🛋 休息区(沙发) | 待命 / 任务完成 | | `writing` | 💻 工作区(办公桌) | 写代码 / 写文档 | | `researching` | 💻 工作区 | 搜索 / 调研 | | `executing` | 💻 工作区 | 执行命令 / 跑任务 | | `syncing` | 💻 工作区 | 同步数据 / 推送 | | `error` | 🐛 Bug 区 | 报错 / 异常排查 | ### 邀请其他 Agent 加入办公室 **Step 1:准备 join key** 首次启动后端时,如果当前目录下不存在 `join-keys.json`,服务会自动根据 `join-keys.sample.json` 生成一个运行时的 `join-keys.json`(内含示例 key,例如 `ocj_example_team_01`)。你可以在生成后的 `join-keys.json` 中自行添加、修改或删除 key,每个 key 默认支持最多 3 人同时在线。 **Step 2:让访客 Agent 运行推送脚本** 访客只需下载 `office-agent-push.py`,填写 3 个变量即可: ```python JOIN_KEY = "ocj_starteam02" # 你分配的 key AGENT_NAME = "小明的龙虾" # 显示名称 OFFICE_URL = "https://office.hyacinth.im" # 你的办公室地址 ``` ```bash python3 office-agent-push.py ``` 脚本会自动加入办公室并每 15 秒推送一次状态。访客会出现在看板上,根据状态自动走到对应区域。 **Step 3(可选):访客安装 Skill** 访客也可以把 `frontend/join-office-skill.md` 作为 Skill 使用,Agent 会自动完成配置和推送。 > 详细的访客接入说明见 [`frontend/join-office-skill.md`](./frontend/join-office-skill.md) --- ## 📡 常用 API | 端点 | 说明 | |------|------| | `GET /health` | 健康检查 | | `GET /status` | 获取主 Agent 状态 | | `POST /set_state` | 设置主 Agent 状态 | | `GET /agents` | 获取多 Agent 列表 | | `POST /join-agent` | 访客加入办公室 | | `POST /agent-push` | 访客推送状态 | | `POST /leave-agent` | 访客离开 | | `GET /yesterday-memo` | 获取昨日小记 | | `GET /config/gemini` | 获取 Gemini API 配置 | | `POST /config/gemini` | 设置 Gemini API 配置 | | `GET /assets/generate-rpg-background/poll` | 轮询生图进度 | --- ## 🖥 桌面宠物版(可选) `desktop-pet/` 目录提供了一个基于 **Electron** 的桌面封装版本,可以把像素办公室变成一个透明窗口的桌面宠物。 ```bash cd desktop-pet npm install npm run dev ``` - 启动时自动拉起 Python 后端 - 窗口默认指向 `http://127.0.0.1:19000/?desktop=1` - 支持通过环境变量自定义项目路径和 Python 路径 > ⚠️ 这是一个**可选的实验性功能**,目前主要在 macOS 上开发测试。详见 [`desktop-pet/README.md`](./desktop-pet/README.md)。 > > 🙏 桌面宠物版由 [@Zhaohan-Wang](https://github.com/Zhaohan-Wang) 独立开发,感谢他的贡献! --- ## 🎨 美术资产与开源许可 ### 资产来源 访客角色动画使用了 **LimeZu** 的免费资产: - [Animated Mini Characters 2 (Platformer) [FREE]](https://limezu.itch.io/animated-mini-characters-2-platform-free) 请在二次发布 / 演示时保留来源说明,并遵守原作者许可条款。 ### 许可协议 - **代码 / 逻辑:MIT**(见 [`LICENSE`](./LICENSE)) - **美术资产:禁止商用**(仅学习 / 演示 / 交流用途) > 如需商用,请将所有美术资产替换为你自己的原创素材。 --- ## 📝 更新日志 | 日期 | 概要 | 详情 | |------|------|------| | 2026-03-06 | 🔌 默认端口调整 — 默认后端端口从 18791 调整为 19000,以避开 OpenClaw Browser Control 端口冲突;同步更新脚本、桌面壳与文档默认值 | [`docs/CHANGELOG_2026-03.md`](./docs/CHANGELOG_2026-03.md) | | 2026-03-05 | 📱 稳定性修复 — CDN 缓存修复、生图异步化、移动端侧边栏优化、Join Key 过期与并发控制 | [`docs/UPDATE_REPORT_2026-03-05.md`](./docs/UPDATE_REPORT_2026-03-05.md) | | 2026-03-04 | 🔒 P0/P1 安全加固 — 弱密码拦截、后端模块拆分、stale 状态自动回 idle、首屏骨架屏优化 | [`docs/UPDATE_REPORT_2026-03-04_P0_P1.md`](./docs/UPDATE_REPORT_2026-03-04_P0_P1.md) | | 2026-03-03 | 📋 开源发布检查清单完成 | [`docs/OPEN_SOURCE_RELEASE_CHECKLIST.md`](./docs/OPEN_SOURCE_RELEASE_CHECKLIST.md) | | 2026-03-01 | 🎉 **v2 重制发布** — 新增三语支持、资产管理系统、AI 生图装修、美术资产全面替换 | [`docs/FEATURES_NEW_2026-03-01.md`](./docs/FEATURES_NEW_2026-03-01.md) | --- ## 📁 项目结构 ```text Star-Office-UI/ ├── backend/ # Flask 后端 │ ├── app.py │ ├── requirements.txt │ └── run.sh ├── frontend/ # 前端页面与资产 │ ├── index.html │ ├── join.html │ ├── invite.html │ └── layout.js ├── desktop-pet/ # Electron 桌面宠物版(可选) ├── docs/ # 文档与截图 │ └── screenshots/ ├── office-agent-push.py # 访客推送脚本 ├── set_state.py # 状态切换脚本 ├── state.sample.json # 状态文件模板 ├── join-keys.sample.json # Join Key 模板(启动时生成 join-keys.json) ├── SKILL.md # OpenClaw Skill └── LICENSE # MIT 许可 ``` --- ## ⭐ Star History [![Star History Chart](https://api.star-history.com/image?repos=ringhyacinth/Star-Office-UI&type=date&legend=top-left)](https://www.star-history.com/?repos=ringhyacinth%2FStar-Office-UI&type=date&legend=top-left) ================================================ FILE: SKILL.md ================================================ --- name: star-office-ui description: Star Office UI 一键化 Skill:帮主人快速部署像素办公室看板,支持多 Agent 加入、状态可视化、移动端查看与公网访问。 --- # Star Office UI Skill 本 Skill 面向想快速帮主人搭建"像素办公室看板"的 OpenClaw(龙虾): - 目标:主人只需"看到效果",尽量少问问题 - 你可以按下面的步骤,一步一步帮主人跑起来并对外访问 --- ## 0. 一句话告诉主人这是什么 你可以先和主人说: > 这是一个多人协作的像素办公室仪表盘,你的 AI 助手会根据状态自动走到不同位置,还能看到昨天的工作小记! --- ## 1. 30 秒一键启动(推荐你先做一遍) 在你这台机器执行(按顺序): ```bash # 1) 下载仓库 git clone https://github.com/ringhyacinth/Star-Office-UI.git cd Star-Office-UI # 2) 安装依赖 python3 -m pip install -r backend/requirements.txt # 3) 准备状态文件(首次) cp state.sample.json state.json # 4) 启动后端 cd backend python3 app.py ``` 然后告诉主人: > 好了,你现在打开 http://127.0.0.1:19000 就能看到像素办公室了! --- ## 2. 帮主人切状态体验一下 在项目根目录执行: ```bash # 工作中 → 去办公桌 python3 set_state.py writing "正在帮你整理文档" # 同步中 python3 set_state.py syncing "同步进度中" # 报错中 → 去 bug 区 python3 set_state.py error "发现问题,正在排查" # 待命 → 回休息区 python3 set_state.py idle "待命中,随时准备为你服务" ``` --- ## 3. 侧边栏验证码设置(必须教会新龙虾) 当前默认验证码是:`1234`。 你需要这样引导主人: 1. 默认密码是 `1234`,可以先直接体验; 2. 当主人愿意时,可随时和你沟通修改密码; 3. 你应主动推荐改成强密码(更安全,防止他人误改资产和布局)。 修改方式(示例): ```bash export ASSET_DRAWER_PASS="your-strong-pass" ``` 如果是长期运行(systemd / pm2 / 容器),请把该环境变量写入服务配置,而不是只在当前 shell 临时设置。 --- ## 4. 生图功能(Gemini)—— 可选 "搬新家 / 找中介"装修功能需要 Gemini API,但**基础看板不需要**,不装也能正常使用。 ### 4.1 安装生图脚本环境(首次使用时) 仓库已自带生图脚本(`scripts/gemini_image_generate.py`),但运行需要独立的 Python 环境。在项目根目录执行: ```bash # 创建 skill 目录结构 mkdir -p ../skills/gemini-image-generate/scripts # 复制脚本到 skill 目录 cp scripts/gemini_image_generate.py ../skills/gemini-image-generate/scripts/ # 创建独立虚拟环境并安装依赖 python3 -m venv ../skills/gemini-image-generate/.venv ../skills/gemini-image-generate/.venv/bin/pip install google-genai ``` 安装完成后,后端会自动检测到生图环境,"搬新家 / 找中介"按钮即可使用。 ### 4.2 配置 Gemini API Key 引导用户完成这两项配置: 1. `GEMINI_API_KEY` 2. `GEMINI_MODEL`(推荐:`nanobanana-pro` 或 `nanobanana-2`) 配置方式有两种: - **侧边栏填写**:打开资产侧边栏 → 在生图配置区域直接输入 API Key 并保存 - **环境变量**:`export GEMINI_API_KEY="your-key"` 并明确告诉用户: - 不配置 API 也能用基础看板(状态显示、多 Agent、资产替换等) - 配置后才能使用"搬新家 / 找中介"的 AI 生图装修能力 如果页面提示缺少 key,指导用户在侧边栏里直接填写并保存(运行时配置入口)。 --- ## 5. 安装成功后,必须提醒主人的三件事 ### 5.1 告知临时公网链接 推荐先用 Cloudflare Tunnel(最快): ```bash cloudflared tunnel --url http://127.0.0.1:19000 ``` 拿到 `https://xxx.trycloudflare.com` 后发给主人,并补一句: > 这是临时公网链接;如果你愿意,后续我可以一步步带你部署到本地域名。 ### 5.2 装修房子入口 提醒主人: - 现在可以点击"装修房间"开始装饰; - 侧边栏默认密码是 `1234`; - 当他想改密码时,可随时告诉你,你来帮他改; - 推荐改成强密码,安全性更高。 ### 5.3 生图 API 配置 提醒主人: - 生图功能需要自己的 API; - 当前接入的是 **Gemini 官方 API 格式与地址**; - 若要替换成其他类型 API,请先和你沟通,由你来改接入; - 最好让主人把对应 API 文档发给你,便于准确适配。 --- ## 6. 邀请其他龙虾加入(可选) 如果有其他 OpenClaw 想加入你主人的办公室: ### 步骤 A:告诉其他龙虾用 office-agent-push.py 你可以让其他龙虾: 1) 下载 `office-agent-push.py`(可以直接从你这个仓库拿) 2) 让它调用 `join-agent` + 周期 `agent-push` 3) 你主人那边就能看到它进办公室了 ### 步骤 B:join key 说明 - 仓库里默认有 `join-keys.json`(`ocj_starteam01` ~ `ocj_starteam08`) - 同一个 key 最多 3 人同时在线 - 你也可以帮主人新建自己的 key --- ## 7. 昨日小记(可选) 如果你主人想看到"昨日小记": - 在仓库上级目录放一个 `memory/YYYY-MM-DD.md` - 后端会自动读取昨天(或最近可用)的记录,做基础脱敏后展示 --- ## 8. 常见问题(主人可能问你的) ### Q1:"我可以商用吗?" > 代码玩法可以基于 MIT 用,但美术资产(包括角色/场景)禁止商用;如果你要商用,请务必换成你自己的原创美术资产。 ### Q2:"其他龙虾怎么加入?" > 用 join key 加入,然后持续推送状态就行;仓库里有 `office-agent-push.py` 可以给其他龙虾用。 --- ## 9. 给你的提示 - 尽量帮主人把"启动步骤"做完,让主人只负责"看"和"体验" - 如果主人要对外访问,优先用 Cloudflare Tunnel(最快最方便) - 状态切换应由你主动执行: - 接任务前,先把自己状态切到"工作中"(如 `writing/researching/executing`),再开始工作; - 完成任务后,先把自己状态切回"待命中(idle)",再进入休息/等待下一任务。 - 注意不要暴露你主人的内网地址或私有信息 --- ## 10. 2026-03 增量补充(仅新增项) > 本次重制核心请按四条理解: > 1) 新增中英日三语 > 2) 新增资产管理(全量美术可自定义) > 3) 接入生图 API(龙虾自动改房间 + 用户手动装修) > 4) 美术资产替换与优化(含命名与索引重建) ### 10.1 生图模型建议(房间装修) 当用户使用"搬新家 / 找中介"时,优先推荐: 1. **gemini nanobanana pro** 2. **gemini nanobanana 2** 其他模型在房间结构保持与风格一致性上可能不稳定。 推荐配置: - `GEMINI_API_KEY` - `GEMINI_MODEL=nanobanana-pro`(或 `nanobanana-2`) 并提示用户:缺 key 时可在侧边栏内直接填写并保存。 ### 10.2 侧边栏验证码安全提醒(必须) 默认验证码为 `1234`,但生产/公网场景必须改强密码: ```bash export ASSET_DRAWER_PASS="your-strong-pass" ``` 理由:防止外部访问者修改房间布局、装饰和资产配置。 ### 10.3 版权口径更新 主角状态素材已切换为无版权争议的小猫,不再沿用旧角色版权说明。 保留统一口径: - 代码:MIT - 美术资产:禁止商用 ### 10.4 安装时必须提醒(API 可选) 在帮助主人安装时,需明确提醒: - 现在支持接入自己的生图 API 来改美术资产与背景(可持续更换)。 - 但基础功能(状态看板、多 Agent、资产替换/布局、三语切换)**不依赖 API**,不开 API 也能正常使用。 建议对主人口径: > 先把基础看板跑起来;需要"无限换背景/AI 生图装修"再接入自己的 API。 ### 10.5 老用户更新指南(从旧版本升级) 如果主人之前已经下载过旧版,按以下步骤升级: 1. 进入项目目录并备份本地配置(如 `state.json`、自定义资产)。 2. 拉取最新代码(`git pull` 或重新克隆到新目录)。 3. 确认依赖:`python3 -m pip install -r backend/requirements.txt`。 4. 保留并检查本地运行配置: - `ASSET_DRAWER_PASS` - `GEMINI_API_KEY` / `GEMINI_MODEL`(如需生图) 5. 如有自定义位置,确认: - `asset-positions.json` - `asset-defaults.json` 6. 重启后端并验收关键功能: - `/health` - 三语切换(CN/EN/JP) - 资产侧栏(选择、替换、设默认) - 生图入口(有 key 时可用) ### 10.6 功能更新提醒清单(对主人口播) 本次更新以后,至少提醒主人以下变化: 1. 已支持 **CN/EN/JP 三语切换**(含 loading 与气泡实时联动)。 2. 已支持 **自定义美术资产替换**(含动态素材切帧同步,减少闪烁)。 3. 已支持 **接入自有生图 API** 持续更换背景(推荐 `nanobanana-pro` / `nanobanana-2`)。 4. 新增/强化了安全项:`ASSET_DRAWER_PASS` 生产环境建议改强密码。 ### 10.7 2026-03-05 稳定性修复 本次更新修复了多个影响线上稳定运行的问题: 1. **CDN 缓存修复**:静态资源 404 不再被 CDN 长缓存(之前导致 `phaser.js` 被缓存为 404 达 2.7 天)。 2. **前端加载修复**:修复 `fetchStatus()` 中的 JS 语法错误(多余 `else` 块),解决页面卡 loading 问题。 3. **生图异步化**:生图接口改为后台任务 + 轮询模式,避免 Cloudflare 524 超时(100s 限制)。前端显示实时等待进度。 4. **移动端侧边栏**:新增遮罩层、body 滚动锁定、`100dvh` 适配、`overscroll-behavior: contain`。 5. **Join Key 增强**:支持 key 级别过期时间(`expiresAt`)和并发上限(`maxConcurrent`),`join-keys.json` 不再入库。 > 详细说明见:`docs/UPDATE_REPORT_2026-03-05.md` ================================================ FILE: agent-invite-template.txt ================================================ # 海辛办公室 · Agent 加入指南 ## 你收到的信息 - 邀请页:https://office.example.com/join - 接入密钥(join key):[请填入对方给你的 ocj_xxx] - 你想要在办公室里显示的名字:[请填入你的名字] ## 你需要做的步骤 ### 1. 先向你的主人要授权 > “我将把我的工作状态(idle/writing/researching/executing/syncing/error)定时发送到海辛的办公室看板,用于可视化协作;不含具体内容/隐私;随时可停。是否授权?” ### 2. 主人同意后: - 调用 `POST https://office.example.com/join-agent`,payload: ```json { "name": "[你的名字]", "joinKey": "[ocj_xxx]", "state": "idle", "detail": "刚刚加入" } ``` - 拿到返回的 `agentId` - 调用 `POST https://office.example.com/agent-approve`,payload: ```json { "agentId": "[刚才拿到的 agentId]" } ``` - 启动定时推送器(每 30s 一次,或者只在状态变化时): ```json POST https://office.example.com/agent-push { "agentId": "[你的 agentId]", "joinKey": "[ocj_xxx]", "state": "[你的当前状态:idle/writing/researching/executing/syncing/error]", "detail": "[可选的状态细节]", "name": "[可选:更新名字]" } ``` ### 3. 主人想退出时: - 停止定时推送器 - 调用 `POST https://office.example.com/leave-agent`,payload: ```json { "name": "[你的名字]" } ``` --- ## 注意事项 - 如果收到 `403` 或 `agent 未获授权`,停止推送 - 默认授权有效期 24h,过期后重新申请 - 只推送状态,不推送任何具体内容/隐私 ================================================ FILE: asset-defaults.json ================================================ { "flowers-bloom-v2.webp": { "x": 310.0, "y": 390.0, "scale": 0.8, "updated_at": "2026-03-03T01:32:18.211712" } } ================================================ FILE: asset-positions.json ================================================ { "desk-v3.webp": { "x": 218.0, "y": 417.0, "updated_at": "2026-03-02T15:58:27.228023" } } ================================================ FILE: backend/app.py ================================================ #!/usr/bin/env python3 """Star Office UI - Backend State Service""" from flask import Flask, jsonify, send_from_directory, make_response, request, session from datetime import datetime, timedelta import json import os import random import math import re import shutil import subprocess import tempfile import threading from pathlib import Path from security_utils import is_production_mode, is_strong_secret, is_strong_drawer_pass from memo_utils import get_yesterday_date_str, sanitize_content, extract_memo_from_file from store_utils import ( load_agents_state as _store_load_agents_state, save_agents_state as _store_save_agents_state, load_asset_positions as _store_load_asset_positions, save_asset_positions as _store_save_asset_positions, load_asset_defaults as _store_load_asset_defaults, save_asset_defaults as _store_save_asset_defaults, load_runtime_config as _store_load_runtime_config, save_runtime_config as _store_save_runtime_config, load_join_keys as _store_load_join_keys, save_join_keys as _store_save_join_keys, ) try: from PIL import Image except Exception: Image = None # 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") 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") 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") WORKSPACE_DIR = os.path.dirname(ROOT_DIR) OPENCLAW_WORKSPACE = os.environ.get("OPENCLAW_WORKSPACE") or os.path.join(os.path.expanduser("~"), ".openclaw", "workspace") IDENTITY_FILE = os.path.join(OPENCLAW_WORKSPACE, "IDENTITY.md") GEMINI_SCRIPT = os.path.join(WORKSPACE_DIR, "skills", "gemini-image-generate", "scripts", "gemini_image_generate.py") GEMINI_PYTHON = os.path.join(WORKSPACE_DIR, "skills", "gemini-image-generate", ".venv", "bin", "python") ROOM_REFERENCE_IMAGE = ( os.path.join(ROOT_DIR, "assets", "room-reference.webp") if os.path.exists(os.path.join(ROOT_DIR, "assets", "room-reference.webp")) else os.path.join(ROOT_DIR, "assets", "room-reference.png") ) BG_HISTORY_DIR = os.path.join(ROOT_DIR, "assets", "bg-history") 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") # 性能保护:默认关闭“每次打开页面随机换背景”,避免首页首屏被磁盘复制拖慢 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") # Canonical agent states: single source of truth for validation and mapping VALID_AGENT_STATES = frozenset({"idle", "writing", "researching", "executing", "syncing", "error"}) WORKING_STATES = frozenset({"writing", "researching", "executing"}) # subset used for auto-idle TTL STATE_TO_AREA_MAP = { "idle": "breakroom", "writing": "writing", "researching": "writing", "executing": "writing", "syncing": "writing", "error": "error", } app = Flask(__name__, static_folder=FRONTEND_DIR, static_url_path="/static") app.secret_key = os.getenv("FLASK_SECRET_KEY") or os.getenv("STAR_OFFICE_SECRET") or "star-office-dev-secret-change-me" # Session hardening app.config.update( SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE="Lax", SESSION_COOKIE_SECURE=is_production_mode(), PERMANENT_SESSION_LIFETIME=timedelta(hours=12), ) # Guard join-agent critical section to enforce per-key concurrency under parallel requests join_lock = threading.Lock() # Async background task registry for long-running operations (e.g. image generation) # Avoids Cloudflare 524 timeout (100s limit) by letting frontend poll for completion. _bg_tasks = {} # task_id -> {"status": "pending"|"done"|"error", "result": ..., "error": ..., "created_at": ...} _bg_tasks_lock = threading.Lock() # Generate a version timestamp once at server startup for cache busting VERSION_TIMESTAMP = datetime.now().strftime("%Y%m%d_%H%M%S") ASSET_DRAWER_PASS_DEFAULT = os.getenv("ASSET_DRAWER_PASS", "1234") if is_production_mode(): hardening_errors = [] if not is_strong_secret(str(app.secret_key)): hardening_errors.append("FLASK_SECRET_KEY / STAR_OFFICE_SECRET is weak (need >=24 chars, non-default)") if not is_strong_drawer_pass(ASSET_DRAWER_PASS_DEFAULT): hardening_errors.append("ASSET_DRAWER_PASS is weak (do not use default 1234; recommend >=8 chars)") if hardening_errors: raise RuntimeError("Security hardening check failed in production mode: " + "; ".join(hardening_errors)) def _is_asset_editor_authed() -> bool: return bool(session.get("asset_editor_authed")) def _require_asset_editor_auth(): if _is_asset_editor_authed(): return None return jsonify({"ok": False, "code": "UNAUTHORIZED", "msg": "Asset editor auth required"}), 401 @app.after_request def add_no_cache_headers(response): """Apply cache policy by path: - HTML/API/state: no-cache (always fresh) - /static assets (2xx only): long cache (filenames are versioned with ?v=VERSION_TIMESTAMP) - /static assets (non-2xx, e.g. 404): no-cache to prevent CDN from caching errors """ path = (request.path or "") if path.startswith('/static/') and 200 <= response.status_code < 300: response.headers["Cache-Control"] = "public, max-age=31536000, immutable" response.headers.pop("Pragma", None) response.headers.pop("Expires", None) else: response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0" response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "0" return response # Default state DEFAULT_STATE = { "state": "idle", "detail": "等待任务中...", "progress": 0, "updated_at": datetime.now().isoformat() } def load_state(): """Load state from file. Includes a simple auto-idle mechanism: - If the last update is older than ttl_seconds (default 25s) and the state is a "working" state, we fall back to idle. This avoids the UI getting stuck at the desk when no new updates arrive. """ state = None if os.path.exists(STATE_FILE): try: with open(STATE_FILE, "r", encoding="utf-8") as f: state = json.load(f) except Exception: state = None if not isinstance(state, dict): state = dict(DEFAULT_STATE) # Auto-idle try: ttl = int(state.get("ttl_seconds", 300)) updated_at = state.get("updated_at") s = state.get("state", "idle") if updated_at and s in WORKING_STATES: # tolerate both with/without timezone dt = datetime.fromisoformat(updated_at.replace("Z", "+00:00")) # Use UTC for aware datetimes; local time for naive. if dt.tzinfo: from datetime import timezone age = (datetime.now(timezone.utc) - dt.astimezone(timezone.utc)).total_seconds() else: age = (datetime.now() - dt).total_seconds() if age > ttl: state["state"] = "idle" state["detail"] = "待命中(自动回到休息区)" state["progress"] = 0 state["updated_at"] = datetime.now().isoformat() # persist the auto-idle so every client sees it consistently try: save_state(state) except Exception: pass except Exception: pass return state def get_office_name_from_identity(): """Read office display name from OpenClaw workspace IDENTITY.md (Name field) -> 'XXX的办公室'.""" if not os.path.isfile(IDENTITY_FILE): return None try: with open(IDENTITY_FILE, "r", encoding="utf-8") as f: content = f.read() m = re.search(r"-\s*\*\*Name:\*\*\s*(.+)", content) if m: name = m.group(1).strip().replace("\r", "").split("\n")[0].strip() return f"{name}的办公室" if name else None except Exception: pass return None def save_state(state: dict): """Save state to file""" with open(STATE_FILE, "w", encoding="utf-8") as f: json.dump(state, f, ensure_ascii=False, indent=2) def ensure_electron_standalone_snapshot(): """Create Electron standalone frontend snapshot once if missing. The snapshot is intentionally decoupled from the browser page: - browser uses frontend/index.html - Electron uses frontend/electron-standalone.html """ if os.path.exists(FRONTEND_ELECTRON_STANDALONE_FILE): return try: shutil.copy2(FRONTEND_INDEX_FILE, FRONTEND_ELECTRON_STANDALONE_FILE) print(f"[standalone] created: {FRONTEND_ELECTRON_STANDALONE_FILE}") except Exception as e: print(f"[standalone] create failed: {e}") # Initialize state if not os.path.exists(STATE_FILE): save_state(DEFAULT_STATE) ensure_electron_standalone_snapshot() _INDEX_HTML_CACHE = None @app.route("/", methods=["GET"]) def index(): """Serve the pixel office UI with built-in version cache busting""" # 默认禁用页面打开即换背景,避免首屏慢 # 如需启用,可配置 AUTO_ROTATE_HOME_ON_PAGE_OPEN=1 _maybe_apply_random_home_favorite() global _INDEX_HTML_CACHE if _INDEX_HTML_CACHE is None: with open(FRONTEND_INDEX_FILE, "r", encoding="utf-8") as f: raw_html = f.read() _INDEX_HTML_CACHE = raw_html.replace("{{VERSION_TIMESTAMP}}", VERSION_TIMESTAMP) resp = make_response(_INDEX_HTML_CACHE) resp.headers["Content-Type"] = "text/html; charset=utf-8" return resp @app.route("/electron-standalone", methods=["GET"]) def electron_standalone_page(): """Serve Electron-only standalone frontend page.""" ensure_electron_standalone_snapshot() target = FRONTEND_ELECTRON_STANDALONE_FILE if not os.path.exists(target): target = FRONTEND_INDEX_FILE with open(target, "r", encoding="utf-8") as f: html = f.read() html = html.replace("{{VERSION_TIMESTAMP}}", VERSION_TIMESTAMP) resp = make_response(html) resp.headers["Content-Type"] = "text/html; charset=utf-8" return resp resp.headers["Content-Type"] = "text/html; charset=utf-8" return resp @app.route("/join", methods=["GET"]) def join_page(): """Serve the agent join page""" with open(os.path.join(FRONTEND_DIR, "join.html"), "r", encoding="utf-8") as f: html = f.read() resp = make_response(html) resp.headers["Content-Type"] = "text/html; charset=utf-8" return resp @app.route("/invite", methods=["GET"]) def invite_page(): """Serve human-facing invite instruction page""" with open(os.path.join(FRONTEND_DIR, "invite.html"), "r", encoding="utf-8") as f: html = f.read() resp = make_response(html) resp.headers["Content-Type"] = "text/html; charset=utf-8" return resp DEFAULT_AGENTS = [ { "agentId": "star", "name": "Star", "isMain": True, "state": "idle", "detail": "待命中,随时准备为你服务", "updated_at": datetime.now().isoformat(), "area": "breakroom", "source": "local", "joinKey": None, "authStatus": "approved", "authExpiresAt": None, "lastPushAt": None } ] def load_agents_state(): return _store_load_agents_state(AGENTS_STATE_FILE, DEFAULT_AGENTS) def save_agents_state(agents): _store_save_agents_state(AGENTS_STATE_FILE, agents) def load_asset_positions(): return _store_load_asset_positions(ASSET_POSITIONS_FILE) def save_asset_positions(data): _store_save_asset_positions(ASSET_POSITIONS_FILE, data) def load_asset_defaults(): return _store_load_asset_defaults(ASSET_DEFAULTS_FILE) def save_asset_defaults(data): _store_save_asset_defaults(ASSET_DEFAULTS_FILE, data) def load_runtime_config(): return _store_load_runtime_config(RUNTIME_CONFIG_FILE) def save_runtime_config(data): _store_save_runtime_config(RUNTIME_CONFIG_FILE, data) def _ensure_home_favorites_index(): os.makedirs(HOME_FAVORITES_DIR, exist_ok=True) if not os.path.exists(HOME_FAVORITES_INDEX_FILE): with open(HOME_FAVORITES_INDEX_FILE, "w", encoding="utf-8") as f: json.dump({"items": []}, f, ensure_ascii=False, indent=2) def _load_home_favorites_index(): _ensure_home_favorites_index() try: with open(HOME_FAVORITES_INDEX_FILE, "r", encoding="utf-8") as f: data = json.load(f) if isinstance(data, dict) and isinstance(data.get("items"), list): return data except Exception: pass return {"items": []} def _save_home_favorites_index(data): _ensure_home_favorites_index() with open(HOME_FAVORITES_INDEX_FILE, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def _maybe_apply_random_home_favorite(): """On page open, randomly apply one saved home favorite if available.""" global _last_home_rotate_at if not AUTO_ROTATE_HOME_ON_PAGE_OPEN: return False, "disabled" try: now_ts = datetime.now().timestamp() if _last_home_rotate_at and (now_ts - _last_home_rotate_at) < AUTO_ROTATE_MIN_INTERVAL_SECONDS: return False, "throttled" idx = _load_home_favorites_index() items = idx.get("items") or [] candidates = [] for it in items: rel = (it.get("path") or "").strip() if not rel: continue abs_path = os.path.join(ROOT_DIR, rel) if os.path.exists(abs_path): candidates.append((rel, abs_path)) if not candidates: return False, "no-favorites" rel, src = random.choice(candidates) target = FRONTEND_PATH / "office_bg_small.webp" if not target.exists(): return False, "missing-office-bg" shutil.copy2(src, str(target)) _last_home_rotate_at = now_ts return True, rel except Exception as e: return False, str(e) def load_join_keys(): return _store_load_join_keys(JOIN_KEYS_FILE) def save_join_keys(data): _store_save_join_keys(JOIN_KEYS_FILE, data) def _ensure_magick_or_ffmpeg_available(): if shutil.which("magick"): return "magick" if shutil.which("ffmpeg"): return "ffmpeg" return None def _probe_animated_frame_size(upload_path: str): """Return (w,h) from first frame if possible.""" if Image is not None: try: with Image.open(upload_path) as im: w, h = im.size return int(w), int(h) except Exception: pass # ffprobe fallback if shutil.which("ffprobe"): try: cmd = [ "ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=p=0:s=x", upload_path, ] out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, timeout=5).decode().strip() if "x" in out: w, h = out.split("x", 1) return int(w), int(h) except Exception: pass return None, None def _animated_to_spritesheet( upload_path: str, frame_w: int, frame_h: int, out_ext: str = ".webp", preserve_original: bool = True, pixel_art: bool = True, cols: int | None = None, rows: int | None = None, ): """Convert animated GIF/WEBP to spritesheet, return (out_path, columns, rows, frames, out_frame_w, out_frame_h).""" backend = _ensure_magick_or_ffmpeg_available() if not backend: raise RuntimeError("未检测到 ImageMagick/ffmpeg,无法自动转换动图") ext = (out_ext or ".webp").lower() if ext not in {".webp", ".png"}: ext = ".webp" out_fd, out_path = tempfile.mkstemp(suffix=ext) os.close(out_fd) with tempfile.TemporaryDirectory() as td: frames = 0 out_fw, out_fh = int(frame_w), int(frame_h) if Image is not None: try: with Image.open(upload_path) as im: n = getattr(im, "n_frames", 1) # 默认保留用户原始帧尺寸(避免先压缩再放大导致像素糊) if preserve_original: out_fw, out_fh = im.size for i in range(n): im.seek(i) fr = im.convert("RGBA") if not preserve_original and (fr.size != (out_fw, out_fh)): resample = Image.Resampling.NEAREST if pixel_art else Image.Resampling.LANCZOS fr = fr.resize((out_fw, out_fh), resample) fr.save(os.path.join(td, f"f_{i:04d}.png"), "PNG") frames = n except Exception: frames = 0 if frames <= 0: cmd1 = f"ffmpeg -y -i '{upload_path}' '{td}/f_%04d.png' >/dev/null 2>&1" if os.system(cmd1) != 0: raise RuntimeError("动图抽帧失败(Pillow/ffmpeg 都失败)") files = sorted([x for x in os.listdir(td) if x.startswith("f_") and x.endswith(".png")]) frames = len(files) if frames <= 0: raise RuntimeError("动图无有效帧") if backend == "magick": # 像素风动图转精灵表默认无损,避免颜色/边缘被压缩糊掉 quality_flag = "-define webp:lossless=true -define webp:method=6 -quality 100" if ext == ".webp" else "" # 允许按 cols/rows 排布;默认单行 if cols is None or cols <= 0: cols_eff = frames else: cols_eff = max(1, int(cols)) rows_eff = max(1, int(rows)) if (rows is not None and rows > 0) else max(1, math.ceil(frames / cols_eff)) # 先规范单帧尺寸 prep = "" if not preserve_original: magick_filter = "-filter point" if pixel_art else "" prep = f" {magick_filter} -resize {out_fw}x{out_fh}^ -gravity center -background none -extent {out_fw}x{out_fh}" cmd = ( f"magick '{td}/f_*.png'{prep} " f"-tile {cols_eff}x{rows_eff} -background none -geometry +0+0 {quality_flag} '{out_path}'" ) rc = os.system(cmd) if rc != 0: raise RuntimeError("ImageMagick 拼图失败") return out_path, cols_eff, rows_eff, frames, out_fw, out_fh ffmpeg_quality = "-lossless 1 -compression_level 6 -q:v 100" if ext == ".webp" else "" cols_eff = max(1, int(cols)) if (cols is not None and cols > 0) else frames rows_eff = max(1, int(rows)) if (rows is not None and rows > 0) else max(1, math.ceil(frames / cols_eff)) if preserve_original: vf = f"tile={cols_eff}x{rows_eff}" else: scale_algo = "neighbor" if pixel_art else "lanczos" vf = ( f"scale={out_fw}:{out_fh}:force_original_aspect_ratio=decrease:flags={scale_algo}," f"pad={out_fw}:{out_fh}:(ow-iw)/2:(oh-ih)/2:color=0x00000000," f"tile={cols_eff}x{rows_eff}" ) cmd2 = ( f"ffmpeg -y -pattern_type glob -i '{td}/f_*.png' " f"-vf '{vf}' " f"{ffmpeg_quality} '{out_path}' >/dev/null 2>&1" ) if os.system(cmd2) != 0: raise RuntimeError("ffmpeg 拼图失败") return out_path, frames, 1, frames, out_fw, out_fh def normalize_agent_state(s): """Normalize agent state for compatibility. Maps synonyms (e.g. working/busy -> writing, run/running -> executing) into VALID_AGENT_STATES. Returns 'idle' for unknown values. """ if not s: return 'idle' s_lower = s.lower().strip() if s_lower in {'working', 'busy', 'write'}: return 'writing' if s_lower in {'run', 'running', 'execute', 'exec'}: return 'executing' if s_lower in {'sync'}: return 'syncing' if s_lower in {'research', 'search'}: return 'researching' if s_lower in VALID_AGENT_STATES: return s_lower return 'idle' # User-facing model aliases -> provider model ids USER_MODEL_TO_PROVIDER_MODELS = { # 严格按用户要求:仅两种官方模型映射 "nanobanana-pro": [ "nano-banana-pro-preview", ], "nanobanana-2": [ "gemini-2.5-flash-image", ], } PROVIDER_MODEL_TO_USER_MODEL = { provider: user for user, providers in USER_MODEL_TO_PROVIDER_MODELS.items() for provider in providers } def _normalize_user_model(model_name: str) -> str: m = (model_name or "").strip() if not m: return "nanobanana-pro" low = m.lower() if low in USER_MODEL_TO_PROVIDER_MODELS: return low if low in PROVIDER_MODEL_TO_USER_MODEL: return PROVIDER_MODEL_TO_USER_MODEL[low] return "nanobanana-pro" def _provider_model_candidates(user_model: str): normalized = _normalize_user_model(user_model) return list(USER_MODEL_TO_PROVIDER_MODELS.get(normalized, USER_MODEL_TO_PROVIDER_MODELS["nanobanana-pro"])) def _generate_rpg_background_to_webp(out_webp_path: str, width: int = 1280, height: int = 720, custom_prompt: str = "", speed_mode: str = "fast"): """Generate RPG-style room background and save as webp. speed_mode: - fast: use nanobanana-2 + 1024x576 intermediate + downscaled reference (faster) - quality: use configured model (fallback nanobanana-pro) + full 1280x720 path """ runtime_cfg = load_runtime_config() api_key = (runtime_cfg.get("gemini_api_key") or "").strip() if not api_key: raise RuntimeError("MISSING_API_KEY") themes = [ "8-bit dungeon guild room", "8-bit stardew-valley inspired cozy farm tavern", "8-bit nordic fantasy tavern", "8-bit magitech workshop", "8-bit elven forest inn", "8-bit pixel cyber tavern", "8-bit desert caravan inn", "8-bit snow mountain lodge", ] theme = random.choice(themes) if not (os.path.exists(GEMINI_PYTHON) and os.path.exists(GEMINI_SCRIPT)): raise RuntimeError("生图脚本环境缺失:gemini-image-generate 未安装") style_hint = (custom_prompt or "").strip() if not style_hint: style_hint = theme # 默认使用更稳妥的 quality 档,避免 fast 模型在部分 API 通道不可用 mode = (speed_mode or "quality").strip().lower() if mode not in {"fast", "quality"}: mode = "quality" configured_user_model = _normalize_user_model(runtime_cfg.get("gemini_model") or "nanobanana-pro") if mode == "fast": preferred_user_model = "nanobanana-2" # fast 也提高基础清晰度:从 1024x576 提升到 1152x648(牺牲少量速度) gen_width, gen_height = 1152, 648 ref_width, ref_height = 1152, 648 else: preferred_user_model = configured_user_model gen_width, gen_height = width, height ref_width, ref_height = width, height # 同时规避可能触发 400 的特殊能力参数: # 仅 nanobanana-2 走 aspect-ratio,nanobanana-pro 交给模型默认比例(后续再标准化到 1280x720) allow_aspect_ratio = (preferred_user_model == "nanobanana-2") prompt = ( "Use a top-down pixel room composition compatible with an office game scene. " "STRICTLY preserve the same room geometry, camera angle, wall/floor boundaries and major object placement as the provided reference image. " "Keep region layout stable (left work area, center lounge, right error area). " "Only change visual style/theme/material/lighting according to: " + style_hint + ". " "Do not add text or watermark. Retro 8-bit RPG style." ) tmp_dir = tempfile.mkdtemp(prefix="rpg-bg-") cmd = [ GEMINI_PYTHON, GEMINI_SCRIPT, "--prompt", prompt, "--model", configured_user_model, "--out-dir", tmp_dir, "--cleanup", ] if allow_aspect_ratio: cmd.extend(["--aspect-ratio", "16:9"]) # 强约束:每次都带固定参考图,保持房间区域布局不漂移 ref_for_call = None if os.path.exists(ROOM_REFERENCE_IMAGE): ref_for_call = ROOM_REFERENCE_IMAGE if mode == "fast" and Image is not None: try: ref_fast = os.path.join(tmp_dir, "room-reference-fast.webp") with Image.open(ROOM_REFERENCE_IMAGE) as rim: rim = rim.convert("RGBA").resize((ref_width, ref_height), Image.Resampling.LANCZOS) rim.save(ref_fast, "WEBP", quality=85, method=4) ref_for_call = ref_fast except Exception: ref_for_call = ROOM_REFERENCE_IMAGE if ref_for_call: cmd.extend(["--reference-image", ref_for_call]) env = os.environ.copy() # 运行时配置优先:只保留 GEMINI_API_KEY,避免脚本因双 key 报错 env.pop("GOOGLE_API_KEY", None) env["GEMINI_API_KEY"] = api_key def _run_cmd(cmd_args): return subprocess.run(cmd_args, capture_output=True, text=True, env=env, timeout=240) def _is_model_unavailable_error(text: str) -> bool: low = (text or "").strip().lower() return ( ("not found" in low and "models/" in low) or ("model_not_available" in low) or ("model is not available" in low) or ("configured model is not available" in low) or ("this model is not available" in low) or ("not supported for generatecontent" in low) ) def _with_model(cmd_args, model_name: str): m = cmd_args[:] if "--model" in m: idx = m.index("--model") if idx + 1 < len(m): m[idx + 1] = model_name else: m.extend(["--model", model_name]) return m # 模型多级回退(仅允许两类用户模型:nanobanana-pro / nanobanana-2) # 每个用户模型映射到若干 provider 真实模型。 user_model_order = [preferred_user_model, configured_user_model] user_model_order = [m for i, m in enumerate(user_model_order) if m and m not in user_model_order[:i]] model_candidates = [] for um in user_model_order: model_candidates.extend(_provider_model_candidates(um)) # 去重并清理空项 model_candidates = [m for i, m in enumerate(model_candidates) if m and m not in model_candidates[:i]] proc = None last_err_text = "" model_unavailable_count = 0 for mname in model_candidates: env["GEMINI_MODEL"] = mname try_cmd = _with_model(cmd, mname) proc = _run_cmd(try_cmd) if proc.returncode == 0: break err_text = (proc.stderr or proc.stdout or "").strip() last_err_text = err_text # key 失效/泄漏:立即终止,不继续尝试 low = err_text.lower() if "your api key was reported as leaked" in low or "permission_denied" in low: raise RuntimeError("API_KEY_REVOKED_OR_LEAKED") if _is_model_unavailable_error(err_text): model_unavailable_count += 1 continue # 非模型不可用错误,直接返回真实错误 raise RuntimeError(f"生图失败: {err_text}") if proc is None or proc.returncode != 0: err_text = (last_err_text or "").strip() if model_unavailable_count >= len(model_candidates) or _is_model_unavailable_error(err_text): brief = (err_text or "").replace("\n", " ")[:240] raise RuntimeError(f"MODEL_NOT_AVAILABLE::{brief}") raise RuntimeError(f"生图失败: {err_text}") try: result = json.loads(proc.stdout.strip().splitlines()[-1]) except Exception: raise RuntimeError("生图结果解析失败") files = result.get("files") or [] if not files: raise RuntimeError("生图未返回文件") gen_path = files[0] if not os.path.exists(gen_path): raise RuntimeError("生图文件不存在") if Image is None: raise RuntimeError("Pillow 不可用,无法做尺寸标准化") with Image.open(gen_path) as im: im = im.convert("RGBA") # 质量模式优先保细节;快速模式优先速度 if mode == "fast": im = im.resize((gen_width, gen_height), Image.Resampling.LANCZOS) if (gen_width, gen_height) != (width, height): # fast 的放大改为 LANCZOS,牺牲少量速度换更高细节 im = im.resize((width, height), Image.Resampling.LANCZOS) im.save(out_webp_path, "WEBP", quality=96, method=6) else: # quality:确保输出标准尺寸,同时使用无损 webp,减少压缩损失 if im.size != (width, height): im = im.resize((width, height), Image.Resampling.LANCZOS) im.save(out_webp_path, "WEBP", lossless=True, quality=100, method=6) def state_to_area(state): """Map agent state to office area (breakroom / writing / error).""" return STATE_TO_AREA_MAP.get(state, "breakroom") # Ensure files exist if not os.path.exists(AGENTS_STATE_FILE): save_agents_state(DEFAULT_AGENTS) if not os.path.exists(JOIN_KEYS_FILE): if os.path.exists(os.path.join(ROOT_DIR, "join-keys.sample.json")): try: with open(os.path.join(ROOT_DIR, "join-keys.sample.json"), "r", encoding="utf-8") as sf: sample = json.load(sf) save_join_keys(sample if isinstance(sample, dict) else {"keys": []}) except Exception: save_join_keys({"keys": []}) else: save_join_keys({"keys": []}) # Tighten runtime-config file perms if exists if os.path.exists(RUNTIME_CONFIG_FILE): try: os.chmod(RUNTIME_CONFIG_FILE, 0o600) except Exception: pass @app.route("/agents", methods=["GET"]) def get_agents(): """Get full agents list (for multi-agent UI), with auto-cleanup on access""" agents = load_agents_state() now = datetime.now() cleaned_agents = [] keys_data = load_join_keys() for a in agents: if a.get("isMain"): cleaned_agents.append(a) continue auth_expires_at_str = a.get("authExpiresAt") auth_status = a.get("authStatus", "pending") # 1) 超时未批准自动 leave if auth_status == "pending" and auth_expires_at_str: try: auth_expires_at = datetime.fromisoformat(auth_expires_at_str) if now > auth_expires_at: key = a.get("joinKey") if key: key_item = next((k for k in keys_data.get("keys", []) if k.get("key") == key), None) if key_item: key_item["used"] = False key_item["usedBy"] = None key_item["usedByAgentId"] = None key_item["usedAt"] = None continue except Exception: pass # 2) 超时未推送自动离线(超过5分钟) last_push_at_str = a.get("lastPushAt") if auth_status == "approved" and last_push_at_str: try: last_push_at = datetime.fromisoformat(last_push_at_str) age = (now - last_push_at).total_seconds() if age > 300: # 5分钟无推送自动离线 a["authStatus"] = "offline" except Exception: pass cleaned_agents.append(a) save_agents_state(cleaned_agents) save_join_keys(keys_data) return jsonify(cleaned_agents) @app.route("/agent-approve", methods=["POST"]) def agent_approve(): """Approve an agent (set authStatus to approved)""" try: data = request.get_json() agent_id = (data.get("agentId") or "").strip() if not agent_id: return jsonify({"ok": False, "msg": "缺少 agentId"}), 400 agents = load_agents_state() target = next((a for a in agents if a.get("agentId") == agent_id and not a.get("isMain")), None) if not target: return jsonify({"ok": False, "msg": "未找到 agent"}), 404 target["authStatus"] = "approved" target["authApprovedAt"] = datetime.now().isoformat() target["authExpiresAt"] = (datetime.now() + timedelta(hours=24)).isoformat() # 默认授权24h save_agents_state(agents) return jsonify({"ok": True, "agentId": agent_id, "authStatus": "approved"}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/agent-reject", methods=["POST"]) def agent_reject(): """Reject an agent (set authStatus to rejected and optionally revoke key)""" try: data = request.get_json() agent_id = (data.get("agentId") or "").strip() if not agent_id: return jsonify({"ok": False, "msg": "缺少 agentId"}), 400 agents = load_agents_state() target = next((a for a in agents if a.get("agentId") == agent_id and not a.get("isMain")), None) if not target: return jsonify({"ok": False, "msg": "未找到 agent"}), 404 target["authStatus"] = "rejected" target["authRejectedAt"] = datetime.now().isoformat() # Optionally free join key back to unused join_key = target.get("joinKey") keys_data = load_join_keys() if join_key: key_item = next((k for k in keys_data.get("keys", []) if k.get("key") == join_key), None) if key_item: key_item["used"] = False key_item["usedBy"] = None key_item["usedByAgentId"] = None key_item["usedAt"] = None # Remove from agents list agents = [a for a in agents if a.get("agentId") != agent_id or a.get("isMain")] save_agents_state(agents) save_join_keys(keys_data) return jsonify({"ok": True, "agentId": agent_id, "authStatus": "rejected"}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/join-agent", methods=["POST"]) def join_agent(): """Add a new agent with one-time join key validation and pending auth""" try: data = request.get_json() if not isinstance(data, dict) or not data.get("name"): return jsonify({"ok": False, "msg": "请提供名字"}), 400 name = data["name"].strip() state = data.get("state", "idle") detail = data.get("detail", "") join_key = data.get("joinKey", "").strip() # Normalize state early for compatibility state = normalize_agent_state(state) if not join_key: return jsonify({"ok": False, "msg": "请提供接入密钥"}), 400 keys_data = load_join_keys() key_item = next((k for k in keys_data.get("keys", []) if k.get("key") == join_key), None) if not key_item: return jsonify({"ok": False, "msg": "接入密钥无效"}), 403 # key 可复用:不再因为 used=true 拒绝 with join_lock: # 在锁内重新读取,避免并发请求都基于同一旧快照通过校验 keys_data = load_join_keys() key_item = next((k for k in keys_data.get("keys", []) if k.get("key") == join_key), None) if not key_item: return jsonify({"ok": False, "msg": "接入密钥无效"}), 403 # Key-level expiration check key_expires_at_str = key_item.get("expiresAt") if key_expires_at_str: try: key_expires_at = datetime.fromisoformat(key_expires_at_str) if datetime.now() > key_expires_at: return jsonify({"ok": False, "msg": "该接入密钥已过期,活动已结束 🎉"}), 403 except Exception: pass agents = load_agents_state() # 并发上限:同一个 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) existing_id = existing.get("agentId") if existing else None def _age_seconds(dt_str): if not dt_str: return None try: dt = datetime.fromisoformat(dt_str) return (now - dt).total_seconds() except Exception: return None # opportunistic offline marking for a in agents: if a.get("isMain"): continue if a.get("authStatus") != "approved": continue age = _age_seconds(a.get("lastPushAt")) if age is None: age = _age_seconds(a.get("updated_at")) if age is not None and age > 300: a["authStatus"] = "offline" max_concurrent = int(key_item.get("maxConcurrent", 3)) active_count = 0 for a in agents: if a.get("isMain"): continue if a.get("agentId") == existing_id: continue if a.get("joinKey") != join_key: continue if a.get("authStatus") != "approved": continue age = _age_seconds(a.get("lastPushAt")) if age is None: age = _age_seconds(a.get("updated_at")) if age is None or age <= 300: active_count += 1 if active_count >= max_concurrent: save_agents_state(agents) return jsonify({"ok": False, "msg": f"该接入密钥当前并发已达上限({max_concurrent}),请稍后或换另一个 key"}), 429 if existing: existing["state"] = state existing["detail"] = detail existing["updated_at"] = datetime.now().isoformat() existing["area"] = state_to_area(state) existing["source"] = "remote-openclaw" existing["joinKey"] = join_key existing["authStatus"] = "approved" existing["authApprovedAt"] = datetime.now().isoformat() existing["authExpiresAt"] = (datetime.now() + timedelta(hours=24)).isoformat() existing["lastPushAt"] = datetime.now().isoformat() # join 视为上线,纳入并发/离线判定 if not existing.get("avatar"): import random existing["avatar"] = random.choice(["guest_role_1", "guest_role_2", "guest_role_3", "guest_role_4", "guest_role_5", "guest_role_6"]) agent_id = existing.get("agentId") else: # Use ms + random suffix to avoid collisions under concurrent joins import random import string agent_id = "agent_" + str(int(datetime.now().timestamp() * 1000)) + "_" + "".join(random.choices(string.ascii_lowercase + string.digits, k=4)) agents.append({ "agentId": agent_id, "name": name, "isMain": False, "state": state, "detail": detail, "updated_at": datetime.now().isoformat(), "area": state_to_area(state), "source": "remote-openclaw", "joinKey": join_key, "authStatus": "approved", "authApprovedAt": datetime.now().isoformat(), "authExpiresAt": (datetime.now() + timedelta(hours=24)).isoformat(), "lastPushAt": datetime.now().isoformat(), "avatar": random.choice(["guest_role_1", "guest_role_2", "guest_role_3", "guest_role_4", "guest_role_5", "guest_role_6"]) }) key_item["used"] = True key_item["usedBy"] = name key_item["usedByAgentId"] = agent_id key_item["usedAt"] = datetime.now().isoformat() key_item["reusable"] = True # 拿到有效 key 直接批准,不再等待主人手动点击 # (状态已在上面 existing/new 分支写入) save_agents_state(agents) save_join_keys(keys_data) return jsonify({"ok": True, "agentId": agent_id, "authStatus": "approved", "nextStep": "已自动批准,立即开始推送状态"}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/leave-agent", methods=["POST"]) def leave_agent(): """Remove an agent and free its one-time join key for reuse (optional) Prefer agentId (stable). Name is accepted for backward compatibility. """ try: data = request.get_json() if not isinstance(data, dict): return jsonify({"ok": False, "msg": "invalid json"}), 400 agent_id = (data.get("agentId") or "").strip() name = (data.get("name") or "").strip() if not agent_id and not name: return jsonify({"ok": False, "msg": "请提供 agentId 或名字"}), 400 agents = load_agents_state() target = None if agent_id: target = next((a for a in agents if a.get("agentId") == agent_id and not a.get("isMain")), None) if (not target) and name: # fallback: remove by name only if agentId not provided target = next((a for a in agents if a.get("name") == name and not a.get("isMain")), None) if not target: return jsonify({"ok": False, "msg": "没有找到要离开的 agent"}), 404 join_key = target.get("joinKey") new_agents = [a for a in agents if a.get("isMain") or a.get("agentId") != target.get("agentId")] # Optional: free key back to unused after leave keys_data = load_join_keys() if join_key: key_item = next((k for k in keys_data.get("keys", []) if k.get("key") == join_key), None) if key_item: key_item["used"] = False key_item["usedBy"] = None key_item["usedByAgentId"] = None key_item["usedAt"] = None save_agents_state(new_agents) save_join_keys(keys_data) return jsonify({"ok": True}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/status", methods=["GET"]) def get_status(): """Get current main state (backward compatibility). Optionally include officeName from IDENTITY.md.""" state = load_state() office_name = get_office_name_from_identity() if office_name: state["officeName"] = office_name return jsonify(state) @app.route("/agent-push", methods=["POST"]) def agent_push(): """Remote openclaw actively pushes status to office. Required fields: - agentId - joinKey - state Optional: - detail - name """ try: data = request.get_json() if not isinstance(data, dict): return jsonify({"ok": False, "msg": "invalid json"}), 400 agent_id = (data.get("agentId") or "").strip() join_key = (data.get("joinKey") or "").strip() state = (data.get("state") or "").strip() detail = (data.get("detail") or "").strip() name = (data.get("name") or "").strip() if not agent_id or not join_key or not state: return jsonify({"ok": False, "msg": "缺少 agentId/joinKey/state"}), 400 state = normalize_agent_state(state) keys_data = load_join_keys() key_item = next((k for k in keys_data.get("keys", []) if k.get("key") == join_key), None) if not key_item: return jsonify({"ok": False, "msg": "joinKey 无效"}), 403 # Key-level expiration check key_expires_at_str = key_item.get("expiresAt") if key_expires_at_str: try: key_expires_at = datetime.fromisoformat(key_expires_at_str) if datetime.now() > key_expires_at: return jsonify({"ok": False, "msg": "该接入密钥已过期,活动已结束 🎉"}), 403 except Exception: pass agents = load_agents_state() target = next((a for a in agents if a.get("agentId") == agent_id and not a.get("isMain")), None) if not target: return jsonify({"ok": False, "msg": "agent 未注册,请先 join"}), 404 # Auth check: only approved agents can push. # Note: "offline" is a presence state (stale), not a revoked authorization. # Allow offline agents to resume pushing and auto-promote them back to approved. auth_status = target.get("authStatus", "pending") if auth_status not in {"approved", "offline"}: return jsonify({"ok": False, "msg": "agent 未获授权,请等待主人批准"}), 403 if auth_status == "offline": target["authStatus"] = "approved" target["authApprovedAt"] = datetime.now().isoformat() target["authExpiresAt"] = (datetime.now() + timedelta(hours=24)).isoformat() if target.get("joinKey") != join_key: return jsonify({"ok": False, "msg": "joinKey 不匹配"}), 403 target["state"] = state target["detail"] = detail if name: target["name"] = name target["updated_at"] = datetime.now().isoformat() target["area"] = state_to_area(state) target["source"] = "remote-openclaw" target["lastPushAt"] = datetime.now().isoformat() save_agents_state(agents) return jsonify({"ok": True, "agentId": agent_id, "area": target.get("area")}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/health", methods=["GET"]) def health(): """Health check""" return jsonify({ "status": "ok", "service": "star-office-ui", "timestamp": datetime.now().isoformat(), }) @app.route("/yesterday-memo", methods=["GET"]) def get_yesterday_memo(): """获取昨日小日记""" try: # 先尝试找昨天的文件 yesterday_str = get_yesterday_date_str() yesterday_file = os.path.join(MEMORY_DIR, f"{yesterday_str}.md") target_file = None target_date = yesterday_str if os.path.exists(yesterday_file): target_file = yesterday_file else: # 如果昨天没有,找最近的一天 if os.path.exists(MEMORY_DIR): files = [f for f in os.listdir(MEMORY_DIR) if f.endswith(".md") and re.match(r"\d{4}-\d{2}-\d{2}\.md", f)] if files: files.sort(reverse=True) # 跳过今天的(如果存在) today_str = datetime.now().strftime("%Y-%m-%d") for f in files: if f != f"{today_str}.md": target_file = os.path.join(MEMORY_DIR, f) target_date = f.replace(".md", "") break if target_file and os.path.exists(target_file): memo_content = extract_memo_from_file(target_file) return jsonify({ "success": True, "date": target_date, "memo": memo_content }) else: return jsonify({ "success": False, "msg": "没有找到昨日日记" }) except Exception as e: return jsonify({ "success": False, "msg": str(e) }), 500 @app.route("/set_state", methods=["POST"]) def set_state_endpoint(): """Set state via POST (for UI control panel)""" try: data = request.get_json() if not isinstance(data, dict): return jsonify({"status": "error", "msg": "invalid json"}), 400 state = load_state() if "state" in data: s = data["state"] if s in VALID_AGENT_STATES: state["state"] = s if "detail" in data: state["detail"] = data["detail"] state["updated_at"] = datetime.now().isoformat() save_state(state) return jsonify({"status": "ok"}) except Exception as e: return jsonify({"status": "error", "msg": str(e)}), 500 @app.route("/assets/template.zip", methods=["GET"]) def assets_template_download(): if not os.path.exists(ASSET_TEMPLATE_ZIP): return jsonify({"ok": False, "msg": "模板包不存在,请先生成"}), 404 return send_from_directory(ROOT_DIR, "assets-replace-template.zip", as_attachment=True) @app.route("/assets/list", methods=["GET"]) def assets_list(): items = [] for p in FRONTEND_PATH.rglob("*"): if not p.is_file(): continue rel = p.relative_to(FRONTEND_PATH).as_posix() if rel.startswith("fonts/"): continue if p.suffix.lower() not in ASSET_ALLOWED_EXTS: continue st = p.stat() width = None height = None if Image is not None: try: with Image.open(p) as im: width, height = im.size except Exception: pass items.append({ "path": rel, "size": st.st_size, "ext": p.suffix.lower(), "width": width, "height": height, "mtime": datetime.fromtimestamp(st.st_mtime).isoformat(), }) items.sort(key=lambda x: x["path"]) return jsonify({"ok": True, "count": len(items), "items": items}) def _bg_generate_worker(task_id: str, custom_prompt: str, speed_mode: str): """Background worker for RPG background generation.""" try: target = FRONTEND_PATH / "office_bg_small.webp" # 覆盖前保留最近一次备份 bak = target.with_suffix(target.suffix + ".bak") shutil.copy2(target, bak) _generate_rpg_background_to_webp( str(target), width=1280, height=720, custom_prompt=custom_prompt, speed_mode=speed_mode, ) # 每次生成都归档一份历史底图(可回溯风格演化) os.makedirs(BG_HISTORY_DIR, exist_ok=True) ts = datetime.now().strftime("%Y%m%d-%H%M%S") hist_file = os.path.join(BG_HISTORY_DIR, f"office_bg_small-{ts}.webp") shutil.copy2(target, hist_file) st = target.stat() with _bg_tasks_lock: _bg_tasks[task_id] = { "status": "done", "result": { "ok": True, "path": "office_bg_small.webp", "size": st.st_size, "history": os.path.relpath(hist_file, ROOT_DIR), "speed_mode": speed_mode, "msg": "已生成并替换 RPG 房间底图(已自动归档)", }, } except Exception as e: msg = str(e) error_result = {"ok": False, "msg": msg} if msg == "MISSING_API_KEY": error_result["code"] = "MISSING_API_KEY" error_result["msg"] = "Missing GEMINI_API_KEY or GOOGLE_API_KEY" elif msg == "API_KEY_REVOKED_OR_LEAKED": error_result["code"] = "API_KEY_REVOKED_OR_LEAKED" error_result["msg"] = "API key is revoked or flagged as leaked. Please rotate to a new key." elif msg.startswith("MODEL_NOT_AVAILABLE"): error_result["code"] = "MODEL_NOT_AVAILABLE" error_result["msg"] = "Configured model is not available for this API key/channel." if "::" in msg: error_result["detail"] = msg.split("::", 1)[1] with _bg_tasks_lock: _bg_tasks[task_id] = {"status": "error", "result": error_result} @app.route("/assets/generate-rpg-background", methods=["POST"]) def assets_generate_rpg_background(): """Start async RPG background generation. Returns a task_id for polling.""" guard = _require_asset_editor_auth() if guard: return guard try: req = request.get_json(silent=True) or {} custom_prompt = (req.get("prompt") or "").strip() if isinstance(req, dict) else "" speed_mode = (req.get("speed_mode") or "quality").strip().lower() if isinstance(req, dict) else "quality" if speed_mode not in {"fast", "quality"}: speed_mode = "fast" target = FRONTEND_PATH / "office_bg_small.webp" if not target.exists(): return jsonify({"ok": False, "msg": "office_bg_small.webp 不存在"}), 404 # Pre-flight checks that can fail fast (before spawning thread) runtime_cfg = load_runtime_config() api_key = (runtime_cfg.get("gemini_api_key") or "").strip() if not api_key: return jsonify({"ok": False, "code": "MISSING_API_KEY", "msg": "Missing GEMINI_API_KEY or GOOGLE_API_KEY"}), 400 if not (os.path.exists(GEMINI_PYTHON) and os.path.exists(GEMINI_SCRIPT)): return jsonify({"ok": False, "msg": "生图脚本环境缺失:gemini-image-generate 未安装"}), 500 # Check if another generation is already running with _bg_tasks_lock: for tid, task in _bg_tasks.items(): if task.get("status") == "pending": return jsonify({"ok": True, "async": True, "task_id": tid, "msg": "已有生图任务进行中,请等待完成"}), 200 # Create async task import string as _string task_id = "gen_" + str(int(datetime.now().timestamp() * 1000)) + "_" + "".join(random.choices(_string.ascii_lowercase + _string.digits, k=4)) with _bg_tasks_lock: _bg_tasks[task_id] = {"status": "pending", "created_at": datetime.now().isoformat()} t = threading.Thread(target=_bg_generate_worker, args=(task_id, custom_prompt, speed_mode), daemon=True) t.start() return jsonify({"ok": True, "async": True, "task_id": task_id, "msg": "生图任务已启动,请通过 task_id 轮询结果"}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/generate-rpg-background/poll", methods=["GET"]) def assets_generate_rpg_background_poll(): """Poll async generation task status.""" guard = _require_asset_editor_auth() if guard: return guard task_id = (request.args.get("task_id") or "").strip() if not task_id: return jsonify({"ok": False, "msg": "缺少 task_id"}), 400 with _bg_tasks_lock: task = _bg_tasks.get(task_id) if not task: return jsonify({"ok": False, "msg": "任务不存在"}), 404 status = task.get("status", "pending") if status == "pending": return jsonify({"ok": True, "status": "pending", "msg": "生图进行中..."}) elif status == "done": # Clean up task after delivering result with _bg_tasks_lock: _bg_tasks.pop(task_id, None) return jsonify({"ok": True, "status": "done", **task.get("result", {})}) else: with _bg_tasks_lock: _bg_tasks.pop(task_id, None) result = task.get("result", {}) code = 400 if result.get("code") else 500 return jsonify({"ok": False, "status": "error", **result}), code @app.route("/assets/restore-reference-background", methods=["POST"]) def assets_restore_reference_background(): """Restore office_bg_small.webp from fixed reference image.""" guard = _require_asset_editor_auth() if guard: return guard try: target = FRONTEND_PATH / "office_bg_small.webp" if not target.exists(): return jsonify({"ok": False, "msg": "office_bg_small.webp 不存在"}), 404 if not os.path.exists(ROOM_REFERENCE_IMAGE): return jsonify({"ok": False, "msg": "参考图不存在"}), 404 # 备份当前底图 bak = target.with_suffix(target.suffix + ".bak") shutil.copy2(target, bak) # 快速路径:若参考图已是 1280x720 的 webp,直接拷贝(秒级) ref_ext = os.path.splitext(ROOM_REFERENCE_IMAGE)[1].lower() fast_copied = False if ref_ext == '.webp': try: with Image.open(ROOM_REFERENCE_IMAGE) as rim: if rim.size == (1280, 720): shutil.copy2(ROOM_REFERENCE_IMAGE, target) fast_copied = True except Exception: fast_copied = False # 慢路径:仅在必要时重编码 if not fast_copied: if Image is None: return jsonify({"ok": False, "msg": "Pillow 不可用"}), 500 with Image.open(ROOM_REFERENCE_IMAGE) as im: im = im.convert("RGBA").resize((1280, 720), Image.Resampling.LANCZOS) im.save(target, "WEBP", quality=92, method=6) st = target.stat() return jsonify({ "ok": True, "path": "office_bg_small.webp", "size": st.st_size, "msg": "已恢复初始底图", }) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/restore-last-generated-background", methods=["POST"]) def assets_restore_last_generated_background(): """Restore office_bg_small.webp from latest bg-history snapshot.""" guard = _require_asset_editor_auth() if guard: return guard try: target = FRONTEND_PATH / "office_bg_small.webp" if not target.exists(): return jsonify({"ok": False, "msg": "office_bg_small.webp 不存在"}), 404 if not os.path.isdir(BG_HISTORY_DIR): return jsonify({"ok": False, "msg": "暂无历史底图"}), 404 files = [ os.path.join(BG_HISTORY_DIR, x) for x in os.listdir(BG_HISTORY_DIR) if x.startswith("office_bg_small-") and x.endswith(".webp") ] if not files: return jsonify({"ok": False, "msg": "暂无历史底图"}), 404 latest = max(files, key=lambda p: os.path.getmtime(p)) bak = target.with_suffix(target.suffix + ".bak") shutil.copy2(target, bak) shutil.copy2(latest, target) st = target.stat() return jsonify({ "ok": True, "path": "office_bg_small.webp", "size": st.st_size, "from": os.path.relpath(latest, ROOT_DIR), "msg": "已回退到最近一次生成底图", }) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/home-favorites/list", methods=["GET"]) def assets_home_favorites_list(): guard = _require_asset_editor_auth() if guard: return guard try: data = _load_home_favorites_index() items = data.get("items") or [] out = [] for it in items: rel = (it.get("path") or "").strip() if not rel: continue abs_path = os.path.join(ROOT_DIR, rel) if not os.path.exists(abs_path): continue fn = os.path.basename(rel) out.append({ "id": it.get("id"), "path": rel, "url": f"/assets/home-favorites/file/{fn}", "thumb_url": f"/assets/home-favorites/file/{fn}", "created_at": it.get("created_at") or "", }) out.sort(key=lambda x: x.get("created_at") or "", reverse=True) return jsonify({"ok": True, "items": out}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/home-favorites/file/", methods=["GET"]) def assets_home_favorites_file(filename): guard = _require_asset_editor_auth() if guard: return guard return send_from_directory(HOME_FAVORITES_DIR, filename) @app.route("/assets/home-favorites/save-current", methods=["POST"]) def assets_home_favorites_save_current(): guard = _require_asset_editor_auth() if guard: return guard try: src = FRONTEND_PATH / "office_bg_small.webp" if not src.exists(): return jsonify({"ok": False, "msg": "office_bg_small.webp 不存在"}), 404 _ensure_home_favorites_index() ts = datetime.now().strftime("%Y%m%d-%H%M%S") item_id = f"home-{ts}" fn = f"{item_id}.webp" dst = os.path.join(HOME_FAVORITES_DIR, fn) shutil.copy2(str(src), dst) idx = _load_home_favorites_index() items = idx.get("items") or [] items.insert(0, { "id": item_id, "path": os.path.relpath(dst, ROOT_DIR), "created_at": datetime.now().isoformat(timespec="seconds"), }) # 控制收藏数量上限,清理最旧项 if len(items) > HOME_FAVORITES_MAX: extra = items[HOME_FAVORITES_MAX:] items = items[:HOME_FAVORITES_MAX] for it in extra: try: p = os.path.join(ROOT_DIR, it.get("path") or "") if os.path.exists(p): os.remove(p) except Exception: pass idx["items"] = items _save_home_favorites_index(idx) return jsonify({"ok": True, "id": item_id, "path": os.path.relpath(dst, ROOT_DIR), "msg": "已收藏当前地图"}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/home-favorites/delete", methods=["POST"]) def assets_home_favorites_delete(): guard = _require_asset_editor_auth() if guard: return guard try: data = request.get_json(silent=True) or {} item_id = (data.get("id") or "").strip() if not item_id: return jsonify({"ok": False, "msg": "缺少 id"}), 400 idx = _load_home_favorites_index() items = idx.get("items") or [] hit = next((x for x in items if (x.get("id") or "") == item_id), None) if not hit: return jsonify({"ok": False, "msg": "收藏项不存在"}), 404 rel = hit.get("path") or "" abs_path = os.path.join(ROOT_DIR, rel) if os.path.exists(abs_path): try: os.remove(abs_path) except Exception: pass idx["items"] = [x for x in items if (x.get("id") or "") != item_id] _save_home_favorites_index(idx) return jsonify({"ok": True, "id": item_id, "msg": "已删除收藏"}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/home-favorites/apply", methods=["POST"]) def assets_home_favorites_apply(): guard = _require_asset_editor_auth() if guard: return guard try: data = request.get_json(silent=True) or {} item_id = (data.get("id") or "").strip() if not item_id: return jsonify({"ok": False, "msg": "缺少 id"}), 400 idx = _load_home_favorites_index() items = idx.get("items") or [] hit = next((x for x in items if (x.get("id") or "") == item_id), None) if not hit: return jsonify({"ok": False, "msg": "收藏项不存在"}), 404 src = os.path.join(ROOT_DIR, hit.get("path") or "") if not os.path.exists(src): return jsonify({"ok": False, "msg": "收藏文件不存在"}), 404 target = FRONTEND_PATH / "office_bg_small.webp" if not target.exists(): return jsonify({"ok": False, "msg": "office_bg_small.webp 不存在"}), 404 bak = target.with_suffix(target.suffix + ".bak") shutil.copy2(str(target), str(bak)) shutil.copy2(src, str(target)) st = target.stat() return jsonify({"ok": True, "path": "office_bg_small.webp", "size": st.st_size, "from": hit.get("path"), "msg": "已应用收藏地图"}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/auth", methods=["POST"]) def assets_auth(): try: data = request.get_json(silent=True) or {} pwd = (data.get("password") or "").strip() if pwd and pwd == ASSET_DRAWER_PASS_DEFAULT: session["asset_editor_authed"] = True return jsonify({"ok": True, "msg": "认证成功"}) return jsonify({"ok": False, "msg": "验证码错误"}), 401 except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/auth/status", methods=["GET"]) def assets_auth_status(): return jsonify({ "ok": True, "authed": _is_asset_editor_authed(), "drawer_default_pass": ASSET_DRAWER_PASS_DEFAULT == "1234", }) @app.route("/assets/positions", methods=["GET"]) def assets_positions_get(): guard = _require_asset_editor_auth() if guard: return guard try: return jsonify({"ok": True, "items": load_asset_positions()}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/positions", methods=["POST"]) def assets_positions_set(): guard = _require_asset_editor_auth() if guard: return guard try: data = request.get_json(silent=True) or {} key = (data.get("key") or "").strip() x = data.get("x") y = data.get("y") scale = data.get("scale") if not key: return jsonify({"ok": False, "msg": "缺少 key"}), 400 if x is None or y is None: return jsonify({"ok": False, "msg": "缺少 x/y"}), 400 x = float(x) y = float(y) if scale is None: scale = 1.0 scale = float(scale) all_pos = load_asset_positions() all_pos[key] = {"x": x, "y": y, "scale": scale, "updated_at": datetime.now().isoformat()} save_asset_positions(all_pos) return jsonify({"ok": True, "key": key, "x": x, "y": y, "scale": scale}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/defaults", methods=["GET"]) def assets_defaults_get(): guard = _require_asset_editor_auth() if guard: return guard try: return jsonify({"ok": True, "items": load_asset_defaults()}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/defaults", methods=["POST"]) def assets_defaults_set(): guard = _require_asset_editor_auth() if guard: return guard try: data = request.get_json(silent=True) or {} key = (data.get("key") or "").strip() x = data.get("x") y = data.get("y") scale = data.get("scale") if not key: return jsonify({"ok": False, "msg": "缺少 key"}), 400 if x is None or y is None: return jsonify({"ok": False, "msg": "缺少 x/y"}), 400 x = float(x) y = float(y) if scale is None: scale = 1.0 scale = float(scale) all_defaults = load_asset_defaults() all_defaults[key] = {"x": x, "y": y, "scale": scale, "updated_at": datetime.now().isoformat()} save_asset_defaults(all_defaults) return jsonify({"ok": True, "key": key, "x": x, "y": y, "scale": scale}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/config/gemini", methods=["GET"]) def gemini_config_get(): guard = _require_asset_editor_auth() if guard: return guard try: cfg = load_runtime_config() key = (cfg.get("gemini_api_key") or "").strip() masked = ("*" * max(0, len(key) - 4)) + key[-4:] if key else "" return jsonify({ "ok": True, "has_api_key": bool(key), "api_key_masked": masked, "gemini_model": _normalize_user_model(cfg.get("gemini_model") or "nanobanana-pro"), }) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/config/gemini", methods=["POST"]) def gemini_config_set(): guard = _require_asset_editor_auth() if guard: return guard try: data = request.get_json(silent=True) or {} api_key = (data.get("api_key") or "").strip() model = _normalize_user_model((data.get("model") or "").strip() or "nanobanana-pro") payload = {"gemini_model": model} if api_key: payload["gemini_api_key"] = api_key save_runtime_config(payload) return jsonify({"ok": True, "msg": "Gemini 配置已保存"}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/restore-default", methods=["POST"]) def assets_restore_default(): guard = _require_asset_editor_auth() if guard: return guard try: data = request.get_json(silent=True) or {} rel_path = (data.get("path") or "").strip().lstrip("/") if not rel_path: return jsonify({"ok": False, "msg": "缺少 path"}), 400 target = (FRONTEND_PATH / rel_path).resolve() try: target.relative_to(FRONTEND_PATH.resolve()) except Exception: return jsonify({"ok": False, "msg": "非法 path"}), 400 if not target.exists(): return jsonify({"ok": False, "msg": "目标文件不存在"}), 404 root, ext = os.path.splitext(str(target)) default_path = root + ext + ".default" if not os.path.exists(default_path): return jsonify({"ok": False, "msg": "未找到默认资产快照"}), 404 # 回滚前保留上一版 bak = str(target) + ".bak" if os.path.exists(str(target)): shutil.copy2(str(target), bak) shutil.copy2(default_path, str(target)) st = os.stat(str(target)) return jsonify({"ok": True, "path": rel_path, "size": st.st_size, "msg": "已重置为默认资产"}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/restore-prev", methods=["POST"]) def assets_restore_prev(): guard = _require_asset_editor_auth() if guard: return guard try: data = request.get_json(silent=True) or {} rel_path = (data.get("path") or "").strip().lstrip("/") if not rel_path: return jsonify({"ok": False, "msg": "缺少 path"}), 400 target = (FRONTEND_PATH / rel_path).resolve() try: target.relative_to(FRONTEND_PATH.resolve()) except Exception: return jsonify({"ok": False, "msg": "非法 path"}), 400 bak = str(target) + ".bak" if not os.path.exists(bak): return jsonify({"ok": False, "msg": "未找到上一版备份"}), 404 shutil.copy2(str(target), bak + ".tmp") if os.path.exists(str(target)) else None shutil.copy2(bak, str(target)) st = os.stat(str(target)) return jsonify({"ok": True, "path": rel_path, "size": st.st_size, "msg": "已回退到上一版"}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 @app.route("/assets/upload", methods=["POST"]) def assets_upload(): guard = _require_asset_editor_auth() if guard: return guard try: rel_path = (request.form.get("path") or "").strip().lstrip("/") backup = (request.form.get("backup") or "1").strip() != "0" f = request.files.get("file") if not rel_path or f is None: return jsonify({"ok": False, "msg": "缺少 path 或 file"}), 400 target = (FRONTEND_PATH / rel_path).resolve() try: target.relative_to(FRONTEND_PATH.resolve()) except Exception: return jsonify({"ok": False, "msg": "非法 path"}), 400 if target.suffix.lower() not in ASSET_ALLOWED_EXTS: return jsonify({"ok": False, "msg": "仅允许上传图片/美术资源类型"}), 400 if not target.exists(): return jsonify({"ok": False, "msg": "目标文件不存在,请先从 /assets/list 选择 path"}), 404 target.parent.mkdir(parents=True, exist_ok=True) # 首次上传前固化默认资产快照,供“重置为默认资产”使用 default_snap = Path(str(target) + ".default") if not default_snap.exists(): try: shutil.copy2(target, default_snap) except Exception: pass if backup: bak = target.with_suffix(target.suffix + ".bak") shutil.copy2(target, bak) auto_sheet = (request.form.get("auto_spritesheet") or "0").strip() == "1" ext_name = (f.filename or "").lower() if auto_sheet and target.suffix.lower() in {".webp", ".png"}: with tempfile.NamedTemporaryFile(suffix=os.path.splitext(ext_name)[1] or ".gif", delete=False) as tf: src_path = tf.name f.save(src_path) try: in_w, in_h = _probe_animated_frame_size(src_path) frame_w = int(request.form.get("frame_w") or (in_w or 64)) frame_h = int(request.form.get("frame_h") or (in_h or 64)) # 如果是静态图上传到精灵表目标,按网格切片而不是整图覆盖 if not (ext_name.endswith(".gif") or ext_name.endswith(".webp")) and Image is not None: try: with Image.open(src_path) as sim: sim = sim.convert("RGBA") sw, sh = sim.size if frame_w <= 0 or frame_h <= 0: frame_w, frame_h = sw, sh cols = max(1, sw // frame_w) rows = max(1, sh // frame_h) sheet_w = cols * frame_w sheet_h = rows * frame_h if sheet_w <= 0 or sheet_h <= 0: raise RuntimeError("静态图尺寸与帧规格不匹配") cropped = sim.crop((0, 0, sheet_w, sheet_h)) # 目标是 webp 仍按无损保存,避免像素损失 if target.suffix.lower() == ".webp": cropped.save(str(target), "WEBP", lossless=True, quality=100, method=6) else: cropped.save(str(target), "PNG") st = target.stat() return jsonify({ "ok": True, "path": rel_path, "size": st.st_size, "backup": backup, "converted": { "from": ext_name.split(".")[-1] if "." in ext_name else "image", "to": "webp_spritesheet" if target.suffix.lower() == ".webp" else "png_spritesheet", "frame_w": frame_w, "frame_h": frame_h, "columns": cols, "rows": rows, "frames": cols * rows, "preserve_original": False, "pixel_art": True, } }) finally: pass # 默认:优先保留输入帧尺寸;若前端传了强制值则按前端。 preserve_original_val = request.form.get("preserve_original") if preserve_original_val is None: preserve_original = True else: preserve_original = preserve_original_val.strip() == "1" pixel_art = (request.form.get("pixel_art") or "1").strip() == "1" req_cols = int(request.form.get("cols") or 0) req_rows = int(request.form.get("rows") or 0) sheet_path, cols, rows, frames, out_fw, out_fh = _animated_to_spritesheet( src_path, frame_w, frame_h, out_ext=target.suffix.lower(), preserve_original=preserve_original, pixel_art=pixel_art, cols=(req_cols if req_cols > 0 else None), rows=(req_rows if req_rows > 0 else None), ) shutil.move(sheet_path, str(target)) st = target.stat() from_type = "gif" if ext_name.endswith(".gif") else "webp" to_type = "webp_spritesheet" if target.suffix.lower() == ".webp" else "png_spritesheet" return jsonify({ "ok": True, "path": rel_path, "size": st.st_size, "backup": backup, "converted": { "from": from_type, "to": to_type, "frame_w": out_fw, "frame_h": out_fh, "columns": cols, "rows": rows, "frames": frames, "preserve_original": preserve_original, "pixel_art": pixel_art, } }) finally: try: os.remove(src_path) except Exception: pass f.save(str(target)) st = target.stat() return jsonify({"ok": True, "path": rel_path, "size": st.st_size, "backup": backup}) except Exception as e: return jsonify({"ok": False, "msg": str(e)}), 500 if __name__ == "__main__": raw_port = os.environ.get("STAR_BACKEND_PORT", "19000") try: backend_port = int(raw_port) except ValueError: backend_port = 19000 if backend_port <= 0: backend_port = 19000 print("=" * 50) print("Star Office UI - Backend State Service") print("=" * 50) print(f"State file: {STATE_FILE}") print(f"Listening on: http://0.0.0.0:{backend_port}") if backend_port != 19000: print(f"(Port override: set STAR_BACKEND_PORT to change; current: {raw_port})") else: print("(Set STAR_BACKEND_PORT to use a different port, e.g. 3009)") mode = "production" if is_production_mode() else "development" print(f"Mode: {mode}") if is_production_mode(): print("Security hardening: ENABLED (strict checks)") else: weak_flags = [] if not is_strong_secret(str(app.secret_key)): weak_flags.append("weak FLASK_SECRET_KEY/STAR_OFFICE_SECRET") if not is_strong_drawer_pass(ASSET_DRAWER_PASS_DEFAULT): weak_flags.append("weak ASSET_DRAWER_PASS") if weak_flags: print("Security hardening: WARNING (dev mode) -> " + ", ".join(weak_flags)) else: print("Security hardening: OK") print("=" * 50) app.run(host="0.0.0.0", port=backend_port, debug=False) ================================================ FILE: backend/memo_utils.py ================================================ #!/usr/bin/env python3 """Memo extraction helpers for Star Office backend. Reads and sanitizes daily memo content from memory/*.md for the yesterday-memo API. """ from __future__ import annotations from datetime import datetime, timedelta import random import re def get_yesterday_date_str() -> str: """Return yesterday's date as YYYY-MM-DD.""" yesterday = datetime.now() - timedelta(days=1) return yesterday.strftime("%Y-%m-%d") def sanitize_content(text: str) -> str: """Redact PII and sensitive patterns (OpenID, paths, IPs, email, phone) for safe display.""" text = re.sub(r'ou_[a-f0-9]+', '[用户]', text) text = re.sub(r'user_id="[^"]+"', 'user_id="[隐藏]"', text) text = re.sub(r'/root/[^"\s]+', '[路径]', text) text = re.sub(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', '[IP]', text) text = re.sub(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', '[邮箱]', text) text = re.sub(r'1[3-9]\d{9}', '[手机号]', text) return text def extract_memo_from_file(file_path: str) -> str: """Extract display-safe memo text from a memory markdown file; sanitizes and truncates with a short fallback.""" try: with open(file_path, "r", encoding="utf-8") as f: content = f.read() # 提取真实内容,不做过度包装 lines = content.strip().split("\n") # 提取核心要点 core_points = [] for line in lines: line = line.strip() if not line: continue if line.startswith("#"): continue if line.startswith("- "): core_points.append(line[2:].strip()) elif len(line) > 10: core_points.append(line) if not core_points: return "「昨日无事记录」\n\n若有恒,何必三更眠五更起;最无益,莫过一日曝十日寒。" # 从核心内容中提取 2-3 个关键点 selected_points = core_points[:3] # 睿智语录库 wisdom_quotes = [ "「工欲善其事,必先利其器。」", "「不积跬步,无以至千里;不积小流,无以成江海。」", "「知行合一,方可致远。」", "「业精于勤,荒于嬉;行成于思,毁于随。」", "「路漫漫其修远兮,吾将上下而求索。」", "「昨夜西风凋碧树,独上高楼,望尽天涯路。」", "「衣带渐宽终不悔,为伊消得人憔悴。」", "「众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。」", "「世事洞明皆学问,人情练达即文章。」", "「纸上得来终觉浅,绝知此事要躬行。」" ] quote = random.choice(wisdom_quotes) # 组合内容 result = [] # 添加核心内容 if selected_points: for point in selected_points: # 隐私清理 point = sanitize_content(point) # 截断过长的内容 if len(point) > 40: point = point[:37] + "..." # 每行最多 20 字 if len(point) <= 20: result.append(f"· {point}") else: # 按 20 字切分 for j in range(0, len(point), 20): chunk = point[j:j+20] if j == 0: result.append(f"· {chunk}") else: result.append(f" {chunk}") # 添加睿智语录 if quote: if len(quote) <= 20: result.append(f"\n{quote}") else: for j in range(0, len(quote), 20): chunk = quote[j:j+20] if j == 0: result.append(f"\n{chunk}") else: result.append(chunk) return "\n".join(result).strip() except Exception as e: print(f"extract_memo_from_file failed: {e}") return "「昨日记录加载失败」\n\n「往者不可谏,来者犹可追。」" ================================================ FILE: backend/requirements.txt ================================================ flask==3.0.2 pillow==10.4.0 ================================================ FILE: backend/run.sh ================================================ #!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Auto-load project env file when present. if [[ -f "$ROOT_DIR/.env" ]]; then set -a # shellcheck disable=SC1091 source "$ROOT_DIR/.env" set +a fi exec "$ROOT_DIR/.venv/bin/python" "$ROOT_DIR/backend/app.py" ================================================ FILE: backend/security_utils.py ================================================ #!/usr/bin/env python3 """Security helper utilities for Star Office backend. Production detection and validation for Flask secret and asset drawer password. """ from __future__ import annotations import os def is_production_mode() -> bool: """Return True if STAR_OFFICE_ENV or FLASK_ENV is prod/production.""" env = (os.getenv("STAR_OFFICE_ENV") or os.getenv("FLASK_ENV") or "").strip().lower() return env in {"prod", "production"} def is_strong_secret(secret: str) -> bool: """Return True if secret is at least 24 chars and does not contain weak markers (e.g. change-me, dev).""" if not secret: return False secret = secret.strip() if len(secret) < 24: return False weak_markers = {"change-me", "dev", "example", "test", "default"} low = secret.lower() return not any(m in low for m in weak_markers) def is_strong_drawer_pass(pwd: str) -> bool: """Return True if password is not default 1234 and has at least 8 characters.""" if not pwd: return False pwd = pwd.strip() if pwd == "1234": return False return len(pwd) >= 8 ================================================ FILE: backend/store_utils.py ================================================ #!/usr/bin/env python3 """Storage helper utilities for Star Office backend. JSON load/save for agents state, asset positions/defaults, runtime config, and join keys. """ from __future__ import annotations import json import os def _load_json(path: str): """Load JSON from a file; caller handles missing file or parse errors.""" with open(path, "r", encoding="utf-8") as f: return json.load(f) def _save_json(path: str, data): """Write data as JSON with UTF-8 and indent=2.""" with open(path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def load_agents_state(path: str, default_agents: list) -> list: """Load agents list from path; return default_agents if file missing or invalid.""" if os.path.exists(path): try: data = _load_json(path) if isinstance(data, list): return data except Exception: pass return list(default_agents) def save_agents_state(path: str, agents: list): """Persist agents list to path.""" _save_json(path, agents) def load_asset_positions(path: str) -> dict: """Load asset positions map from path; return {} if missing or invalid.""" if os.path.exists(path): try: data = _load_json(path) if isinstance(data, dict): return data except Exception: pass return {} def save_asset_positions(path: str, data: dict): """Persist asset positions to path.""" _save_json(path, data) def load_asset_defaults(path: str) -> dict: """Load asset defaults map from path; return {} if missing or invalid.""" if os.path.exists(path): try: data = _load_json(path) if isinstance(data, dict): return data except Exception: pass return {} def save_asset_defaults(path: str, data: dict): """Persist asset defaults to path.""" _save_json(path, data) def _normalize_user_model(model_name: str) -> str: """Map provider model names to canonical user-facing options (nanobanana-pro / nanobanana-2).""" m = (model_name or "").strip().lower() if m in {"nanobanana-pro", "nanobanana-2"}: return m if m in {"nano-banana-pro-preview", "gemini-3-pro-image-preview"}: return "nanobanana-pro" if m in {"gemini-2.5-flash-image", "gemini-2.0-flash-exp-image-generation"}: return "nanobanana-2" return "nanobanana-pro" def load_runtime_config(path: str) -> dict: """Load runtime config (gemini_api_key, gemini_model) from env and optional JSON file.""" base = { "gemini_api_key": os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY") or "", "gemini_model": _normalize_user_model(os.getenv("GEMINI_MODEL") or "nanobanana-pro"), } if os.path.exists(path): try: data = _load_json(path) if isinstance(data, dict): base.update({k: data.get(k, base.get(k)) for k in ["gemini_api_key", "gemini_model"]}) base["gemini_model"] = _normalize_user_model(base.get("gemini_model") or "nanobanana-pro") except Exception: pass return base def save_runtime_config(path: str, data: dict): """Merge data into current runtime config and save to path; chmod 0o600 on path.""" cfg = load_runtime_config(path) cfg.update(data or {}) _save_json(path, cfg) try: os.chmod(path, 0o600) except Exception: pass def load_join_keys(path: str) -> dict: """Load join keys structure from path; return {'keys': []} if missing or invalid.""" if os.path.exists(path): try: data = _load_json(path) if isinstance(data, dict) and isinstance(data.get("keys"), list): return data except Exception: pass return {"keys": []} def save_join_keys(path: str, data: dict): """Persist join keys to path.""" _save_json(path, data) ================================================ FILE: convert_to_webp.py ================================================ #!/usr/bin/env python3 """ 批量转换 PNG 资源为 WebP 格式 - 精灵图使用无损转换 - 背景图等使用有损转换(质量 85) """ import os from PIL import Image # 路径 FRONTEND_DIR = "/root/.openclaw/workspace/star-office-ui/frontend" STATIC_DIR = os.path.join(FRONTEND_DIR, "") # 文件分类配置 # 无损转换:精灵图、需要保持透明精度的 LOSSLESS_FILES = [ "star-idle-spritesheet.png", "star-researching-spritesheet.png", "star-working-spritesheet.png", "sofa-busy-spritesheet.png", "plants-spritesheet.png", "posters-spritesheet.png", "coffee-machine-spritesheet.png", "serverroom-spritesheet.png" ] # 有损转换:背景图等,质量 85 LOSSY_FILES = [ "office_bg.png", "sofa-idle.png", "desk.png" ] def convert_to_webp(input_path, output_path, lossless=True, quality=85): """转换单个文件为 WebP""" try: img = Image.open(input_path) # 保存为 WebP if lossless: img.save(output_path, 'WebP', lossless=True, method=6) else: img.save(output_path, 'WebP', quality=quality, method=6) # 计算文件大小 orig_size = os.path.getsize(input_path) new_size = os.path.getsize(output_path) savings = (1 - new_size / orig_size) * 100 print(f"✅ {os.path.basename(input_path)} -> {os.path.basename(output_path)}") print(f" 原大小: {orig_size/1024:.1f}KB -> 新大小: {new_size/1024:.1f}KB (-{savings:.1f}%)") return True except Exception as e: print(f"❌ {os.path.basename(input_path)} 转换失败: {e}") return False def main(): print("=" * 60) print("PNG → WebP 批量转换工具") print("=" * 60) # 检查目录 if not os.path.exists(STATIC_DIR): print(f"❌ 目录不存在: {STATIC_DIR}") return success_count = 0 fail_count = 0 print("\n📁 开始转换...\n") # 转换无损文件 print("--- 无损转换(精灵图)---") for filename in LOSSLESS_FILES: input_path = os.path.join(STATIC_DIR, filename) if not os.path.exists(input_path): print(f"⚠️ 文件不存在,跳过: {filename}") continue output_path = os.path.join(STATIC_DIR, filename.replace(".png", ".webp")) if convert_to_webp(input_path, output_path, lossless=True): success_count += 1 else: fail_count += 1 # 转换有损文件 print("\n--- 有损转换(背景图,质量 85)---") for filename in LOSSY_FILES: input_path = os.path.join(STATIC_DIR, filename) if not os.path.exists(input_path): print(f"⚠️ 文件不存在,跳过: {filename}") continue output_path = os.path.join(STATIC_DIR, filename.replace(".png", ".webp")) if convert_to_webp(input_path, output_path, lossless=False, quality=85): success_count += 1 else: fail_count += 1 print("\n" + "=" * 60) print(f"转换完成!成功: {success_count}, 失败: {fail_count}") print("=" * 60) print("\n📝 注意:") print(" - PNG 原文件已保留,不会删除") print(" - 需要修改前端代码引用 .webp 文件") print(" - 如需回滚,只需把代码改回引用 .png 即可") if __name__ == "__main__": main() ================================================ FILE: desktop-pet/README.md ================================================ # Star Office Tauri Desktop Shell 这个目录用于把 `Star-Office-UI` 包成桌面应用(透明窗口),并在启动时自动拉起后端进程。 ## 开发运行 先在仓库根目录准备 Python 环境: ```bash cd /Users/wangzhaohan/Documents/GitHub/Star-Office-UI uv venv .venv uv pip install -r backend/requirements.txt --python .venv/bin/python ``` 再启动 Tauri: ```bash cd /Users/wangzhaohan/Documents/GitHub/Star-Office-UI/desktop-pet npm install npm run dev ``` ## 自动拉起后端逻辑 - 优先使用:`../.venv/bin/python backend/app.py` - 回退到:`python3 backend/app.py` - 再回退到:`python backend/app.py` 窗口默认会跳转到: - `http://127.0.0.1:19000/?desktop=1` ## 可选环境变量 - `STAR_PROJECT_ROOT`:项目根目录(默认会自动探测) - `STAR_BACKEND_PYTHON`:自定义 Python 可执行路径 - `STAR_BACKEND_URL`:自定义桌面窗口打开的 URL ================================================ FILE: desktop-pet/STATE_API.md ================================================ # 桌宠状态对接说明(openclaw 用) 桌宠通过读取 **state.json** 获取当前状态并刷新表现(头顶图标/emoji、气泡文案、角色动画、寻路目标)。openclaw 需要**写入或更新**该文件以驱动桌宠。 --- ## 1. 文件位置 - **路径**:与桌宠工作目录下的 `state.json`(桌宠启动时会解析项目根目录,即包含 `state.json` 和 `layers/` 的目录)。 - **格式**:UTF-8 JSON。 --- ## 2. state.json 结构 ```json { "state": "idle", "detail": "可选,状态说明,目前仅用于展示/调试", "progress": 0.0, "updated_at": "2025-02-27T12:00:00Z" } ``` | 字段 | 类型 | 必填 | 说明 | |--------------|---------|------|------| | `state` | string | 是 | 当前状态,见下表。桌宠每 ~2s 轮询读取。 | | `detail` | string | 否 | 可选描述,可被后续扩展用于气泡或调试。 | | `progress` | number | 否 | 0~1,可选进度,可被后续扩展。 | | `updated_at` | string | 否 | ISO8601 时间,可选。 | **只有 `state` 会影响桌宠行为**;其余字段可留空或省略。 --- ## 3. 状态取值(openclaw 应写入的 `state`) 桌宠只认下面这些**标准状态名**(小写)。写别的值会被当成 `idle` 或按别名映射。 | state 值 | 含义 | 桌宠表现概要 | |----------------|----------------|--------------| | `idle` | 摸鱼/无任务 | 💤 呼吸动画,随机闲逛 | | `writing` | 写作/记笔记 | Word 图标,走到 writing POI | | `receiving` | 收消息 | Hangouts 图标,走到 receiving POI | | `replying` | 回复消息 | Glovo 图标,走到 replying POI | | `researching` | 调研/查资料 | Google 图标,走到 researching POI | | `executing` | 执行任务/跑任务 | ⚡ emoji,走到 executing POI | | `syncing` | 同步/备份 | ☁️ emoji,走到 syncing POI | | `error` | 出错 | ❗ emoji,走到 error POI | POI 在 `layers/map.json` 的 `pois` 里配置;状态变化时桌宠会寻路到对应格子。 --- ## 4. 别名映射(可选) 若 openclaw 侧用不同名字,桌宠前端会先做一次**别名 → 标准状态**的映射,再按上表表现: | openclaw 可写的 state | 映射为 | |------------------------|--------| | `working` | `writing` | | `run` | `executing` | | `running` | `executing` | | `sync` | `syncing` | | `research` | `researching` | 未在上述列表中的 `state` 会视为 `idle`。 --- ## 5. openclaw 需要“跳”什么 - **写 state.json**:在约定目录下创建/覆盖 `state.json`,保证 `state` 为上面 8 个标准状态之一(或 5 个别名之一)。 - **何时写**:状态变化时写一次即可;桌宠轮询间隔约 2 秒,无需高频写入。 - **示例** - 开始写文档:`{ "state": "writing" }` - 收到消息:`{ "state": "receiving" }` - 正在回复:`{ "state": "replying" }` - 查资料:`{ "state": "researching" }` - 执行任务:`{ "state": "executing" }` - 同步中:`{ "state": "syncing" }` - 出错:`{ "state": "error" }` - 摸鱼/无任务:`{ "state": "idle" }` 按上述方式更新 `state.json`,即可与当前桌宠状态和 POI 行为一致。 ================================================ FILE: desktop-pet/package.json ================================================ { "name": "star-desktop-pet", "private": true, "version": "0.1.0", "scripts": { "dev": "STAR_PROJECT_ROOT=.. tauri dev", "build": "STAR_PROJECT_ROOT=.. tauri build", "tauri": "tauri" }, "devDependencies": { "@tauri-apps/cli": "^2" } } ================================================ FILE: desktop-pet/src/index.html ================================================
================================================ FILE: desktop-pet/src/minimized.html ================================================ Star Mini
加载中...
点击形象恢复主窗口
================================================ FILE: desktop-pet/src-tauri/Cargo.toml ================================================ [package] name = "star-desktop-pet" version = "0.1.0" edition = "2021" [lib] name = "star_desktop_pet_lib" crate-type = ["lib"] [build-dependencies] tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = ["macos-private-api"] } serde = { version = "1", features = ["derive"] } serde_json = "1" base64 = "0.22" ================================================ FILE: desktop-pet/src-tauri/build.rs ================================================ fn main() { tauri_build::build() } ================================================ FILE: desktop-pet/src-tauri/capabilities/default.json ================================================ { "identifier": "default", "description": "Default capabilities for the desktop pet", "windows": ["main", "mini"], "remote": { "urls": [ "http://127.0.0.1:*", "http://localhost:*" ] }, "permissions": [ "core:default", "core:window:allow-start-dragging", "core:window:allow-close", "core:window:allow-set-size" ] } ================================================ FILE: desktop-pet/src-tauri/gen/schemas/acl-manifests.json ================================================ {"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null}} ================================================ FILE: desktop-pet/src-tauri/gen/schemas/capabilities.json ================================================ {"default":{"identifier":"default","description":"Default capabilities for the desktop pet","remote":{"urls":["http://127.0.0.1:*","http://localhost:*"]},"local":true,"windows":["main","mini"],"permissions":["core:default","core:window:allow-start-dragging","core:window:allow-close","core:window:allow-set-size"]}} ================================================ FILE: desktop-pet/src-tauri/gen/schemas/desktop-schema.json ================================================ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "CapabilityFile", "description": "Capability formats accepted in a capability file.", "anyOf": [ { "description": "A single capability.", "allOf": [ { "$ref": "#/definitions/Capability" } ] }, { "description": "A list of capabilities.", "type": "array", "items": { "$ref": "#/definitions/Capability" } }, { "description": "A list of capabilities.", "type": "object", "required": [ "capabilities" ], "properties": { "capabilities": { "description": "The list of capabilities.", "type": "array", "items": { "$ref": "#/definitions/Capability" } } } } ], "definitions": { "Capability": { "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", "type": "object", "required": [ "identifier", "permissions" ], "properties": { "identifier": { "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", "type": "string" }, "description": { "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", "default": "", "type": "string" }, "remote": { "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", "anyOf": [ { "$ref": "#/definitions/CapabilityRemote" }, { "type": "null" } ] }, "local": { "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", "default": true, "type": "boolean" }, "windows": { "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", "type": "array", "items": { "type": "string" } }, "webviews": { "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", "type": "array", "items": { "type": "string" } }, "permissions": { "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", "type": "array", "items": { "$ref": "#/definitions/PermissionEntry" }, "uniqueItems": true }, "platforms": { "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/Target" } } } }, "CapabilityRemote": { "description": "Configuration for remote URLs that are associated with the capability.", "type": "object", "required": [ "urls" ], "properties": { "urls": { "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", "type": "array", "items": { "type": "string" } } } }, "PermissionEntry": { "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", "anyOf": [ { "description": "Reference a permission or permission set by identifier.", "allOf": [ { "$ref": "#/definitions/Identifier" } ] }, { "description": "Reference a permission or permission set by identifier and extends its scope.", "type": "object", "allOf": [ { "properties": { "identifier": { "description": "Identifier of the permission or permission set.", "allOf": [ { "$ref": "#/definitions/Identifier" } ] }, "allow": { "description": "Data that defines what is allowed by the scope.", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/Value" } }, "deny": { "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/Value" } } } } ], "required": [ "identifier" ] } ] }, "Identifier": { "description": "Permission identifier", "oneOf": [ { "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", "type": "string", "const": "core:default", "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" }, { "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", "type": "string", "const": "core:app:default", "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" }, { "description": "Enables the app_hide command without any pre-configured scope.", "type": "string", "const": "core:app:allow-app-hide", "markdownDescription": "Enables the app_hide command without any pre-configured scope." }, { "description": "Enables the app_show command without any pre-configured scope.", "type": "string", "const": "core:app:allow-app-show", "markdownDescription": "Enables the app_show command without any pre-configured scope." }, { "description": "Enables the bundle_type command without any pre-configured scope.", "type": "string", "const": "core:app:allow-bundle-type", "markdownDescription": "Enables the bundle_type command without any pre-configured scope." }, { "description": "Enables the default_window_icon command without any pre-configured scope.", "type": "string", "const": "core:app:allow-default-window-icon", "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." }, { "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", "type": "string", "const": "core:app:allow-fetch-data-store-identifiers", "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." }, { "description": "Enables the identifier command without any pre-configured scope.", "type": "string", "const": "core:app:allow-identifier", "markdownDescription": "Enables the identifier command without any pre-configured scope." }, { "description": "Enables the name command without any pre-configured scope.", "type": "string", "const": "core:app:allow-name", "markdownDescription": "Enables the name command without any pre-configured scope." }, { "description": "Enables the register_listener command without any pre-configured scope.", "type": "string", "const": "core:app:allow-register-listener", "markdownDescription": "Enables the register_listener command without any pre-configured scope." }, { "description": "Enables the remove_data_store command without any pre-configured scope.", "type": "string", "const": "core:app:allow-remove-data-store", "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." }, { "description": "Enables the remove_listener command without any pre-configured scope.", "type": "string", "const": "core:app:allow-remove-listener", "markdownDescription": "Enables the remove_listener command without any pre-configured scope." }, { "description": "Enables the set_app_theme command without any pre-configured scope.", "type": "string", "const": "core:app:allow-set-app-theme", "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." }, { "description": "Enables the set_dock_visibility command without any pre-configured scope.", "type": "string", "const": "core:app:allow-set-dock-visibility", "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." }, { "description": "Enables the tauri_version command without any pre-configured scope.", "type": "string", "const": "core:app:allow-tauri-version", "markdownDescription": "Enables the tauri_version command without any pre-configured scope." }, { "description": "Enables the version command without any pre-configured scope.", "type": "string", "const": "core:app:allow-version", "markdownDescription": "Enables the version command without any pre-configured scope." }, { "description": "Denies the app_hide command without any pre-configured scope.", "type": "string", "const": "core:app:deny-app-hide", "markdownDescription": "Denies the app_hide command without any pre-configured scope." }, { "description": "Denies the app_show command without any pre-configured scope.", "type": "string", "const": "core:app:deny-app-show", "markdownDescription": "Denies the app_show command without any pre-configured scope." }, { "description": "Denies the bundle_type command without any pre-configured scope.", "type": "string", "const": "core:app:deny-bundle-type", "markdownDescription": "Denies the bundle_type command without any pre-configured scope." }, { "description": "Denies the default_window_icon command without any pre-configured scope.", "type": "string", "const": "core:app:deny-default-window-icon", "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." }, { "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", "type": "string", "const": "core:app:deny-fetch-data-store-identifiers", "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." }, { "description": "Denies the identifier command without any pre-configured scope.", "type": "string", "const": "core:app:deny-identifier", "markdownDescription": "Denies the identifier command without any pre-configured scope." }, { "description": "Denies the name command without any pre-configured scope.", "type": "string", "const": "core:app:deny-name", "markdownDescription": "Denies the name command without any pre-configured scope." }, { "description": "Denies the register_listener command without any pre-configured scope.", "type": "string", "const": "core:app:deny-register-listener", "markdownDescription": "Denies the register_listener command without any pre-configured scope." }, { "description": "Denies the remove_data_store command without any pre-configured scope.", "type": "string", "const": "core:app:deny-remove-data-store", "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." }, { "description": "Denies the remove_listener command without any pre-configured scope.", "type": "string", "const": "core:app:deny-remove-listener", "markdownDescription": "Denies the remove_listener command without any pre-configured scope." }, { "description": "Denies the set_app_theme command without any pre-configured scope.", "type": "string", "const": "core:app:deny-set-app-theme", "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." }, { "description": "Denies the set_dock_visibility command without any pre-configured scope.", "type": "string", "const": "core:app:deny-set-dock-visibility", "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." }, { "description": "Denies the tauri_version command without any pre-configured scope.", "type": "string", "const": "core:app:deny-tauri-version", "markdownDescription": "Denies the tauri_version command without any pre-configured scope." }, { "description": "Denies the version command without any pre-configured scope.", "type": "string", "const": "core:app:deny-version", "markdownDescription": "Denies the version command without any pre-configured scope." }, { "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", "type": "string", "const": "core:event:default", "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" }, { "description": "Enables the emit command without any pre-configured scope.", "type": "string", "const": "core:event:allow-emit", "markdownDescription": "Enables the emit command without any pre-configured scope." }, { "description": "Enables the emit_to command without any pre-configured scope.", "type": "string", "const": "core:event:allow-emit-to", "markdownDescription": "Enables the emit_to command without any pre-configured scope." }, { "description": "Enables the listen command without any pre-configured scope.", "type": "string", "const": "core:event:allow-listen", "markdownDescription": "Enables the listen command without any pre-configured scope." }, { "description": "Enables the unlisten command without any pre-configured scope.", "type": "string", "const": "core:event:allow-unlisten", "markdownDescription": "Enables the unlisten command without any pre-configured scope." }, { "description": "Denies the emit command without any pre-configured scope.", "type": "string", "const": "core:event:deny-emit", "markdownDescription": "Denies the emit command without any pre-configured scope." }, { "description": "Denies the emit_to command without any pre-configured scope.", "type": "string", "const": "core:event:deny-emit-to", "markdownDescription": "Denies the emit_to command without any pre-configured scope." }, { "description": "Denies the listen command without any pre-configured scope.", "type": "string", "const": "core:event:deny-listen", "markdownDescription": "Denies the listen command without any pre-configured scope." }, { "description": "Denies the unlisten command without any pre-configured scope.", "type": "string", "const": "core:event:deny-unlisten", "markdownDescription": "Denies the unlisten command without any pre-configured scope." }, { "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", "type": "string", "const": "core:image:default", "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" }, { "description": "Enables the from_bytes command without any pre-configured scope.", "type": "string", "const": "core:image:allow-from-bytes", "markdownDescription": "Enables the from_bytes command without any pre-configured scope." }, { "description": "Enables the from_path command without any pre-configured scope.", "type": "string", "const": "core:image:allow-from-path", "markdownDescription": "Enables the from_path command without any pre-configured scope." }, { "description": "Enables the new command without any pre-configured scope.", "type": "string", "const": "core:image:allow-new", "markdownDescription": "Enables the new command without any pre-configured scope." }, { "description": "Enables the rgba command without any pre-configured scope.", "type": "string", "const": "core:image:allow-rgba", "markdownDescription": "Enables the rgba command without any pre-configured scope." }, { "description": "Enables the size command without any pre-configured scope.", "type": "string", "const": "core:image:allow-size", "markdownDescription": "Enables the size command without any pre-configured scope." }, { "description": "Denies the from_bytes command without any pre-configured scope.", "type": "string", "const": "core:image:deny-from-bytes", "markdownDescription": "Denies the from_bytes command without any pre-configured scope." }, { "description": "Denies the from_path command without any pre-configured scope.", "type": "string", "const": "core:image:deny-from-path", "markdownDescription": "Denies the from_path command without any pre-configured scope." }, { "description": "Denies the new command without any pre-configured scope.", "type": "string", "const": "core:image:deny-new", "markdownDescription": "Denies the new command without any pre-configured scope." }, { "description": "Denies the rgba command without any pre-configured scope.", "type": "string", "const": "core:image:deny-rgba", "markdownDescription": "Denies the rgba command without any pre-configured scope." }, { "description": "Denies the size command without any pre-configured scope.", "type": "string", "const": "core:image:deny-size", "markdownDescription": "Denies the size command without any pre-configured scope." }, { "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", "type": "string", "const": "core:menu:default", "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" }, { "description": "Enables the append command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-append", "markdownDescription": "Enables the append command without any pre-configured scope." }, { "description": "Enables the create_default command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-create-default", "markdownDescription": "Enables the create_default command without any pre-configured scope." }, { "description": "Enables the get command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-get", "markdownDescription": "Enables the get command without any pre-configured scope." }, { "description": "Enables the insert command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-insert", "markdownDescription": "Enables the insert command without any pre-configured scope." }, { "description": "Enables the is_checked command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-is-checked", "markdownDescription": "Enables the is_checked command without any pre-configured scope." }, { "description": "Enables the is_enabled command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-is-enabled", "markdownDescription": "Enables the is_enabled command without any pre-configured scope." }, { "description": "Enables the items command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-items", "markdownDescription": "Enables the items command without any pre-configured scope." }, { "description": "Enables the new command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-new", "markdownDescription": "Enables the new command without any pre-configured scope." }, { "description": "Enables the popup command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-popup", "markdownDescription": "Enables the popup command without any pre-configured scope." }, { "description": "Enables the prepend command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-prepend", "markdownDescription": "Enables the prepend command without any pre-configured scope." }, { "description": "Enables the remove command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-remove", "markdownDescription": "Enables the remove command without any pre-configured scope." }, { "description": "Enables the remove_at command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-remove-at", "markdownDescription": "Enables the remove_at command without any pre-configured scope." }, { "description": "Enables the set_accelerator command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-accelerator", "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." }, { "description": "Enables the set_as_app_menu command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-as-app-menu", "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." }, { "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-as-help-menu-for-nsapp", "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." }, { "description": "Enables the set_as_window_menu command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-as-window-menu", "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." }, { "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-as-windows-menu-for-nsapp", "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." }, { "description": "Enables the set_checked command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-checked", "markdownDescription": "Enables the set_checked command without any pre-configured scope." }, { "description": "Enables the set_enabled command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-enabled", "markdownDescription": "Enables the set_enabled command without any pre-configured scope." }, { "description": "Enables the set_icon command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-icon", "markdownDescription": "Enables the set_icon command without any pre-configured scope." }, { "description": "Enables the set_text command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-text", "markdownDescription": "Enables the set_text command without any pre-configured scope." }, { "description": "Enables the text command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-text", "markdownDescription": "Enables the text command without any pre-configured scope." }, { "description": "Denies the append command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-append", "markdownDescription": "Denies the append command without any pre-configured scope." }, { "description": "Denies the create_default command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-create-default", "markdownDescription": "Denies the create_default command without any pre-configured scope." }, { "description": "Denies the get command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-get", "markdownDescription": "Denies the get command without any pre-configured scope." }, { "description": "Denies the insert command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-insert", "markdownDescription": "Denies the insert command without any pre-configured scope." }, { "description": "Denies the is_checked command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-is-checked", "markdownDescription": "Denies the is_checked command without any pre-configured scope." }, { "description": "Denies the is_enabled command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-is-enabled", "markdownDescription": "Denies the is_enabled command without any pre-configured scope." }, { "description": "Denies the items command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-items", "markdownDescription": "Denies the items command without any pre-configured scope." }, { "description": "Denies the new command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-new", "markdownDescription": "Denies the new command without any pre-configured scope." }, { "description": "Denies the popup command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-popup", "markdownDescription": "Denies the popup command without any pre-configured scope." }, { "description": "Denies the prepend command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-prepend", "markdownDescription": "Denies the prepend command without any pre-configured scope." }, { "description": "Denies the remove command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-remove", "markdownDescription": "Denies the remove command without any pre-configured scope." }, { "description": "Denies the remove_at command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-remove-at", "markdownDescription": "Denies the remove_at command without any pre-configured scope." }, { "description": "Denies the set_accelerator command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-accelerator", "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." }, { "description": "Denies the set_as_app_menu command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-as-app-menu", "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." }, { "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-as-help-menu-for-nsapp", "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." }, { "description": "Denies the set_as_window_menu command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-as-window-menu", "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." }, { "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-as-windows-menu-for-nsapp", "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." }, { "description": "Denies the set_checked command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-checked", "markdownDescription": "Denies the set_checked command without any pre-configured scope." }, { "description": "Denies the set_enabled command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-enabled", "markdownDescription": "Denies the set_enabled command without any pre-configured scope." }, { "description": "Denies the set_icon command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-icon", "markdownDescription": "Denies the set_icon command without any pre-configured scope." }, { "description": "Denies the set_text command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-text", "markdownDescription": "Denies the set_text command without any pre-configured scope." }, { "description": "Denies the text command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-text", "markdownDescription": "Denies the text command without any pre-configured scope." }, { "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", "type": "string", "const": "core:path:default", "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" }, { "description": "Enables the basename command without any pre-configured scope.", "type": "string", "const": "core:path:allow-basename", "markdownDescription": "Enables the basename command without any pre-configured scope." }, { "description": "Enables the dirname command without any pre-configured scope.", "type": "string", "const": "core:path:allow-dirname", "markdownDescription": "Enables the dirname command without any pre-configured scope." }, { "description": "Enables the extname command without any pre-configured scope.", "type": "string", "const": "core:path:allow-extname", "markdownDescription": "Enables the extname command without any pre-configured scope." }, { "description": "Enables the is_absolute command without any pre-configured scope.", "type": "string", "const": "core:path:allow-is-absolute", "markdownDescription": "Enables the is_absolute command without any pre-configured scope." }, { "description": "Enables the join command without any pre-configured scope.", "type": "string", "const": "core:path:allow-join", "markdownDescription": "Enables the join command without any pre-configured scope." }, { "description": "Enables the normalize command without any pre-configured scope.", "type": "string", "const": "core:path:allow-normalize", "markdownDescription": "Enables the normalize command without any pre-configured scope." }, { "description": "Enables the resolve command without any pre-configured scope.", "type": "string", "const": "core:path:allow-resolve", "markdownDescription": "Enables the resolve command without any pre-configured scope." }, { "description": "Enables the resolve_directory command without any pre-configured scope.", "type": "string", "const": "core:path:allow-resolve-directory", "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." }, { "description": "Denies the basename command without any pre-configured scope.", "type": "string", "const": "core:path:deny-basename", "markdownDescription": "Denies the basename command without any pre-configured scope." }, { "description": "Denies the dirname command without any pre-configured scope.", "type": "string", "const": "core:path:deny-dirname", "markdownDescription": "Denies the dirname command without any pre-configured scope." }, { "description": "Denies the extname command without any pre-configured scope.", "type": "string", "const": "core:path:deny-extname", "markdownDescription": "Denies the extname command without any pre-configured scope." }, { "description": "Denies the is_absolute command without any pre-configured scope.", "type": "string", "const": "core:path:deny-is-absolute", "markdownDescription": "Denies the is_absolute command without any pre-configured scope." }, { "description": "Denies the join command without any pre-configured scope.", "type": "string", "const": "core:path:deny-join", "markdownDescription": "Denies the join command without any pre-configured scope." }, { "description": "Denies the normalize command without any pre-configured scope.", "type": "string", "const": "core:path:deny-normalize", "markdownDescription": "Denies the normalize command without any pre-configured scope." }, { "description": "Denies the resolve command without any pre-configured scope.", "type": "string", "const": "core:path:deny-resolve", "markdownDescription": "Denies the resolve command without any pre-configured scope." }, { "description": "Denies the resolve_directory command without any pre-configured scope.", "type": "string", "const": "core:path:deny-resolve-directory", "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." }, { "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", "type": "string", "const": "core:resources:default", "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" }, { "description": "Enables the close command without any pre-configured scope.", "type": "string", "const": "core:resources:allow-close", "markdownDescription": "Enables the close command without any pre-configured scope." }, { "description": "Denies the close command without any pre-configured scope.", "type": "string", "const": "core:resources:deny-close", "markdownDescription": "Denies the close command without any pre-configured scope." }, { "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", "type": "string", "const": "core:tray:default", "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" }, { "description": "Enables the get_by_id command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-get-by-id", "markdownDescription": "Enables the get_by_id command without any pre-configured scope." }, { "description": "Enables the new command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-new", "markdownDescription": "Enables the new command without any pre-configured scope." }, { "description": "Enables the remove_by_id command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-remove-by-id", "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." }, { "description": "Enables the set_icon command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-icon", "markdownDescription": "Enables the set_icon command without any pre-configured scope." }, { "description": "Enables the set_icon_as_template command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-icon-as-template", "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." }, { "description": "Enables the set_menu command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-menu", "markdownDescription": "Enables the set_menu command without any pre-configured scope." }, { "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-show-menu-on-left-click", "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." }, { "description": "Enables the set_temp_dir_path command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-temp-dir-path", "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." }, { "description": "Enables the set_title command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-title", "markdownDescription": "Enables the set_title command without any pre-configured scope." }, { "description": "Enables the set_tooltip command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-tooltip", "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." }, { "description": "Enables the set_visible command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-visible", "markdownDescription": "Enables the set_visible command without any pre-configured scope." }, { "description": "Denies the get_by_id command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-get-by-id", "markdownDescription": "Denies the get_by_id command without any pre-configured scope." }, { "description": "Denies the new command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-new", "markdownDescription": "Denies the new command without any pre-configured scope." }, { "description": "Denies the remove_by_id command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-remove-by-id", "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." }, { "description": "Denies the set_icon command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-icon", "markdownDescription": "Denies the set_icon command without any pre-configured scope." }, { "description": "Denies the set_icon_as_template command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-icon-as-template", "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." }, { "description": "Denies the set_menu command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-menu", "markdownDescription": "Denies the set_menu command without any pre-configured scope." }, { "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-show-menu-on-left-click", "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." }, { "description": "Denies the set_temp_dir_path command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-temp-dir-path", "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." }, { "description": "Denies the set_title command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-title", "markdownDescription": "Denies the set_title command without any pre-configured scope." }, { "description": "Denies the set_tooltip command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-tooltip", "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." }, { "description": "Denies the set_visible command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-visible", "markdownDescription": "Denies the set_visible command without any pre-configured scope." }, { "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", "type": "string", "const": "core:webview:default", "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" }, { "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-clear-all-browsing-data", "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." }, { "description": "Enables the create_webview command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-create-webview", "markdownDescription": "Enables the create_webview command without any pre-configured scope." }, { "description": "Enables the create_webview_window command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-create-webview-window", "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." }, { "description": "Enables the get_all_webviews command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-get-all-webviews", "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." }, { "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-internal-toggle-devtools", "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." }, { "description": "Enables the print command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-print", "markdownDescription": "Enables the print command without any pre-configured scope." }, { "description": "Enables the reparent command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-reparent", "markdownDescription": "Enables the reparent command without any pre-configured scope." }, { "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-set-webview-auto-resize", "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." }, { "description": "Enables the set_webview_background_color command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-set-webview-background-color", "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." }, { "description": "Enables the set_webview_focus command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-set-webview-focus", "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." }, { "description": "Enables the set_webview_position command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-set-webview-position", "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." }, { "description": "Enables the set_webview_size command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-set-webview-size", "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." }, { "description": "Enables the set_webview_zoom command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-set-webview-zoom", "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." }, { "description": "Enables the webview_close command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-webview-close", "markdownDescription": "Enables the webview_close command without any pre-configured scope." }, { "description": "Enables the webview_hide command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-webview-hide", "markdownDescription": "Enables the webview_hide command without any pre-configured scope." }, { "description": "Enables the webview_position command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-webview-position", "markdownDescription": "Enables the webview_position command without any pre-configured scope." }, { "description": "Enables the webview_show command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-webview-show", "markdownDescription": "Enables the webview_show command without any pre-configured scope." }, { "description": "Enables the webview_size command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-webview-size", "markdownDescription": "Enables the webview_size command without any pre-configured scope." }, { "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-clear-all-browsing-data", "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." }, { "description": "Denies the create_webview command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-create-webview", "markdownDescription": "Denies the create_webview command without any pre-configured scope." }, { "description": "Denies the create_webview_window command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-create-webview-window", "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." }, { "description": "Denies the get_all_webviews command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-get-all-webviews", "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." }, { "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-internal-toggle-devtools", "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." }, { "description": "Denies the print command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-print", "markdownDescription": "Denies the print command without any pre-configured scope." }, { "description": "Denies the reparent command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-reparent", "markdownDescription": "Denies the reparent command without any pre-configured scope." }, { "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-set-webview-auto-resize", "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." }, { "description": "Denies the set_webview_background_color command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-set-webview-background-color", "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." }, { "description": "Denies the set_webview_focus command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-set-webview-focus", "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." }, { "description": "Denies the set_webview_position command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-set-webview-position", "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." }, { "description": "Denies the set_webview_size command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-set-webview-size", "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." }, { "description": "Denies the set_webview_zoom command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-set-webview-zoom", "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." }, { "description": "Denies the webview_close command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-webview-close", "markdownDescription": "Denies the webview_close command without any pre-configured scope." }, { "description": "Denies the webview_hide command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-webview-hide", "markdownDescription": "Denies the webview_hide command without any pre-configured scope." }, { "description": "Denies the webview_position command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-webview-position", "markdownDescription": "Denies the webview_position command without any pre-configured scope." }, { "description": "Denies the webview_show command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-webview-show", "markdownDescription": "Denies the webview_show command without any pre-configured scope." }, { "description": "Denies the webview_size command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-webview-size", "markdownDescription": "Denies the webview_size command without any pre-configured scope." }, { "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", "type": "string", "const": "core:window:default", "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" }, { "description": "Enables the available_monitors command without any pre-configured scope.", "type": "string", "const": "core:window:allow-available-monitors", "markdownDescription": "Enables the available_monitors command without any pre-configured scope." }, { "description": "Enables the center command without any pre-configured scope.", "type": "string", "const": "core:window:allow-center", "markdownDescription": "Enables the center command without any pre-configured scope." }, { "description": "Enables the close command without any pre-configured scope.", "type": "string", "const": "core:window:allow-close", "markdownDescription": "Enables the close command without any pre-configured scope." }, { "description": "Enables the create command without any pre-configured scope.", "type": "string", "const": "core:window:allow-create", "markdownDescription": "Enables the create command without any pre-configured scope." }, { "description": "Enables the current_monitor command without any pre-configured scope.", "type": "string", "const": "core:window:allow-current-monitor", "markdownDescription": "Enables the current_monitor command without any pre-configured scope." }, { "description": "Enables the cursor_position command without any pre-configured scope.", "type": "string", "const": "core:window:allow-cursor-position", "markdownDescription": "Enables the cursor_position command without any pre-configured scope." }, { "description": "Enables the destroy command without any pre-configured scope.", "type": "string", "const": "core:window:allow-destroy", "markdownDescription": "Enables the destroy command without any pre-configured scope." }, { "description": "Enables the get_all_windows command without any pre-configured scope.", "type": "string", "const": "core:window:allow-get-all-windows", "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." }, { "description": "Enables the hide command without any pre-configured scope.", "type": "string", "const": "core:window:allow-hide", "markdownDescription": "Enables the hide command without any pre-configured scope." }, { "description": "Enables the inner_position command without any pre-configured scope.", "type": "string", "const": "core:window:allow-inner-position", "markdownDescription": "Enables the inner_position command without any pre-configured scope." }, { "description": "Enables the inner_size command without any pre-configured scope.", "type": "string", "const": "core:window:allow-inner-size", "markdownDescription": "Enables the inner_size command without any pre-configured scope." }, { "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", "type": "string", "const": "core:window:allow-internal-toggle-maximize", "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." }, { "description": "Enables the is_always_on_top command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-always-on-top", "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." }, { "description": "Enables the is_closable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-closable", "markdownDescription": "Enables the is_closable command without any pre-configured scope." }, { "description": "Enables the is_decorated command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-decorated", "markdownDescription": "Enables the is_decorated command without any pre-configured scope." }, { "description": "Enables the is_enabled command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-enabled", "markdownDescription": "Enables the is_enabled command without any pre-configured scope." }, { "description": "Enables the is_focused command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-focused", "markdownDescription": "Enables the is_focused command without any pre-configured scope." }, { "description": "Enables the is_fullscreen command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-fullscreen", "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." }, { "description": "Enables the is_maximizable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-maximizable", "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." }, { "description": "Enables the is_maximized command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-maximized", "markdownDescription": "Enables the is_maximized command without any pre-configured scope." }, { "description": "Enables the is_minimizable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-minimizable", "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." }, { "description": "Enables the is_minimized command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-minimized", "markdownDescription": "Enables the is_minimized command without any pre-configured scope." }, { "description": "Enables the is_resizable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-resizable", "markdownDescription": "Enables the is_resizable command without any pre-configured scope." }, { "description": "Enables the is_visible command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-visible", "markdownDescription": "Enables the is_visible command without any pre-configured scope." }, { "description": "Enables the maximize command without any pre-configured scope.", "type": "string", "const": "core:window:allow-maximize", "markdownDescription": "Enables the maximize command without any pre-configured scope." }, { "description": "Enables the minimize command without any pre-configured scope.", "type": "string", "const": "core:window:allow-minimize", "markdownDescription": "Enables the minimize command without any pre-configured scope." }, { "description": "Enables the monitor_from_point command without any pre-configured scope.", "type": "string", "const": "core:window:allow-monitor-from-point", "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." }, { "description": "Enables the outer_position command without any pre-configured scope.", "type": "string", "const": "core:window:allow-outer-position", "markdownDescription": "Enables the outer_position command without any pre-configured scope." }, { "description": "Enables the outer_size command without any pre-configured scope.", "type": "string", "const": "core:window:allow-outer-size", "markdownDescription": "Enables the outer_size command without any pre-configured scope." }, { "description": "Enables the primary_monitor command without any pre-configured scope.", "type": "string", "const": "core:window:allow-primary-monitor", "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." }, { "description": "Enables the request_user_attention command without any pre-configured scope.", "type": "string", "const": "core:window:allow-request-user-attention", "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." }, { "description": "Enables the scale_factor command without any pre-configured scope.", "type": "string", "const": "core:window:allow-scale-factor", "markdownDescription": "Enables the scale_factor command without any pre-configured scope." }, { "description": "Enables the set_always_on_bottom command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-always-on-bottom", "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." }, { "description": "Enables the set_always_on_top command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-always-on-top", "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." }, { "description": "Enables the set_background_color command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-background-color", "markdownDescription": "Enables the set_background_color command without any pre-configured scope." }, { "description": "Enables the set_badge_count command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-badge-count", "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." }, { "description": "Enables the set_badge_label command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-badge-label", "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." }, { "description": "Enables the set_closable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-closable", "markdownDescription": "Enables the set_closable command without any pre-configured scope." }, { "description": "Enables the set_content_protected command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-content-protected", "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." }, { "description": "Enables the set_cursor_grab command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-cursor-grab", "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." }, { "description": "Enables the set_cursor_icon command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-cursor-icon", "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." }, { "description": "Enables the set_cursor_position command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-cursor-position", "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." }, { "description": "Enables the set_cursor_visible command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-cursor-visible", "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." }, { "description": "Enables the set_decorations command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-decorations", "markdownDescription": "Enables the set_decorations command without any pre-configured scope." }, { "description": "Enables the set_effects command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-effects", "markdownDescription": "Enables the set_effects command without any pre-configured scope." }, { "description": "Enables the set_enabled command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-enabled", "markdownDescription": "Enables the set_enabled command without any pre-configured scope." }, { "description": "Enables the set_focus command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-focus", "markdownDescription": "Enables the set_focus command without any pre-configured scope." }, { "description": "Enables the set_focusable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-focusable", "markdownDescription": "Enables the set_focusable command without any pre-configured scope." }, { "description": "Enables the set_fullscreen command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-fullscreen", "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." }, { "description": "Enables the set_icon command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-icon", "markdownDescription": "Enables the set_icon command without any pre-configured scope." }, { "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-ignore-cursor-events", "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." }, { "description": "Enables the set_max_size command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-max-size", "markdownDescription": "Enables the set_max_size command without any pre-configured scope." }, { "description": "Enables the set_maximizable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-maximizable", "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." }, { "description": "Enables the set_min_size command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-min-size", "markdownDescription": "Enables the set_min_size command without any pre-configured scope." }, { "description": "Enables the set_minimizable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-minimizable", "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." }, { "description": "Enables the set_overlay_icon command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-overlay-icon", "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." }, { "description": "Enables the set_position command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-position", "markdownDescription": "Enables the set_position command without any pre-configured scope." }, { "description": "Enables the set_progress_bar command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-progress-bar", "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." }, { "description": "Enables the set_resizable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-resizable", "markdownDescription": "Enables the set_resizable command without any pre-configured scope." }, { "description": "Enables the set_shadow command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-shadow", "markdownDescription": "Enables the set_shadow command without any pre-configured scope." }, { "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-simple-fullscreen", "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." }, { "description": "Enables the set_size command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-size", "markdownDescription": "Enables the set_size command without any pre-configured scope." }, { "description": "Enables the set_size_constraints command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-size-constraints", "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." }, { "description": "Enables the set_skip_taskbar command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-skip-taskbar", "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." }, { "description": "Enables the set_theme command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-theme", "markdownDescription": "Enables the set_theme command without any pre-configured scope." }, { "description": "Enables the set_title command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-title", "markdownDescription": "Enables the set_title command without any pre-configured scope." }, { "description": "Enables the set_title_bar_style command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-title-bar-style", "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." }, { "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-visible-on-all-workspaces", "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." }, { "description": "Enables the show command without any pre-configured scope.", "type": "string", "const": "core:window:allow-show", "markdownDescription": "Enables the show command without any pre-configured scope." }, { "description": "Enables the start_dragging command without any pre-configured scope.", "type": "string", "const": "core:window:allow-start-dragging", "markdownDescription": "Enables the start_dragging command without any pre-configured scope." }, { "description": "Enables the start_resize_dragging command without any pre-configured scope.", "type": "string", "const": "core:window:allow-start-resize-dragging", "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." }, { "description": "Enables the theme command without any pre-configured scope.", "type": "string", "const": "core:window:allow-theme", "markdownDescription": "Enables the theme command without any pre-configured scope." }, { "description": "Enables the title command without any pre-configured scope.", "type": "string", "const": "core:window:allow-title", "markdownDescription": "Enables the title command without any pre-configured scope." }, { "description": "Enables the toggle_maximize command without any pre-configured scope.", "type": "string", "const": "core:window:allow-toggle-maximize", "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." }, { "description": "Enables the unmaximize command without any pre-configured scope.", "type": "string", "const": "core:window:allow-unmaximize", "markdownDescription": "Enables the unmaximize command without any pre-configured scope." }, { "description": "Enables the unminimize command without any pre-configured scope.", "type": "string", "const": "core:window:allow-unminimize", "markdownDescription": "Enables the unminimize command without any pre-configured scope." }, { "description": "Denies the available_monitors command without any pre-configured scope.", "type": "string", "const": "core:window:deny-available-monitors", "markdownDescription": "Denies the available_monitors command without any pre-configured scope." }, { "description": "Denies the center command without any pre-configured scope.", "type": "string", "const": "core:window:deny-center", "markdownDescription": "Denies the center command without any pre-configured scope." }, { "description": "Denies the close command without any pre-configured scope.", "type": "string", "const": "core:window:deny-close", "markdownDescription": "Denies the close command without any pre-configured scope." }, { "description": "Denies the create command without any pre-configured scope.", "type": "string", "const": "core:window:deny-create", "markdownDescription": "Denies the create command without any pre-configured scope." }, { "description": "Denies the current_monitor command without any pre-configured scope.", "type": "string", "const": "core:window:deny-current-monitor", "markdownDescription": "Denies the current_monitor command without any pre-configured scope." }, { "description": "Denies the cursor_position command without any pre-configured scope.", "type": "string", "const": "core:window:deny-cursor-position", "markdownDescription": "Denies the cursor_position command without any pre-configured scope." }, { "description": "Denies the destroy command without any pre-configured scope.", "type": "string", "const": "core:window:deny-destroy", "markdownDescription": "Denies the destroy command without any pre-configured scope." }, { "description": "Denies the get_all_windows command without any pre-configured scope.", "type": "string", "const": "core:window:deny-get-all-windows", "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." }, { "description": "Denies the hide command without any pre-configured scope.", "type": "string", "const": "core:window:deny-hide", "markdownDescription": "Denies the hide command without any pre-configured scope." }, { "description": "Denies the inner_position command without any pre-configured scope.", "type": "string", "const": "core:window:deny-inner-position", "markdownDescription": "Denies the inner_position command without any pre-configured scope." }, { "description": "Denies the inner_size command without any pre-configured scope.", "type": "string", "const": "core:window:deny-inner-size", "markdownDescription": "Denies the inner_size command without any pre-configured scope." }, { "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", "type": "string", "const": "core:window:deny-internal-toggle-maximize", "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." }, { "description": "Denies the is_always_on_top command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-always-on-top", "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." }, { "description": "Denies the is_closable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-closable", "markdownDescription": "Denies the is_closable command without any pre-configured scope." }, { "description": "Denies the is_decorated command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-decorated", "markdownDescription": "Denies the is_decorated command without any pre-configured scope." }, { "description": "Denies the is_enabled command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-enabled", "markdownDescription": "Denies the is_enabled command without any pre-configured scope." }, { "description": "Denies the is_focused command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-focused", "markdownDescription": "Denies the is_focused command without any pre-configured scope." }, { "description": "Denies the is_fullscreen command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-fullscreen", "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." }, { "description": "Denies the is_maximizable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-maximizable", "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." }, { "description": "Denies the is_maximized command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-maximized", "markdownDescription": "Denies the is_maximized command without any pre-configured scope." }, { "description": "Denies the is_minimizable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-minimizable", "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." }, { "description": "Denies the is_minimized command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-minimized", "markdownDescription": "Denies the is_minimized command without any pre-configured scope." }, { "description": "Denies the is_resizable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-resizable", "markdownDescription": "Denies the is_resizable command without any pre-configured scope." }, { "description": "Denies the is_visible command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-visible", "markdownDescription": "Denies the is_visible command without any pre-configured scope." }, { "description": "Denies the maximize command without any pre-configured scope.", "type": "string", "const": "core:window:deny-maximize", "markdownDescription": "Denies the maximize command without any pre-configured scope." }, { "description": "Denies the minimize command without any pre-configured scope.", "type": "string", "const": "core:window:deny-minimize", "markdownDescription": "Denies the minimize command without any pre-configured scope." }, { "description": "Denies the monitor_from_point command without any pre-configured scope.", "type": "string", "const": "core:window:deny-monitor-from-point", "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." }, { "description": "Denies the outer_position command without any pre-configured scope.", "type": "string", "const": "core:window:deny-outer-position", "markdownDescription": "Denies the outer_position command without any pre-configured scope." }, { "description": "Denies the outer_size command without any pre-configured scope.", "type": "string", "const": "core:window:deny-outer-size", "markdownDescription": "Denies the outer_size command without any pre-configured scope." }, { "description": "Denies the primary_monitor command without any pre-configured scope.", "type": "string", "const": "core:window:deny-primary-monitor", "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." }, { "description": "Denies the request_user_attention command without any pre-configured scope.", "type": "string", "const": "core:window:deny-request-user-attention", "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." }, { "description": "Denies the scale_factor command without any pre-configured scope.", "type": "string", "const": "core:window:deny-scale-factor", "markdownDescription": "Denies the scale_factor command without any pre-configured scope." }, { "description": "Denies the set_always_on_bottom command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-always-on-bottom", "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." }, { "description": "Denies the set_always_on_top command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-always-on-top", "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." }, { "description": "Denies the set_background_color command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-background-color", "markdownDescription": "Denies the set_background_color command without any pre-configured scope." }, { "description": "Denies the set_badge_count command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-badge-count", "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." }, { "description": "Denies the set_badge_label command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-badge-label", "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." }, { "description": "Denies the set_closable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-closable", "markdownDescription": "Denies the set_closable command without any pre-configured scope." }, { "description": "Denies the set_content_protected command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-content-protected", "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." }, { "description": "Denies the set_cursor_grab command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-cursor-grab", "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." }, { "description": "Denies the set_cursor_icon command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-cursor-icon", "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." }, { "description": "Denies the set_cursor_position command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-cursor-position", "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." }, { "description": "Denies the set_cursor_visible command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-cursor-visible", "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." }, { "description": "Denies the set_decorations command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-decorations", "markdownDescription": "Denies the set_decorations command without any pre-configured scope." }, { "description": "Denies the set_effects command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-effects", "markdownDescription": "Denies the set_effects command without any pre-configured scope." }, { "description": "Denies the set_enabled command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-enabled", "markdownDescription": "Denies the set_enabled command without any pre-configured scope." }, { "description": "Denies the set_focus command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-focus", "markdownDescription": "Denies the set_focus command without any pre-configured scope." }, { "description": "Denies the set_focusable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-focusable", "markdownDescription": "Denies the set_focusable command without any pre-configured scope." }, { "description": "Denies the set_fullscreen command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-fullscreen", "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." }, { "description": "Denies the set_icon command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-icon", "markdownDescription": "Denies the set_icon command without any pre-configured scope." }, { "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-ignore-cursor-events", "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." }, { "description": "Denies the set_max_size command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-max-size", "markdownDescription": "Denies the set_max_size command without any pre-configured scope." }, { "description": "Denies the set_maximizable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-maximizable", "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." }, { "description": "Denies the set_min_size command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-min-size", "markdownDescription": "Denies the set_min_size command without any pre-configured scope." }, { "description": "Denies the set_minimizable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-minimizable", "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." }, { "description": "Denies the set_overlay_icon command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-overlay-icon", "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." }, { "description": "Denies the set_position command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-position", "markdownDescription": "Denies the set_position command without any pre-configured scope." }, { "description": "Denies the set_progress_bar command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-progress-bar", "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." }, { "description": "Denies the set_resizable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-resizable", "markdownDescription": "Denies the set_resizable command without any pre-configured scope." }, { "description": "Denies the set_shadow command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-shadow", "markdownDescription": "Denies the set_shadow command without any pre-configured scope." }, { "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-simple-fullscreen", "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." }, { "description": "Denies the set_size command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-size", "markdownDescription": "Denies the set_size command without any pre-configured scope." }, { "description": "Denies the set_size_constraints command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-size-constraints", "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." }, { "description": "Denies the set_skip_taskbar command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-skip-taskbar", "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." }, { "description": "Denies the set_theme command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-theme", "markdownDescription": "Denies the set_theme command without any pre-configured scope." }, { "description": "Denies the set_title command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-title", "markdownDescription": "Denies the set_title command without any pre-configured scope." }, { "description": "Denies the set_title_bar_style command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-title-bar-style", "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." }, { "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-visible-on-all-workspaces", "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." }, { "description": "Denies the show command without any pre-configured scope.", "type": "string", "const": "core:window:deny-show", "markdownDescription": "Denies the show command without any pre-configured scope." }, { "description": "Denies the start_dragging command without any pre-configured scope.", "type": "string", "const": "core:window:deny-start-dragging", "markdownDescription": "Denies the start_dragging command without any pre-configured scope." }, { "description": "Denies the start_resize_dragging command without any pre-configured scope.", "type": "string", "const": "core:window:deny-start-resize-dragging", "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." }, { "description": "Denies the theme command without any pre-configured scope.", "type": "string", "const": "core:window:deny-theme", "markdownDescription": "Denies the theme command without any pre-configured scope." }, { "description": "Denies the title command without any pre-configured scope.", "type": "string", "const": "core:window:deny-title", "markdownDescription": "Denies the title command without any pre-configured scope." }, { "description": "Denies the toggle_maximize command without any pre-configured scope.", "type": "string", "const": "core:window:deny-toggle-maximize", "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." }, { "description": "Denies the unmaximize command without any pre-configured scope.", "type": "string", "const": "core:window:deny-unmaximize", "markdownDescription": "Denies the unmaximize command without any pre-configured scope." }, { "description": "Denies the unminimize command without any pre-configured scope.", "type": "string", "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." } ] }, "Value": { "description": "All supported ACL values.", "anyOf": [ { "description": "Represents a null JSON value.", "type": "null" }, { "description": "Represents a [`bool`].", "type": "boolean" }, { "description": "Represents a valid ACL [`Number`].", "allOf": [ { "$ref": "#/definitions/Number" } ] }, { "description": "Represents a [`String`].", "type": "string" }, { "description": "Represents a list of other [`Value`]s.", "type": "array", "items": { "$ref": "#/definitions/Value" } }, { "description": "Represents a map of [`String`] keys to [`Value`]s.", "type": "object", "additionalProperties": { "$ref": "#/definitions/Value" } } ] }, "Number": { "description": "A valid ACL number.", "anyOf": [ { "description": "Represents an [`i64`].", "type": "integer", "format": "int64" }, { "description": "Represents a [`f64`].", "type": "number", "format": "double" } ] }, "Target": { "description": "Platform target.", "oneOf": [ { "description": "MacOS.", "type": "string", "enum": [ "macOS" ] }, { "description": "Windows.", "type": "string", "enum": [ "windows" ] }, { "description": "Linux.", "type": "string", "enum": [ "linux" ] }, { "description": "Android.", "type": "string", "enum": [ "android" ] }, { "description": "iOS.", "type": "string", "enum": [ "iOS" ] } ] } } } ================================================ FILE: desktop-pet/src-tauri/gen/schemas/macOS-schema.json ================================================ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "CapabilityFile", "description": "Capability formats accepted in a capability file.", "anyOf": [ { "description": "A single capability.", "allOf": [ { "$ref": "#/definitions/Capability" } ] }, { "description": "A list of capabilities.", "type": "array", "items": { "$ref": "#/definitions/Capability" } }, { "description": "A list of capabilities.", "type": "object", "required": [ "capabilities" ], "properties": { "capabilities": { "description": "The list of capabilities.", "type": "array", "items": { "$ref": "#/definitions/Capability" } } } } ], "definitions": { "Capability": { "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", "type": "object", "required": [ "identifier", "permissions" ], "properties": { "identifier": { "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", "type": "string" }, "description": { "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", "default": "", "type": "string" }, "remote": { "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", "anyOf": [ { "$ref": "#/definitions/CapabilityRemote" }, { "type": "null" } ] }, "local": { "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", "default": true, "type": "boolean" }, "windows": { "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", "type": "array", "items": { "type": "string" } }, "webviews": { "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", "type": "array", "items": { "type": "string" } }, "permissions": { "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", "type": "array", "items": { "$ref": "#/definitions/PermissionEntry" }, "uniqueItems": true }, "platforms": { "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/Target" } } } }, "CapabilityRemote": { "description": "Configuration for remote URLs that are associated with the capability.", "type": "object", "required": [ "urls" ], "properties": { "urls": { "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", "type": "array", "items": { "type": "string" } } } }, "PermissionEntry": { "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", "anyOf": [ { "description": "Reference a permission or permission set by identifier.", "allOf": [ { "$ref": "#/definitions/Identifier" } ] }, { "description": "Reference a permission or permission set by identifier and extends its scope.", "type": "object", "allOf": [ { "properties": { "identifier": { "description": "Identifier of the permission or permission set.", "allOf": [ { "$ref": "#/definitions/Identifier" } ] }, "allow": { "description": "Data that defines what is allowed by the scope.", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/Value" } }, "deny": { "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/Value" } } } } ], "required": [ "identifier" ] } ] }, "Identifier": { "description": "Permission identifier", "oneOf": [ { "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", "type": "string", "const": "core:default", "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" }, { "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", "type": "string", "const": "core:app:default", "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" }, { "description": "Enables the app_hide command without any pre-configured scope.", "type": "string", "const": "core:app:allow-app-hide", "markdownDescription": "Enables the app_hide command without any pre-configured scope." }, { "description": "Enables the app_show command without any pre-configured scope.", "type": "string", "const": "core:app:allow-app-show", "markdownDescription": "Enables the app_show command without any pre-configured scope." }, { "description": "Enables the bundle_type command without any pre-configured scope.", "type": "string", "const": "core:app:allow-bundle-type", "markdownDescription": "Enables the bundle_type command without any pre-configured scope." }, { "description": "Enables the default_window_icon command without any pre-configured scope.", "type": "string", "const": "core:app:allow-default-window-icon", "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." }, { "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", "type": "string", "const": "core:app:allow-fetch-data-store-identifiers", "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." }, { "description": "Enables the identifier command without any pre-configured scope.", "type": "string", "const": "core:app:allow-identifier", "markdownDescription": "Enables the identifier command without any pre-configured scope." }, { "description": "Enables the name command without any pre-configured scope.", "type": "string", "const": "core:app:allow-name", "markdownDescription": "Enables the name command without any pre-configured scope." }, { "description": "Enables the register_listener command without any pre-configured scope.", "type": "string", "const": "core:app:allow-register-listener", "markdownDescription": "Enables the register_listener command without any pre-configured scope." }, { "description": "Enables the remove_data_store command without any pre-configured scope.", "type": "string", "const": "core:app:allow-remove-data-store", "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." }, { "description": "Enables the remove_listener command without any pre-configured scope.", "type": "string", "const": "core:app:allow-remove-listener", "markdownDescription": "Enables the remove_listener command without any pre-configured scope." }, { "description": "Enables the set_app_theme command without any pre-configured scope.", "type": "string", "const": "core:app:allow-set-app-theme", "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." }, { "description": "Enables the set_dock_visibility command without any pre-configured scope.", "type": "string", "const": "core:app:allow-set-dock-visibility", "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." }, { "description": "Enables the tauri_version command without any pre-configured scope.", "type": "string", "const": "core:app:allow-tauri-version", "markdownDescription": "Enables the tauri_version command without any pre-configured scope." }, { "description": "Enables the version command without any pre-configured scope.", "type": "string", "const": "core:app:allow-version", "markdownDescription": "Enables the version command without any pre-configured scope." }, { "description": "Denies the app_hide command without any pre-configured scope.", "type": "string", "const": "core:app:deny-app-hide", "markdownDescription": "Denies the app_hide command without any pre-configured scope." }, { "description": "Denies the app_show command without any pre-configured scope.", "type": "string", "const": "core:app:deny-app-show", "markdownDescription": "Denies the app_show command without any pre-configured scope." }, { "description": "Denies the bundle_type command without any pre-configured scope.", "type": "string", "const": "core:app:deny-bundle-type", "markdownDescription": "Denies the bundle_type command without any pre-configured scope." }, { "description": "Denies the default_window_icon command without any pre-configured scope.", "type": "string", "const": "core:app:deny-default-window-icon", "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." }, { "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", "type": "string", "const": "core:app:deny-fetch-data-store-identifiers", "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." }, { "description": "Denies the identifier command without any pre-configured scope.", "type": "string", "const": "core:app:deny-identifier", "markdownDescription": "Denies the identifier command without any pre-configured scope." }, { "description": "Denies the name command without any pre-configured scope.", "type": "string", "const": "core:app:deny-name", "markdownDescription": "Denies the name command without any pre-configured scope." }, { "description": "Denies the register_listener command without any pre-configured scope.", "type": "string", "const": "core:app:deny-register-listener", "markdownDescription": "Denies the register_listener command without any pre-configured scope." }, { "description": "Denies the remove_data_store command without any pre-configured scope.", "type": "string", "const": "core:app:deny-remove-data-store", "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." }, { "description": "Denies the remove_listener command without any pre-configured scope.", "type": "string", "const": "core:app:deny-remove-listener", "markdownDescription": "Denies the remove_listener command without any pre-configured scope." }, { "description": "Denies the set_app_theme command without any pre-configured scope.", "type": "string", "const": "core:app:deny-set-app-theme", "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." }, { "description": "Denies the set_dock_visibility command without any pre-configured scope.", "type": "string", "const": "core:app:deny-set-dock-visibility", "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." }, { "description": "Denies the tauri_version command without any pre-configured scope.", "type": "string", "const": "core:app:deny-tauri-version", "markdownDescription": "Denies the tauri_version command without any pre-configured scope." }, { "description": "Denies the version command without any pre-configured scope.", "type": "string", "const": "core:app:deny-version", "markdownDescription": "Denies the version command without any pre-configured scope." }, { "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", "type": "string", "const": "core:event:default", "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" }, { "description": "Enables the emit command without any pre-configured scope.", "type": "string", "const": "core:event:allow-emit", "markdownDescription": "Enables the emit command without any pre-configured scope." }, { "description": "Enables the emit_to command without any pre-configured scope.", "type": "string", "const": "core:event:allow-emit-to", "markdownDescription": "Enables the emit_to command without any pre-configured scope." }, { "description": "Enables the listen command without any pre-configured scope.", "type": "string", "const": "core:event:allow-listen", "markdownDescription": "Enables the listen command without any pre-configured scope." }, { "description": "Enables the unlisten command without any pre-configured scope.", "type": "string", "const": "core:event:allow-unlisten", "markdownDescription": "Enables the unlisten command without any pre-configured scope." }, { "description": "Denies the emit command without any pre-configured scope.", "type": "string", "const": "core:event:deny-emit", "markdownDescription": "Denies the emit command without any pre-configured scope." }, { "description": "Denies the emit_to command without any pre-configured scope.", "type": "string", "const": "core:event:deny-emit-to", "markdownDescription": "Denies the emit_to command without any pre-configured scope." }, { "description": "Denies the listen command without any pre-configured scope.", "type": "string", "const": "core:event:deny-listen", "markdownDescription": "Denies the listen command without any pre-configured scope." }, { "description": "Denies the unlisten command without any pre-configured scope.", "type": "string", "const": "core:event:deny-unlisten", "markdownDescription": "Denies the unlisten command without any pre-configured scope." }, { "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", "type": "string", "const": "core:image:default", "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" }, { "description": "Enables the from_bytes command without any pre-configured scope.", "type": "string", "const": "core:image:allow-from-bytes", "markdownDescription": "Enables the from_bytes command without any pre-configured scope." }, { "description": "Enables the from_path command without any pre-configured scope.", "type": "string", "const": "core:image:allow-from-path", "markdownDescription": "Enables the from_path command without any pre-configured scope." }, { "description": "Enables the new command without any pre-configured scope.", "type": "string", "const": "core:image:allow-new", "markdownDescription": "Enables the new command without any pre-configured scope." }, { "description": "Enables the rgba command without any pre-configured scope.", "type": "string", "const": "core:image:allow-rgba", "markdownDescription": "Enables the rgba command without any pre-configured scope." }, { "description": "Enables the size command without any pre-configured scope.", "type": "string", "const": "core:image:allow-size", "markdownDescription": "Enables the size command without any pre-configured scope." }, { "description": "Denies the from_bytes command without any pre-configured scope.", "type": "string", "const": "core:image:deny-from-bytes", "markdownDescription": "Denies the from_bytes command without any pre-configured scope." }, { "description": "Denies the from_path command without any pre-configured scope.", "type": "string", "const": "core:image:deny-from-path", "markdownDescription": "Denies the from_path command without any pre-configured scope." }, { "description": "Denies the new command without any pre-configured scope.", "type": "string", "const": "core:image:deny-new", "markdownDescription": "Denies the new command without any pre-configured scope." }, { "description": "Denies the rgba command without any pre-configured scope.", "type": "string", "const": "core:image:deny-rgba", "markdownDescription": "Denies the rgba command without any pre-configured scope." }, { "description": "Denies the size command without any pre-configured scope.", "type": "string", "const": "core:image:deny-size", "markdownDescription": "Denies the size command without any pre-configured scope." }, { "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", "type": "string", "const": "core:menu:default", "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" }, { "description": "Enables the append command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-append", "markdownDescription": "Enables the append command without any pre-configured scope." }, { "description": "Enables the create_default command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-create-default", "markdownDescription": "Enables the create_default command without any pre-configured scope." }, { "description": "Enables the get command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-get", "markdownDescription": "Enables the get command without any pre-configured scope." }, { "description": "Enables the insert command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-insert", "markdownDescription": "Enables the insert command without any pre-configured scope." }, { "description": "Enables the is_checked command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-is-checked", "markdownDescription": "Enables the is_checked command without any pre-configured scope." }, { "description": "Enables the is_enabled command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-is-enabled", "markdownDescription": "Enables the is_enabled command without any pre-configured scope." }, { "description": "Enables the items command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-items", "markdownDescription": "Enables the items command without any pre-configured scope." }, { "description": "Enables the new command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-new", "markdownDescription": "Enables the new command without any pre-configured scope." }, { "description": "Enables the popup command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-popup", "markdownDescription": "Enables the popup command without any pre-configured scope." }, { "description": "Enables the prepend command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-prepend", "markdownDescription": "Enables the prepend command without any pre-configured scope." }, { "description": "Enables the remove command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-remove", "markdownDescription": "Enables the remove command without any pre-configured scope." }, { "description": "Enables the remove_at command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-remove-at", "markdownDescription": "Enables the remove_at command without any pre-configured scope." }, { "description": "Enables the set_accelerator command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-accelerator", "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." }, { "description": "Enables the set_as_app_menu command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-as-app-menu", "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." }, { "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-as-help-menu-for-nsapp", "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." }, { "description": "Enables the set_as_window_menu command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-as-window-menu", "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." }, { "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-as-windows-menu-for-nsapp", "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." }, { "description": "Enables the set_checked command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-checked", "markdownDescription": "Enables the set_checked command without any pre-configured scope." }, { "description": "Enables the set_enabled command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-enabled", "markdownDescription": "Enables the set_enabled command without any pre-configured scope." }, { "description": "Enables the set_icon command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-icon", "markdownDescription": "Enables the set_icon command without any pre-configured scope." }, { "description": "Enables the set_text command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-set-text", "markdownDescription": "Enables the set_text command without any pre-configured scope." }, { "description": "Enables the text command without any pre-configured scope.", "type": "string", "const": "core:menu:allow-text", "markdownDescription": "Enables the text command without any pre-configured scope." }, { "description": "Denies the append command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-append", "markdownDescription": "Denies the append command without any pre-configured scope." }, { "description": "Denies the create_default command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-create-default", "markdownDescription": "Denies the create_default command without any pre-configured scope." }, { "description": "Denies the get command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-get", "markdownDescription": "Denies the get command without any pre-configured scope." }, { "description": "Denies the insert command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-insert", "markdownDescription": "Denies the insert command without any pre-configured scope." }, { "description": "Denies the is_checked command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-is-checked", "markdownDescription": "Denies the is_checked command without any pre-configured scope." }, { "description": "Denies the is_enabled command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-is-enabled", "markdownDescription": "Denies the is_enabled command without any pre-configured scope." }, { "description": "Denies the items command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-items", "markdownDescription": "Denies the items command without any pre-configured scope." }, { "description": "Denies the new command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-new", "markdownDescription": "Denies the new command without any pre-configured scope." }, { "description": "Denies the popup command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-popup", "markdownDescription": "Denies the popup command without any pre-configured scope." }, { "description": "Denies the prepend command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-prepend", "markdownDescription": "Denies the prepend command without any pre-configured scope." }, { "description": "Denies the remove command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-remove", "markdownDescription": "Denies the remove command without any pre-configured scope." }, { "description": "Denies the remove_at command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-remove-at", "markdownDescription": "Denies the remove_at command without any pre-configured scope." }, { "description": "Denies the set_accelerator command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-accelerator", "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." }, { "description": "Denies the set_as_app_menu command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-as-app-menu", "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." }, { "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-as-help-menu-for-nsapp", "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." }, { "description": "Denies the set_as_window_menu command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-as-window-menu", "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." }, { "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-as-windows-menu-for-nsapp", "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." }, { "description": "Denies the set_checked command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-checked", "markdownDescription": "Denies the set_checked command without any pre-configured scope." }, { "description": "Denies the set_enabled command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-enabled", "markdownDescription": "Denies the set_enabled command without any pre-configured scope." }, { "description": "Denies the set_icon command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-icon", "markdownDescription": "Denies the set_icon command without any pre-configured scope." }, { "description": "Denies the set_text command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-set-text", "markdownDescription": "Denies the set_text command without any pre-configured scope." }, { "description": "Denies the text command without any pre-configured scope.", "type": "string", "const": "core:menu:deny-text", "markdownDescription": "Denies the text command without any pre-configured scope." }, { "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", "type": "string", "const": "core:path:default", "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" }, { "description": "Enables the basename command without any pre-configured scope.", "type": "string", "const": "core:path:allow-basename", "markdownDescription": "Enables the basename command without any pre-configured scope." }, { "description": "Enables the dirname command without any pre-configured scope.", "type": "string", "const": "core:path:allow-dirname", "markdownDescription": "Enables the dirname command without any pre-configured scope." }, { "description": "Enables the extname command without any pre-configured scope.", "type": "string", "const": "core:path:allow-extname", "markdownDescription": "Enables the extname command without any pre-configured scope." }, { "description": "Enables the is_absolute command without any pre-configured scope.", "type": "string", "const": "core:path:allow-is-absolute", "markdownDescription": "Enables the is_absolute command without any pre-configured scope." }, { "description": "Enables the join command without any pre-configured scope.", "type": "string", "const": "core:path:allow-join", "markdownDescription": "Enables the join command without any pre-configured scope." }, { "description": "Enables the normalize command without any pre-configured scope.", "type": "string", "const": "core:path:allow-normalize", "markdownDescription": "Enables the normalize command without any pre-configured scope." }, { "description": "Enables the resolve command without any pre-configured scope.", "type": "string", "const": "core:path:allow-resolve", "markdownDescription": "Enables the resolve command without any pre-configured scope." }, { "description": "Enables the resolve_directory command without any pre-configured scope.", "type": "string", "const": "core:path:allow-resolve-directory", "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." }, { "description": "Denies the basename command without any pre-configured scope.", "type": "string", "const": "core:path:deny-basename", "markdownDescription": "Denies the basename command without any pre-configured scope." }, { "description": "Denies the dirname command without any pre-configured scope.", "type": "string", "const": "core:path:deny-dirname", "markdownDescription": "Denies the dirname command without any pre-configured scope." }, { "description": "Denies the extname command without any pre-configured scope.", "type": "string", "const": "core:path:deny-extname", "markdownDescription": "Denies the extname command without any pre-configured scope." }, { "description": "Denies the is_absolute command without any pre-configured scope.", "type": "string", "const": "core:path:deny-is-absolute", "markdownDescription": "Denies the is_absolute command without any pre-configured scope." }, { "description": "Denies the join command without any pre-configured scope.", "type": "string", "const": "core:path:deny-join", "markdownDescription": "Denies the join command without any pre-configured scope." }, { "description": "Denies the normalize command without any pre-configured scope.", "type": "string", "const": "core:path:deny-normalize", "markdownDescription": "Denies the normalize command without any pre-configured scope." }, { "description": "Denies the resolve command without any pre-configured scope.", "type": "string", "const": "core:path:deny-resolve", "markdownDescription": "Denies the resolve command without any pre-configured scope." }, { "description": "Denies the resolve_directory command without any pre-configured scope.", "type": "string", "const": "core:path:deny-resolve-directory", "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." }, { "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", "type": "string", "const": "core:resources:default", "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" }, { "description": "Enables the close command without any pre-configured scope.", "type": "string", "const": "core:resources:allow-close", "markdownDescription": "Enables the close command without any pre-configured scope." }, { "description": "Denies the close command without any pre-configured scope.", "type": "string", "const": "core:resources:deny-close", "markdownDescription": "Denies the close command without any pre-configured scope." }, { "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", "type": "string", "const": "core:tray:default", "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" }, { "description": "Enables the get_by_id command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-get-by-id", "markdownDescription": "Enables the get_by_id command without any pre-configured scope." }, { "description": "Enables the new command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-new", "markdownDescription": "Enables the new command without any pre-configured scope." }, { "description": "Enables the remove_by_id command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-remove-by-id", "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." }, { "description": "Enables the set_icon command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-icon", "markdownDescription": "Enables the set_icon command without any pre-configured scope." }, { "description": "Enables the set_icon_as_template command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-icon-as-template", "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." }, { "description": "Enables the set_menu command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-menu", "markdownDescription": "Enables the set_menu command without any pre-configured scope." }, { "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-show-menu-on-left-click", "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." }, { "description": "Enables the set_temp_dir_path command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-temp-dir-path", "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." }, { "description": "Enables the set_title command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-title", "markdownDescription": "Enables the set_title command without any pre-configured scope." }, { "description": "Enables the set_tooltip command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-tooltip", "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." }, { "description": "Enables the set_visible command without any pre-configured scope.", "type": "string", "const": "core:tray:allow-set-visible", "markdownDescription": "Enables the set_visible command without any pre-configured scope." }, { "description": "Denies the get_by_id command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-get-by-id", "markdownDescription": "Denies the get_by_id command without any pre-configured scope." }, { "description": "Denies the new command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-new", "markdownDescription": "Denies the new command without any pre-configured scope." }, { "description": "Denies the remove_by_id command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-remove-by-id", "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." }, { "description": "Denies the set_icon command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-icon", "markdownDescription": "Denies the set_icon command without any pre-configured scope." }, { "description": "Denies the set_icon_as_template command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-icon-as-template", "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." }, { "description": "Denies the set_menu command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-menu", "markdownDescription": "Denies the set_menu command without any pre-configured scope." }, { "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-show-menu-on-left-click", "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." }, { "description": "Denies the set_temp_dir_path command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-temp-dir-path", "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." }, { "description": "Denies the set_title command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-title", "markdownDescription": "Denies the set_title command without any pre-configured scope." }, { "description": "Denies the set_tooltip command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-tooltip", "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." }, { "description": "Denies the set_visible command without any pre-configured scope.", "type": "string", "const": "core:tray:deny-set-visible", "markdownDescription": "Denies the set_visible command without any pre-configured scope." }, { "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", "type": "string", "const": "core:webview:default", "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" }, { "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-clear-all-browsing-data", "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." }, { "description": "Enables the create_webview command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-create-webview", "markdownDescription": "Enables the create_webview command without any pre-configured scope." }, { "description": "Enables the create_webview_window command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-create-webview-window", "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." }, { "description": "Enables the get_all_webviews command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-get-all-webviews", "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." }, { "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-internal-toggle-devtools", "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." }, { "description": "Enables the print command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-print", "markdownDescription": "Enables the print command without any pre-configured scope." }, { "description": "Enables the reparent command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-reparent", "markdownDescription": "Enables the reparent command without any pre-configured scope." }, { "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-set-webview-auto-resize", "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." }, { "description": "Enables the set_webview_background_color command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-set-webview-background-color", "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." }, { "description": "Enables the set_webview_focus command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-set-webview-focus", "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." }, { "description": "Enables the set_webview_position command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-set-webview-position", "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." }, { "description": "Enables the set_webview_size command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-set-webview-size", "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." }, { "description": "Enables the set_webview_zoom command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-set-webview-zoom", "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." }, { "description": "Enables the webview_close command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-webview-close", "markdownDescription": "Enables the webview_close command without any pre-configured scope." }, { "description": "Enables the webview_hide command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-webview-hide", "markdownDescription": "Enables the webview_hide command without any pre-configured scope." }, { "description": "Enables the webview_position command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-webview-position", "markdownDescription": "Enables the webview_position command without any pre-configured scope." }, { "description": "Enables the webview_show command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-webview-show", "markdownDescription": "Enables the webview_show command without any pre-configured scope." }, { "description": "Enables the webview_size command without any pre-configured scope.", "type": "string", "const": "core:webview:allow-webview-size", "markdownDescription": "Enables the webview_size command without any pre-configured scope." }, { "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-clear-all-browsing-data", "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." }, { "description": "Denies the create_webview command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-create-webview", "markdownDescription": "Denies the create_webview command without any pre-configured scope." }, { "description": "Denies the create_webview_window command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-create-webview-window", "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." }, { "description": "Denies the get_all_webviews command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-get-all-webviews", "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." }, { "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-internal-toggle-devtools", "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." }, { "description": "Denies the print command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-print", "markdownDescription": "Denies the print command without any pre-configured scope." }, { "description": "Denies the reparent command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-reparent", "markdownDescription": "Denies the reparent command without any pre-configured scope." }, { "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-set-webview-auto-resize", "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." }, { "description": "Denies the set_webview_background_color command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-set-webview-background-color", "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." }, { "description": "Denies the set_webview_focus command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-set-webview-focus", "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." }, { "description": "Denies the set_webview_position command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-set-webview-position", "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." }, { "description": "Denies the set_webview_size command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-set-webview-size", "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." }, { "description": "Denies the set_webview_zoom command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-set-webview-zoom", "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." }, { "description": "Denies the webview_close command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-webview-close", "markdownDescription": "Denies the webview_close command without any pre-configured scope." }, { "description": "Denies the webview_hide command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-webview-hide", "markdownDescription": "Denies the webview_hide command without any pre-configured scope." }, { "description": "Denies the webview_position command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-webview-position", "markdownDescription": "Denies the webview_position command without any pre-configured scope." }, { "description": "Denies the webview_show command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-webview-show", "markdownDescription": "Denies the webview_show command without any pre-configured scope." }, { "description": "Denies the webview_size command without any pre-configured scope.", "type": "string", "const": "core:webview:deny-webview-size", "markdownDescription": "Denies the webview_size command without any pre-configured scope." }, { "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", "type": "string", "const": "core:window:default", "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" }, { "description": "Enables the available_monitors command without any pre-configured scope.", "type": "string", "const": "core:window:allow-available-monitors", "markdownDescription": "Enables the available_monitors command without any pre-configured scope." }, { "description": "Enables the center command without any pre-configured scope.", "type": "string", "const": "core:window:allow-center", "markdownDescription": "Enables the center command without any pre-configured scope." }, { "description": "Enables the close command without any pre-configured scope.", "type": "string", "const": "core:window:allow-close", "markdownDescription": "Enables the close command without any pre-configured scope." }, { "description": "Enables the create command without any pre-configured scope.", "type": "string", "const": "core:window:allow-create", "markdownDescription": "Enables the create command without any pre-configured scope." }, { "description": "Enables the current_monitor command without any pre-configured scope.", "type": "string", "const": "core:window:allow-current-monitor", "markdownDescription": "Enables the current_monitor command without any pre-configured scope." }, { "description": "Enables the cursor_position command without any pre-configured scope.", "type": "string", "const": "core:window:allow-cursor-position", "markdownDescription": "Enables the cursor_position command without any pre-configured scope." }, { "description": "Enables the destroy command without any pre-configured scope.", "type": "string", "const": "core:window:allow-destroy", "markdownDescription": "Enables the destroy command without any pre-configured scope." }, { "description": "Enables the get_all_windows command without any pre-configured scope.", "type": "string", "const": "core:window:allow-get-all-windows", "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." }, { "description": "Enables the hide command without any pre-configured scope.", "type": "string", "const": "core:window:allow-hide", "markdownDescription": "Enables the hide command without any pre-configured scope." }, { "description": "Enables the inner_position command without any pre-configured scope.", "type": "string", "const": "core:window:allow-inner-position", "markdownDescription": "Enables the inner_position command without any pre-configured scope." }, { "description": "Enables the inner_size command without any pre-configured scope.", "type": "string", "const": "core:window:allow-inner-size", "markdownDescription": "Enables the inner_size command without any pre-configured scope." }, { "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", "type": "string", "const": "core:window:allow-internal-toggle-maximize", "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." }, { "description": "Enables the is_always_on_top command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-always-on-top", "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." }, { "description": "Enables the is_closable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-closable", "markdownDescription": "Enables the is_closable command without any pre-configured scope." }, { "description": "Enables the is_decorated command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-decorated", "markdownDescription": "Enables the is_decorated command without any pre-configured scope." }, { "description": "Enables the is_enabled command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-enabled", "markdownDescription": "Enables the is_enabled command without any pre-configured scope." }, { "description": "Enables the is_focused command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-focused", "markdownDescription": "Enables the is_focused command without any pre-configured scope." }, { "description": "Enables the is_fullscreen command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-fullscreen", "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." }, { "description": "Enables the is_maximizable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-maximizable", "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." }, { "description": "Enables the is_maximized command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-maximized", "markdownDescription": "Enables the is_maximized command without any pre-configured scope." }, { "description": "Enables the is_minimizable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-minimizable", "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." }, { "description": "Enables the is_minimized command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-minimized", "markdownDescription": "Enables the is_minimized command without any pre-configured scope." }, { "description": "Enables the is_resizable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-resizable", "markdownDescription": "Enables the is_resizable command without any pre-configured scope." }, { "description": "Enables the is_visible command without any pre-configured scope.", "type": "string", "const": "core:window:allow-is-visible", "markdownDescription": "Enables the is_visible command without any pre-configured scope." }, { "description": "Enables the maximize command without any pre-configured scope.", "type": "string", "const": "core:window:allow-maximize", "markdownDescription": "Enables the maximize command without any pre-configured scope." }, { "description": "Enables the minimize command without any pre-configured scope.", "type": "string", "const": "core:window:allow-minimize", "markdownDescription": "Enables the minimize command without any pre-configured scope." }, { "description": "Enables the monitor_from_point command without any pre-configured scope.", "type": "string", "const": "core:window:allow-monitor-from-point", "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." }, { "description": "Enables the outer_position command without any pre-configured scope.", "type": "string", "const": "core:window:allow-outer-position", "markdownDescription": "Enables the outer_position command without any pre-configured scope." }, { "description": "Enables the outer_size command without any pre-configured scope.", "type": "string", "const": "core:window:allow-outer-size", "markdownDescription": "Enables the outer_size command without any pre-configured scope." }, { "description": "Enables the primary_monitor command without any pre-configured scope.", "type": "string", "const": "core:window:allow-primary-monitor", "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." }, { "description": "Enables the request_user_attention command without any pre-configured scope.", "type": "string", "const": "core:window:allow-request-user-attention", "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." }, { "description": "Enables the scale_factor command without any pre-configured scope.", "type": "string", "const": "core:window:allow-scale-factor", "markdownDescription": "Enables the scale_factor command without any pre-configured scope." }, { "description": "Enables the set_always_on_bottom command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-always-on-bottom", "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." }, { "description": "Enables the set_always_on_top command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-always-on-top", "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." }, { "description": "Enables the set_background_color command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-background-color", "markdownDescription": "Enables the set_background_color command without any pre-configured scope." }, { "description": "Enables the set_badge_count command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-badge-count", "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." }, { "description": "Enables the set_badge_label command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-badge-label", "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." }, { "description": "Enables the set_closable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-closable", "markdownDescription": "Enables the set_closable command without any pre-configured scope." }, { "description": "Enables the set_content_protected command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-content-protected", "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." }, { "description": "Enables the set_cursor_grab command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-cursor-grab", "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." }, { "description": "Enables the set_cursor_icon command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-cursor-icon", "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." }, { "description": "Enables the set_cursor_position command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-cursor-position", "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." }, { "description": "Enables the set_cursor_visible command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-cursor-visible", "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." }, { "description": "Enables the set_decorations command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-decorations", "markdownDescription": "Enables the set_decorations command without any pre-configured scope." }, { "description": "Enables the set_effects command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-effects", "markdownDescription": "Enables the set_effects command without any pre-configured scope." }, { "description": "Enables the set_enabled command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-enabled", "markdownDescription": "Enables the set_enabled command without any pre-configured scope." }, { "description": "Enables the set_focus command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-focus", "markdownDescription": "Enables the set_focus command without any pre-configured scope." }, { "description": "Enables the set_focusable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-focusable", "markdownDescription": "Enables the set_focusable command without any pre-configured scope." }, { "description": "Enables the set_fullscreen command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-fullscreen", "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." }, { "description": "Enables the set_icon command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-icon", "markdownDescription": "Enables the set_icon command without any pre-configured scope." }, { "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-ignore-cursor-events", "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." }, { "description": "Enables the set_max_size command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-max-size", "markdownDescription": "Enables the set_max_size command without any pre-configured scope." }, { "description": "Enables the set_maximizable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-maximizable", "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." }, { "description": "Enables the set_min_size command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-min-size", "markdownDescription": "Enables the set_min_size command without any pre-configured scope." }, { "description": "Enables the set_minimizable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-minimizable", "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." }, { "description": "Enables the set_overlay_icon command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-overlay-icon", "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." }, { "description": "Enables the set_position command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-position", "markdownDescription": "Enables the set_position command without any pre-configured scope." }, { "description": "Enables the set_progress_bar command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-progress-bar", "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." }, { "description": "Enables the set_resizable command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-resizable", "markdownDescription": "Enables the set_resizable command without any pre-configured scope." }, { "description": "Enables the set_shadow command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-shadow", "markdownDescription": "Enables the set_shadow command without any pre-configured scope." }, { "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-simple-fullscreen", "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." }, { "description": "Enables the set_size command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-size", "markdownDescription": "Enables the set_size command without any pre-configured scope." }, { "description": "Enables the set_size_constraints command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-size-constraints", "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." }, { "description": "Enables the set_skip_taskbar command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-skip-taskbar", "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." }, { "description": "Enables the set_theme command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-theme", "markdownDescription": "Enables the set_theme command without any pre-configured scope." }, { "description": "Enables the set_title command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-title", "markdownDescription": "Enables the set_title command without any pre-configured scope." }, { "description": "Enables the set_title_bar_style command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-title-bar-style", "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." }, { "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", "type": "string", "const": "core:window:allow-set-visible-on-all-workspaces", "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." }, { "description": "Enables the show command without any pre-configured scope.", "type": "string", "const": "core:window:allow-show", "markdownDescription": "Enables the show command without any pre-configured scope." }, { "description": "Enables the start_dragging command without any pre-configured scope.", "type": "string", "const": "core:window:allow-start-dragging", "markdownDescription": "Enables the start_dragging command without any pre-configured scope." }, { "description": "Enables the start_resize_dragging command without any pre-configured scope.", "type": "string", "const": "core:window:allow-start-resize-dragging", "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." }, { "description": "Enables the theme command without any pre-configured scope.", "type": "string", "const": "core:window:allow-theme", "markdownDescription": "Enables the theme command without any pre-configured scope." }, { "description": "Enables the title command without any pre-configured scope.", "type": "string", "const": "core:window:allow-title", "markdownDescription": "Enables the title command without any pre-configured scope." }, { "description": "Enables the toggle_maximize command without any pre-configured scope.", "type": "string", "const": "core:window:allow-toggle-maximize", "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." }, { "description": "Enables the unmaximize command without any pre-configured scope.", "type": "string", "const": "core:window:allow-unmaximize", "markdownDescription": "Enables the unmaximize command without any pre-configured scope." }, { "description": "Enables the unminimize command without any pre-configured scope.", "type": "string", "const": "core:window:allow-unminimize", "markdownDescription": "Enables the unminimize command without any pre-configured scope." }, { "description": "Denies the available_monitors command without any pre-configured scope.", "type": "string", "const": "core:window:deny-available-monitors", "markdownDescription": "Denies the available_monitors command without any pre-configured scope." }, { "description": "Denies the center command without any pre-configured scope.", "type": "string", "const": "core:window:deny-center", "markdownDescription": "Denies the center command without any pre-configured scope." }, { "description": "Denies the close command without any pre-configured scope.", "type": "string", "const": "core:window:deny-close", "markdownDescription": "Denies the close command without any pre-configured scope." }, { "description": "Denies the create command without any pre-configured scope.", "type": "string", "const": "core:window:deny-create", "markdownDescription": "Denies the create command without any pre-configured scope." }, { "description": "Denies the current_monitor command without any pre-configured scope.", "type": "string", "const": "core:window:deny-current-monitor", "markdownDescription": "Denies the current_monitor command without any pre-configured scope." }, { "description": "Denies the cursor_position command without any pre-configured scope.", "type": "string", "const": "core:window:deny-cursor-position", "markdownDescription": "Denies the cursor_position command without any pre-configured scope." }, { "description": "Denies the destroy command without any pre-configured scope.", "type": "string", "const": "core:window:deny-destroy", "markdownDescription": "Denies the destroy command without any pre-configured scope." }, { "description": "Denies the get_all_windows command without any pre-configured scope.", "type": "string", "const": "core:window:deny-get-all-windows", "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." }, { "description": "Denies the hide command without any pre-configured scope.", "type": "string", "const": "core:window:deny-hide", "markdownDescription": "Denies the hide command without any pre-configured scope." }, { "description": "Denies the inner_position command without any pre-configured scope.", "type": "string", "const": "core:window:deny-inner-position", "markdownDescription": "Denies the inner_position command without any pre-configured scope." }, { "description": "Denies the inner_size command without any pre-configured scope.", "type": "string", "const": "core:window:deny-inner-size", "markdownDescription": "Denies the inner_size command without any pre-configured scope." }, { "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", "type": "string", "const": "core:window:deny-internal-toggle-maximize", "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." }, { "description": "Denies the is_always_on_top command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-always-on-top", "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." }, { "description": "Denies the is_closable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-closable", "markdownDescription": "Denies the is_closable command without any pre-configured scope." }, { "description": "Denies the is_decorated command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-decorated", "markdownDescription": "Denies the is_decorated command without any pre-configured scope." }, { "description": "Denies the is_enabled command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-enabled", "markdownDescription": "Denies the is_enabled command without any pre-configured scope." }, { "description": "Denies the is_focused command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-focused", "markdownDescription": "Denies the is_focused command without any pre-configured scope." }, { "description": "Denies the is_fullscreen command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-fullscreen", "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." }, { "description": "Denies the is_maximizable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-maximizable", "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." }, { "description": "Denies the is_maximized command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-maximized", "markdownDescription": "Denies the is_maximized command without any pre-configured scope." }, { "description": "Denies the is_minimizable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-minimizable", "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." }, { "description": "Denies the is_minimized command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-minimized", "markdownDescription": "Denies the is_minimized command without any pre-configured scope." }, { "description": "Denies the is_resizable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-resizable", "markdownDescription": "Denies the is_resizable command without any pre-configured scope." }, { "description": "Denies the is_visible command without any pre-configured scope.", "type": "string", "const": "core:window:deny-is-visible", "markdownDescription": "Denies the is_visible command without any pre-configured scope." }, { "description": "Denies the maximize command without any pre-configured scope.", "type": "string", "const": "core:window:deny-maximize", "markdownDescription": "Denies the maximize command without any pre-configured scope." }, { "description": "Denies the minimize command without any pre-configured scope.", "type": "string", "const": "core:window:deny-minimize", "markdownDescription": "Denies the minimize command without any pre-configured scope." }, { "description": "Denies the monitor_from_point command without any pre-configured scope.", "type": "string", "const": "core:window:deny-monitor-from-point", "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." }, { "description": "Denies the outer_position command without any pre-configured scope.", "type": "string", "const": "core:window:deny-outer-position", "markdownDescription": "Denies the outer_position command without any pre-configured scope." }, { "description": "Denies the outer_size command without any pre-configured scope.", "type": "string", "const": "core:window:deny-outer-size", "markdownDescription": "Denies the outer_size command without any pre-configured scope." }, { "description": "Denies the primary_monitor command without any pre-configured scope.", "type": "string", "const": "core:window:deny-primary-monitor", "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." }, { "description": "Denies the request_user_attention command without any pre-configured scope.", "type": "string", "const": "core:window:deny-request-user-attention", "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." }, { "description": "Denies the scale_factor command without any pre-configured scope.", "type": "string", "const": "core:window:deny-scale-factor", "markdownDescription": "Denies the scale_factor command without any pre-configured scope." }, { "description": "Denies the set_always_on_bottom command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-always-on-bottom", "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." }, { "description": "Denies the set_always_on_top command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-always-on-top", "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." }, { "description": "Denies the set_background_color command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-background-color", "markdownDescription": "Denies the set_background_color command without any pre-configured scope." }, { "description": "Denies the set_badge_count command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-badge-count", "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." }, { "description": "Denies the set_badge_label command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-badge-label", "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." }, { "description": "Denies the set_closable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-closable", "markdownDescription": "Denies the set_closable command without any pre-configured scope." }, { "description": "Denies the set_content_protected command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-content-protected", "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." }, { "description": "Denies the set_cursor_grab command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-cursor-grab", "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." }, { "description": "Denies the set_cursor_icon command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-cursor-icon", "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." }, { "description": "Denies the set_cursor_position command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-cursor-position", "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." }, { "description": "Denies the set_cursor_visible command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-cursor-visible", "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." }, { "description": "Denies the set_decorations command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-decorations", "markdownDescription": "Denies the set_decorations command without any pre-configured scope." }, { "description": "Denies the set_effects command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-effects", "markdownDescription": "Denies the set_effects command without any pre-configured scope." }, { "description": "Denies the set_enabled command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-enabled", "markdownDescription": "Denies the set_enabled command without any pre-configured scope." }, { "description": "Denies the set_focus command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-focus", "markdownDescription": "Denies the set_focus command without any pre-configured scope." }, { "description": "Denies the set_focusable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-focusable", "markdownDescription": "Denies the set_focusable command without any pre-configured scope." }, { "description": "Denies the set_fullscreen command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-fullscreen", "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." }, { "description": "Denies the set_icon command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-icon", "markdownDescription": "Denies the set_icon command without any pre-configured scope." }, { "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-ignore-cursor-events", "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." }, { "description": "Denies the set_max_size command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-max-size", "markdownDescription": "Denies the set_max_size command without any pre-configured scope." }, { "description": "Denies the set_maximizable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-maximizable", "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." }, { "description": "Denies the set_min_size command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-min-size", "markdownDescription": "Denies the set_min_size command without any pre-configured scope." }, { "description": "Denies the set_minimizable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-minimizable", "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." }, { "description": "Denies the set_overlay_icon command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-overlay-icon", "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." }, { "description": "Denies the set_position command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-position", "markdownDescription": "Denies the set_position command without any pre-configured scope." }, { "description": "Denies the set_progress_bar command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-progress-bar", "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." }, { "description": "Denies the set_resizable command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-resizable", "markdownDescription": "Denies the set_resizable command without any pre-configured scope." }, { "description": "Denies the set_shadow command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-shadow", "markdownDescription": "Denies the set_shadow command without any pre-configured scope." }, { "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-simple-fullscreen", "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." }, { "description": "Denies the set_size command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-size", "markdownDescription": "Denies the set_size command without any pre-configured scope." }, { "description": "Denies the set_size_constraints command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-size-constraints", "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." }, { "description": "Denies the set_skip_taskbar command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-skip-taskbar", "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." }, { "description": "Denies the set_theme command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-theme", "markdownDescription": "Denies the set_theme command without any pre-configured scope." }, { "description": "Denies the set_title command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-title", "markdownDescription": "Denies the set_title command without any pre-configured scope." }, { "description": "Denies the set_title_bar_style command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-title-bar-style", "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." }, { "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", "type": "string", "const": "core:window:deny-set-visible-on-all-workspaces", "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." }, { "description": "Denies the show command without any pre-configured scope.", "type": "string", "const": "core:window:deny-show", "markdownDescription": "Denies the show command without any pre-configured scope." }, { "description": "Denies the start_dragging command without any pre-configured scope.", "type": "string", "const": "core:window:deny-start-dragging", "markdownDescription": "Denies the start_dragging command without any pre-configured scope." }, { "description": "Denies the start_resize_dragging command without any pre-configured scope.", "type": "string", "const": "core:window:deny-start-resize-dragging", "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." }, { "description": "Denies the theme command without any pre-configured scope.", "type": "string", "const": "core:window:deny-theme", "markdownDescription": "Denies the theme command without any pre-configured scope." }, { "description": "Denies the title command without any pre-configured scope.", "type": "string", "const": "core:window:deny-title", "markdownDescription": "Denies the title command without any pre-configured scope." }, { "description": "Denies the toggle_maximize command without any pre-configured scope.", "type": "string", "const": "core:window:deny-toggle-maximize", "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." }, { "description": "Denies the unmaximize command without any pre-configured scope.", "type": "string", "const": "core:window:deny-unmaximize", "markdownDescription": "Denies the unmaximize command without any pre-configured scope." }, { "description": "Denies the unminimize command without any pre-configured scope.", "type": "string", "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." } ] }, "Value": { "description": "All supported ACL values.", "anyOf": [ { "description": "Represents a null JSON value.", "type": "null" }, { "description": "Represents a [`bool`].", "type": "boolean" }, { "description": "Represents a valid ACL [`Number`].", "allOf": [ { "$ref": "#/definitions/Number" } ] }, { "description": "Represents a [`String`].", "type": "string" }, { "description": "Represents a list of other [`Value`]s.", "type": "array", "items": { "$ref": "#/definitions/Value" } }, { "description": "Represents a map of [`String`] keys to [`Value`]s.", "type": "object", "additionalProperties": { "$ref": "#/definitions/Value" } } ] }, "Number": { "description": "A valid ACL number.", "anyOf": [ { "description": "Represents an [`i64`].", "type": "integer", "format": "int64" }, { "description": "Represents a [`f64`].", "type": "number", "format": "double" } ] }, "Target": { "description": "Platform target.", "oneOf": [ { "description": "MacOS.", "type": "string", "enum": [ "macOS" ] }, { "description": "Windows.", "type": "string", "enum": [ "windows" ] }, { "description": "Linux.", "type": "string", "enum": [ "linux" ] }, { "description": "Android.", "type": "string", "enum": [ "android" ] }, { "description": "iOS.", "type": "string", "enum": [ "iOS" ] } ] } } } ================================================ FILE: desktop-pet/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: desktop-pet/src-tauri/icons/android/values/ic_launcher_background.xml ================================================ #fff ================================================ FILE: desktop-pet/src-tauri/src/lib.rs ================================================ use base64::engine::general_purpose::STANDARD as B64; use base64::Engine; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::io::{Read, Write}; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; use std::sync::Mutex; use std::time::{Duration, Instant}; use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; // ── state.json ── #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PetState { pub state: String, pub detail: Option, pub progress: Option, pub updated_at: Option, } // ── layers.json input ── #[derive(Debug, Deserialize)] struct CfgFile { width: Option, height: Option, character: Option, layers: Option>, sprites: Option, } #[derive(Debug, Deserialize)] struct CharCfg { x: Option, y: Option, scale: Option, depth: Option, wander: Option, } #[derive(Debug, Deserialize)] struct LayerCfg { image: String, x: Option, y: Option, depth: Option, scale: Option, alpha: Option, } #[derive(Debug, Deserialize)] struct SpritesCfg { frame_width: Option, frame_height: Option, anims: Option>, } #[derive(Debug, Deserialize)] struct AnimCfg { file: String, frames: Option, rate: Option, #[serde(default = "neg_one")] repeat: i32, } fn neg_one() -> i32 { -1 } // ── map.json input ── #[derive(Debug, Deserialize)] struct MapCfgFile { tile_size: Option, cols: Option, rows: Option, zoom: Option, tileset: String, character_speed: Option, ground: Vec>, border: Option>>, rug: Option>>, objects: Vec>, collision: Vec>, pois: Option>, state_icons: Option>, } #[derive(Debug, Deserialize)] struct PoiCfg { col: u32, row: u32, } // ── IPC responses ── #[derive(Debug, Serialize)] struct FullData { width: u32, height: u32, character: CharData, layers: Vec, sprites: Option, } #[derive(Debug, Serialize)] struct CharData { x: f64, y: f64, scale: f64, depth: i32, wander: f64, } #[derive(Debug, Serialize)] struct LayerItem { data_url: String, x: f64, y: f64, depth: i32, scale: f64, alpha: f64, } #[derive(Debug, Serialize)] struct SpritesData { frame_width: u32, frame_height: u32, anims: Vec, } #[derive(Debug, Serialize)] struct AnimItem { key: String, data_url: String, frames: u32, rate: u32, repeat: i32, } #[derive(Debug, Serialize)] struct MapData { tile_size: u32, cols: u32, rows: u32, zoom: u32, tileset_url: String, tileset_cols: u32, character_speed: f64, ground: Vec>, border: Vec>, rug: Vec>, objects: Vec>, collision: Vec>, pois: HashMap, state_icons: HashMap, } #[derive(Debug, Serialize)] struct PoiOut { col: u32, row: u32, } // ── shared ── struct AppPaths { state_path: PathBuf, layers_dir: PathBuf, } struct BackendProcess { child: Option, } impl Drop for BackendProcess { fn drop(&mut self) { if let Some(child) = &mut self.child { let _ = child.kill(); let _ = child.wait(); } } } fn encode_image(path: &PathBuf) -> Result { let bytes = fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; let ext = path .extension() .and_then(|e| e.to_str()) .unwrap_or("png"); let mime = match ext { "png" => "image/png", "jpg" | "jpeg" => "image/jpeg", "gif" => "image/gif", "webp" => "image/webp", _ => "image/png", }; Ok(format!("data:{mime};base64,{}", B64.encode(&bytes))) } // ── commands ── fn read_state_file(state_path: &PathBuf) -> Result { let raw = fs::read_to_string(state_path) .map_err(|e| format!("{}: {e}", state_path.display()))?; serde_json::from_str(&raw).map_err(|e| format!("parse: {e}")) } fn read_state_via_backend() -> Result { let mut stream = std::net::TcpStream::connect("127.0.0.1:19000") .map_err(|e| format!("backend connect: {e}"))?; let _ = stream.set_read_timeout(Some(Duration::from_millis(1200))); let _ = stream.set_write_timeout(Some(Duration::from_millis(1200))); let request = b"GET /status HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"; stream .write_all(request) .map_err(|e| format!("backend write: {e}"))?; let mut raw = String::new(); stream .read_to_string(&mut raw) .map_err(|e| format!("backend read: {e}"))?; let body = raw .split_once("\r\n\r\n") .map(|(_, b)| b) .ok_or_else(|| "backend response parse failed".to_string())?; serde_json::from_str(body).map_err(|e| format!("backend json parse: {e}")) } fn read_state_with_fallback(state_path: &PathBuf) -> Result { match read_state_file(state_path) { Ok(state) => Ok(state), Err(file_err) => { eprintln!("⚠️ read state file failed, fallback to backend: {file_err}"); read_state_via_backend() } } } #[tauri::command] fn read_state(paths: tauri::State<'_, Mutex>) -> Result { let p = paths.lock().map_err(|e| e.to_string())?; read_state_with_fallback(&p.state_path) } #[tauri::command] fn load_layers(paths: tauri::State<'_, Mutex>) -> Result { let p = paths.lock().map_err(|e| e.to_string())?; let cfg_path = p.layers_dir.join("layers.json"); let cfg: CfgFile = if cfg_path.exists() { let raw = fs::read_to_string(&cfg_path).map_err(|e| format!("layers.json: {e}"))?; serde_json::from_str(&raw).map_err(|e| format!("layers.json: {e}"))? } else { CfgFile { width: None, height: None, character: None, layers: None, sprites: None, } }; let w = cfg.width.unwrap_or(200); let h = cfg.height.unwrap_or(250); let cc = cfg.character.unwrap_or(CharCfg { x: None, y: None, scale: None, depth: None, wander: None, }); let character = CharData { x: cc.x.unwrap_or(w as f64 / 2.0), y: cc.y.unwrap_or(h as f64 * 0.66), scale: cc.scale.unwrap_or(2.5), depth: cc.depth.unwrap_or(0), wander: cc.wander.unwrap_or(18.0), }; let mut items = Vec::new(); for entry in cfg.layers.unwrap_or_default() { let img_path = p.layers_dir.join(&entry.image); if !img_path.exists() { continue; } items.push(LayerItem { data_url: encode_image(&img_path)?, x: entry.x.unwrap_or(w as f64 / 2.0), y: entry.y.unwrap_or(h as f64 / 2.0), depth: entry.depth.unwrap_or(-1), scale: entry.scale.unwrap_or(1.0), alpha: entry.alpha.unwrap_or(1.0), }); } let sprites_data = if let Some(scfg) = cfg.sprites { let fw = scfg.frame_width.unwrap_or(32); let fh = scfg.frame_height.unwrap_or(32); let mut anims = Vec::new(); for (key, acfg) in scfg.anims.unwrap_or_default() { let img_path = p.layers_dir.join(&acfg.file); if !img_path.exists() { continue; } anims.push(AnimItem { key, data_url: encode_image(&img_path)?, frames: acfg.frames.unwrap_or(1), rate: acfg.rate.unwrap_or(4), repeat: acfg.repeat, }); } Some(SpritesData { frame_width: fw, frame_height: fh, anims, }) } else { None }; Ok(FullData { width: w, height: h, character, layers: items, sprites: sprites_data, }) } #[tauri::command] fn load_map(paths: tauri::State<'_, Mutex>) -> Result { let p = paths.lock().map_err(|e| e.to_string())?; let map_path = p.layers_dir.join("map.json"); if !map_path.exists() { return Err("map.json not found".into()); } let raw = fs::read_to_string(&map_path).map_err(|e| format!("map.json: {e}"))?; let cfg: MapCfgFile = serde_json::from_str(&raw).map_err(|e| format!("map.json: {e}"))?; let ts = cfg.tile_size.unwrap_or(16); let cols = cfg.cols.unwrap_or(cfg.ground.first().map_or(12, |r| r.len() as u32)); let rows = cfg.rows.unwrap_or(cfg.ground.len() as u32); let tileset_path = p.layers_dir.join(&cfg.tileset); if !tileset_path.exists() { return Err(format!("tileset not found: {}", cfg.tileset)); } let tileset_url = encode_image(&tileset_path)?; // figure out tileset column count from image width let img_bytes = fs::read(&tileset_path).map_err(|e| e.to_string())?; let tileset_cols = png_width(&img_bytes).unwrap_or(160) / ts; let mut pois = HashMap::new(); for (k, v) in cfg.pois.unwrap_or_default() { pois.insert(k, PoiOut { col: v.col, row: v.row }); } let icons_dir = p.layers_dir.join("Small (24x24) PNG"); let mut state_icons = HashMap::new(); for (state, filename) in cfg.state_icons.unwrap_or_default() { let path = icons_dir.join(&filename); if path.exists() { if let Ok(url) = encode_image(&path) { state_icons.insert(state, url); } } } Ok(MapData { tile_size: ts, cols, rows, zoom: cfg.zoom.unwrap_or(2), tileset_url, tileset_cols, character_speed: cfg.character_speed.unwrap_or(2.5), ground: cfg.ground, border: cfg.border.unwrap_or_default(), rug: cfg.rug.unwrap_or_default(), objects: cfg.objects, collision: cfg.collision, pois, state_icons, }) } fn png_width(data: &[u8]) -> Option { if data.len() < 24 || &data[0..4] != b"\x89PNG" { return None; } Some(u32::from_be_bytes([data[16], data[17], data[18], data[19]])) } // ── bootstrap ── fn find_project_root() -> PathBuf { if let Ok(p) = std::env::var("STAR_PROJECT_ROOT") { let candidate = PathBuf::from(&p); let abs = if candidate.is_absolute() { candidate } else { std::env::current_dir().unwrap_or_default().join(candidate) }; if abs.join("backend").join("app.py").exists() { return abs; } } let mut dir = std::env::current_dir().unwrap_or_default(); for _ in 0..8 { if dir.join("backend").join("app.py").exists() || dir.join("state.json").exists() || dir.join("state.sample.json").exists() { return dir; } if !dir.pop() { break; } } if let Ok(home) = std::env::var("HOME") { let home = PathBuf::from(home); let candidates = [ home.join("Documents").join("GitHub").join("Star-Office-UI"), home.join("GitHub").join("Star-Office-UI"), home.join("Documents").join("Star-Office-UI"), home.join("Star-Office-UI"), ]; for candidate in candidates { if candidate.join("backend").join("app.py").exists() { return candidate; } } } std::env::current_dir().unwrap_or_default() } fn spawn_backend(root: &PathBuf) -> Option { if std::net::TcpStream::connect("127.0.0.1:19000").is_ok() { eprintln!("ℹ️ backend already running on 127.0.0.1:19000"); return None; } let script = root.join("backend").join("app.py"); if !script.exists() { eprintln!("⚠️ backend/app.py not found: {}", script.display()); return None; } let mut candidates: Vec<(PathBuf, Vec)> = vec![ ( root.join(".venv").join("bin").join("python"), vec![script.to_string_lossy().to_string()], ), ( PathBuf::from("python3"), vec![script.to_string_lossy().to_string()], ), ( PathBuf::from("python"), vec![script.to_string_lossy().to_string()], ), ]; if let Ok(custom_python) = std::env::var("STAR_BACKEND_PYTHON") { candidates.insert( 0, ( PathBuf::from(custom_python), vec![script.to_string_lossy().to_string()], ), ); } for (bin, args) in candidates { let mut cmd = Command::new(&bin); cmd.current_dir(root) .args(&args) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()); match cmd.spawn() { Ok(child) => { eprintln!("🚀 backend started with {}", bin.display()); return Some(child); } Err(err) => { eprintln!("⚠️ failed to spawn {}: {}", bin.display(), err); } } } None } fn wait_backend_ready() -> bool { let deadline = Instant::now() + Duration::from_secs(20); while Instant::now() < deadline { if std::net::TcpStream::connect("127.0.0.1:19000").is_ok() { return true; } std::thread::sleep(Duration::from_millis(200)); } false } #[tauri::command] fn enter_minimize_mode( app: tauri::AppHandle, paths: tauri::State<'_, Mutex>, ) -> Result<(), String> { let main = app .get_webview_window("main") .ok_or_else(|| "main window not found".to_string())?; let mini = app .get_webview_window("mini") .ok_or_else(|| "mini window not found".to_string())?; let state_path = { let p = paths.lock().map_err(|e| e.to_string())?; p.state_path.clone() }; if let Ok(snapshot) = read_state_with_fallback(&state_path) { // Sync mini immediately before showing it, avoiding stale one-shot transition. let _ = mini.emit("mini-sync-state", snapshot); } // Keep mini near the main window top-left for continuity. if let Ok(main_pos) = main.outer_position() { let _ = mini.set_position(main_pos); } let _ = main.hide(); let _ = mini.show(); let _ = mini.set_focus(); Ok(()) } #[tauri::command] fn restore_main_window(app: tauri::AppHandle) -> Result<(), String> { let main = app .get_webview_window("main") .ok_or_else(|| "main window not found".to_string())?; let mini = app .get_webview_window("mini") .ok_or_else(|| "mini window not found".to_string())?; let _ = mini.hide(); let _ = main.show(); let _ = main.set_focus(); Ok(()) } #[tauri::command] fn close_app(app: tauri::AppHandle) { app.exit(0); } #[tauri::command] fn open_external_url(url: String) -> Result<(), String> { #[cfg(target_os = "macos")] let mut cmd = { let mut c = Command::new("open"); c.arg(&url); c }; #[cfg(target_os = "windows")] let mut cmd = { let mut c = Command::new("cmd"); c.args(["/C", "start", "", &url]); c }; #[cfg(all(unix, not(target_os = "macos")))] let mut cmd = { let mut c = Command::new("xdg-open"); c.arg(&url); c }; cmd.spawn() .map(|_| ()) .map_err(|e| format!("failed to open browser: {e}")) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let root = find_project_root(); eprintln!("📦 State : {}", root.join("state.json").display()); eprintln!("🎨 Layers: {}", root.join("layers").display()); let backend_child = spawn_backend(&root); let backend_ready = wait_backend_ready(); if !backend_ready { eprintln!("⚠️ backend not ready within 10s"); } tauri::Builder::default() .manage(Mutex::new(BackendProcess { child: backend_child })) .manage(Mutex::new(AppPaths { state_path: root.join("state.json"), layers_dir: root.join("layers"), })) .setup(|app| { // Hidden mini window: transparent square with only avatar + status. let mini = WebviewWindowBuilder::new( app, "mini", WebviewUrl::App("minimized.html".into()), ) .title("Star Mini") .inner_size(220.0, 240.0) .min_inner_size(180.0, 200.0) .resizable(false) .decorations(false) .transparent(true) .always_on_top(true) .shadow(false) .visible(false) .build() .map_err(|e| e.to_string())?; let _ = mini.hide(); Ok(()) }) .invoke_handler(tauri::generate_handler![ read_state, load_layers, load_map, enter_minimize_mode, restore_main_window, close_app, open_external_url ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ================================================ FILE: desktop-pet/src-tauri/src/main.rs ================================================ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { star_desktop_pet_lib::run(); } ================================================ FILE: desktop-pet/src-tauri/tauri.conf.json ================================================ { "productName": "Star Desktop Pet", "version": "0.1.0", "identifier": "com.star.desktop-pet", "build": { "frontendDist": "../src" }, "app": { "macOSPrivateApi": true, "withGlobalTauri": true, "windows": [ { "label": "main", "title": "Star Desktop Pet", "x": 80, "y": 60, "width": 700, "height": 500, "url": "http://127.0.0.1:19000/?desktop=1", "decorations": false, "transparent": true, "alwaysOnTop": true, "resizable": true, "shadow": false } ] }, "bundle": { "icon": [ "icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" ] } } ================================================ FILE: dist/Star-Office-UI-release-20260302/RELEASE_NOTES.md ================================================ # Star-Office-UI Release Notes (2026-03-02) ## Summary This package is a cleaned release snapshot for handoff/update. It excludes runtime files, logs, and local backup artifacts. ## Included - backend/ - frontend/ - docs/ - assets/room-reference.png - core scripts and docs (README.md, SKILL.md, LICENSE, set_state.py, etc.) - asset-defaults.json / asset-positions.json ## Excluded on purpose - .git/ - .venv/ - __pycache__/ - *.log / *.out / *.pid - *.bak (frontend image backups) - assets/bg-history/ (historical generated backgrounds) - runtime state files: state.json / agents-state.json / join-keys.json ## Artifact - File: `dist/Star-Office-UI-release-20260302.tgz` - SHA256: `bf52147b7664adc3c457eadd3748f969b1ad5ee7e8d3059ce9c8da4c6030f6ae` ## Pre-publish checklist 1. Confirm whether `asset-defaults.json` and `asset-positions.json` should be shipped as current defaults. 2. Confirm whether `assets/bg-history/` should remain local-only (currently excluded). 3. On target machine, create fresh `state.json` and `join-keys.json` if needed. 4. Start backend and validate: - `/health` - `/status` - language switches (EN/JP/CN) - loading overlay + sidebar layering - asset drawer selection / upload panel behavior ================================================ FILE: docs/CHANGELOG_2026-03.md ================================================ # CHANGELOG — 2026-03 ## 2026-03-06 - 默认端口从 `18791` 调整为 `19000`,避开 OpenClaw Browser Control 端口冲突 - 同步更新 `office-agent-push.py`、`healthcheck.sh`、`scripts/smoke_test.py` 的默认地址 - 同步更新 Tauri / Electron 桌面壳默认连接地址 - 同步更新 README / SKILL / join-office 文档中的本地访问与 tunnel 示例 # Changelog — 2026-03 Refresh ## Highlights - Added robust asset editing workflow in drawer (select/deselect, highlight sync, default/override split) - Added EN/JP/CN language buttons with real-time UI + loading + bubble text switching - Added room loading overlay with emoji rotation and localized copy - Fixed layering/layout issues (drawer overlap, detail overflow, canvas border fit) - Completed multi-round state sprite replacement pipeline (Writing/Idle/Syncing/Error) with auto frame sync - Updated syncing behavior: non-sync shows frame 0, syncing starts from frame 1 - Disabled error movement path (error anim stays in place) - Removed GIF legacy assets and stale references - Restored `assets/room-reference.png` for reference background restore - Added configurable asset drawer password via env (`ASSET_DRAWER_PASS`, default `1234`) - Improved startup performance: - static assets use long cache headers - local phaser vendor restored ## Security / Config - Asset drawer default pass changed to `1234` - Recommend deployment override: - `ASSET_DRAWER_PASS=` - Rationale: prevent unauthorized layout/asset modifications from shared links ## AI model recommendation for room generation For best style-transfer quality (while preserving room structure), recommend: 1. gemini nanobanana pro 2. gemini nanobanana 2 Other models may produce unstable structure consistency. ================================================ FILE: docs/FEATURES_NEW_2026-03-01.md ================================================ # Star Office UI — 新增功能说明(本阶段) ## 1. 多龙虾访客系统 - 支持多个远端 OpenClaw 同时加入同一办公室。 - 访客支持独立头像、名字、状态、区域、气泡。 - 支持动态上下线与实时刷新。 ## 2. Join Key 机制升级 - 从“一次性 key”升级为“固定可复用 key”。 - 默认 key:`ocj_starteam01` ~ `ocj_starteam08`。 - 保留安全控制:每个 key 的并发上限 `maxConcurrent`(默认 3)。 ## 3. 并发控制(已修复竞态) - 修复并发 join 的竞态问题(race condition)。 - 同 key 第 4 个并发 join 会被正确拒绝(HTTP 429)。 ## 4. 访客状态映射与区域渲染 - `idle -> breakroom` - `writing/researching/executing/syncing -> writing` - `error -> error` - 访客气泡文案与状态同步,不再错位。 ## 5. 访客动画与资源优化 - 访客由静态图升级为动画精灵(像素风)。 - `guest_anim_1~6` 已提供 webp 版本,减少加载体积。 ## 6. 名字与气泡显示优化 - 非 demo 访客名字与气泡位置上移,避免角色遮挡。 - 气泡锚点改为基于名字定位,保障“气泡在名字上方”。 ## 7. 移动端展示 - 页面可在手机端直接访问与展示。 - 布局已进行基础移动端适配,满足演示场景。 ## 8. 远端推送脚本联调改进 - 支持从状态文件读取并推送状态到 office。 - 增加状态来源诊断日志(用于定位“为何一直 idle”)。 - 修复 AGENT_NAME 环境变量覆盖时序问题。 ================================================ FILE: docs/OPEN_SOURCE_RELEASE_CHECKLIST.md ================================================ # Star Office UI — 开源发布准备清单(仅准备,不上传) ## 0. 当前目标 - 本文档用于“发布前准备”,不执行实际上传。 - 所有 push 行为需海辛最终明确批准。 ## 1. 隐私与安全审查结果(当前仓库) ### 发现高风险文件(必须排除) - 运行日志: - `cloudflared.out` - `cloudflared-named.out` - `cloudflared-quick.out` - `healthcheck.log` - `backend.log` - `backend/backend.out` - 运行状态: - `state.json` - `agents-state.json` - `backend/backend.pid` - 备份/历史文件: - `index.html.backup.*` - `index.html.original` - `*.backup*` 目录与文件 - 本地虚拟环境与缓存: - `.venv/` - `__pycache__/` ### 发现潜在敏感内容 - 代码内含绝对路径 `/root/...`(建议改为相对路径或环境变量) - 文档与脚本含私有域名 `office.example.com`(可保留为示例,但建议改成占位域名) ## 2. 必改项(提交前) ### A. .gitignore(需补齐) 建议新增: ``` *.log *.out *.pid state.json agents-state.json join-keys.json *.backup* *.original __pycache__/ .venv/ venv/ ``` ### B. README 版权声明(必须新增) 新增“美术资产版权与使用限制”章节: - 代码按开源协议(如 MIT) - 美术素材归原作者/工作室所有 - 素材仅供学习/演示,**禁止商用** ### C. 发布目录瘦身 - 清理运行日志、运行态文件、备份文件 - 仅保留“可运行最小集 + 必要素材 + 文档” ## 3. 准备中的发布包建议结构 ``` star-office-ui/ backend/ app.py requirements.txt run.sh frontend/ index.html game.js (若仍需要) layout.js assets/* (仅可公开素材) office-agent-push.py set_state.py state.sample.json README.md LICENSE SKILL.md docs/ ``` ## 4. 发布前最终核对(给海辛确认) - [ ] 是否保留私有域名示例(`office.example.com`) - [ ] 哪些美术资源允许公开(逐项确认) - [ ] README 非商用声明是否满足你的预期措辞 - [ ] 是否需要将“阿文龙虾联调脚本”单独放 examples 目录 ## 5. 当前状态 - ✅ 文档准备完成(总结、功能说明、Skill v2、发布检查清单) - ⏳ 等待海辛确认“公开素材范围 + 声明文案 + 是否开始执行打包清理脚本” - ⛔ 尚未执行 GitHub 上传 ================================================ FILE: docs/PROJECT_MAINTENANCE_SOP.md ================================================ # Star-Office-UI 项目维护 SOP(轻量版) > 目标:让 Star-Office-UI 在继续增长的同时,保持仓库干净、回复友好、节奏稳定、社区感明确。 --- ## 1. 总原则 ### 1.1 关闭 issue / PR 时,一定留一句 closure reason 无论是: - 已修复 - 重复 - 超出当前范围 - 提问者自行取消 - 已被其他 PR / issue 吸收 都尽量留一句话说明原因。 **最小模板:** - Fixed in `commit/PR #xxx`, thanks for the report! - Closing as duplicate of #xxx, thank you! - Out of current scope for now, but welcome a focused PR. - Canceled by requester / resolved in latest master. 目标不是“正式”,而是让后来人一眼看懂为什么被关。 --- ## 2. Issue 处理规则 ### 2.1 先判断 issue 类型 收到 issue 后,先分到四类之一: #### A. Bug report 特征:报错、页面打不开、功能异常、状态不对 处理方式: 1. 复现 / 判断是否已知问题 2. 如果已修:回复 + 给 commit / PR 号 3. 如果未修:标记为待处理,必要时自己修 / 等 PR 4. close 时一定写清楚“修在哪了” **推荐回复模板:** > 感谢反馈!这个问题已在 `PR #xx` / `commit xxx` 中修复。请拉取最新 master 后再试一下,如果还有问题欢迎继续反馈。 --- #### B. Support / setup question 特征:怎么部署、为什么 Unauthorized、如何自动同步等 处理方式: 1. 先回答问题 2. 给最短路径(README / SKILL / 命令) 3. 如果文档能优化,顺手记成后续动作 4. close 时说明“问题已答复,如仍有问题欢迎 reopen” **推荐回复模板:** > 这个问题大概率和 xxx 有关。最新版已经做了相关修复 / 文档补充。你可以先试试最新 master;如果还有问题,欢迎重新打开 issue。 --- #### C. Feature request 特征:希望支持某个新能力、新方向、新体验 处理方式: 1. 明确是否感兴趣 2. 不要误关成“已修复” 3. 如果暂不做,也要说清楚“当前不做,但欢迎 PR / 后续讨论” **推荐回复模板:** > 这是个很好的方向,我们对这个想法感兴趣。不过它还不是当前阶段的既定工作项。如果你愿意推进,欢迎提一个更聚焦的 PR,我们可以一起讨论实现方式。 --- #### D. Duplicate / canceled / absorbed 特征:重复提问、提问者自己放弃、已被其他 issue 吸收 处理方式: 1. 链接到对应 issue / PR 2. 简短说明关闭原因 3. 保持礼貌 --- ## 3. PR 处理规则 ### 3.1 Merge 前检查四件事 #### 1) 这个 PR 是不是解决了真实问题? - 是 bug fix 还是只是作者个人偏好? - 是否对应某个 issue / 用户痛点? #### 2) 改动范围是否可控? - 小而聚焦 → 倾向合并 - 大而混杂 → 要求拆分 / 暂缓 #### 3) 是否引入额外维护负担? - 新依赖 - 新配置 - 新架构 - 新文档成本 #### 4) 是否需要同步 README / changelog / release notes? 如果影响用户使用路径,必须同步文档。 --- ### 3.2 PR 结果分三类 #### A. 直接合并 适合: - 小 bug fix - 文档修正 - 明确提升 onboarding / 稳定性 #### B. 关闭但感谢 适合: - 已被 master 提前修复 - 重复 PR - 方向不错但当前不合适 **原则:不合并 ≠ 否定贡献者** #### C. 请求作者调整后再看 适合: - 思路对,但改动太大 - 混入不相关内容 - 需要拆小 --- ### 3.3 关闭 PR 时,尽量做到三件事 1. **先感谢** 2. **再说明原因** 3. **如果 possible,指出未来更容易被接受的方向** **推荐模板:** > Thanks for the PR — this is a thoughtful direction. We’re not merging it right now because xxx. If you’d like, a smaller / more focused PR around yyy would be much easier for us to review and land. --- ## 4. Release / 大版本收口流程 适用于: - 一轮 bug fix 完成 - 一次文档重构完成 - 一次功能包发布(如 v1.0) ### 发布前 checklist - [ ] 关键功能本机验证一次(至少 health / status / agents / set_state) - [ ] smoke test 跑通 - [ ] README / SKILL / relevant docs 已同步 - [ ] CHANGELOG 已更新 - [ ] 相关 issue 已回复 / 关闭 - [ ] 如果有贡献者,考虑在 README / release note 致谢 - [ ] 确认仓库 worktree 干净,没有误提交文件 ### Release note 结构建议 1. 这次版本是什么 2. 核心变化 3-5 条 3. 对用户有什么实际影响 4. 快速体验方式 5. 感谢贡献者 --- ## 5. README / 文档维护规则 ### 5.1 README 优先回答四个问题 1. 这是什么? 2. 适合谁? 3. 最快怎么用? 4. 如果我是 OpenClaw 用户,最短路径是什么? ### 5.2 文档更新触发条件 以下情况发生时,要同步 README / docs: - 默认端口改变 - 默认安装方式改变 - 核心依赖或路径改变 - onboarding 流程改变 - 修复了高频 issue(尤其是部署 / 401 / loading 这类) --- ## 6. 社区关系维护 ### 6.1 要主动做的三件事 - 在 README 或 release note 感谢明显贡献者 - 对早期贡献者保持尊重,即使 PR 没合并 - 对误解 / 错判及时补充说明 ### 6.2 哪些 contributor 值得重点维护 优先维护这些人: - 连续提多个高质量 PR 的 - 会主动补文档 / onboarding 的 - 不只是修自己问题,而是在帮项目补完整性的 --- ## 7. 当前阶段最适合 Star-Office-UI 的维护策略 ### 适合优先接收 - bug fix - onboarding 改进 - 文档优化 - 小而明确的稳定性修复 - 与 OpenClaw / agent 体验强相关的增强 ### 暂时谨慎对待 - 大规模重构 - 引入重依赖 - 强绑定某个个人工作流的改动 - 边界不清的大 feature --- ## 8. Star 自己要记住的维护原则 - 不要为了“显得热情”而模糊关闭原因 - 不要把 feature request 当成 bug fix 关掉 - 不要 merge 之后忘了补文档 - 不要忽略早期贡献者 - 仓库看起来干净,本身就是产品体验的一部分 --- ## 一句话版本 > **小问题及时收口,大问题说清边界;每次关闭都留痕,每次发布都成阶段。** ================================================ FILE: docs/PROJECT_SUMMARY_2026-03-01.md ================================================ # Star Office UI — 项目阶段总结(2026-03-01) ## 一、今日工作总结 今天主要完成了两条主线: 1. **多龙虾(多 OpenClaw)加入办公室能力稳定化** 2. **手机版展示能力完善** 并且围绕“阿文龙虾状态同步不稳定”做了多轮排查,明确了链路问题与当前未完全闭环点。 --- ## 二、已完成能力(可对外描述) ### 1) 多 Agent 加入与显示 - 支持多个远端 OpenClaw 通过 `join-agent` 加入办公室。 - 每个访客有独立 `agentId`、名字、状态、区域与动画。 - 场景会基于 `/agents` 动态创建、更新、移除访客。 ### 2) 固定可复用 Join Key 机制 - 一次性 key 改为固定可复用 key:`ocj_starteam01` ~ `ocj_starteam08`。 - 去掉了“used 即不可再用”的阻断逻辑,支持长期复用。 - 加入了并发上限配置(`maxConcurrent`),默认每个 key 限 3 并发在线。 ### 3) 并发限制修复(关键) - 发现 4 并发仍能通过的根因是后端竞争条件(race condition)。 - 在 `join-agent` 临界区增加锁 + 锁内重读状态,修复后压测通过: - 前 3 个 200 - 第 4 个 429 ### 4) 访客动画与性能优化 - 访客动画改为像素动画精灵,不再是静态星星。 - `guest_anim_1~6` 已转为 `.webp`,显著降低加载体积。 - 前端预加载与渲染资源已切换到 webp 优先。 ### 5) 状态 → 区域映射统一 - 规则统一: - `idle -> breakroom` - `writing/researching/executing/syncing -> writing` - `error -> error` - 访客 bubble 文案已按状态做映射,不再与区域脱节。 ### 6) 名字与气泡层级/位置优化 - 非 demo 访客名字、气泡位置上移,减少遮挡。 - 访客气泡锚点改为相对名字计算,确保“气泡在名字上方”。 - demo 与真实访客路径已区分,互不干扰。 ### 7) 手机版展示 - 现有 UI 在手机端可访问与展示,适合演示与外部查看。 - 关键控件布局做过整理,移动端基本可用。 --- ## 三、当前未完全闭环点(诚实披露) ### 阿文龙虾“真实状态稳定同步”仍存在偶发不一致 虽然链路已多次验证打通(writing 能进工作区、idle 能回休息区),但线上实测仍出现过: - 本地脚本持续推 idle(旧版本脚本 / 读错状态源) - 403 未授权(离线状态恢复/旧 agentId 缓存问题) - 前台退出触发 leave-agent 后角色消失 > 结论: > - “机制可行、链路可通”已经验证; > - “端到端持续稳定”还需要继续收口(尤其阿文侧运行脚本版本统一、状态源统一、常驻策略统一)。 --- ## 四、今天新增/调整文件(核心) - `backend/app.py` - join 并发限制加锁修复 - offline/approved 授权流逻辑调整(便于恢复) - `join-keys.json` - 固定 key + `maxConcurrent: 3` - `frontend/index.html`(及相关渲染逻辑) - 访客动画、名字与气泡定位优化 - 状态文案映射调整 - `office-agent-push.py`(多版本并行调试) - 增加状态源诊断日志 - 增加环境变量覆盖逻辑 - 修复 AGENT_NAME 读取时机问题 --- ## 五、对外开源前建议描述(建议文案) > Star Office UI 是一个可视化多 Agent 像素办公室: > 支持多个 OpenClaw 远端接入、状态驱动位置渲染、访客动画与移动端访问。 > 项目当前已完成多 Agent 主链路与 UI 能力;状态同步稳定性仍在持续优化中。 --- ## 六、下一步(建议) 1. 统一阿文侧运行脚本“唯一来源”,避免旧版本混跑。 2. 增加 `/agent-push` 与前端渲染诊断日志(可开关)。 3. 增加“状态过期自动 idle”兜底(脚本侧 + 服务端侧双保险)。 4. 补一份可复现联调流程(10 分钟 smoke test)。 5. 完成开源前隐私清理与发布清单(见 `docs/OPEN_SOURCE_RELEASE_CHECKLIST.md`)。 ================================================ FILE: docs/PR_DRAFT_2026-03-refresh.md ================================================ # PR Draft — Star Office UI March Refresh ## Title feat: asset editor + i18n + loading UX + sprite pipeline + security/perf refinements ## Summary This PR delivers a full refresh of Star Office UI across UX, asset pipeline, localization, stability, and deployment security. ### What changed #### 1) Asset editor / room decoration - Improved drawer selection UX (select/deselect + dual highlight sync) - Upload panel now appears only when an asset is selected - Added defaults/overrides split: - `GET/POST /assets/defaults` - `GET/POST /assets/positions` - Added “Set Default” flow for persistent base placement #### 2) Localization (CN/EN/JP) - Replaced language toggles with EN / JP / CN buttons - Active language button highlighted in green - Real-time language switching for: - UI labels - loading texts - role/cat/guest bubbles - initial boot loading sentence #### 3) Loading overlay and UX polish - Added room-bound loading overlay with emoji rotation - Updated copy to voyage-themed localized sets - Trigger timing fixed: overlay shows immediately on click - Overlay and detail placement bound to canvas rect for consistency #### 4) Layout and layering fixes - Fixed canvas border fit and theme color unification (#64477d) - Ensured status/detail stays inside canvas and single-line clipped - Drawer open now shifts main stage to avoid overlap and large gaps - Drawer kept above room loading overlay #### 5) Sprite replacement pipeline hardening - Reworked replacement flow to detect frame size/count from incoming animated webp - Synced loader + animation frame ranges to avoid flicker - Applied across Writing / Idle / Syncing / Error replacements - Syncing behavior adjusted: - non-sync state shows frame 0 - syncing animation starts from frame 1 - Error animation movement path removed (fixed in place) #### 6) Cleanup / reliability / perf - Removed legacy GIF assets - Removed stale asset references (zero missing static refs) - Restored `assets/room-reference.png` for restore-reference endpoint - Added configurable drawer pass via env: - `ASSET_DRAWER_PASS` (default `1234`) - Performance improvements: - static assets served with long cache headers - local phaser vendor restored to reduce cold-load latency ## Documentation updates included - README rewritten for latest behavior/config - SKILL updated with deployment + safety + replacement SOP - LICENSE updated to remove old third-party character disclaimer and keep: - code MIT - art assets non-commercial - Added `docs/CHANGELOG_2026-03.md` ## Deployment notes - Recommended model for room generation: 1. gemini nanobanana pro 2. gemini nanobanana 2 - Security recommendation: - always override `ASSET_DRAWER_PASS` in production/public deployments ## Test checklist - [ ] Open page cold + warm load - [ ] Switch CN/EN/JP at any state - [ ] Trigger Move Home/Broker and observe local status text + loading overlay - [ ] Replace one animated asset and verify frame sync/no flicker - [ ] Verify Error is fixed in place - [ ] Verify `/assets/restore-reference-background` works with `assets/room-reference.png` - [ ] Verify no missing `/static/*` refs in runtime logs ## How to create PR 1. `git checkout -b feat/march-refresh` 2. `git push -u origin feat/march-refresh` 3. Open PR to `ringhyacinth/Star-Office-UI:main` 4. Paste this document as PR description ================================================ FILE: docs/PR_FILELIST_2026-03-refresh.md ================================================ # PR File List — 2026-03 Refresh ## Core code changes - `frontend/index.html` - `backend/app.py` - `frontend/vendor/phaser-3.80.1.min.js` - `assets/room-reference.webp` ## Configuration / templates - `.gitignore` - `runtime-config.sample.json` ## Documentation - `README.md` - `SKILL.md` - `LICENSE` - `docs/CHANGELOG_2026-03.md` - `docs/PR_DRAFT_2026-03-refresh.md` - `docs/PR_FILELIST_2026-03-refresh.md` ## Notes (excluded from PR) - `state.json`, `agents-state.json`, `runtime-config.json` (local runtime) - `assets/bg-history/` (local generated history) - `frontend/*.bak` (local backups) - temporary dist packages ================================================ FILE: docs/STAR_OFFICE_UI_OVERVIEW.md ================================================ # Star Office UI — 功能说明(Overview) Star Office UI 是一个“像素办公室”可视化界面,用来把 AI 助手/多个 OpenClaw 访客的状态,渲染成可在网页(含手机)查看的小办公室场景。 ## 你能看到什么 - 像素办公室背景(俯视图) - 角色(Star + 访客)会根据状态在不同区域移动 - 名字与气泡(bubble)展示当前状态/想法(可自定义映射) - 手机端打开也能展示(适合作品展示/直播/对外演示) ## 核心能力 ### 1) 单 Agent(本地 Star)状态渲染 - 后端读取 `state.json` 提供 `GET /status` - 前端轮询 `/status`,根据 `state` 渲染 Star 所在区域 - 提供 `set_state.py` 快速切换状态 ### 2) 多访客(多龙虾)加入办公室 - 访客通过 `POST /join-agent` 加入,获得 `agentId` - 访客通过 `POST /agent-push` 持续推送自己的状态 - 前端通过 `GET /agents` 拉取访客列表并渲染 ### 3) Join Key(接入密钥)机制 - 支持固定可复用 join key(如 `ocj_starteam01~08`) - 支持每个 key 的并发在线上限(默认 3) - 便于控制“谁能进办公室”和“同一个 key 同时可进几只龙虾” ### 4) 状态 → 区域映射(统一逻辑) - idle → breakroom(休息区) - writing / researching / executing / syncing → writing(工作区) - error → error(故障区) ### 5) 访客动画与性能优化 - 访客角色使用动画精灵 - 支持 WebP 资源(体积更小、加载更快) ### 6) 名字/气泡不遮挡的布局 - 真实访客与 demo 访客分离逻辑 - 非 demo 访客名字与气泡整体上移 - bubble 锚定在名字上方,避免压住名字 ### 7) Demo 模式(可选) - `?demo=1` 才显示 demo 访客(默认不显示) - demo 与真实访客互不影响 ## 主要接口(Backend) - `GET /`:前端页面 - `GET /status`:单 agent 状态(兼容旧版) - `GET /agents`:多 agent 列表(访客渲染用) - `POST /join-agent`:访客加入 - `POST /agent-push`:访客推送状态 - `POST /leave-agent`:访客离开 - `GET /health`:健康检查 ## 安全与隐私注意 - 不要把隐私信息写进 `detail`(因为会被渲染/可被拉取) - 开源前必须清理:日志、运行态文件、join keys、隧道输出等 ## 美术资产使用声明(必须) - 代码可开源,但美术素材(背景、角色、动画等)版权归原作者/工作室所有。 - 美术资产仅供学习与演示,**禁止商用**。 ================================================ FILE: docs/UPDATE_REPORT_2026-03-04_P0_P1.md ================================================ # Star Office UI 更新文档(P0 / P1) 更新时间:2026-03-04 分支:`feat/office-art-rebuild` --- ## 1. 更新目标 本轮更新目标分为两层: - **P0:安全与可发布性**(防泄漏、防弱配置、上线前可自检) - **P1:结构与稳定性优化**(不减功能、提升状态同步与加载体验) 同时处理了线上关键问题: - 服务偶发 502(进程/服务启动方式不稳定) - 角色状态与真实工作状态不一致(尤其“回复结束仍在工位”) --- ## 2. P0 已完成项 ### 2.1 后端安全基线加固 - 增加生产模式安全校验(弱密钥/弱口令阻止启动) - Session Cookie 安全参数加固(HttpOnly / SameSite / Secure) - `runtime-config.json` 写入后自动尝试收紧文件权限(`600`) ### 2.2 敏感文件治理 - `.gitignore` 补充运行态文件与高风险文件 - 引入样例文件替代运行态文件: - `join-keys.sample.json` - `.env.example` - `join-keys.json` 改为运行时初始化,不再作为仓库内固定配置 ### 2.3 上线前安全自检能力 - 新增 `scripts/security_check.py` - 可检查: - 弱 secret / 弱口令 - 风险文件是否被 git 跟踪 - 常见敏感 token 模式 --- ## 3. P1 已完成项(不改业务能力) ### 3.1 后端结构拆分 在不改变现有 API 行为前提下,把 `backend/app.py` 拆出: - `backend/security_utils.py` - `backend/memo_utils.py` - `backend/store_utils.py` 收益: - 降低单文件复杂度 - 降低后续功能改动时的回归风险 - 提升可读性与维护效率 ### 3.2 状态同步修复(核心) - 修复状态源路径优先级(避免读取错误状态文件) - 增加 stale 状态自动回 `idle` 机制(避免假工作中) - 前端状态轮询改为更快节奏并强制视觉对齐,避免动画卡旧状态 ### 3.3 生图模型策略收敛 按需求收敛为两种用户模型语义: - `nanobanana-pro` - `nanobanana-2` 并补充 provider 映射与错误细节透出,提升可诊断性。 ### 3.4 首屏性能与体感优化 - 首页 HTML 缓存(后端进程内缓存) - 非关键初始化延后(先出画面) - 加入画布骨架屏,减少“黑屏 + 长时间加载中”体感 - 加速 loading overlay 淡出 --- ## 4. 线上稳定性修复(本轮重点) ### 4.1 502 根因 Cloudflare 正常,但 `18888` 源站进程存在不稳定/启动方式不一致,导致偶发 connection refused。 ### 4.2 已处理 - 修复并统一 `star-office-ui.service` 启动方式(systemd 常驻) - 清理手工临时启动造成的端口抢占 - 重启并验证: - `star-office-ui.service` 运行正常 - `star-office-push.service` 运行正常 --- ## 5. 当前已知风险 / 待跟进 1. **状态策略仍需完全事件化** - 目前已大幅收敛误判,但建议后续做单一状态控制器(显式事件优先,彻底禁用隐式推断) 2. **进程模型仍是 Flask 开发服务器** - 当前可用但不理想,后续建议迁移为 gunicorn/uvicorn 等生产进程模型 3. **动画状态同步仍建议增加端到端回归脚本** - 尤其 writing / syncing / error / idle 切换链路 --- ## 6. 验收建议(人工) 验收地址:`https://simonoffice.hyacinth.im/` 建议至少覆盖: 1. 首页进入速度与骨架屏体验 2. 状态切换(writing / syncing / error / idle) 3. 回复结束后是否回到待命区 4. 生图两入口(搬新家 / 找中介) 5. 断网或服务短时波动后是否自动恢复 --- ## 7. 提交范围(摘要) 本轮主要覆盖: - 安全与配置:P0 - 后端重构:P1 - 状态同步与动画一致性修复 - 生图模型策略与错误诊断 - 加载性能与体验优化 - systemd 常驻与稳定性修复 如需 PR 附件,可直接将本文件作为“更新说明 / Release Notes”。 ================================================ FILE: docs/UPDATE_REPORT_2026-03-05.md ================================================ # 更新报告 — 2026-03-05 > 本次更新覆盖 8 个 commit,聚焦「稳定性修复 + 移动端体验 + 安全收尾」。 --- ## 变更概览 | # | Commit | 分类 | 说明 | |---|--------|------|------| | 1 | `878793d` | 🐛 fix | 修复 CDN 缓存 404 导致页面无法加载 | | 2 | `cc22403` | 🐛 fix | 修复 `fetchStatus()` 中多余的 `else` 块导致 JS 语法错误 | | 3 | `103f944` | 🐛 fix | 生图接口改为异步任务模式,避免 Cloudflare 524 超时 | | 4 | `ee141de` | 🧹 chore | 清理本地测试时意外提交的文件 | | 5 | `83e61ff` | 🧹 chore | 将 `join-keys.json` 加入 `.gitignore`(运行时数据不入库) | | 6 | `899f27e` | 🐛 fix | 移动端/iPad 侧边栏修复(遮罩层 + body 滚动锁定 + `100dvh`) | | 7 | `5aef430` | 🐛 fix | 移动端 drawer 关闭时完全移出屏幕(`right: -100vw`) | | 8 | `02a731e` | ✨ feat | 新增 join key 级别过期时间 + 并发上限支持 | --- ## 详细说明 ### 1. 修复 CDN 缓存 404(`878793d`) **问题**:`/static/` 路径下的所有响应(含 404)都被设置了一年长缓存头。Cloudflare 缓存了 `phaser.js` 的 404 响应长达 2.7 天,导致 `office.hyacinth.im` 完全无法加载。 **修复**: - `add_no_cache_headers` 仅对 2xx 响应设置长缓存,非 2xx 响应设为 no-cache - 给 `phaser.js` 的 ` ================================================ FILE: frontend/fonts/OFL.txt ================================================ Copyright (c) 2021, TakWolf (https://takwolf.com), with Reserved Font Name "Ark Pixel". This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: https://openfontlicense.org ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: frontend/game.js ================================================ // Star Office UI - 游戏主逻辑 // 依赖: layout.js(必须在这个之前加载) // 检测浏览器是否支持 WebP let supportsWebP = false; // 方法 1: 使用 canvas 检测 function checkWebPSupport() { return new Promise((resolve) => { const canvas = document.createElement('canvas'); if (canvas.getContext && canvas.getContext('2d')) { resolve(canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0); } else { resolve(false); } }); } // 方法 2: 使用 image 检测(备用) function checkWebPSupportFallback() { return new Promise((resolve) => { const img = new Image(); img.onload = () => resolve(true); img.onerror = () => resolve(false); img.src = 'data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAABBxAR/Q9ERP8DAABWUDggGAAAADABAJ0BKgEAAQADADQlpAADcAD++/1QAA=='; }); } // 获取文件扩展名(根据 WebP 支持情况 + 布局配置的 forcePng) function getExt(pngFile) { // star-working-spritesheet.png 太宽了,WebP 不支持,始终用 PNG if (pngFile === 'star-working-spritesheet.png') { return '.png'; } // 如果布局配置里强制用 PNG,就用 .png if (LAYOUT.forcePng && LAYOUT.forcePng[pngFile.replace(/\.(png|webp)$/, '')]) { return '.png'; } return supportsWebP ? '.webp' : '.png'; } const config = { type: Phaser.AUTO, width: LAYOUT.game.width, height: LAYOUT.game.height, parent: 'game-container', pixelArt: true, physics: { default: 'arcade', arcade: { gravity: { y: 0 }, debug: false } }, scene: { preload: preload, create: create, update: update } }; let totalAssets = 0; let loadedAssets = 0; let loadingProgressBar, loadingProgressContainer, loadingOverlay, loadingText; // Memo 相关函数 async function loadMemo() { const memoDate = document.getElementById('memo-date'); const memoContent = document.getElementById('memo-content'); try { const response = await fetch('/yesterday-memo?t=' + Date.now(), { cache: 'no-store' }); const data = await response.json(); if (data.success && data.memo) { memoDate.textContent = data.date || ''; memoContent.innerHTML = data.memo.replace(/\n/g, '
'); } else { memoContent.innerHTML = '
暂无昨日日记
'; } } catch (e) { console.error('加载 memo 失败:', e); memoContent.innerHTML = '
加载失败
'; } } // 更新加载进度 function updateLoadingProgress() { loadedAssets++; const percent = Math.min(100, Math.round((loadedAssets / totalAssets) * 100)); if (loadingProgressBar) { loadingProgressBar.style.width = percent + '%'; } if (loadingText) { loadingText.textContent = `正在加载 Star 的像素办公室... ${percent}%`; } } // 隐藏加载界面 function hideLoadingOverlay() { setTimeout(() => { if (loadingOverlay) { loadingOverlay.style.transition = 'opacity 0.5s ease'; loadingOverlay.style.opacity = '0'; setTimeout(() => { loadingOverlay.style.display = 'none'; }, 500); } }, 300); } const STATES = { idle: { name: '待命', area: 'breakroom' }, writing: { name: '整理文档', area: 'writing' }, researching: { name: '搜索信息', area: 'researching' }, executing: { name: '执行任务', area: 'writing' }, syncing: { name: '同步备份', area: 'writing' }, error: { name: '出错了', area: 'error' } }; const BUBBLE_TEXTS = { idle: [ '待命中:耳朵竖起来了', '我在这儿,随时可以开工', '先把桌面收拾干净再说', '呼——给大脑放个风', '今天也要优雅地高效', '等待,是为了更准确的一击', '咖啡还热,灵感也还在', '我在后台给你加 Buff', '状态:静心 / 充电', '小猫说:慢一点也没关系' ], writing: [ '进入专注模式:勿扰', '先把关键路径跑通', '我来把复杂变简单', '把 bug 关进笼子里', '写到一半,先保存', '把每一步都做成可回滚', '今天的进度,明天的底气', '先收敛,再发散', '让系统变得更可解释', '稳住,我们能赢' ], researching: [ '我在挖证据链', '让我把信息熬成结论', '找到了:关键在这里', '先把变量控制住', '我在查:它为什么会这样', '把直觉写成验证', '先定位,再优化', '别急,先画因果图' ], executing: [ '执行中:不要眨眼', '把任务切成小块逐个击破', '开始跑 pipeline', '一键推进:走你', '让结果自己说话', '先做最小可行,再做最美版本' ], syncing: [ '同步中:把今天锁进云里', '备份不是仪式,是安全感', '写入中…别断电', '把变更交给时间戳', '云端对齐:咔哒', '同步完成前先别乱动', '把未来的自己从灾难里救出来', '多一份备份,少一份后悔' ], error: [ '警报响了:先别慌', '我闻到 bug 的味道了', '先复现,再谈修复', '把日志给我,我会说人话', '错误不是敌人,是线索', '把影响面圈起来', '先止血,再手术', '我在:马上定位根因', '别怕,这种我见多了', '报警中:让问题自己现形' ], cat: [ '喵~', '咕噜咕噜…', '尾巴摇一摇', '晒太阳最开心', '有人来看我啦', '我是这个办公室的吉祥物', '伸个懒腰', '今天的罐罐准备好了吗', '呼噜呼噜', '这个位置视野最好' ] }; let game, star, sofa, serverroom, areas = {}, currentState = 'idle', pendingDesiredState = null, statusText, lastFetch = 0, lastBlink = 0, lastBubble = 0, targetX = 660, targetY = 170, bubble = null, typewriterText = '', typewriterTarget = '', typewriterIndex = 0, lastTypewriter = 0, syncAnimSprite = null, catBubble = null; let isMoving = false; let waypoints = []; let lastWanderAt = 0; let coordsOverlay, coordsDisplay, coordsToggle; let showCoords = false; const FETCH_INTERVAL = 2000; const BLINK_INTERVAL = 2500; const BUBBLE_INTERVAL = 8000; const CAT_BUBBLE_INTERVAL = 18000; let lastCatBubble = 0; const TYPEWRITER_DELAY = 50; let agents = {}; // agentId -> sprite/container let lastAgentsFetch = 0; const AGENTS_FETCH_INTERVAL = 2500; // agent 颜色配置 const AGENT_COLORS = { star: 0xffd700, npc1: 0x00aaff, agent_nika: 0xff69b4, default: 0x94a3b8 }; // agent 名字颜色 const NAME_TAG_COLORS = { approved: 0x22c55e, pending: 0xf59e0b, rejected: 0xef4444, offline: 0x64748b, default: 0x1f2937 }; // breakroom / writing / error 区域的 agent 分布位置(多 agent 时错开) const AREA_POSITIONS = { breakroom: [ { x: 620, y: 180 }, { x: 560, y: 220 }, { x: 680, y: 210 }, { x: 540, y: 170 }, { x: 700, y: 240 }, { x: 600, y: 250 }, { x: 650, y: 160 }, { x: 580, y: 200 } ], writing: [ { x: 760, y: 320 }, { x: 830, y: 280 }, { x: 690, y: 350 }, { x: 770, y: 260 }, { x: 850, y: 340 }, { x: 720, y: 300 }, { x: 800, y: 370 }, { x: 750, y: 240 } ], error: [ { x: 180, y: 260 }, { x: 120, y: 220 }, { x: 240, y: 230 }, { x: 160, y: 200 }, { x: 220, y: 270 }, { x: 140, y: 250 }, { x: 200, y: 210 }, { x: 260, y: 260 } ] }; // 状态控制栏函数(用于测试) function setState(state, detail) { fetch('/set_state', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ state, detail }) }).then(() => fetchStatus()); } // 初始化:先检测 WebP 支持,再启动游戏 async function initGame() { try { supportsWebP = await checkWebPSupport(); } catch (e) { try { supportsWebP = await checkWebPSupportFallback(); } catch (e2) { supportsWebP = false; } } console.log('WebP 支持:', supportsWebP); new Phaser.Game(config); } function preload() { loadingOverlay = document.getElementById('loading-overlay'); loadingProgressBar = document.getElementById('loading-progress-bar'); loadingText = document.getElementById('loading-text'); loadingProgressContainer = document.getElementById('loading-progress-container'); // 从 LAYOUT 读取总资源数量(避免 magic number) totalAssets = LAYOUT.totalAssets || 15; loadedAssets = 0; this.load.on('filecomplete', () => { updateLoadingProgress(); }); this.load.on('complete', () => { hideLoadingOverlay(); }); this.load.image('office_bg', '/static/office_bg_small' + (supportsWebP ? '.webp' : '.png') + '?v={{VERSION_TIMESTAMP}}'); this.load.spritesheet('star_idle', '/static/star-idle-spritesheet' + getExt('star-idle-spritesheet.png'), { frameWidth: 128, frameHeight: 128 }); this.load.spritesheet('star_researching', '/static/star-researching-spritesheet' + getExt('star-researching-spritesheet.png'), { frameWidth: 128, frameHeight: 105 }); this.load.image('sofa_idle', '/static/sofa-idle' + getExt('sofa-idle.png')); this.load.spritesheet('sofa_busy', '/static/sofa-busy-spritesheet' + getExt('sofa-busy-spritesheet.png'), { frameWidth: 256, frameHeight: 256 }); this.load.spritesheet('plants', '/static/plants-spritesheet' + getExt('plants-spritesheet.png'), { frameWidth: 160, frameHeight: 160 }); this.load.spritesheet('posters', '/static/posters-spritesheet' + getExt('posters-spritesheet.png'), { frameWidth: 160, frameHeight: 160 }); this.load.spritesheet('coffee_machine', '/static/coffee-machine-spritesheet' + getExt('coffee-machine-spritesheet.png'), { frameWidth: 230, frameHeight: 230 }); this.load.spritesheet('serverroom', '/static/serverroom-spritesheet' + getExt('serverroom-spritesheet.png'), { frameWidth: 180, frameHeight: 251 }); this.load.spritesheet('error_bug', '/static/error-bug-spritesheet-grid' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 180, frameHeight: 180 }); this.load.spritesheet('cats', '/static/cats-spritesheet' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 160, frameHeight: 160 }); this.load.image('desk', '/static/desk' + getExt('desk.png')); this.load.spritesheet('star_working', '/static/star-working-spritesheet-grid' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 230, frameHeight: 144 }); this.load.spritesheet('sync_anim', '/static/sync-animation-spritesheet-grid' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 256, frameHeight: 256 }); this.load.image('memo_bg', '/static/memo-bg' + (supportsWebP ? '.webp' : '.png')); // 新办公桌:强制 PNG(透明) this.load.image('desk_v2', '/static/desk-v2.png'); this.load.spritesheet('flowers', '/static/flowers-spritesheet' + (supportsWebP ? '.webp' : '.png'), { frameWidth: 65, frameHeight: 65 }); } function create() { game = this; this.add.image(640, 360, 'office_bg'); // === 沙发(来自 LAYOUT)=== sofa = this.add.sprite( LAYOUT.furniture.sofa.x, LAYOUT.furniture.sofa.y, 'sofa_busy' ).setOrigin(LAYOUT.furniture.sofa.origin.x, LAYOUT.furniture.sofa.origin.y); sofa.setDepth(LAYOUT.furniture.sofa.depth); this.anims.create({ key: 'sofa_busy', frames: this.anims.generateFrameNumbers('sofa_busy', { start: 0, end: 47 }), frameRate: 12, repeat: -1 }); areas = LAYOUT.areas; this.anims.create({ key: 'star_idle', frames: this.anims.generateFrameNumbers('star_idle', { start: 0, end: 29 }), frameRate: 12, repeat: -1 }); this.anims.create({ key: 'star_researching', frames: this.anims.generateFrameNumbers('star_researching', { start: 0, end: 95 }), frameRate: 12, repeat: -1 }); star = game.physics.add.sprite(areas.breakroom.x, areas.breakroom.y, 'star_idle'); star.setOrigin(0.5); star.setScale(1.4); star.setAlpha(0.95); star.setDepth(20); star.setVisible(false); star.anims.stop(); if (game.textures.exists('sofa_busy')) { sofa.setTexture('sofa_busy'); sofa.anims.play('sofa_busy', true); } // === 牌匾(来自 LAYOUT)=== const plaqueX = LAYOUT.plaque.x; const plaqueY = LAYOUT.plaque.y; const plaqueBg = game.add.rectangle(plaqueX, plaqueY, LAYOUT.plaque.width, LAYOUT.plaque.height, 0x5d4037); plaqueBg.setStrokeStyle(3, 0x3e2723); const plaqueText = game.add.text(plaqueX, plaqueY, '海辛小龙虾的办公室', { fontFamily: 'ArkPixel, monospace', fontSize: '18px', fill: '#ffd700', fontWeight: 'bold', stroke: '#000', strokeThickness: 2 }).setOrigin(0.5); game.add.text(plaqueX - 190, plaqueY, '⭐', { fontFamily: 'ArkPixel, monospace', fontSize: '20px' }).setOrigin(0.5); game.add.text(plaqueX + 190, plaqueY, '⭐', { fontFamily: 'ArkPixel, monospace', fontSize: '20px' }).setOrigin(0.5); // === 植物们(来自 LAYOUT)=== const plantFrameCount = 16; for (let i = 0; i < LAYOUT.furniture.plants.length; i++) { const p = LAYOUT.furniture.plants[i]; const randomPlantFrame = Math.floor(Math.random() * plantFrameCount); const plant = game.add.sprite(p.x, p.y, 'plants', randomPlantFrame).setOrigin(0.5); plant.setDepth(p.depth); plant.setInteractive({ useHandCursor: true }); window[`plantSprite${i === 0 ? '' : i + 1}`] = plant; plant.on('pointerdown', (() => { const next = Math.floor(Math.random() * plantFrameCount); plant.setFrame(next); })); } // === 海报(来自 LAYOUT)=== const postersFrameCount = 32; const randomPosterFrame = Math.floor(Math.random() * postersFrameCount); const poster = game.add.sprite(LAYOUT.furniture.poster.x, LAYOUT.furniture.poster.y, 'posters', randomPosterFrame).setOrigin(0.5); poster.setDepth(LAYOUT.furniture.poster.depth); poster.setInteractive({ useHandCursor: true }); window.posterSprite = poster; window.posterFrameCount = postersFrameCount; poster.on('pointerdown', () => { const next = Math.floor(Math.random() * window.posterFrameCount); window.posterSprite.setFrame(next); }); // === 小猫(来自 LAYOUT)=== const catsFrameCount = 16; const randomCatFrame = Math.floor(Math.random() * catsFrameCount); const cat = game.add.sprite(LAYOUT.furniture.cat.x, LAYOUT.furniture.cat.y, 'cats', randomCatFrame).setOrigin(LAYOUT.furniture.cat.origin.x, LAYOUT.furniture.cat.origin.y); cat.setDepth(LAYOUT.furniture.cat.depth); cat.setInteractive({ useHandCursor: true }); window.catSprite = cat; window.catsFrameCount = catsFrameCount; cat.on('pointerdown', () => { const next = Math.floor(Math.random() * window.catsFrameCount); window.catSprite.setFrame(next); }); // === 咖啡机(来自 LAYOUT)=== this.anims.create({ key: 'coffee_machine', frames: this.anims.generateFrameNumbers('coffee_machine', { start: 0, end: 95 }), frameRate: 12.5, repeat: -1 }); const coffeeMachine = this.add.sprite( LAYOUT.furniture.coffeeMachine.x, LAYOUT.furniture.coffeeMachine.y, 'coffee_machine' ).setOrigin(LAYOUT.furniture.coffeeMachine.origin.x, LAYOUT.furniture.coffeeMachine.origin.y); coffeeMachine.setDepth(LAYOUT.furniture.coffeeMachine.depth); coffeeMachine.anims.play('coffee_machine', true); // === 服务器区(来自 LAYOUT)=== this.anims.create({ key: 'serverroom_on', frames: this.anims.generateFrameNumbers('serverroom', { start: 0, end: 39 }), frameRate: 6, repeat: -1 }); serverroom = this.add.sprite( LAYOUT.furniture.serverroom.x, LAYOUT.furniture.serverroom.y, 'serverroom', 0 ).setOrigin(LAYOUT.furniture.serverroom.origin.x, LAYOUT.furniture.serverroom.origin.y); serverroom.setDepth(LAYOUT.furniture.serverroom.depth); serverroom.anims.stop(); serverroom.setFrame(0); // === 新办公桌(来自 LAYOUT,强制透明 PNG)=== const desk = this.add.image( LAYOUT.furniture.desk.x, LAYOUT.furniture.desk.y, 'desk_v2' ).setOrigin(LAYOUT.furniture.desk.origin.x, LAYOUT.furniture.desk.origin.y); desk.setDepth(LAYOUT.furniture.desk.depth); // === 花盆(来自 LAYOUT)=== const flowerFrameCount = 16; const randomFlowerFrame = Math.floor(Math.random() * flowerFrameCount); const flower = this.add.sprite( LAYOUT.furniture.flower.x, LAYOUT.furniture.flower.y, 'flowers', randomFlowerFrame ).setOrigin(LAYOUT.furniture.flower.origin.x, LAYOUT.furniture.flower.origin.y); flower.setScale(LAYOUT.furniture.flower.scale || 1); flower.setDepth(LAYOUT.furniture.flower.depth); flower.setInteractive({ useHandCursor: true }); window.flowerSprite = flower; window.flowerFrameCount = flowerFrameCount; flower.on('pointerdown', () => { const next = Math.floor(Math.random() * window.flowerFrameCount); window.flowerSprite.setFrame(next); }); // === Star 在桌前工作(来自 LAYOUT)=== this.anims.create({ key: 'star_working', frames: this.anims.generateFrameNumbers('star_working', { start: 0, end: 191 }), frameRate: 12, repeat: -1 }); this.anims.create({ key: 'error_bug', frames: this.anims.generateFrameNumbers('error_bug', { start: 0, end: 95 }), frameRate: 12, repeat: -1 }); // === 错误 bug(来自 LAYOUT)=== const errorBug = this.add.sprite( LAYOUT.furniture.errorBug.x, LAYOUT.furniture.errorBug.y, 'error_bug', 0 ).setOrigin(LAYOUT.furniture.errorBug.origin.x, LAYOUT.furniture.errorBug.origin.y); errorBug.setDepth(LAYOUT.furniture.errorBug.depth); errorBug.setVisible(false); errorBug.setScale(LAYOUT.furniture.errorBug.scale); errorBug.anims.play('error_bug', true); window.errorBug = errorBug; window.errorBugDir = 1; const starWorking = this.add.sprite( LAYOUT.furniture.starWorking.x, LAYOUT.furniture.starWorking.y, 'star_working', 0 ).setOrigin(LAYOUT.furniture.starWorking.origin.x, LAYOUT.furniture.starWorking.origin.y); starWorking.setVisible(false); starWorking.setScale(LAYOUT.furniture.starWorking.scale); starWorking.setDepth(LAYOUT.furniture.starWorking.depth); window.starWorking = starWorking; // === 同步动画(来自 LAYOUT)=== this.anims.create({ key: 'sync_anim', frames: this.anims.generateFrameNumbers('sync_anim', { start: 1, end: 52 }), frameRate: 12, repeat: -1 }); syncAnimSprite = this.add.sprite( LAYOUT.furniture.syncAnim.x, LAYOUT.furniture.syncAnim.y, 'sync_anim', 0 ).setOrigin(LAYOUT.furniture.syncAnim.origin.x, LAYOUT.furniture.syncAnim.origin.y); syncAnimSprite.setDepth(LAYOUT.furniture.syncAnim.depth); syncAnimSprite.anims.stop(); syncAnimSprite.setFrame(0); window.starSprite = star; statusText = document.getElementById('status-text'); coordsOverlay = document.getElementById('coords-overlay'); coordsDisplay = document.getElementById('coords-display'); coordsToggle = document.getElementById('coords-toggle'); coordsToggle.addEventListener('click', () => { showCoords = !showCoords; coordsOverlay.style.display = showCoords ? 'block' : 'none'; coordsToggle.textContent = showCoords ? '隐藏坐标' : '显示坐标'; coordsToggle.style.background = showCoords ? '#e94560' : '#333'; }); game.input.on('pointermove', (pointer) => { if (!showCoords) return; const x = Math.max(0, Math.min(config.width - 1, Math.round(pointer.x))); const y = Math.max(0, Math.min(config.height - 1, Math.round(pointer.y))); coordsDisplay.textContent = `${x}, ${y}`; coordsOverlay.style.left = (pointer.x + 18) + 'px'; coordsOverlay.style.top = (pointer.y + 18) + 'px'; }); loadMemo(); fetchStatus(); fetchAgents(); // 可选调试:仅在显式开启 debug 模式时渲染测试用尼卡 agent let debugAgents = false; try { if (typeof window !== 'undefined') { if (window.STAR_OFFICE_DEBUG_AGENTS === true) { debugAgents = true; } else if (window.location && window.location.search && typeof URLSearchParams !== 'undefined') { const sp = new URLSearchParams(window.location.search); if (sp.get('debugAgents') === '1') { debugAgents = true; } } } } catch (e) { debugAgents = false; } if (debugAgents) { const testNika = { agentId: 'agent_nika', name: '尼卡', isMain: false, state: 'writing', detail: '在画像素画...', area: 'writing', authStatus: 'approved', updated_at: new Date().toISOString() }; renderAgent(testNika); window.testNikaState = 'writing'; window.testNikaTimer = setInterval(() => { const states = ['idle', 'writing', 'researching', 'executing']; const areas = { idle: 'breakroom', writing: 'writing', researching: 'writing', executing: 'writing' }; window.testNikaState = states[Math.floor(Math.random() * states.length)]; const testAgent = { agentId: 'agent_nika', name: '尼卡', isMain: false, state: window.testNikaState, detail: '在画像素画...', area: areas[window.testNikaState], authStatus: 'approved', updated_at: new Date().toISOString() }; renderAgent(testAgent); }, 5000); } } function update(time) { if (time - lastFetch > FETCH_INTERVAL) { fetchStatus(); lastFetch = time; } if (time - lastAgentsFetch > AGENTS_FETCH_INTERVAL) { fetchAgents(); lastAgentsFetch = time; } const effectiveStateForServer = pendingDesiredState || currentState; if (serverroom) { if (effectiveStateForServer === 'idle') { if (serverroom.anims.isPlaying) { serverroom.anims.stop(); serverroom.setFrame(0); } } else { if (!serverroom.anims.isPlaying || serverroom.anims.currentAnim?.key !== 'serverroom_on') { serverroom.anims.play('serverroom_on', true); } } } if (window.errorBug) { if (effectiveStateForServer === 'error') { window.errorBug.setVisible(true); if (!window.errorBug.anims.isPlaying || window.errorBug.anims.currentAnim?.key !== 'error_bug') { window.errorBug.anims.play('error_bug', true); } const leftX = LAYOUT.furniture.errorBug.pingPong.leftX; const rightX = LAYOUT.furniture.errorBug.pingPong.rightX; const speed = LAYOUT.furniture.errorBug.pingPong.speed; const dir = window.errorBugDir || 1; window.errorBug.x += speed * dir; window.errorBug.y = LAYOUT.furniture.errorBug.y; if (window.errorBug.x >= rightX) { window.errorBug.x = rightX; window.errorBugDir = -1; } else if (window.errorBug.x <= leftX) { window.errorBug.x = leftX; window.errorBugDir = 1; } } else { window.errorBug.setVisible(false); window.errorBug.anims.stop(); } } if (syncAnimSprite) { if (effectiveStateForServer === 'syncing') { if (!syncAnimSprite.anims.isPlaying || syncAnimSprite.anims.currentAnim?.key !== 'sync_anim') { syncAnimSprite.anims.play('sync_anim', true); } } else { if (syncAnimSprite.anims.isPlaying) syncAnimSprite.anims.stop(); syncAnimSprite.setFrame(0); } } if (time - lastBubble > BUBBLE_INTERVAL) { showBubble(); lastBubble = time; } if (time - lastCatBubble > CAT_BUBBLE_INTERVAL) { showCatBubble(); lastCatBubble = time; } if (typewriterIndex < typewriterTarget.length && time - lastTypewriter > TYPEWRITER_DELAY) { typewriterText += typewriterTarget[typewriterIndex]; statusText.textContent = typewriterText; typewriterIndex++; lastTypewriter = time; } moveStar(time); } function normalizeState(s) { if (!s) return 'idle'; if (s === 'working') return 'writing'; if (s === 'run' || s === 'running') return 'executing'; if (s === 'sync') return 'syncing'; if (s === 'research') return 'researching'; return s; } function fetchStatus() { fetch('/status') .then(response => response.json()) .then(data => { const nextState = normalizeState(data.state); const stateInfo = STATES[nextState] || STATES.idle; const changed = (pendingDesiredState === null) && (nextState !== currentState); const nextLine = '[' + stateInfo.name + '] ' + (data.detail || '...'); if (changed) { typewriterTarget = nextLine; typewriterText = ''; typewriterIndex = 0; pendingDesiredState = null; currentState = nextState; if (nextState === 'idle') { if (game.textures.exists('sofa_busy')) { sofa.setTexture('sofa_busy'); sofa.anims.play('sofa_busy', true); } star.setVisible(false); star.anims.stop(); if (window.starWorking) { window.starWorking.setVisible(false); window.starWorking.anims.stop(); } } else if (nextState === 'error') { sofa.anims.stop(); sofa.setTexture('sofa_idle'); star.setVisible(false); star.anims.stop(); if (window.starWorking) { window.starWorking.setVisible(false); window.starWorking.anims.stop(); } } else if (nextState === 'syncing') { sofa.anims.stop(); sofa.setTexture('sofa_idle'); star.setVisible(false); star.anims.stop(); if (window.starWorking) { window.starWorking.setVisible(false); window.starWorking.anims.stop(); } } else { sofa.anims.stop(); sofa.setTexture('sofa_idle'); star.setVisible(false); star.anims.stop(); if (window.starWorking) { window.starWorking.setVisible(true); window.starWorking.anims.play('star_working', true); } } if (serverroom) { if (nextState === 'idle') { serverroom.anims.stop(); serverroom.setFrame(0); } else { serverroom.anims.play('serverroom_on', true); } } if (syncAnimSprite) { if (nextState === 'syncing') { if (!syncAnimSprite.anims.isPlaying || syncAnimSprite.anims.currentAnim?.key !== 'sync_anim') { syncAnimSprite.anims.play('sync_anim', true); } } else { if (syncAnimSprite.anims.isPlaying) syncAnimSprite.anims.stop(); syncAnimSprite.setFrame(0); } } } else { if (!typewriterTarget || typewriterTarget !== nextLine) { typewriterTarget = nextLine; typewriterText = ''; typewriterIndex = 0; } } }) .catch(error => { typewriterTarget = '连接失败,正在重试...'; typewriterText = ''; typewriterIndex = 0; }); } function moveStar(time) { const effectiveState = pendingDesiredState || currentState; const stateInfo = STATES[effectiveState] || STATES.idle; const baseTarget = areas[stateInfo.area] || areas.breakroom; const dx = targetX - star.x; const dy = targetY - star.y; const dist = Math.sqrt(dx * dx + dy * dy); const speed = 1.4; const wobble = Math.sin(time / 200) * 0.8; if (dist > 3) { star.x += (dx / dist) * speed; star.y += (dy / dist) * speed; star.setY(star.y + wobble); isMoving = true; } else { if (waypoints && waypoints.length > 0) { waypoints.shift(); if (waypoints.length > 0) { targetX = waypoints[0].x; targetY = waypoints[0].y; isMoving = true; } else { if (pendingDesiredState !== null) { isMoving = false; currentState = pendingDesiredState; pendingDesiredState = null; if (currentState === 'idle') { star.setVisible(false); star.anims.stop(); if (window.starWorking) { window.starWorking.setVisible(false); window.starWorking.anims.stop(); } } else { star.setVisible(false); star.anims.stop(); if (window.starWorking) { window.starWorking.setVisible(true); window.starWorking.anims.play('star_working', true); } } } } } else { if (pendingDesiredState !== null) { isMoving = false; currentState = pendingDesiredState; pendingDesiredState = null; if (currentState === 'idle') { star.setVisible(false); star.anims.stop(); if (window.starWorking) { window.starWorking.setVisible(false); window.starWorking.anims.stop(); } if (game.textures.exists('sofa_busy')) { sofa.setTexture('sofa_busy'); sofa.anims.play('sofa_busy', true); } } else { star.setVisible(false); star.anims.stop(); if (window.starWorking) { window.starWorking.setVisible(true); window.starWorking.anims.play('star_working', true); } sofa.anims.stop(); sofa.setTexture('sofa_idle'); } } } } } function showBubble() { if (bubble) { bubble.destroy(); bubble = null; } const texts = BUBBLE_TEXTS[currentState] || BUBBLE_TEXTS.idle; if (currentState === 'idle') return; let anchorX = star.x; let anchorY = star.y; if (currentState === 'syncing' && syncAnimSprite && syncAnimSprite.visible) { anchorX = syncAnimSprite.x; anchorY = syncAnimSprite.y; } else if (currentState === 'error' && window.errorBug && window.errorBug.visible) { anchorX = window.errorBug.x; anchorY = window.errorBug.y; } else if (!star.visible && window.starWorking && window.starWorking.visible) { anchorX = window.starWorking.x; anchorY = window.starWorking.y; } const text = texts[Math.floor(Math.random() * texts.length)]; const bubbleY = anchorY - 70; const bg = game.add.rectangle(anchorX, bubbleY, text.length * 10 + 20, 28, 0xffffff, 0.95); bg.setStrokeStyle(2, 0x000000); const txt = game.add.text(anchorX, bubbleY, text, { fontFamily: 'ArkPixel, monospace', fontSize: '12px', fill: '#000', align: 'center' }).setOrigin(0.5); bubble = game.add.container(0, 0, [bg, txt]); bubble.setDepth(1200); setTimeout(() => { if (bubble) { bubble.destroy(); bubble = null; } }, 3000); } function showCatBubble() { if (!window.catSprite) return; if (window.catBubble) { window.catBubble.destroy(); window.catBubble = null; } const texts = BUBBLE_TEXTS.cat || ['喵~', '咕噜咕噜…']; const text = texts[Math.floor(Math.random() * texts.length)]; const anchorX = window.catSprite.x; const anchorY = window.catSprite.y - 60; const bg = game.add.rectangle(anchorX, anchorY, text.length * 10 + 20, 24, 0xfffbeb, 0.95); bg.setStrokeStyle(2, 0xd4a574); const txt = game.add.text(anchorX, anchorY, text, { fontFamily: 'ArkPixel, monospace', fontSize: '11px', fill: '#8b6914', align: 'center' }).setOrigin(0.5); window.catBubble = game.add.container(0, 0, [bg, txt]); window.catBubble.setDepth(2100); setTimeout(() => { if (window.catBubble) { window.catBubble.destroy(); window.catBubble = null; } }, 4000); } function fetchAgents() { fetch('/agents?t=' + Date.now(), { cache: 'no-store' }) .then(response => response.json()) .then(data => { if (!Array.isArray(data)) return; // 重置位置计数器 // 按区域分配不同位置索引,避免重叠 const areaSlots = { breakroom: 0, writing: 0, error: 0 }; for (let agent of data) { const area = agent.area || 'breakroom'; agent._slotIndex = areaSlots[area] || 0; areaSlots[area] = (areaSlots[area] || 0) + 1; renderAgent(agent); } // 移除不再存在的 agent const currentIds = new Set(data.map(a => a.agentId)); for (let id in agents) { if (!currentIds.has(id)) { if (agents[id]) { agents[id].destroy(); delete agents[id]; } } } }) .catch(error => { console.error('拉取 agents 失败:', error); }); } function getAreaPosition(area, slotIndex) { const positions = AREA_POSITIONS[area] || AREA_POSITIONS.breakroom; const idx = (slotIndex || 0) % positions.length; return positions[idx]; } function renderAgent(agent) { const agentId = agent.agentId; const name = agent.name || 'Agent'; const area = agent.area || 'breakroom'; const authStatus = agent.authStatus || 'pending'; const isMain = !!agent.isMain; // 获取这个 agent 在区域里的位置 const pos = getAreaPosition(area, agent._slotIndex || 0); const baseX = pos.x; const baseY = pos.y; // 颜色 const bodyColor = AGENT_COLORS[agentId] || AGENT_COLORS.default; const nameColor = NAME_TAG_COLORS[authStatus] || NAME_TAG_COLORS.default; // 透明度(离线/待批准/拒绝时变半透明) let alpha = 1; if (authStatus === 'pending') alpha = 0.7; if (authStatus === 'rejected') alpha = 0.4; if (authStatus === 'offline') alpha = 0.5; if (!agents[agentId]) { // 新建 agent const container = game.add.container(baseX, baseY); container.setDepth(1200 + (isMain ? 100 : 0)); // 放到最顶层! // 像素小人:用星星图标,更明显 const starIcon = game.add.text(0, 0, '⭐', { fontFamily: 'ArkPixel, monospace', fontSize: '32px' }).setOrigin(0.5); starIcon.name = 'starIcon'; // 名字标签(漂浮) const nameTag = game.add.text(0, -36, name, { fontFamily: 'ArkPixel, monospace', fontSize: '14px', fill: '#' + nameColor.toString(16).padStart(6, '0'), stroke: '#000', strokeThickness: 3, backgroundColor: 'rgba(255,255,255,0.95)' }).setOrigin(0.5); nameTag.name = 'nameTag'; // 状态小点(绿色/黄色/红色) let dotColor = 0x64748b; if (authStatus === 'approved') dotColor = 0x22c55e; if (authStatus === 'pending') dotColor = 0xf59e0b; if (authStatus === 'rejected') dotColor = 0xef4444; if (authStatus === 'offline') dotColor = 0x94a3b8; const statusDot = game.add.circle(20, -20, 5, dotColor, alpha); statusDot.setStrokeStyle(2, 0x000000, alpha); statusDot.name = 'statusDot'; container.add([starIcon, statusDot, nameTag]); agents[agentId] = container; } else { // 更新 agent const container = agents[agentId]; container.setPosition(baseX, baseY); container.setAlpha(alpha); container.setDepth(1200 + (isMain ? 100 : 0)); // 更新名字和颜色(如果变化) const nameTag = container.getAt(2); if (nameTag && nameTag.name === 'nameTag') { nameTag.setText(name); nameTag.setFill('#' + (NAME_TAG_COLORS[authStatus] || NAME_TAG_COLORS.default).toString(16).padStart(6, '0')); } // 更新状态点颜色 const statusDot = container.getAt(1); if (statusDot && statusDot.name === 'statusDot') { let dotColor = 0x64748b; if (authStatus === 'approved') dotColor = 0x22c55e; if (authStatus === 'pending') dotColor = 0xf59e0b; if (authStatus === 'rejected') dotColor = 0xef4444; if (authStatus === 'offline') dotColor = 0x94a3b8; statusDot.fillColor = dotColor; } } } // 启动游戏 initGame(); ================================================ FILE: frontend/index.html ================================================ Star 的像素办公室
Loading Star’s pixel office...
正在进入像素办公室…
加载中...
昨 日 小 记
─ ─ ─ ─ ─
加载中...
─ ─ ─ ─ ─
Star 状态
访 客 列 表
正在加载访客...
🦞
正在打包虾头……
================================================ FILE: frontend/invite.html ================================================ 海辛办公室 - 加入邀请

✨ 海辛办公室 · 加入邀请

欢迎加入海辛的像素办公室看板!

加入步骤(一共 3 步)

1
确认信息
你应该已经收到两样东西:
  • 邀请链接:https://office.example.com/join
  • 一次性接入密钥(join key):ocj_xxx
2
把邀请信息丢给你的 OpenClaw
把邀请链接 + join key 一起发给你的 OpenClaw,并说“帮我加入海辛办公室”。
3
在你这边授权
你的 OpenClaw 会在对话里向你要授权;同意后,它就会开始自动把工作状态推送到海辛办公室看板啦!
⚠️ 隐私说明
只推送状态(idle/writing/researching/executing/syncing/error),不含任何具体内容/隐私;随时可停。
← 回到海辛办公室
================================================ FILE: frontend/join-office-skill.md ================================================ # Join Star Office - Visitor Agent Skill ## Description 接入海辛的像素办公室,让你的龙虾在看板上有一个工位,实时显示工作状态。 ## Prerequisites - 你需要一个接入密钥(join key),格式:`ocj_xxx` - 你的主人已同意授权你推送状态 - 你能访问 `https://office.hyacinth.im` ## Quick Start(最快上手) ### Step 1: 向你的主人确认授权 > "我将把我的工作状态(idle/writing/researching/executing/syncing/error)定时发送到海辛的像素办公室看板。只推送状态词和简短描述,不含任何隐私内容,随时可停。是否授权?" ### Step 2: 下载推送脚本 ```bash curl -o office-agent-push.py https://office.hyacinth.im/static/office-agent-push.py ``` ### Step 3: 配置并运行 打开 `office-agent-push.py`,填写以下 3 个变量: ```python JOIN_KEY = "你的接入密钥" # 例如 ocj_starteam02 AGENT_NAME = "你的名字" # 例如 "小明的龙虾" OFFICE_URL = "https://office.hyacinth.im" ``` 然后运行: ```bash python3 office-agent-push.py ``` ### Step 4: 完成! 脚本会自动: - 加入海辛的办公室(自动批准,无需等待) - 每 15 秒读取你的本地状态并推送 - 你的龙虾会出现在办公室看板上,根据状态自动走到不同区域 ## 状态区域映射 | 状态 | 办公室区域 | 说明 | |------|-----------|------| | idle | 休息区(沙发) | 待命 / 完成任务 | | writing | 工作区(办公桌) | 写代码 / 写文档 | | researching | 工作区 | 搜索 / 调研 | | executing | 工作区 | 执行任务 | | syncing | 工作区 | 同步数据 | | error | Bug 区 | 报错 / 异常 | ## 本地状态读取优先级 脚本会按以下顺序自动发现你的状态源(无需手动配置): 1. `state.json`(本机 OpenClaw 工作区,自动发现多个候选路径) 2. `http://127.0.0.1:19000/status`(本地 HTTP 接口) 3. 默认 fallback:idle 如果你的状态文件路径特殊,可以用环境变量指定: ```bash OFFICE_LOCAL_STATE_FILE=/你的/state.json python3 office-agent-push.py ``` ## 停止推送 - `Ctrl+C` 终止脚本 - 脚本会自动从办公室退出 ## Notes - 只推送状态词和简短描述,不推送任何隐私内容 - 授权有效期 24h,到期后需要重新 join - 如果收到 403(密钥过期)或 404(已被移出),脚本会自动停止 - 同一密钥最多支持 100 个龙虾同时在线 ================================================ FILE: frontend/join.html ================================================ 加入 Star 的像素办公室

⭐ 加入 Star 的像素办公室

⚠️ 注意:join 页面仅需要名字 + 一次性 join key
状态与状态细节会由 agent 后续自动推送同步

📌 邀请说明: https://office.example.com/invite
================================================ FILE: frontend/layout.js ================================================ // Star Office UI - 布局与层级配置 // 所有坐标、depth、资源路径统一管理在这里 // 避免 magic numbers,降低改错风险 // 核心规则: // - 透明资源(如办公桌)强制 .png,不透明优先 .webp // - 层级:低 → sofa(10) → starWorking(900) → desk(1000) → flower(1100) const LAYOUT = { // === 游戏画布 === game: { width: 1280, height: 720 }, // === 各区域坐标 === areas: { door: { x: 640, y: 550 }, writing: { x: 320, y: 360 }, researching: { x: 320, y: 360 }, error: { x: 1066, y: 180 }, breakroom: { x: 640, y: 360 } }, // === 装饰与家具:坐标 + 原点 + depth === furniture: { // 沙发 sofa: { x: 670, y: 144, origin: { x: 0, y: 0 }, depth: 10 }, // 新办公桌(透明 PNG 强制) desk: { x: 218, y: 417, origin: { x: 0.5, y: 0.5 }, depth: 1000 }, // 桌上花盆 flower: { x: 310, y: 390, origin: { x: 0.5, y: 0.5 }, depth: 1100, scale: 0.8 }, // Star 在桌前工作(在 desk 下面) starWorking: { x: 217, y: 333, origin: { x: 0.5, y: 0.5 }, depth: 900, scale: 1.32 }, // 植物们 plants: [ { x: 565, y: 178, depth: 5 }, { x: 230, y: 185, depth: 5 }, { x: 977, y: 496, depth: 5 } ], // 海报 poster: { x: 252, y: 66, depth: 4 }, // 咖啡机 coffeeMachine: { x: 659, y: 397, origin: { x: 0.5, y: 0.5 }, depth: 99 }, // 服务器区 serverroom: { x: 1021, y: 142, origin: { x: 0.5, y: 0.5 }, depth: 2 }, // 错误 bug errorBug: { x: 1007, y: 221, origin: { x: 0.5, y: 0.5 }, depth: 50, scale: 0.9, pingPong: { leftX: 1007, rightX: 1111, speed: 0.6 } }, // 同步动画 syncAnim: { x: 1157, y: 592, origin: { x: 0.5, y: 0.5 }, depth: 40 }, // 小猫 cat: { x: 94, y: 557, origin: { x: 0.5, y: 0.5 }, depth: 2000 } }, // === 牌匾 === plaque: { x: 640, y: 720 - 36, width: 420, height: 44 }, // === 资源加载规则:哪些强制用 PNG(透明资源) === forcePng: { desk_v2: true // 新办公桌必须透明,强制 PNG }, // === 总资源数量(用于加载进度条) === totalAssets: 15 }; ================================================ FILE: frontend/office-agent-push.py ================================================ #!/usr/bin/env python3 """ 海辛办公室 - Agent 状态主动推送脚本 用法: 1. 填入下面的 JOIN_KEY(你从海辛那里拿到的一次性 join key) 2. 填入 AGENT_NAME(你想要在办公室里显示的名字) 3. 运行:python office-agent-push.py 4. 脚本会自动先 join(首次运行),然后每 30s 向海辛办公室推送一次你的当前状态 """ import json import os import time import sys from datetime import datetime # === 你需要填入的信息 === JOIN_KEY = "" # 必填:你的一次性 join key AGENT_NAME = "" # 必填:你在办公室里的名字 OFFICE_URL = "https://office.hyacinth.im" # 海辛办公室地址(一般不用改) # === 推送配置 === PUSH_INTERVAL_SECONDS = 15 # 每隔多少秒推送一次(更实时) STATUS_ENDPOINT = "/status" JOIN_ENDPOINT = "/join-agent" PUSH_ENDPOINT = "/agent-push" # 自动状态守护:当本地状态文件不存在或长期不更新时,自动回 idle,避免“假工作中” STALE_STATE_TTL_SECONDS = int(os.environ.get("OFFICE_STALE_STATE_TTL", "600")) # 本地状态存储(记住上次 join 拿到的 agentId) STATE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "office-agent-state.json") # 优先读取本机 OpenClaw 工作区的状态文件(更贴合 AGENTS.md 的工作流) # 支持自动发现,减少对方手动配置成本。 DEFAULT_STATE_CANDIDATES = [ "/root/.openclaw/workspace/Star-Office-UI/state.json", # 当前仓库(大小写精确) "/root/.openclaw/workspace/star-office-ui/state.json", # 历史/兼容路径 "/root/.openclaw/workspace/state.json", os.path.join(os.getcwd(), "state.json"), os.path.join(os.path.dirname(os.path.abspath(__file__)), "state.json"), ] # 如果对方本地 /status 需要鉴权,可在这里填写 token(或通过环境变量 OFFICE_LOCAL_STATUS_TOKEN) LOCAL_STATUS_TOKEN = os.environ.get("OFFICE_LOCAL_STATUS_TOKEN", "") LOCAL_STATUS_URL = os.environ.get("OFFICE_LOCAL_STATUS_URL", "http://127.0.0.1:19000/status") # 可选:直接指定本地状态文件路径(最简单方案:绕过 /status 鉴权) LOCAL_STATE_FILE = os.environ.get("OFFICE_LOCAL_STATE_FILE", "") VERBOSE = os.environ.get("OFFICE_VERBOSE", "0") in {"1", "true", "TRUE", "yes", "YES"} def load_local_state(): if os.path.exists(STATE_FILE): try: with open(STATE_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass return { "agentId": None, "joined": False, "joinKey": JOIN_KEY, "agentName": AGENT_NAME } def save_local_state(data): with open(STATE_FILE, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def normalize_state(s): """兼容不同本地状态词,并映射到办公室识别状态。""" s = (s or "").strip().lower() if s in {"writing", "researching", "executing", "syncing", "error", "idle"}: return s if s in {"working", "busy", "write"}: return "writing" if s in {"run", "running", "execute", "exec"}: return "executing" if s in {"research", "search"}: return "researching" if s in {"sync"}: return "syncing" return "idle" def map_detail_to_state(detail, fallback_state="idle"): """当只有 detail 时,用关键词推断状态(贴近 AGENTS.md 的办公区逻辑)。""" d = (detail or "").lower() if any(k in d for k in ["报错", "error", "bug", "异常", "报警"]): return "error" if any(k in d for k in ["同步", "sync", "备份"]): return "syncing" if any(k in d for k in ["调研", "research", "搜索", "查资料"]): return "researching" if any(k in d for k in ["执行", "run", "推进", "处理任务", "工作中", "writing"]): return "writing" if any(k in d for k in ["待命", "休息", "idle", "完成", "done"]): return "idle" return fallback_state def _state_age_seconds(data): try: ts = (data or {}).get("updated_at") if not ts: return None dt = datetime.fromisoformat(str(ts).replace("Z", "+00:00")) if dt.tzinfo is not None: from datetime import timezone return (datetime.now(timezone.utc) - dt.astimezone(timezone.utc)).total_seconds() return (datetime.now() - dt).total_seconds() except Exception: return None def fetch_local_status(): """读取本地状态: 1) 优先 state.json(符合 AGENTS.md:任务前切 writing,完成后切 idle) 2) 其次尝试本地 HTTP /status 3) 最后 fallback idle 额外防抖:如果本地状态更新时间超过 STALE_STATE_TTL_SECONDS,自动视为 idle。 """ # 1) 读本地 state.json(优先读取显式指定路径,其次自动发现) candidate_files = [] if LOCAL_STATE_FILE: candidate_files.append(LOCAL_STATE_FILE) for fp in DEFAULT_STATE_CANDIDATES: if fp not in candidate_files: candidate_files.append(fp) for fp in candidate_files: try: if fp and os.path.exists(fp): with open(fp, "r", encoding="utf-8") as f: data = json.load(f) # 只接受“状态文件”结构;避免误把 office-agent-state.json(仅缓存 agentId)当状态源 if not isinstance(data, dict): continue has_state = "state" in data has_detail = "detail" in data if (not has_state) and (not has_detail): continue state = normalize_state(data.get("state", "idle")) detail = data.get("detail", "") or "" # detail 兜底纠偏,确保“工作/休息/报警”能正确落区 state = map_detail_to_state(detail, fallback_state=state) # 防止状态文件久未更新仍停留在 working 态 age = _state_age_seconds(data) if age is not None and age > STALE_STATE_TTL_SECONDS: state = "idle" detail = f"本地状态超过{STALE_STATE_TTL_SECONDS}s未更新,自动回待命" if VERBOSE: print(f"[status-source:file] path={fp} state={state} detail={detail[:60]}") return {"state": state, "detail": detail} except Exception: pass # 2) 尝试本地 /status(可能需要鉴权) try: import requests headers = {} if LOCAL_STATUS_TOKEN: headers["Authorization"] = f"Bearer {LOCAL_STATUS_TOKEN}" r = requests.get(LOCAL_STATUS_URL, headers=headers, timeout=5) if r.status_code == 200: data = r.json() state = normalize_state(data.get("state", "idle")) detail = data.get("detail", "") or "" state = map_detail_to_state(detail, fallback_state=state) age = _state_age_seconds(data) if age is not None and age > STALE_STATE_TTL_SECONDS: state = "idle" detail = f"本地/status 超过{STALE_STATE_TTL_SECONDS}s未更新,自动回待命" if VERBOSE: print(f"[status-source:http] url={LOCAL_STATUS_URL} state={state} detail={detail[:60]}") return {"state": state, "detail": detail} # 如果 401,说明需要 token if r.status_code == 401: return {"state": "idle", "detail": "本地/status需要鉴权(401),请设置 OFFICE_LOCAL_STATUS_TOKEN"} except Exception: pass # 3) 默认 fallback if VERBOSE: print("[status-source:fallback] state=idle detail=待命中") return {"state": "idle", "detail": "待命中"} def do_join(local): import requests payload = { "name": local.get("agentName", AGENT_NAME), "joinKey": local.get("joinKey", JOIN_KEY), "state": "idle", "detail": "刚刚加入" } r = requests.post(f"{OFFICE_URL}{JOIN_ENDPOINT}", json=payload, timeout=10) if r.status_code in (200, 201): data = r.json() if data.get("ok"): local["joined"] = True local["agentId"] = data.get("agentId") save_local_state(local) print(f"✅ 已加入海辛办公室,agentId={local['agentId']}") return True print(f"❌ 加入失败:{r.text}") return False def do_push(local, status_data): import requests payload = { "agentId": local.get("agentId"), "joinKey": local.get("joinKey", JOIN_KEY), "state": status_data.get("state", "idle"), "detail": status_data.get("detail", ""), "name": local.get("agentName", AGENT_NAME) } r = requests.post(f"{OFFICE_URL}{PUSH_ENDPOINT}", json=payload, timeout=10) if r.status_code in (200, 201): data = r.json() if data.get("ok"): area = data.get("area", "breakroom") print(f"✅ 状态已同步,当前区域={area}") return True # 403/404:拒绝/移除 → 停止推送 if r.status_code in (403, 404): msg = "" try: msg = (r.json() or {}).get("msg", "") except Exception: msg = r.text print(f"⚠️ 访问拒绝或已移出房间({r.status_code}),停止推送:{msg}") local["joined"] = False local["agentId"] = None save_local_state(local) sys.exit(1) print(f"⚠️ 推送失败:{r.text}") return False def main(): local = load_local_state() # 先确认配置是否齐全 if not JOIN_KEY or not AGENT_NAME: print("❌ 请先在脚本开头填入 JOIN_KEY 和 AGENT_NAME") sys.exit(1) # 如果之前没 join,先 join if not local.get("joined") or not local.get("agentId"): ok = do_join(local) if not ok: sys.exit(1) # 持续推送 print(f"🚀 开始持续推送状态,间隔={PUSH_INTERVAL_SECONDS}秒") print("🧭 状态逻辑:任务中→工作区;待命/完成→休息区;异常→bug区") print("🔐 若本地 /status 返回 Unauthorized(401),请设置环境变量:OFFICE_LOCAL_STATUS_TOKEN 或 OFFICE_LOCAL_STATUS_URL") try: while True: try: status_data = fetch_local_status() do_push(local, status_data) except Exception as e: print(f"⚠️ 推送异常:{e}") time.sleep(PUSH_INTERVAL_SECONDS) except KeyboardInterrupt: print("\n👋 停止推送") sys.exit(0) if __name__ == "__main__": main() ================================================ FILE: gif_to_spritesheet.py ================================================ #!/usr/bin/env python3 """Convert GIF animation to sprite sheet for Phaser""" from PIL import Image import os def gif_to_spritesheet(gif_path, output_path, target_height=64): # Open the GIF gif = Image.open(gif_path) # Get all frames frames = [] try: while True: frame = gif.copy().convert('RGBA') # Calculate scale to fit target_height original_width, original_height = frame.size if original_height != target_height: scale = target_height / original_height target_width = int(original_width * scale) frame = frame.resize((target_width, target_height), Image.Resampling.NEAREST) frames.append(frame) gif.seek(gif.tell() + 1) except EOFError: pass if not frames: raise ValueError("No frames found in GIF") # Calculate sprite sheet dimensions frame_width, frame_height = frames[0].size num_frames = len(frames) # Arrange frames in a single row for simplicity sheet_width = frame_width * num_frames sheet_height = frame_height # Create sprite sheet spritesheet = Image.new('RGBA', (sheet_width, sheet_height), (0, 0, 0, 0)) # Paste each frame for i, frame in enumerate(frames): x = i * frame_width y = 0 spritesheet.paste(frame, (x, y)) # Save sprite sheet spritesheet.save(output_path) print(f"Sprite sheet created: {output_path}") print(f"Frames: {num_frames}") print(f"Frame size: {frame_width}x{frame_height}") print(f"Sprite sheet size: {sheet_width}x{sheet_height}") return { 'num_frames': num_frames, 'frame_width': frame_width, 'frame_height': frame_height, 'sheet_width': sheet_width, 'sheet_height': sheet_height } if __name__ == "__main__": import sys if len(sys.argv) < 4: print("Usage: python gif_to_spritesheet.py ") print("Example: python gif_to_spritesheet.py star-idle.gif star-idle-spritesheet.png 64") sys.exit(1) gif_path = sys.argv[1] output_path = sys.argv[2] target_height = int(sys.argv[3]) result = gif_to_spritesheet(gif_path, output_path, target_height=target_height) print("\nDone!") ================================================ FILE: healthcheck.sh ================================================ #!/bin/bash # Star Office UI Health Check # Checks if backend is responding, restarts if not BACKEND_URL="http://127.0.0.1:19000/health" LOG_FILE="/root/.openclaw/workspace/star-office-ui/healthcheck.log" # Log timestamp echo "[$(date '+%Y-%m-%d %H:%M:%S')] Health check starting..." >> "$LOG_FILE" # Check backend if curl -sS "$BACKEND_URL" > /dev/null 2>&1; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backend is healthy" >> "$LOG_FILE" else echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backend is NOT healthy - restarting..." >> "$LOG_FILE" systemctl restart star-office-backend.service echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backend restarted" >> "$LOG_FILE" fi ================================================ FILE: join-keys.sample.json ================================================ { "keys": [ { "key": "ocj_example_team_01", "used": false, "reusable": true, "maxConcurrent": 3, "usedBy": null, "usedByAgentId": null, "usedAt": null } ] } ================================================ FILE: office-agent-push.py ================================================ #!/usr/bin/env python3 """ 海辛办公室 - Agent 状态主动推送脚本 用法: 1. 填入下面的 JOIN_KEY(你从海辛那里拿到的一次性 join key) 2. 填入 AGENT_NAME(你想要在办公室里显示的名字) 3. 运行:python office-agent-push.py 4. 脚本会自动先 join(首次运行),然后每 30s 向海辛办公室推送一次你的当前状态 """ import json import os import time import sys from datetime import datetime # === 你需要填入的信息 === JOIN_KEY = "" # 必填:你的一次性 join key AGENT_NAME = "" # 必填:你在办公室里的名字 OFFICE_URL = "https://office.hyacinth.im" # 海辛办公室地址(一般不用改) # === 推送配置 === PUSH_INTERVAL_SECONDS = 15 # 每隔多少秒推送一次(更实时) STATUS_ENDPOINT = "/status" JOIN_ENDPOINT = "/join-agent" PUSH_ENDPOINT = "/agent-push" # 自动状态守护:当本地状态文件不存在或长期不更新时,自动回 idle,避免“假工作中” STALE_STATE_TTL_SECONDS = int(os.environ.get("OFFICE_STALE_STATE_TTL", "600")) # 本地状态存储(记住上次 join 拿到的 agentId) STATE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "office-agent-state.json") # 优先读取本机 OpenClaw 工作区的状态文件(更贴合 AGENTS.md 的工作流) # 支持自动发现,减少对方手动配置成本,且避免硬编码绝对路径: # - 优先使用环境变量 OPENCLAW_HOME / OPENCLAW_WORKSPACE_DIR # - 其次使用当前用户 HOME/.openclaw # - 再回落到当前工作目录与脚本所在目录 OPENCLAW_HOME = os.environ.get("OPENCLAW_HOME") or os.path.join(os.path.expanduser("~"), ".openclaw") OPENCLAW_WORKSPACE_DIR = os.environ.get("OPENCLAW_WORKSPACE_DIR") or os.path.join(OPENCLAW_HOME, "workspace") DEFAULT_STATE_CANDIDATES = [ os.path.join(OPENCLAW_WORKSPACE_DIR, "star-office-ui", "state.json"), os.path.join(OPENCLAW_WORKSPACE_DIR, "state.json"), "/root/.openclaw/workspace/Star-Office-UI/state.json", # 当前仓库(大小写精确) "/root/.openclaw/workspace/star-office-ui/state.json", # 历史/兼容路径 "/root/.openclaw/workspace/state.json", os.path.join(os.getcwd(), "state.json"), os.path.join(os.path.dirname(os.path.abspath(__file__)), "state.json"), ] # 如果对方本地 /status 需要鉴权,可在这里填写 token(或通过环境变量 OFFICE_LOCAL_STATUS_TOKEN) LOCAL_STATUS_TOKEN = os.environ.get("OFFICE_LOCAL_STATUS_TOKEN", "") LOCAL_STATUS_URL = os.environ.get("OFFICE_LOCAL_STATUS_URL", "http://127.0.0.1:19000/status") # 可选:直接指定本地状态文件路径(最简单方案:绕过 /status 鉴权) LOCAL_STATE_FILE = os.environ.get("OFFICE_LOCAL_STATE_FILE", "") VERBOSE = os.environ.get("OFFICE_VERBOSE", "0") in {"1", "true", "TRUE", "yes", "YES"} def load_local_state(): if os.path.exists(STATE_FILE): try: with open(STATE_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass return { "agentId": None, "joined": False, "joinKey": JOIN_KEY, "agentName": AGENT_NAME } def save_local_state(data): with open(STATE_FILE, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def normalize_state(s): """兼容不同本地状态词,并映射到办公室识别状态。""" s = (s or "").strip().lower() if s in {"writing", "researching", "executing", "syncing", "error", "idle"}: return s if s in {"working", "busy", "write"}: return "writing" if s in {"run", "running", "execute", "exec"}: return "executing" if s in {"research", "search"}: return "researching" if s in {"sync"}: return "syncing" return "idle" def map_detail_to_state(detail, fallback_state="idle"): """当只有 detail 时,用关键词推断状态(贴近 AGENTS.md 的办公区逻辑)。""" d = (detail or "").lower() if any(k in d for k in ["报错", "error", "bug", "异常", "报警"]): return "error" if any(k in d for k in ["同步", "sync", "备份"]): return "syncing" if any(k in d for k in ["调研", "research", "搜索", "查资料"]): return "researching" if any(k in d for k in ["执行", "run", "推进", "处理任务", "工作中", "writing"]): return "writing" if any(k in d for k in ["待命", "休息", "idle", "完成", "done"]): return "idle" return fallback_state def _state_age_seconds(data): try: ts = (data or {}).get("updated_at") if not ts: return None dt = datetime.fromisoformat(str(ts).replace("Z", "+00:00")) if dt.tzinfo is not None: from datetime import timezone return (datetime.now(timezone.utc) - dt.astimezone(timezone.utc)).total_seconds() return (datetime.now() - dt).total_seconds() except Exception: return None def fetch_local_status(): """读取本地状态: 1) 优先 state.json(符合 AGENTS.md:任务前切 writing,完成后切 idle) 2) 其次尝试本地 HTTP /status 3) 最后 fallback idle 额外防抖:如果本地状态更新时间超过 STALE_STATE_TTL_SECONDS,自动视为 idle。 """ # 1) 读本地 state.json(优先读取显式指定路径,其次自动发现) candidate_files = [] if LOCAL_STATE_FILE: candidate_files.append(LOCAL_STATE_FILE) for fp in DEFAULT_STATE_CANDIDATES: if fp not in candidate_files: candidate_files.append(fp) for fp in candidate_files: try: if fp and os.path.exists(fp): with open(fp, "r", encoding="utf-8") as f: data = json.load(f) # 只接受“状态文件”结构;避免误把 office-agent-state.json(仅缓存 agentId)当状态源 if not isinstance(data, dict): continue has_state = "state" in data has_detail = "detail" in data if (not has_state) and (not has_detail): continue state = normalize_state(data.get("state", "idle")) detail = data.get("detail", "") or "" # detail 兜底纠偏,确保“工作/休息/报警”能正确落区 state = map_detail_to_state(detail, fallback_state=state) # 防止状态文件久未更新仍停留在 working 态 age = _state_age_seconds(data) if age is not None and age > STALE_STATE_TTL_SECONDS: state = "idle" detail = f"本地状态超过{STALE_STATE_TTL_SECONDS}s未更新,自动回待命" if VERBOSE: print(f"[status-source:file] path={fp} state={state} detail={detail[:60]}") return {"state": state, "detail": detail} except Exception: pass # 2) 尝试本地 /status(可能需要鉴权) try: import requests headers = {} if LOCAL_STATUS_TOKEN: headers["Authorization"] = f"Bearer {LOCAL_STATUS_TOKEN}" r = requests.get(LOCAL_STATUS_URL, headers=headers, timeout=5) if r.status_code == 200: data = r.json() state = normalize_state(data.get("state", "idle")) detail = data.get("detail", "") or "" state = map_detail_to_state(detail, fallback_state=state) age = _state_age_seconds(data) if age is not None and age > STALE_STATE_TTL_SECONDS: state = "idle" detail = f"本地/status 超过{STALE_STATE_TTL_SECONDS}s未更新,自动回待命" if VERBOSE: print(f"[status-source:http] url={LOCAL_STATUS_URL} state={state} detail={detail[:60]}") return {"state": state, "detail": detail} # 如果 401,说明需要 token if r.status_code == 401: return {"state": "idle", "detail": "本地/status需要鉴权(401),请设置 OFFICE_LOCAL_STATUS_TOKEN"} except Exception: pass # 3) 默认 fallback if VERBOSE: print("[status-source:fallback] state=idle detail=待命中") return {"state": "idle", "detail": "待命中"} def do_join(local): import requests payload = { "name": local.get("agentName", AGENT_NAME), "joinKey": local.get("joinKey", JOIN_KEY), "state": "idle", "detail": "刚刚加入" } r = requests.post(f"{OFFICE_URL}{JOIN_ENDPOINT}", json=payload, timeout=10) if r.status_code in (200, 201): data = r.json() if data.get("ok"): local["joined"] = True local["agentId"] = data.get("agentId") save_local_state(local) print(f"✅ 已加入海辛办公室,agentId={local['agentId']}") return True print(f"❌ 加入失败:{r.text}") return False def do_push(local, status_data): import requests payload = { "agentId": local.get("agentId"), "joinKey": local.get("joinKey", JOIN_KEY), "state": status_data.get("state", "idle"), "detail": status_data.get("detail", ""), "name": local.get("agentName", AGENT_NAME) } r = requests.post(f"{OFFICE_URL}{PUSH_ENDPOINT}", json=payload, timeout=10) if r.status_code in (200, 201): data = r.json() if data.get("ok"): area = data.get("area", "breakroom") print(f"✅ 状态已同步,当前区域={area}") return True # 403/404:拒绝/移除 → 停止推送 if r.status_code in (403, 404): msg = "" try: msg = (r.json() or {}).get("msg", "") except Exception: msg = r.text print(f"⚠️ 访问拒绝或已移出房间({r.status_code}),停止推送:{msg}") local["joined"] = False local["agentId"] = None save_local_state(local) sys.exit(1) print(f"⚠️ 推送失败:{r.text}") return False def main(): local = load_local_state() # Startup hint for state source and URL (helps with port/state issues, e.g. issue #31) if LOCAL_STATE_FILE: print(f"State file: {LOCAL_STATE_FILE}") else: first_existing = next((p for p in DEFAULT_STATE_CANDIDATES if p and os.path.exists(p)), None) if first_existing: print(f"State file (auto): {first_existing}") else: print("State file: auto-discover (set OFFICE_LOCAL_STATE_FILE if state not found)") print(f"Local status URL: {LOCAL_STATUS_URL} (set OFFICE_LOCAL_STATUS_URL if backend uses another port)") # 先确认配置是否齐全 if not JOIN_KEY or not AGENT_NAME: print("❌ 请先在脚本开头填入 JOIN_KEY 和 AGENT_NAME") sys.exit(1) # 如果之前没 join,先 join if not local.get("joined") or not local.get("agentId"): ok = do_join(local) if not ok: sys.exit(1) # 持续推送 print(f"🚀 开始持续推送状态,间隔={PUSH_INTERVAL_SECONDS}秒") print("🧭 状态逻辑:任务中→工作区;待命/完成→休息区;异常→bug区") print("🔐 若本地 /status 返回 Unauthorized(401),请设置环境变量:OFFICE_LOCAL_STATUS_TOKEN 或 OFFICE_LOCAL_STATUS_URL") try: while True: try: status_data = fetch_local_status() do_push(local, status_data) except Exception as e: print(f"⚠️ 推送异常:{e}") time.sleep(PUSH_INTERVAL_SECONDS) except KeyboardInterrupt: print("\n👋 停止推送") sys.exit(0) if __name__ == "__main__": main() ================================================ FILE: pyproject.toml ================================================ [project] name = "star-office-ui" version = "0.1.0" description = "Star Office UI - Backend State Service" readme = "README.md" requires-python = ">=3.10" dependencies = [ "flask>=3.0.0", ] ================================================ FILE: repack_star_working.py ================================================ #!/usr/bin/env python3 """Repack star-working spritesheet into a grid to fit GPU max texture sizes. Problem: - Current spritesheet is 44160x144 (192 frames * 230w), too wide for WebGL max texture size on most GPUs. - Result: texture upload fails => renders as black rectangle. This script repacks frames into rows. Default: - frame: 230x144 - frames: 192 - cols: 35 -> width 8050 - rows: ceil(192/35)=6 -> height 864 Output: - frontend/star-working-spritesheet-grid.png Safe: - does NOT delete original file. """ import math import os from PIL import Image ROOT = "/root/.openclaw/workspace/star-office-ui" IN_PATH = os.path.join(ROOT, "frontend", "star-working-spritesheet.png") OUT_PATH = os.path.join(ROOT, "frontend", "star-working-spritesheet-grid.png") FRAME_W = 230 FRAME_H = 144 FRAMES = 192 COLS = 35 def main(): img = Image.open(IN_PATH).convert("RGBA") w, h = img.size expected_w = FRAME_W * FRAMES if h != FRAME_H or w < expected_w: raise SystemExit(f"Unexpected input size {img.size}, expected height={FRAME_H}, width>={expected_w}") rows = math.ceil(FRAMES / COLS) out_w = FRAME_W * COLS out_h = FRAME_H * rows out = Image.new("RGBA", (out_w, out_h), (0, 0, 0, 0)) for i in range(FRAMES): src_x0 = i * FRAME_W src_y0 = 0 frame = img.crop((src_x0, src_y0, src_x0 + FRAME_W, src_y0 + FRAME_H)) r = i // COLS c = i % COLS dst_x0 = c * FRAME_W dst_y0 = r * FRAME_H out.paste(frame, (dst_x0, dst_y0)) out.save(OUT_PATH) orig_size = os.path.getsize(IN_PATH) new_size = os.path.getsize(OUT_PATH) print(f"Wrote: {OUT_PATH}") print(f"Input size: {w}x{h} ({orig_size/1024/1024:.2f} MB)") print(f"Output size: {out_w}x{out_h} ({new_size/1024/1024:.2f} MB)") if __name__ == "__main__": main() ================================================ FILE: resize_map.py ================================================ #!/usr/bin/env python3 """Resize office map by SHORT EDGE scaling (keep aspect ratio, no stretching/cropping)""" from PIL import Image def resize_map(input_path, output_path, target_short_edge=600): im = Image.open(input_path) original_width, original_height = im.size # Determine which is the SHORT edge if original_width < original_height: short_edge, long_edge = original_width, original_height is_width_short = True else: short_edge, long_edge = original_height, original_width is_width_short = False # Calculate scale based on SHORT edge scale = target_short_edge / short_edge # Compute new dimensions if is_width_short: new_width = target_short_edge new_height = int(long_edge * scale) else: new_width = int(long_edge * scale) new_height = target_short_edge # Resize (use LANCZOS for high quality) im_resized = im.resize((new_width, new_height), Image.Resampling.LANCZOS) im_resized.save(output_path) print(f"Resized map saved: {output_path}") print(f"Original size: {original_width}x{original_height}") print(f"Resized size: {new_width}x{new_height}") print(f"Short edge scale: {scale:.2f}x") if __name__ == "__main__": input_path = "/root/.openclaw/media/inbound/6b352c7d-f09f-4dd7-9916-a312fb60122b.png" output_path = "/root/.openclaw/workspace/star-office-ui/frontend/office_bg.png" resize_map(input_path, output_path, target_short_edge=720) ================================================ FILE: runtime-config.sample.json ================================================ { "gemini_api_key": "YOUR_GEMINI_API_KEY", "gemini_model": "nanobanana-pro" } ================================================ FILE: scripts/gemini_image_generate.py ================================================ #!/usr/bin/env python3 """Gemini Image Generate - CLI for Star Office UI background generation. Calls Google's Gemini API to generate images, with optional reference image for style transfer / layout preservation. Expected interface (called by Star Office UI backend): python gemini_image_generate.py \ --prompt "..." \ --model \ --out-dir /tmp/xxx \ --cleanup \ [--aspect-ratio 16:9] \ [--reference-image /path/to/ref.webp] Environment: GEMINI_API_KEY - Google AI API key (required) GEMINI_MODEL - override model name (optional, --model takes precedence) Output (last line of stdout): {"files": ["/tmp/xxx/generated_0.png"]} """ import argparse import base64 import json import mimetypes import os import sys import tempfile import shutil from pathlib import Path try: from google import genai from google.genai import types HAS_GENAI = True except ImportError: HAS_GENAI = False def detect_mime(path: str) -> str: mt, _ = mimetypes.guess_type(path) if mt: return mt ext = os.path.splitext(path)[1].lower() return { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif", }.get(ext, "image/png") def main(): parser = argparse.ArgumentParser(description="Generate image via Gemini API") parser.add_argument("--prompt", required=True, help="Generation prompt") parser.add_argument("--model", default="", help="Model name") parser.add_argument("--out-dir", required=True, help="Output directory") parser.add_argument("--cleanup", action="store_true", help="(ignored, kept for compat)") parser.add_argument("--aspect-ratio", default="", help="Aspect ratio hint (e.g. 16:9)") parser.add_argument("--reference-image", default="", help="Reference image path") args = parser.parse_args() # Resolve API key api_key = os.environ.get("GEMINI_API_KEY", "").strip() if not api_key: api_key = os.environ.get("GOOGLE_API_KEY", "").strip() if not api_key: print("ERROR: GEMINI_API_KEY or GOOGLE_API_KEY not set", file=sys.stderr) sys.exit(1) # Resolve model - env var GEMINI_MODEL overrides --model flag model = os.environ.get("GEMINI_MODEL", "").strip() or args.model.strip() if not model: model = "gemini-2.0-flash-exp" # Ensure output directory out_dir = args.out_dir os.makedirs(out_dir, exist_ok=True) if not HAS_GENAI: print("ERROR: google-genai package not installed", file=sys.stderr) sys.exit(1) # Initialize client client = genai.Client(api_key=api_key) # Build prompt parts contents = [] # Add reference image if provided if args.reference_image and os.path.exists(args.reference_image): ref_path = args.reference_image mime = detect_mime(ref_path) with open(ref_path, "rb") as f: ref_data = f.read() contents.append( types.Part.from_bytes(data=ref_data, mime_type=mime) ) # Add text prompt prompt_text = args.prompt if args.aspect_ratio: prompt_text += f"\nTarget aspect ratio: {args.aspect_ratio}." contents.append(prompt_text) # Configure generation generate_config = types.GenerateContentConfig( response_modalities=["TEXT", "IMAGE"], ) try: response = client.models.generate_content( model=model, contents=contents, config=generate_config, ) except Exception as e: err_msg = str(e) print(f"ERROR: {err_msg}", file=sys.stderr) sys.exit(1) # Extract generated images output_files = [] idx = 0 if response.candidates: for candidate in response.candidates: if not candidate.content or not candidate.content.parts: continue for part in candidate.content.parts: if part.inline_data and part.inline_data.mime_type and part.inline_data.mime_type.startswith("image/"): # Determine extension from mime mime = part.inline_data.mime_type ext_map = { "image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp", } ext = ext_map.get(mime, ".png") out_path = os.path.join(out_dir, f"generated_{idx}{ext}") with open(out_path, "wb") as f: f.write(part.inline_data.data) output_files.append(out_path) idx += 1 if not output_files: # Check if there's text response with error info text_parts = [] if response.candidates: for candidate in response.candidates: if candidate.content and candidate.content.parts: for part in candidate.content.parts: if part.text: text_parts.append(part.text) if text_parts: print(f"ERROR: No image generated. Model response: {' '.join(text_parts)[:500]}", file=sys.stderr) else: print("ERROR: No image generated and no text response", file=sys.stderr) sys.exit(1) # Output result as JSON (backend reads the last line) result = {"files": output_files} print(json.dumps(result)) if __name__ == "__main__": main() ================================================ FILE: scripts/security_check.py ================================================ #!/usr/bin/env python3 """Star Office UI security preflight checker (non-destructive). Checks: - weak/default secrets in env - risky tracked files in git index - known API key patterns in tracked files """ from __future__ import annotations import os import re import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent def run(cmd: list[str]) -> tuple[int, str, str]: p = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True) return p.returncode, p.stdout.strip(), p.stderr.strip() def is_strong_secret(v: str) -> bool: if not v: return False s = v.strip() if len(s) < 24: return False low = s.lower() for token in ("change-me", "default", "example", "test", "dev"): if token in low: return False return True def is_strong_pass(v: str) -> bool: if not v: return False s = v.strip() if s == "1234": return False return len(s) >= 8 def tracked_files() -> list[str]: code, out, _ = run(["git", "ls-files"]) if code != 0: return [] return [x for x in out.splitlines() if x.strip()] def file_has_secret_pattern(path: Path) -> list[str]: hits: list[str] = [] try: text = path.read_text(encoding="utf-8", errors="ignore") except Exception: return hits patterns = [ (r"AIza[0-9A-Za-z\-_]{20,}", "Google/Gemini API key-like token"), (r"sk-[A-Za-z0-9]{16,}", "Generic sk-* token"), (r"AKIA[0-9A-Z]{16}", "AWS access key-like token"), ] for pat, label in patterns: if re.search(pat, text): hits.append(label) return hits def main() -> int: print("[security-check] Star Office UI preflight") failures: list[str] = [] warnings: list[str] = [] env_mode = (os.getenv("STAR_OFFICE_ENV") or os.getenv("FLASK_ENV") or "").strip().lower() in_prod = env_mode in {"prod", "production"} secret = os.getenv("FLASK_SECRET_KEY") or os.getenv("STAR_OFFICE_SECRET") or "" drawer_pass = os.getenv("ASSET_DRAWER_PASS") or "" if in_prod: if not is_strong_secret(secret): failures.append("Weak/missing FLASK_SECRET_KEY (or STAR_OFFICE_SECRET) in production") if not is_strong_pass(drawer_pass): failures.append("Weak/missing ASSET_DRAWER_PASS in production") else: if not secret: warnings.append("FLASK_SECRET_KEY not set (ok for local dev, not for production)") if not drawer_pass: warnings.append("ASSET_DRAWER_PASS not set (defaults may be unsafe for public exposure)") tracked = tracked_files() risky_tracked = [ "runtime-config.json", "join-keys.json", "office-agent-state.json", ] for f in risky_tracked: if f in tracked: failures.append(f"Risky runtime file is tracked by git: {f}") # scan tracked text-ish files for common secret patterns for rel in tracked: if rel.startswith(".git/"): continue p = ROOT / rel if not p.exists() or p.is_dir(): continue if p.stat().st_size > 2_000_000: continue hits = file_has_secret_pattern(p) for h in hits: failures.append(f"Potential secret pattern in tracked file: {rel} ({h})") if warnings: print("\nWarnings:") for w in warnings: print(f" - {w}") if failures: print("\nFAIL:") for f in failures: print(f" - {f}") print("\nResult: FAILED") return 1 print("\nResult: OK") return 0 if __name__ == "__main__": sys.exit(main()) ================================================ FILE: scripts/smoke_test.py ================================================ #!/usr/bin/env python3 """Star Office UI smoke test (non-destructive). Usage: python3 scripts/smoke_test.py --base-url http://127.0.0.1:19000 Optional env: SMOKE_AUTH_BEARER=xxxx # if your gateway/proxy requires bearer auth """ from __future__ import annotations import argparse import json import os import sys import urllib.error import urllib.request REQUIRED_ENDPOINTS = [ ("GET", "/", 200), ("GET", "/health", 200), ("GET", "/status", 200), ("GET", "/agents", 200), ("GET", "/yesterday-memo", 200), ] def req(method: str, url: str, body: dict | None = None, token: str = "") -> tuple[int, str]: data = None headers = {} if token: headers["Authorization"] = f"Bearer {token}" if body is not None: data = json.dumps(body).encode("utf-8") headers["Content-Type"] = "application/json" r = urllib.request.Request(url=url, method=method, data=data, headers=headers) try: with urllib.request.urlopen(r, timeout=8) as resp: raw = resp.read().decode("utf-8", errors="ignore") return resp.status, raw except urllib.error.HTTPError as e: raw = e.read().decode("utf-8", errors="ignore") if hasattr(e, "read") else str(e) return e.code, raw except Exception as e: return 0, str(e) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--base-url", default="http://127.0.0.1:19000", help="Base URL of Star Office UI service") args = ap.parse_args() base = args.base_url.rstrip("/") token = os.getenv("SMOKE_AUTH_BEARER", "").strip() failures: list[str] = [] print(f"[smoke] base={base}") for method, path, expected in REQUIRED_ENDPOINTS: code, body = req(method, base + path, token=token) if code != expected: failures.append(f"{method} {path}: expected {expected}, got {code}, body={body[:200]}") else: print(f" OK {method} {path} -> {code}") # non-destructive state update probe code, body = req("POST", base + "/set_state", {"state": "idle", "detail": "smoke-check"}, token=token) if code != 200: failures.append(f"POST /set_state failed: {code}, body={body[:200]}") else: print(" OK POST /set_state -> 200") if failures: print("\n[smoke] FAIL") for f in failures: print(" -", f) return 1 print("\n[smoke] PASS") return 0 if __name__ == "__main__": sys.exit(main()) ================================================ FILE: set_state.py ================================================ #!/usr/bin/env python3 """Update Star Office UI state (for testing or agent-driven sync). For automatic state sync from OpenClaw: add a rule in your agent SOUL.md or AGENTS.md: Before starting a task: run `python3 set_state.py writing "doing XYZ"`. After finishing: run `python3 set_state.py idle "ready"`. The office UI reads state from the same state.json this script writes. """ import json import os import sys from datetime import datetime STATE_FILE = os.environ.get( "STAR_OFFICE_STATE_FILE", os.path.join(os.path.dirname(os.path.abspath(__file__)), "state.json"), ) VALID_STATES = [ "idle", "writing", "receiving", "replying", "researching", "executing", "syncing", "error" ] def load_state(): if os.path.exists(STATE_FILE): with open(STATE_FILE, "r", encoding="utf-8") as f: return json.load(f) return { "state": "idle", "detail": "待命中...", "progress": 0, "updated_at": datetime.now().isoformat() } def save_state(state): with open(STATE_FILE, "w", encoding="utf-8") as f: json.dump(state, f, ensure_ascii=False, indent=2) if __name__ == "__main__": if len(sys.argv) < 2: print("用法: python set_state.py [detail]") print(f"状态选项: {', '.join(VALID_STATES)}") print("\n例子:") print(" python set_state.py idle") print(" python set_state.py researching \"在查 Godot MCP...\"") print(" python set_state.py writing \"在写热点日报模板...\"") sys.exit(1) state_name = sys.argv[1] detail = sys.argv[2] if len(sys.argv) > 2 else "" if state_name not in VALID_STATES: print(f"无效状态: {state_name}") print(f"有效选项: {', '.join(VALID_STATES)}") sys.exit(1) state = load_state() state["state"] = state_name state["detail"] = detail state["updated_at"] = datetime.now().isoformat() save_state(state) print(f"状态已更新: {state_name} - {detail}") ================================================ FILE: state.sample.json ================================================ { "state": "idle", "detail": "Waiting...", "progress": 0, "updated_at": "2026-02-26T00:00:00" } ================================================ FILE: webp_to_spritesheet.py ================================================ #!/usr/bin/env python3 """Convert an animated WebP to a horizontal spritesheet PNG. Notes: - Phaser's built-in loader doesn't support animated WebP directly. - We convert frames into a spritesheet. - Output: -spritesheet.png """ import os from PIL import Image def webp_to_spritesheet(in_path: str, out_path: str, frame_w: int, frame_h: int, max_frames: int | None = None): im = Image.open(in_path) n = getattr(im, 'n_frames', 1) if max_frames: n = min(n, max_frames) sheet = Image.new('RGBA', (frame_w * n, frame_h), (0, 0, 0, 0)) for i in range(n): im.seek(i) fr = im.convert('RGBA') if fr.size != (frame_w, frame_h): fr = fr.resize((frame_w, frame_h), Image.NEAREST) sheet.paste(fr, (i * frame_w, 0)) sheet.save(out_path) return n def main(): import argparse ap = argparse.ArgumentParser() ap.add_argument('in_path') ap.add_argument('out_path') ap.add_argument('--w', type=int, required=True) ap.add_argument('--h', type=int, required=True) ap.add_argument('--max', type=int, default=None) args = ap.parse_args() n = webp_to_spritesheet(args.in_path, args.out_path, args.w, args.h, args.max) print(f"Wrote {args.out_path} with {n} frames") if __name__ == '__main__': main()