跳轉到

新增一個 App

這個平台是 multi-App(多 App)(#89)。一個 App 就是程式碼裡 src/workspace_app/apps/<slug>/ 底下的一個目錄。在那裡丟進一個新目錄,就會產生一個平行、 獨立品牌的 dashboard——launcher 卡片、item 清單、create 流程、workspace shell、agent——全部由該 App 的 app.json + model 驅動。RCA(apps/rca/)只是 其中一個 App;scaffold(腳手架)apps/_template/ 則是一份「複製我」的起點。

註冊是開機時對 apps/ 的一次掃描(apps/registry.py):任何含有 app.json + model.py 的目錄都會被探索、註冊,並顯示在 launcher 上。沒有需要編輯的中央清單。(像 _template 這種 _ 開頭的目錄會被 略過——它們是內部用的,不對使用者公開。)

快速開始

  1. 複製 scaffold:
    cp -r src/workspace_app/apps/_template src/workspace_app/apps/<your-slug>
    
    目錄名稱就是 slug,所以它必須是合法的 Python package 名稱 (小寫、不能有連字號):ticketsauditsincidents
  2. 編輯 <your-slug>/app.json——把 slug 設成與目錄相符,然後填入 identity / agent / item / layout / lifecycle(參考下方)。
  3. 編輯 <your-slug>/model.py——把 TemplateItem 及其 enum 改名成你的 領域;把 INDEXED_FIELDS 設成你用來 filter / sort / 上色的欄位。
  4. 編輯 <your-slug>/prompts/system.md——agent 的 base prompt。
  5. 編輯 <your-slug>/profiles/default/——create 流程會 seed 的起始內容 (再多加幾個 profile 就能提供可挑選的多樣選項)。
  6. 開機(uv run python -m workspace_app)。App 會出現在 launcher 上;不需要動 任何其他檔案。

各個檔案

apps/<slug>/
├── app.json                     # identity、agent 上限、layout、lifecycle、各種開關
├── model.py                     # WorkItem Struct(MODEL + INDEXED_FIELDS)
├── prompts/
│   └── system.md                # agent 的 base system prompt
└── profiles/
    └── default/                 # 一份起始內容 bundle(create 流程的預設)
        ├── _prompt.md           # 附加在這個 profile 的 system prompt 之後
        ├── _profile.json        # (選用)收窄 tools/presets、suggestions
        ├── .skill/<name>/SKILL.md  # (選用)可被 read_skill 載入的 skill
        └── *.tpl                # seed 進 item 的檔案($title/$owner/… 會被代入)

app.json 參考

欄位 意義
slug App id——必須等於目錄名稱
title / description launcher 卡片文字
icon flame(具名)、一個 emoji,或 icon.svg(同層檔案,內嵌)
color 一個 hex → App 的 --accent 三色組(App 內整套重新配色)
function.workspace file IDE(tree + editor + file tools)。false → 只有 chat 的 shell
function.sandbox exec + package tools。不需要 terminal;控制 exec 相關功能的開啟
function.terminal 人用的 shell 分頁。需要 sandbox: true
resources 這個 App 的一個 item 可以用多少 {cpu, memory, disk}。三個欄位各自可省略,見下
agent.prompt_file base system prompt 的路徑(相對於 App 目錄)
agent.tools App 的 tool 上限;profile 可以收窄成其子集
agent.picker [{preset, name}]——model picker;presetconfig.yamlagents.presets
agent.suggestions App 層級的 quick-prompt chips(profile 可覆寫)
item.{noun,noun_plural,create_label} 給人看的字串(「Start Investigation」)
layout.{breadcrumb,statusbar,list,form} 每個 surface 上顯示哪些領域欄位
layout.default_tabs workspace 進場時開啟的檔案(只篩出有 seed 的那些)
lifecycle {status_field, closing_states}——驅動 Close 功能
labels 各欄位的顯示 label
field_styles enum option → tone token(err/warn/ok/info/muted)——把 chip 顏色當作資料
default_profile 使用者沒挑選時,create 流程 seed 的 profile

開關的一致性在開機時強制檢查(validate_function_coherence):例如 agent.tools 裡有 execsandbox: false,或 terminal: truesandbox: false,都會讓開機大聲失敗。_template App 出貨時帶 sandbox: false + 只有 file 的 tools,用來示範一個 workspace-only(無 sandbox)的 App。

resources:這個 App 一個 item 吃多少

一個資料分析 App 需要的記憶體本來就比聊天 App 多,而這件事屬於 App 自己,不屬於部署端一張 按 slug 排的表——那種表新增 App 時沒人會記得改。所以 App 宣告胃口,部署端定天花板,就像 k8s 的 Pod requests 對上 namespace 的 LimitRange

// src/workspace_app/apps/<slug>/app.json
"resources": {
  "cpu": 2,           // 核心數
  "memory": "2G",
  "disk": "10G"       // 這是**一個 item 的 workspace** 上限,不是全 App 加總
}

三個欄位各自獨立、各自可省略。 只在意記憶體就只寫 memory,cpu / disk 會各自往下掉到 部署端的 resources.per_app.default,再掉到今天的舊旋鈕 (sandbox.isolation.* / filestore.workspace_quota)。什麼都不寫的 App 行為跟今天完全一樣 ——目前四個內建 App 都沒有宣告。

三件出手前要知道的事:

  • 超過天花板是開機失敗,不是默默截斷。 超過 resources.per_app.max 會讓服務起不來,錯誤 訊息裡有 App 名字。「設定寫 4 核、實際只拿到 2 核」這種靜默失效的旋鈕,這個 codebase 不出貨。
  • cpu / memory 由 sandbox 後端執行。 正式環境是 kind: http,所以改完要重新部署 sandbox-host,否則 app.json 設了也沒有效果。disk 由本體執行,不受此限。
  • disk: "0" 是「明確無上限」,會停止往下掉;cpu: 0 是「沒宣告」,會繼續往下掉。 這個 不對稱是刻意的——零核心不是任何人的本意,零 disk 上限卻是。

部署端那一半(per_app.default / per_app.max、以及每個人的總量與特規使用者)寫在 設定指南 §6.5

model.py 合約

匯出 MODEL(一個 WorkItemBase 子類)+ INDEXED_FIELDS:

  • Tier 1(從 WorkItemBase 免費取得):titleownerdescriptionprofileattached_preset
  • Tier 2(opt-in):memberstopics——如果你的 App 有用到,就重新宣告成具體的 list[str]
  • Tier 3:你自己的型別化領域欄位(enum / scalar)。把它們標好型別讓它們 原生建索引——把你用來 filter / sort / 上色的那些列進 INDEXED_FIELDS

欄位的 kind + enum options 會從 model 投射進 manifest (GET /apps/{slug}.fields),所以 FE 不需要重述型別就能 render + inline-edit 它們—— enum → selectstr → text

Profiles

一個 profile 就是一份起始內容 bundle。default 是必要的;多出貨幾個就能給 create 流程一個 profile picker(當數量 >1 時才會出現)。每個 profile:

  • *.tpl 檔 → 在 create 時 seed 進 item,並把 $title / $owner / 你的 Tier-3 欄位代入(.tpl 後綴會被去掉)。
  • _prompt.md → 附加在這個 profile 的 system prompt 之後。
  • _profile.json(選用,apps.profiles.ProfileManifest):titledescriptionsuggestionstools(⊆ agent.tools)、presets (⊆ agent.picker)、default_preset。省略則繼承 App 的完整上限。
  • .skill/<name>/SKILL.md(選用):可被 read_skill 載入的 skill,帶 name + description frontmatter;當 profile 有出貨任何 skill 時,agent 會拿到一份 「## Available skills」索引 + read_skill tool。

Presets

agent.picker 用名稱參照 presets;presets 住在 config.yamlagents.presets 底下(model + creds + sandbox image + idle timeout)。沿用 bundled 的 qwen3-local / claude-opus / openai-mini,或自己加。

限制

  • 目錄名稱就是 slug——一個合法的 Python package 名稱(registry 會 import apps.<slug>.model)。不能有連字號。
  • _ 開頭的目錄不會被探索(拿來放 scaffold / 內部 helper)。
  • 資料不會跨 App 共用——每個 App 有自己的 resource table。