裝一個 App 我的 Arcrun 就多一個功能——現在每加一個能力都要改 Portal 程式碼 #82
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
我希望有一個功能是:裝一個 App,我的 Arcrun 就多一個功能出來——就像 WordPress 裝外掛那樣。
這不是一個功能,是一個機制
現在的狀況是:每加一個能力,就要有人去改 Portal 的程式碼。
⇒ 能力與界面焊死 ⇒ 沒有人能在不動核心的情況下加東西
⇒ 而 Mira 身上那些能力(筆記、專案管理、萃取…)每一個都要被搬出來(
Leo/mira#2),如果沒有這個機制,就是把它們一個一個焊進 Portal——焊三次之後 Portal 就沒人敢動了。
一個 App 要能帶什麼(leo 列的三件)
🔴 要先回答的設計題(leo 直接點名)
「UI 怎麼被加入 Portal?」
這題答不出來,其餘都是空談。要回答的至少有:
第一個實例:手機寫筆記
Leo/mira#2的萃取清單第三項「河道寫筆記」)Leo/InkStoneCo#2—— leo:「這個 inkstoneco 的是那個的前置任務,可以直接提供 mira 版筆記」⇒ 短期可以先給 mira 版,讓 leo 手機上先能用
⇒ 但終局是它要變成一個裝得起來的 App,不是再焊一塊進 Portal
怎麼驗
相關
Leo/mira#2—— Mira 現代化與能力萃取(本票是那些能力的落腳形態)Leo/InkStoneCo#2—— 手機寫筆記(第一個實例的短期前置)Leo/Arcrun#81—— AI 開場的全景圖(同一個方向:能力要能被發現,不論發現者是人還是 AI)Leo/arcrun-rag#40的教訓:別為了整合而在兩處各維護一份。App 的宣告只能有一個地方。設計提案:Arcrun App ↔ Portal 掛載協定 v0
一、先講結論
mount/slot/menu/tab在安裝器全樹零命中)。二、現況實查(給下一個人省時間用)
2.1 Portal 前端
matrix/arcrun/console-ui/public/portal/index.html(2057 行,HTML/CSS/JS 全內嵌單檔):253-259:510-516(同一組項目寫第二遍):265/:300/:312/:322/:337/:346/:459VIEWS:718、HOME:719、LOADERS:747allowedViews():721-729、showApp():790-799Leo/arcrun-rag#40說的那個形狀,Portal 裡面已經有了。2.2 能力旗標從哪來
GET /portal/session=matrix/arcrun/cypher-executor/src/routes/portal.ts:495-515,回role/libraries/graph_allowed/workflows_visible/upload_enabled:371-385(workflowsVisible()讀PORTAL_SHOW_WORKFLOWS;uploadEnabled()讀三個 env 齊不齊):492-494已經立好規矩,本協定完全沿用:前端旗標只是顯示提示,真閘在路由層2.3 Portal 怎麼送到用戶手上(決定了「App 前端能不能不改 Portal 就出現」)
products/arcrun-rag/installer/scripts/build-ui-bundle.mjs:154const FILES = ${JSON.stringify(files)}—— 把整個console-ui/public/目錄內嵌成單檔 workerarcrun-rag-ui:216找不到路徑就 SPA fallback 回/portal/index.html:172/__version回ui_fingerprint(指紋由內容算,安裝器據此判斷要不要重推)2.4 安裝器現況(現役那一顆,實查)
products/arcrun-rag/installer/oauth-prototype/worker.js(3267 行,worker 名arcrun-installer,install.arcrun.dev就是它)——不是
installer/src/index.js(舊工地主任,已非出貨路徑):154-162;主流程runInstall():1324;裝完導向…/portal/(:1709、:1798)/init/seed)installer/scripts/bundle-components.mjs:46-79的BUNDLE_COMPONENTS(「一個 bundle 該有哪幾顆」的唯一真相源,是 JS 常數不是資料檔)manifest.json(不在本 repo 版控裡;release/fingerprint/sha256由release.mjs依內容算出來)installer/scripts/compile-workflows.mjs:讀取清單硬編在:30-37(4 支),並在:63-127產生/保全預編圖(flow 變了就必須重編,沒有圖一律 exit 1)worker.js:1212-1253pushWorkflowTo():直接用預編圖,全程 0 次/cypher/search(理由:1218-1223:冷實例編圖 25.7s > 15s timeout),再POST {cypher}/webhooks/namedacr push;只有本機版install/install.sh:180-203才用acr pushmount/slot/menu/tab全樹零命中;nav/card的命中全是安裝器自己那幾頁的 HTMLschemas/裡沒有「可安裝單位」的 schema(只有 collector-trigger 與尚未實作的 kbdb-ingest 兩份)。現有「可安裝單位」的粒度是 worker,沒有任何欄位描述「這個單位會在 UI 露出什麼」。2.5 後端也已經焊過一次(反面教材)
cypher-executor/src/routes/portal-data.ts:487-528的POST /portal/data/upload:507)——安全形狀是對的,但它只服務一個能力2.6 工作流的觸發權在誰手上
cypher-executor/src/routes/webhooks-named.ts:260-272:/webhooks/named/:name/trigger需要X-Arcrun-API-Key(實例擁有者金鑰)portal/index.html:340「不能觸發執行(觸發屬系統擁有者權限)」;portal-data.ts:533-585回read_only: true且不回 webhook_url2.7 KBDB 這一層現成的路
cypher-executor/src/lib/portal-seeds.ts:21(PORTAL_TEMPLATE_SEEDS)portal.ts:99ensurePortalTemplates(),由/init/seed(routes/init-seed.ts:95)與/portal/admin/bootstrap(portal.ts:549)呼叫routes/kbdb-proxy.ts:49(POST /kbdb/templates)、:85(POST /kbdb/records)portal_library:portal-seeds.ts:36;刪庫portal.ts:12642.8 今天沒有任何 App/plugin 概念
matrix/arcrun/cli/src/commands/十八支指令裡沒有 app/pluginmatrix/arcrun/system-dev/wiki/cards/decisions/Recipe-UUID市場模型.md),那是零件層的市場,不含前端與掛載三、協定 v0
3.1 一個 Arcrun App = 四件東西
3.2 宣告只有一份(
#40那條教訓的落點)arcrun-app.yaml:arcrun_app(新 seed 一列,不加表),slots:app_id/name/version/icon/ui_url/mount_json/actions_json/source_hash/status/installed_atarcrun-app.yaml= 作者原稿;KBDB record = 安裝器產生的安裝態。人只維護前者。source_hash(yaml 內容 sha256)綁住:yaml 變了 ⇒ hash 不同 ⇒ 判 stale ⇒ 重裝。build-ui-bundle.mjs:150的uiFingerprint+:172的/__version,以及release.mjs的「版本由內容算出來,不靠任何人記得改」。bundle-components.mjs不得加 App;安裝器不得另存一份 app 列表。Portal 唯一的來源是/portal/session回的apps(§3.5)。3.3 掛載點(slot)——只列真的存在的
nav(v0 唯一開放)#sidenav(portal/index.html:251-260)+ 手機底部#tabbar(:510-517)#/app/<id>,容器<div class="view page" id="v-app-<id>">(Portal 動態建立)settings_panel(之後)——落點=設定頁的.panel卡列(:365-455)search_mode(之後)——落點=搜尋頁的.modebtn(:273-276)home_card(不存在,別規劃)——Portal 目前沒有首頁,HOME就是搜尋頁(:719)。要開這個槽得先有首頁。nav就足以承載手機寫筆記,而且它是列表型、最不會撞。3.4 兩個 App 想掛同一個位置怎麼辦
nav是有序列表不是單一位置 ⇒ 兩個 App 都掛 nav 沒有衝突order升冪,同 order 用app_id字典序(穩定、可重現)id(record 主鍵+路由前綴)id不同來源 ⇒ 拒裝,要求改名id同來源(source_hash不同)⇒ 視為更新,覆寫該筆 record3.5 UI 怎麼被加入 Portal(leo 點名那題的正面回答)
Portal 改一次,此後 0 行。 那一次要改什麼,具體到行:
/portal/session多回一格apps(cypher-executor/src/routes/portal.ts:495-515的 response 物件加一個 key)arcrun_app裡status='enabled'的 records,依visible_to× 登入者 role 過濾[{ app_id, name, icon, version, ui_url, mount:[{slot,label,order}] }]portal/index.html五個小改VIEWS(:718)→BUILTIN_VIEWS.concat(apps.map(a => 'app-' + a.app_id))allowedViews()(:721-729)→ 多一條泛用分支:app-*一律可見(能不能看已在 server 過濾)showApp()(:790-799)→ 依apps動態生成側欄div.nav、tabdiv.tab、空的<div class="view page" id="v-app-<id>">LOADERS(:747)→ 多一條泛用分支:app-<id>→mountApp(app)mountApp(app):import(app.ui_url).then(m => m.mount(container, host));切走時m.unmount?.(container)App 前端要實作的就這一個介面(ES module):
host是 Portal 交給 App 的唯一通道;App 拿不到 session token 本身:/portal/data/*(server 側 owner_id + library enforce 已經在portal-data.ts做好了)。這正面回答 leo 那題:走既有的 KBDB/cypher API,不另開一條。
前端放哪(實體):
installer/oauth-prototype/worker.js:86的BUNDLE_BASE=cdn.jsdelivr.net/gh/<org>/arcrun-rag-bundles@<sha>)import()不支援 SRI,所以靠不可變 URL)Access-Control-Allow-Origin: *,import()直接可用/__version(照抄build-ui-bundle.mjs:172)ui.url換成那顆 worker 的位址而已為什麼是 ES module 不是 iframe(我先裁了,但這是單向門,見 §七):
host合約刻意設計成傳輸無關 ⇒ 哪天要換成 iframe + postMessage,App 那一側一行都不用改3.6 寫入怎麼做:一支泛用端點,不是每個 App 一支
requirePortalUser()(與/portal/data/*同一套)arcrun_apprecord → App 存在且status='enabled'?否則 404action在該 record 的actions_json白名單裡?否則 403/webhooks/named/:name/trigger(webhooks-named.ts:260)display_name/role)與owner_id,回工作流結果X-Arcrun-API-Key(webhooks-named.ts:261),是實例擁有者的金鑰,portal 用戶絕不能拿到——同portal.ts:475「絕不回租戶字串」那條portal-data.ts:507),差別是這支是泛用的,不會為第 N 個能力再長第 N 支端點/portal/data/upload之後可收編成apps/uploader/actions/upload——表示這個抽象沒有為筆記量身訂做3.7 資料層(守鐵律)
portal.ts:99)→POST /kbdb/templates(kbdb-proxy.ts:49)portal-seeds.ts,而是安裝時從 App 宣告帶進來portal_library(portal-seeds.ts:36),照既有庫權限走,不另立權限模型3.8 安裝與移除
arcrun_apprecord 與 ensure template 都需要 tenant 權限;而且「裝什麼」是實例層的決定acr app build(作者端,出貨前跑一次)arcrun-app.yaml,把workflows[]編成含預編圖的 JSON——直接沿用installer/scripts/compile-workflows.mjs:63-127的保全閘邏輯(flow 變了必須重編,沒圖就 exit 1)dist/app.js(前端)+dist/app.json(宣告+預編工作流),推上 App 自己的 bundle repo,拿到 commit shaworker.js:1218-1223的實測——冷實例即時編圖 25.7s > 15s timeout。裝 App 會踩同一顆地雷。acr app install <url|path>(實例端)app.json、算source_hashPOST {cypher}/webhooks/named帶預編圖(形狀同worker.js:1212-1253,0 次/cypher/search)arcrun_apprecord(status=enabled)acr app remove <id>status='removed'(或刪 record)⇒ 下一次/portal/session就不再回它 ⇒ Portal 上乾淨消失,沒有殘骸/webhooks/named刪除路徑)--purge,走既有DELETE /portal/admin/libraries/by-name/:name(portal.ts:1264)bundle-components.mjs。那份清單是「基礎 bundle 該有哪幾顆 worker」的真相源(installer/scripts/bundle-components.mjs:46-79),ship.mjs:625會機械核對它與 manifest 名字集合恰好相等。把 App 加進去 = 每加一個能力就要重出核心版本 = 病原封不動搬家。四、拿「手機寫筆記」走一遍(檢驗抽象夠不夠用)
Leo/mira#2工作項目第三項「手機寫筆記:河道寫筆記」;細節在Leo/InkStoneCo#2,本機留底system-dev/docs/issues-log/Leo-InkStoneCo-2.md)matrix/arcrun/landing/app/mira/feed/page.tsx(2542 行 Next.js client component)<textarea>(:560-566),送出submit()(:567-610)POST https://kbdb-create-block.arcrun.dev(:576-590),body{type:"note", user_id:"inkstone_mira_post", source:"km-writer-direct", …}GET https://kbdb.finally.click/blocks/documents?limit=50(:109/:135)wiki_synthesis(:430)與project_detector(:454)polaris/mira/arcrun/mira_feed_watcher.yaml:29-31(每 5 分鐘 cron)撈source=km-writer-direct(:34-40)system-dev/docs/3-specs/mira-dissolve/requirements.md:100-102;polaris/mira/CLAUDE.md:3角色已蒸發)。四個硬編端點全是舊世代 URL。arcrun-app.yaml= §3.2 那份範例,一字不改app.js;mount(el, host)裡畫 composer + 河道時間軸 + 小日曆host.action('save', { title, body })→POST /portal/apps/notes/actions/save→ server 代打notes_saveworkflow → 走既有 KBDB API 寫一筆noterecord 並掛 libraryhost.search()/host.entry()(既有/portal/data/*,server 側已 enforce owner_id + library)notes_saveworkflow 末端接既有萃取鏈(不需要 cron 掃描器,寫入當下就觸發)slot: nav,Portal 上多一個「筆記」;手機底部 tab 同步多一個host/portal/data/*與 action 端點,server 側 enforce五、要新增的東西(總清單)
arcrun_app,加進cypher-executor/src/lib/portal-seeds.ts:21的PORTAL_TEMPLATE_SEEDS(零新表)/portal/session多回apps——routes/portal.ts:495-515,唯讀彙整POST /portal/apps/:id/actions/:nameconsole-ui/public/portal/index.html,約 60 行,一次性acr app build|install|remove|list——cli/src/commands/app.ts;build直接複用compile-workflows.mjs的預編圖保全閘arcrun-app.yaml的 JSON Schema —— 目前 repo 裡沒有任何描述「可安裝單位」的 schema,這會是第一份(放matrix/arcrun/schemas/arcrun-app.v1.schema.json,形狀比照products/arcrun-rag/schemas/collector-trigger.v1.schema.json的凍結版本慣例)六、對照票上三條判準的自檢
acr app install寫一筆 record ⇒ 下一次/portal/session就回它 ⇒ Portal 動態長出 nav + 頁面 ⇒ 進頁時才import()它的前端。arcrun-rag-ui出貨流程。apps/uploader(slot: nav+ 一個uploadaction),不需要對協定做任何加工。Leo/mira#2另兩項也落在同一形狀:專案管理 =slot: nav+ 讀/portal/data/search+ 幾個 action;RAG 萃取鏈是純後端 workflow,本來就不需要掛 UI。arcrun-app.yaml),安裝態由它算出來、用source_hash綁住,沒有人手動維護第二份。bundle-components.mjs(那是第二份清單的入口)。七、需要 leo 拍板的(只有兩題,其餘我自己裁了)
host合約刻意設計成傳輸無關 ⇒ 那天改的時候 App 那一側零改動是 ⇒ 照 module 做;不是 ⇒ 我改設計成 iframe(多花一輪,手機體驗會差一點)
nav一個掛載點,夠不夠?settings_panel與search_mode先不開(開了就要處理排序與撞位,而現在沒有需求逼它)八、還缺什麼(誠實標明)
Leo/arcrun-rag#40的 18 處清單我沒拿到原文——本機system-dev/docs/issues-log/沒有它的留底。我照票上給的原則辦(宣告只能有一個地方),沒有逐項核對那 18 處各是什麼。acr app install的認證形式還沒定死——acr現在打的是 cypher(X-Arcrun-API-Key),這條夠用;但「要不要同時在 Portal 管理頁做一個裝/移除的畫面」我還沒設計(v0 可以只有 CLI)。pending-changes.md提案、等 leo 明說 confirm 才開新 SDD。arcrun 現行 active SDD =
matrix/arcrun/system-dev/docs/3-specs/workflow-discovery/(frontmatter 實查)。我沒有自行開 SDD,也沒有動任何功能程式碼。
leo 回 §七 ①:答案是「是,但你問錯了方向」(2026-08-09)
⇒ 提案問的是「短期都是我們自己寫嗎 ⇒ 決定 module 還是 iframe」。
leo 沒有照這個框架回答,他把框架換掉了。
這句話改變什麼
App 不是一個新發明的東西,它是既有那疊的最外層:
⇒ 不要為 App 另外發明一套「怎麼被裝進來」的機制。
前面三層已經各自有註冊與分享的做法(
acr parts/acr recipe/acr push/公庫)——App 應該長在同一個形狀上,而不是旁邊多開一條路。
⇒ 真正要回答的問題被 leo 換成:「哪裡可以註冊一個頁面,然後它就直接掛上?」
對 ① 那道單向門的影響
提案原本的邏輯是「短期自己寫 ⇒ 先 module,哪天開放第三方再改 iframe」。
但既然這一層從設計上就是要能分享的,「哪天」不是假設性的未來,是既定方向。
⇒ 接手的人要重新權衡:
host合約刻意設計成傳輸無關(那天改,App 那一側零改動)——這個設計是對的,留著——這次要把「分享是既定方向」放進算式,而不是當成低機率事件
§七 ② 尚未回覆
「v0 只開
nav一個掛載點夠不夠」——leo 還沒回。但注意:若照上面重新權衡的結果改變了掛載模型,這一題的答案可能跟著變,
接手的人先處理 ①,再回頭看 ②。
👤 leo 裁決:掛載點 v0 選側欄,不選 nav(2026-08-10)
leo 問:「以後還可以增加?如果我有 20 個 app 掛載呢?會變成側邊欄 hamburger button 的列表嗎?」
leo 答:「側欄。」
⇒ 本票 §七② 那題(v0 只開
nav一個掛載點夠不夠)結案:夠,但那一個不是nav,是側欄。為什麼這個問題比它看起來重要
真正的問題不是「開幾個掛載點」,是「一個掛載點塞爆了會怎樣」。
(「每加一個能力就要有人去改 Portal 的程式碼⋯⋯焊三次之後 Portal 就沒人敢動了」)
垂直側欄可以一直往下長、可捲動、可分組;水平不行。
而且 leo 要的是「像 WP Plugin」——WordPress 正是這樣解的:
外掛用
add_menu_page/add_submenu_page宣告,長在垂直側欄。連解法都同一套。v0 的形態(兩條一起才成立)
⇒ 20 個 App 是設計內的情況,不是災難,不必回頭改核心
連帶
只有這樣,之後要加
card/page這些掛載點才是純增量,已裝好的 App 一行都不用改decisions-summary.mdD63):設計擴充點時先問「它被塞爆會怎樣」,而不是「先開幾個」。
撐不住的那一刻要改的若是核心,這個擴充點就是假的。
— [總管] 2026-08-10