App 平台(apps-platform)¶
把 RCA、Topic Hub、Playground 從「被特例化的畫面」變成「純資料定義的 App」的子系統(#89)。每個 App 就是一個資料目錄 apps/<slug>/(app.json 身分/功能/agent/版面 + model.py 的 WorkItem Struct + prompts/ + profiles/),靠掃描自動註冊——丟一個新目錄進去就上線,不需要改任何平台程式碼。共用後端把這些資料檔轉成一個註冊到 specstar 的 resource、一份給前端用的欄位 schema,以及每回合一個 AgentConfig(透過 App ◇ profile ◇ preset 三層 resolve)。
看這篇之前:先讀 架構總覽 抓全貌。
職責與邊界¶
這個子系統負責:
- App 發現與註冊:掃描
apps/找出帶app.json的目錄,匯入各 App 的model.py,把MODEL(WorkItemBase子類)add_model到 specstar(registry.py)。 - 資料契約解碼:把
app.json→AppManifest、_profile.json→ProfileManifest(manifest.py/profiles.py)。 - 三層 agent resolve:每回合把 App 上限 ◇ profile 子集 ◇ preset(model/creds)合成一個
AgentConfig(catalog.py)。 - 欄位 schema 投影:從 model 的 msgspec/OpenAPI schema 推導出前端可渲染/可編輯的欄位清單(
schema.py)。 - 建立 item 時的 starter 內容 seeding(
seeding.py)、每回合的 context-file 注入(context_files.py)、漸進揭露 skills(skills.py/shared_skills.py)。
不負責(留給鄰居):
- 回合執行、SSE、取消——那是 API 與回合引擎 與 Agent 執行時。
- 工具的實際實作(
exec/ask_knowledge_base/make_deck…)——app.json只列名字,實作在 工具套件與 Sandbox Host +AgentToolContext。 - WorkItem 的儲存後端——交給 資料層(specstar);本層只
add_model註冊。 - 前端的
WorkspaceShell/ 欄位渲染——本層只產出 manifest + 欄位 schema,消費端見 前端(web/)。 - profile 帶的 workflow
run.py的實際編排——那是 Workflow 引擎;本層只在啟動時跑validate_workflow_profiles。
核心模組¶
| 路徑 | 角色 |
|---|---|
src/workspace_app/apps/manifest.py |
app.json 的型別形狀:AppManifest + FunctionToggles / AgentManifest / PickerEntry / Layout / Lifecycle / ItemNouns / Onboarding。load_app_manifest(slug) 解碼。欄位型別/選項不在這裡——從 model 的 schema 讀(decision 19)。 |
src/workspace_app/apps/base.py |
WorkItemBase——每個 App 的 item Struct 都繼承它。Tier1 title/owner(+ profile/attached_preset/attached_tool_prefs——#322 per-item tool tri-state 覆寫,dict[str, bool],attached_preset 的姊妹欄位——加 attached_skill_prefs——#380 同形的 per-item skill tri-state 覆寫,dict[str, bool]),Tier2 members/topics(T \| UnsetType,redeclaring 才 opt-in),Tier3 = App 自己的 domain 欄位。也定義 IndexedFields 型別別名。 |
src/workspace_app/apps/registry.py |
發現 + specstar 註冊。_app_models 掃 discover_app_slugs()、匯入各 model.py(slug 是合法識別字→import_module;含連字號如 topic-hub→_load_model_module 用 file-path exec),斷言 MODEL 是 WorkItemBase 子類。register_apps 對每個 App add_model。resource_route 把 model 名 kebab-case 成 FE CRUD 路由。@cache。 |
src/workspace_app/apps/catalog.py |
AppCatalog——每回合三層 resolve。resolve(*, …, tool_prefs=None, skill_prefs=None) → AgentConfig:tools/presets 是 App ceiling 的 profile 子集(否則整個 ceiling),_apply_tool_prefs 再把 per-item tool_prefs tri-state 覆寫(#322)疊上 default set 並以 ceiling 順序輸出,preset 給 model/creds/sandbox_image/idle/env,prompt = base ◇ #241 preamble ◇ profile appendix ◇ skill index——skill index 也走 effective_item_skills(…, skill_prefs) 濾掉關閉的 skill(#380)。validate_function_coherence + validate_all_apps 在啟動時 enforce decision 11。 |
src/workspace_app/tooling/catalog.py |
tool 顯示 metadata 的單一真相(#322)。ToolMeta{name,label,description};humanize_tool_label 給永不外露 raw snake_case 的 fallback label;flat_catalog(packages) 每個可呼叫工具名 → ToolMeta(餵 chat tool cards),picker_units(app_tools, packages) 每個 app.json tools[] 一個顯示單元(餵 per-item picker)。built-in 描述來自 agent/tools.py:builtin_tool_descriptions(取 LLM-facing description),picker 與 tool card 都 label 自同一文字。 |
src/workspace_app/apps/profiles.py |
ProfileManifest(_profile.json)——narrows tools ⊆ app.tools / skills ⊆ app.skills(#380;UNSET→繼承全部)/ presets ⊆ app.picker,加 suggestions / default_preset / collections(#280)/ upload_dir(#198)/ workflows(#100)。load_profile / load_profile_appendix / list_profiles + workflow 正規化 helper。 |
src/workspace_app/apps/resolve.py |
find_work_item(spec, item_id)——「id → 哪個 App 擁有它 + 那個 item」的唯一接縫(掃 registered_apps,item_id == resource_id),per-turn 與 mention 路徑共用。resolve_item_agent_config 把它橋接到 AppCatalog.resolve。 |
src/workspace_app/apps/schema.py |
project_fields(model) → list[FieldSpec{name,label,kind,options?}](走 msgspec.inspect)。Enum→select(+options),list/Unset-wrapped-list→tags,其餘→text。折進 GET /apps/{slug} 當 data['fields']。 |
src/workspace_app/apps/seeding.py |
seed_item(...)——把 apps/<slug>/profiles/<profile>/ 複製進新 item 的 FileStore;.tpl 檔用 string.Template 以 case_from_item(item) 代換並去掉 .tpl;_profile.json/_prompt.md/.skill/run.py/__init__ 跳過。鍵在 opaque item_id。 |
src/workspace_app/apps/context_files.py |
每回合 deterministic 注入(Topic Hub §6)。build_context_block 每回合即時從 FileStore 讀 agent.context_files(如 MEMORY.md、collections.json),render 成標籤區塊 prepend 到 agent content;永不存進 history(idempotent / replay-safe)。 |
src/workspace_app/apps/skills.py |
漸進揭露 skills。Package skill 在 profiles/<p>/.skill/<name>/SKILL.md(frontmatter name/description + body,read_skill 按需載入)。list_skills/load_skill(@cache)、workspace skills(#298,即時讀不快取)、merged_profile_skills,加上 save_skill 工具(在 agent/tools.py)用的 helper slugify_skill_name/render_skill_md。#380:SkillState{name,description,source,default_on,effective} + 單一真相 resolver effective_item_skills(app_slug, profile, prefs, workspace_metas)(去重優先序 workspace>profile>shared;shared default_on = name in profile.skills-or-all)——picker 端點與 prompt 的「## Available skills」index 都走它(anti-drift);build_workspace_skills_block(…, prefs) 用它濾掉 pref is False 的 skill;resolve_skill_body/build_applied_skills_block 供 apply 這回合硬預載 skill body。 |
src/workspace_app/apps/shared_skills.py |
SHARED_SKILLS registry(name→sample-skills/ 下的源目錄,mirror tooling PACKAGES)。App 透過 app.json agent.skills opt-in。v1 ships author-skill(co-authoring meta-skill)。 |
src/workspace_app/apps/rca/(app.json + model.py) |
RCA App:完整 workspace+sandbox+terminal、最大工具 ceiling、primary_surface=ide、lifecycle status→resolved/abandoned。RcaInvestigation Tier3 severity/status(enums)/product(str),INDEXED_FIELDS=[severity,status,product]。 |
src/workspace_app/apps/topic-hub/(app.json + model.py) |
連字號 slug → model.py 由 file path 載入(絕對 import、不 from __future__ import annotations)。terminal:false、chat_switcher=always、context_files=[MEMORY.md,collections.json],collection set 是 workspace 檔案不是 model 欄位。TopicHub Tier3 只有 status。 |
src/workspace_app/apps/playground/(app.json + model.py) |
最小 baseline App(#102 診斷)。PlaygroundItem:topics(Tier2) + status(Tier3)。single-preset picker。 |
src/workspace_app/apps/_template/ |
Copy-me 樣板(app.json/__init__.py/model.py)。_-prefixed → 被 discover_app_slugs 跳過,永不註冊成真 App。對應 adding-an-app。 |
介面與接縫¶
本層的多型靠資料契約 + base Struct + resolver,而非 Protocol/ABC。主要接縫:
| 接縫 | 種類 | 定義位置 | 實作 |
|---|---|---|---|
WorkItemBase |
base Struct(per-App 子類) | src/workspace_app/apps/base.py |
apps/rca/model.py:RcaInvestigation、apps/topic-hub/model.py:TopicHub、apps/playground/model.py:PlaygroundItem、apps/_template/model.py |
app.json → AppManifest |
資料契約(msgspec decode) | src/workspace_app/apps/manifest.py |
apps/rca/app.json、apps/topic-hub/app.json、apps/playground/app.json |
_profile.json → ProfileManifest |
資料契約(msgspec decode) | src/workspace_app/apps/profiles.py |
apps/<slug>/profiles/*/_profile.json |
AppCatalog.resolve(三層) |
resolver | src/workspace_app/apps/catalog.py |
apps/resolve.py:resolve_item_agent_config、factories.py:get_app_catalog |
find_work_item(id → App+item) |
lookup 接縫 | src/workspace_app/apps/resolve.py |
per-turn resolution + mention 路徑 |
SHARED_SKILLS registry |
registry(mirror tooling PACKAGES) |
src/workspace_app/apps/shared_skills.py |
sample-skills/author-skill |
運作方式 / 資料流¶
flowchart TD
subgraph data["apps/<slug>/ (純資料)"]
AJ[app.json] --> MAN[AppManifest]
MP["model.py<br/>MODEL / INDEXED_FIELDS"] --> WIB[WorkItemBase 子類]
PROF["profiles/<name>/<br/>_profile.json / _prompt.md<br/>.skill / *.tpl"]
end
subgraph boot["啟動"]
DISC[discover_app_slugs] --> VAL["validate_all_apps<br/>function coherence + workflows"]
DISC --> REG["register_apps<br/>add_model + INDEXED_FIELDS"]
PRES[config.yaml presets] --> CAT[AppCatalog]
end
MAN --> VAL
MP --> REG
subgraph runtime["每個 request / turn"]
GET["GET /apps/{slug}"] --> SCH["project_fields → 欄位 schema"]
CREATE["POST /a/{slug}/items"] --> SEED["seed_item → FileStore"]
TURN[chat 回合] --> RES[resolve_item_agent_config]
RES --> FIND["find_work_item id→App+item"]
FIND --> RESOLVE["AppCatalog.resolve<br/>App ceiling ◇ profile subset ◇ preset"]
RESOLVE --> AC[AgentConfig]
TURN --> CTX["build_context_block 即時 / 不存"]
end
MAN --> GET
WIB --> SCH
PROF --> SEED
CAT --> RESOLVE
MAN --> RESOLVE
PROF --> RESOLVE
啟動(組裝根)¶
factories.get_app_catalog(settings)(由 啟動與組裝根 在啟動時呼叫)先跑 validate_all_apps():對每個 discover_app_slugs() 跑 validate_function_coherence(load_app_manifest(slug))(decision 11 硬錯)+ validate_workflow_profiles(slug),然後用 settings.agents.presets 建 AppCatalog。同一啟動路徑下,resources/__init__.py(make_spec)呼叫 register_apps(spec),對每個 App add_model(MODEL, indexed_fields=INDEXED_FIELDS)。_app_models() 被 @cache 起來——一個 process 的 App 集合固定。
Manifest 抓取¶
GET /apps/{slug}(api/meta_routes.py:get_app_manifest,經 register_meta_routes 掛載):load_app_manifest 轉 builtins 後,補上 resource_route、project_fields(app_model(slug)) 當 data['fields']、各 profile 的 picker(list_profiles/load_profile,含 upload_dir),若 icon 以 .svg 結尾就 inline 進 data['icon']。前端拿這份 manifest × 欄位 schema 驅動整個 WorkspaceShell。
建立 item¶
POST /a/{slug}/items(api/item_routes.py:create_app_item,經 register_item_routes 掛載):msgspec.convert(body + owner + 預設 profile → model) → spec.create → seed_item 把 profile 的 starter 檔(.tpl 用 case_from_item 代換)複製進 item 的 FileStore(item_id == resource_id)→ #280 resolve_profile_collections 把 profile 預設 collection set 解析成 live id 寫進 collections.json。
每回合 resolve¶
api/locator.py:ItemLocator.resolve_agent_config(item_id)(post-#54;原 app.py 的 _resolve_agent_config closure 改名搬進 ItemLocator,去掉前綴底線)委派給 resolve_item_agent_config(spec, app_catalog, item_id):find_work_item 掃 registered_apps() 得到 (slug, item),再 app_catalog.resolve(app_slug=slug, profile=item.profile, attached_preset=item.attached_preset or None, tool_prefs=item.attached_tool_prefs or None, skill_prefs=item.attached_skill_prefs or None)(keyword-only 簽名)合成 tools(profile ⊆ app ceiling)、allowed presets(profile ⊆ app.picker)、chosen preset(attached → profile default → 第一個),以及 system prompt(base prompt_file ◇ #241 workspace preamble(僅 workspace App)◇ profile _prompt.md appendix ◇ merged skill index——關閉的 skill(skill_prefs pin False)不進 index,#380)。同一回合,api/chat_send.py:ChatSendService.send(post-#54;原 app.py 的 _send_into closure)也呼叫 build_context_block 把 App agent.context_files 的即時內容 prepend 到 agent content(永不存進 history),另外 build_workspace_skills_block(…, prefs) 把使用者在此 workspace 共創的 skill 索引一併 prepend(同樣濾掉關閉的);若這回合帶了 apply_skills,build_applied_skills_block 再把被選 skill 的整段 body 硬預載進 content(一次性,不進 history)。
Per-item tool tri-state 覆寫(#322):app.json agent.tools 是 ceiling——可勾選工具的全集,也是任何覆寫的硬上限;profile _profile.json tools 只是 default-checked 子集(省略則整個 ceiling)。item 的 attached_tool_prefs(sparse dict[str, bool])逐工具 pin:True 強制開、False 強制關、不在 dict 的鍵則 follow default。覆寫 ceiling 是 app.json 而非 profile,所以 force-on 能把 profile 收窄掉的工具加回來;ceiling 以外的鍵 no-op。catalog.py:_apply_tool_prefs 以 ceiling 順序輸出(deterministic)。picker state 由 GET /a/{slug}/items/{item_id}/tools 提供(api/tools_routes.py:register_tools_routes):每個 picker_units(ceiling, packages) 一列 ItemToolState{key,label,description,default_on,pref(follow/on/off),effective},其中 effective 取自 locator.resolve_agent_config(item_id) 的 allowed_tools——與真正回合走的同一個 resolve(anti-drift);GET /tools 則回傳 flat catalog 給 chat tool cards 標 label(用 backend catalog 取代 FE raw-name fallback)。
Per-item skill tri-state 覆寫 + apply-this-turn(#380,對齊 #322):app.json agent.skills 是 skill ceiling;profile _profile.json skills(⊆ ceiling,UNSET→繼承全部)決定 shared skill 的 default_on。item 的 attached_skill_prefs(sparse dict[str, bool])逐 skill pin follow/on/off——關閉的 skill 不進 prompt 的「## Available skills」index,read_skill 也拒絕它(除非這回合被 apply)。picker state 由 GET /a/{slug}/items/{item_id}/skills 提供(api/file_routes.py):每列 _ItemSkillState{name,description,source(shared/profile/workspace),default_on,pref(follow/on/off),effective},全部走 effective_item_skills 這同一個 resolver(與回合 index 同源,anti-drift)。Apply 是另一條軸:composer 的 apply_skills(一次性、送出後自動清)把被選 skill 的整段 body 硬預載進該回合 content(小型本地模型可能略過 read_skill 這種工具呼叫,所以用硬預載而非軟指示),且 apply 蓋過 disable(關掉的 skill 仍可被 apply)。只作用於 App item chat;KB chat 不帶。
關鍵不變式與眉角¶
function toggles ↔ tools[] 一致性是啟動硬錯
validate_function_coherence 在啟動時 raise(不是回合時靜默失敗):terminal 需 sandbox;tools 含 exec 需 sandbox:true;檔案工具(read_file/write_file/edit_file/list_files/exists/delete_file)需 workspace:true;primary_surface=ide 需 workspace:true;每個 agent.skills 名字都必須存在於 SHARED_SKILLS。一個 typo 就 fail boot loud,而不是回合時神祕誤動作。
profile 只能 narrow,不能 widen
profile 的 tools 必須 ⊆ app.tools、presets ⊆ app.picker(_subset_or_raise)——profile 收窄 App ceiling,永不放大。省略(UNSET)→ 繼承整個 ceiling。
連字號 slug 的 model.py 規矩
含連字號的 slug(topic-hub)不是可 import 的 package,其 model.py 由 file path exec(_load_model_module),所以它必須用絕對 import、且不可 from __future__ import annotations(msgspec 會 eager 解析欄位型別,而 path-exec 的合成模組名不保證在 sys.modules 裡供 stringised forward-ref 解析)。同理見 workflow run.py。
item_id 就是 WorkItem 的 specstar resource_id
它全域唯一(不是 uid),是 FileStore / Conversation / sandbox 共用的 opaque cross-cutting key。永不 parse 它。
Tier-2 opt-in 靠 redeclaring,不要 extra:dict
把 base 的 members/topics(T | UnsetType)在子類 redeclare 成具體 list[str] 才啟用該功能;留 UNSET 代表 App 沒這功能,msgspec 在 wire 上省略它。不要加 extra: dict。
欄位型別/選項來自 model schema,app.json 只做顯示
project_fields 從 model 的 OpenAPI/msgspec schema 推導 kind/options;model 是 enum 選項的唯一真相。app.json 只 overlay 顯示(layout/labels/field_styles)。注意:schema.py 實際會吐第三種 kind——tags(給 list 欄位),不只 select/text(以程式碼為準)。
context_files 每回合即時注入、永不存
區塊每回合從 FileStore 重新讀取,只有最新回合帶它,絕不寫進 conversation history(idempotent + replay-safe)。
profile metadata 永不被 seed 進 workspace
seeding._SKIP = {__init__.py, __pycache__, _profile.json, _prompt.md, .skill, run.py}——只有 starter 內容會進 item workspace。
package skill 的 frontmatter name 必須等於目錄名
否則 list_skills 會靜默跳過(save_skill 工具用 slugify_skill_name 讓 frontmatter name 與目錄名同步,因此不會寫出會被跳過的 skill);SKILL.md body 上限 50_000 字元(超過 raise,絕不截斷)。
_-prefixed 目錄永遠不是 App
discover_app_slugs 跳過 _-prefixed 目錄(_template、_base.md、__pycache__)。一個 App 目錄 = 帶 app.json(marker)+ model.py(暴露 MODEL 為 WorkItemBase 子類 + INDEXED_FIELDS)的子目錄。
設計決策與出處¶
| 決策 | 理由 | 出處 |
|---|---|---|
| 三層 resolve:App ceiling ◇ profile subset ◇ preset model/creds | 把能力創作(App)、starter 內容/收窄(profile)、部署 LLM 配方(config.yaml preset)分開獨立變化;App 不會被 profile 放大。 | #89 decision 25;catalog.py docstring |
| tools[] ↔ function toggle 不一致是啟動硬錯,不靜默丟棄 | exec 配 sandbox:false(或拼錯的 shared skill)應該 fail boot loud,而非回合時神祕誤動作。 |
#89 decision 11;#298 Q7;validate_function_coherence |
| 欄位型別來自 model schema,app.json 只做顯示 | enum 選項單一真相;呼應 specstar autocrud 的 ResourceField,但 runtime 端瘦身,前端免 codegen。 | #89 decision 19/P7b;schema.py |
WorkItemBase Tier 模型(1 具體 / 2 opt-in / 3 App domain) |
平台結構欄位保證存在,選用平台功能 redeclare 才 opt-in(msgspec 在 wire 省略 UNSET),App 專屬欄位 typed 且 per-resource 原生索引(無跨 App 混料、無 extra:dict)。 |
#89;base.py |
App 靠掃描 apps/ 的 app.json+model.py 發現;連字號 slug 由 file path 載入 |
丟一個目錄就註冊一個 App,零平台程式碼改動;連字號 slug 不是 importable package,model 像 workflow run.py 那樣 exec。 |
#89 P3;registry.py;adding-an-app |
| context_files 每回合即時注入、永不存 | agent 永遠看到當下的 MEMORY.md/collections.json,而 history 保持乾淨;idempotent + replay-safe;泛化 #106 的 context-card prepend。 |
Topic Hub §6;context_files.py |
primary_surface(chat vs ide)+ chat_switcher(auto vs always)是 manifest 資料 |
chat-first App(Topic Hub)vs IDE-first(RCA evidence/brief)純由 app.json 資料驅動前端 chrome,不是 shell 程式碼。 |
#159 / #200;manifest.Layout |
與其他子系統的關係¶
- 啟動與組裝根——
factories.get_app_catalog從settings.agents.presets建AppCatalog並跑validate_all_apps;resources/__init__.py的register_apps(spec)。 - API 與回合引擎——
GET /apps/{slug}(manifest+fields+profiles)、POST /a/{slug}/items(create+seed+collections)、per-turnresolve_item_agent_config、build_context_blockprepend。 - Agent 執行時——
resolve吐出的AgentConfig餵給 runner;allowed_tools由它消費。 - 資料層(specstar)——
register_apps把每個 App 的 WorkItemadd_model;find_work_item走 resource manager 查 id。config/schema.py:Preset是 catalog 折進AgentConfig的 model/creds 層。 - Sandbox、FileStore 與同步——
seed_item寫 /context_files讀,皆鍵於item_id。 - Workflow 引擎——
validate_workflow_profiles在啟動驗證;ProfileManifest.workflows(WorkflowManifest)+ profile 的workflows/<id>/run.py。 - 知識庫:攝取與索引——#280
kb/collections.py:resolve_profile_collections從 profile 預設 set seedcollections.json。 - 工具套件與 Sandbox Host——
app.jsonagent.tools列的工具名;SHARED_SKILLSmirror toolingPACKAGES。 - 前端(web/)——通用
WorkspaceShell/DomainFields由 manifest layout × 欄位 schema 驅動。 - 新增一個 App 的步驟手冊:adding-an-app;Topic Hub 細節:topic-hub。
原始碼錨點¶
接手者建議先讀的真實檔案:
src/workspace_app/apps/catalog.py——AppCatalog.resolve、validate_function_coherence、discover_app_slugs、validate_all_apps(三層 resolve + 啟動 gate)。src/workspace_app/apps/manifest.py——AppManifest/FunctionToggles/Layout/Lifecycle/load_app_manifest(app.json的型別形狀)。src/workspace_app/apps/base.py——WorkItemBase三層欄位模型 +IndexedFields。src/workspace_app/apps/registry.py——_load_model_module/_app_models/register_apps/resource_route(發現 + specstar 註冊)。src/workspace_app/apps/resolve.py——find_work_item/resolve_item_agent_config(id → App+item 接縫)。src/workspace_app/apps/profiles.py——ProfileManifest/load_profile/normalize_workflows。src/workspace_app/apps/schema.py——project_fields/FieldSpec。src/workspace_app/apps/seeding.py——seed_item/case_from_item。src/workspace_app/apps/context_files.py——build_context_block。src/workspace_app/apps/skills.py+shared_skills.py——merged_profile_skills/list_skills/SHARED_SKILLS。src/workspace_app/apps/rca/model.py、apps/topic-hub/model.py、apps/playground/model.py——三個對照範例(含連字號 slug 的 file-path 載入特例)。- post-#54 後,原
api/app.py的這幾個符號各自搬進了專屬 route/deep 模組(app.py現在只是組裝根):api/meta_routes.py:get_app_manifest、api/item_routes.py:create_app_item、api/locator.py:ItemLocator.resolve_agent_config(委派apps/resolve.py:resolve_item_agent_config)、api/chat_send.py:ChatSendService.send(build_context_block/build_workspace_skills_blockprepend 的呼叫點)。 src/workspace_app/tooling/catalog.py——ToolMeta/humanize_tool_label/picker_units/flat_catalog(tool 顯示 metadata 單一真相,#322)。src/workspace_app/api/tools_routes.py——register_tools_routes(GET /toolsflat catalog +GET /a/{slug}/items/{item_id}/toolsper-item picker state,reuse 同一個resolveanti-drift)。src/workspace_app/factories.py——get_app_catalog。