原生 App
原生 App 是由 DSM 以 CGI 執行的 Go 後端,加上在自己的 DSM 視窗中開啟的 UI。它以未簽署的 SPK 安裝,以專屬的套件帳號執行,不需要容器,預設也沒有 root 權限。synopkgland init -t native-go 會建立可運作的專案:登入狀態、管理員可編輯的問候語,以及建置設定。
你需要安裝 synopkgland CLI 的 Node.js,以及 Go 1.25 或更新版本。
synopkgland init -t native-go my-app此命令不會覆寫檔案(INIT_TARGET_NOT_EMPTY)。預設 UI 是 Vue。加上 --ui plain 可使用手寫的 ui/app.js,請見 UI 元件。加上 --with files,jobs,login 可啟用能力,請見能力。
| 路徑 | 內容 | 要編輯嗎? |
|---|---|---|
dsm-package.yaml | 套件識別、版本、建置編號、架構、後端與 UI 位置 | 要 |
backend/internal/app/ | 你的 API:route 與 handler | 要 |
backend/internal/api/permissions.json | 誰可以呼叫各個方法 | 要 |
frontend/src/ | Vue UI 外框。文字放在 frontend/src/i18n/<locale>.json | 要 |
i18n/messages/<locale>.json | 九種語言的套件文字 | 要 |
i18n/glossary.json | DSM 本身的用詞,以及必須使用這些用詞的訊息 | 新增詞彙 |
AGENTS.md、CLAUDE.md | 給在專案中工作的 AI 助理的指示 | 要 |
backend/internal/api/permissions.go、backend/cmd/ | 載入權限,並組裝 CGI 與 jobs worker | 很少 |
dist/ | 建置出的 SPK 檔案 | 不要 |
請求防護、DSM 登入檢查、File Station、jobs 與 delegate 來自 SDK,不是專案檔案:Go module github.com/synopackageland/syno-pkg-land-sdk 與 npm 套件 @synopackageland/sdk。backend/go.mod 中的 require github.com/synopackageland/syno-pkg-land-sdk 是專案的 SDK 版本。frontend/package.json 必須把 @synopackageland/sdk 固定為同一版本,否則 check 會以 SDK_VERSION_MISMATCH 失敗。CLI 只建置與自己相同 minor 版本的 SDK(SDK_UNSUPPORTED)。
使用 --ui plain 時,專案是 ui/app.js 與 ui/app.css,沒有 frontend/。其餘內容由建置產生:INFO、權限(run-as: package)、生命週期腳本、圖示、視窗載入器與 CGI。
若要封裝已建置好的可執行檔而非 Go 原始碼,請使用 -t binary。binary 後端不能使用 files、jobs、login 或 delegate。若已有自己的 payload 與生命週期的產品要保留,請見既有原生產品。
新增後端 route
Section titled “新增後端 route”route 有兩部分:在 backend/internal/app/routes.go 註冊的 Go handler,以及 backend/internal/api/permissions.json 中的一筆設定。
func Routes(cfg Config) []api.Route { routes := cfg.delegateRoutes() routes = append(routes, []api.Route{ api.Handle("app.status", cfg.status), api.Handle("greeting.get", cfg.getGreeting), api.Handle("greeting.set", cfg.setGreeting), api.Handle("notes.list", cfg.listNotes), }...) // ...the rest of the function stays as generated}
func (cfg Config) listNotes(_ context.Context, caller api.Caller, params json.RawMessage) (any, error) { var in struct { Limit int `json:"limit"` } if err := decodeParams(params, &in); err != nil { return nil, err } if in.Limit < 1 || in.Limit > 100 { return nil, api.BadRequest("limit must be 1 to 100") } return map[string]any{"owner": caller.User(), "notes": []string{}}, nil}{ "schema": 1, "default": "administrators", "methods": { "notes.list": "authenticated" }}接著在 backend/internal/app/routes_test.go 的 sampleParams 中,加入此 route 的有效參數,例如鍵 notes.list 搭配值 {"limit":20}。在你加入之前,產生的測試會失敗。以 (cd backend && go test ./...) 執行測試。
route 規則:
- handler 是
func(ctx, caller, params) (any, error)。decodeParams會拒絕未知欄位。請回傳api.BadRequest(message)或&api.Error{Code, Message}。其他錯誤到達 UI 時一律是internal。 - handler 需要 HTTP 請求時用
api.HandleRequest,要設定 cookie 或寫入回應時用api.HandleResponse,要做傳輸時用api.HandleStream。 caller.User()是已登入的 DSM 使用者帳號,caller.Admin()對管理員群組成員為 true。兩者都來自 DSM,絕不取自請求。- 在一般使用者共用的 route 內,
api.RequireAdmin(caller)可將單一動作保留給管理員。它會回傳admin_required。 - 方法名稱是以句點連接的小寫單字,例如
notes.list。列在permissions.json的名稱必須已註冊。 - 修改
permissions.json需要重新建置。請求無法變更它。
permissions.json 的等級 | 沒有工作階段 | 一般使用者 | 管理員 |
|---|---|---|---|
administrators(未列出的方法也是此結果) | session_required | admin_required | 執行 |
authenticated | session_required | 執行 | 執行 |
public | 執行 | 執行 | 執行 |
default 必須保持為 administrators。只有在必須不登入也能運作的 route 才使用 public。authenticated 或 public 的 route 也會讓一般使用者能開啟 App 視窗,請見下方的 entry。
誰能開啟 App
Section titled “誰能開啟 App”dsm-package.yaml 中可選的 entry 欄位,會在每個 route 之前加上一道檢查。
entry | 誰能進入 | 拒絕時的錯誤碼 |
|---|---|---|
| 省略 | 視窗依你的 route 而定:只要至少有一個 route(包含 files 或 jobs 加入的)不是 administrators,所有使用者都看得到 | 依各 route |
admin-only | 僅管理員 | admin_required |
app-privilege | 管理員,以及在控制台 > 應用程式權限中獲准的帳號 | app_privilege_denied |
app-privilege 至少需要一個 authenticated route。public route 會略過此檢查。若 DSM 無法回答權限問題,呼叫會以 auth_unavailable 失敗。
從 UI 呼叫
Section titled “從 UI 呼叫”UI 會收到 context 物件 ctx。ctx.call(method, params) 以已登入的 DSM 工作階段向你的後端送出請求,並回傳結果。錯誤會以 { code, message } 拒絕。
try { const { notes } = await ctx.call<{ notes: string[] }>('notes.list', { limit: 20 }) console.log(notes)} catch (error) { const { code } = error as { code?: string } // show a message for `code`, for example admin_required or bad_request}要顯示僅限管理員的控制項,請用 ctx.user?.isAdmin。這只會隱藏控制項,真正決定誰能呼叫的是後端。其他 context 成員:ctx.reopen(fn) 會在 DSM 每次重新顯示視窗時執行 fn,並回傳取消訂閱的函式;ctx.locale 是視窗語言。
所有回應都使用 HTTP 200。失敗時為 {"success": false, "error": {"code", "message"}},與 DSM 自己的 Web API 相同。UI 會看到的錯誤碼列於錯誤碼。
翻譯 App
Section titled “翻譯 App”App 提供九種語言:en、zh-TW、zh-CN、ja、ko、de、fr、es 與 it。DSM 的其他語言顯示英文。視窗會依 DSM 的語言設定切換。
i18n/messages/<locale>.json存放扁平的"dotted.key": "text"配對。en.json為必要。每個語言檔的鍵必須與en.json完全相同,{placeholder}名稱也要相同。- 新增訊息時,把鍵加到
en.json與其他八個檔案。 - 後端會回傳的每個錯誤碼,都要加入對應的
error.<code>訊息,UI 才能以使用者的語言顯示文字。 i18n/glossary.json以九種語言列出 DSM 本身的用詞(「儲存」、「取消」),以及哪些訊息必須包含它們。要使用 DSM 用詞時,複製其文字,並把你的鍵加到該詞的keys。dsm-package.yaml中的i18n.info設定套件中心依語言顯示的顯示名稱與說明。英文來自最上層欄位。- 在 plain UI 中,
ctx.t("greeting.setBy", { text, user })會讀取這些檔案。Vue UI 將畫面上的文字放在frontend/src/i18n/<locale>.json。請把新文字加到那裡的九個檔案。
check 與 build 會在鍵缺少或多出、遺漏 placeholder、語言不支援,或訊息與詞彙表不一致時失敗。
開發、部署、檢查
Section titled “開發、部署、檢查”synopkgland checksynopkgland buildsynopkgland deploy --host my-nassynopkgland status --host my-nassynopkgland logs --host my-nas| 命令 | 作用 |
|---|---|
check | 驗證 manifest、能力組合、permissions.json 與翻譯 |
build | 為每個架構寫出 dist/<package>-<version>-<build>-<arch>.spk。Vue 專案請先在 frontend/ 執行 npm install |
deploy | 建置單一架構並安裝到 NAS。版本與建置編號的組合必須高於已安裝的(DEPLOY_VERSION_INVALID)。無法偵測 CPU 類型時請用 --arch(DEPLOY_ARCH_UNDETERMINED) |
status | 套件狀態、開啟 App 的網址,以及各能力的就緒狀態 |
logs | 套件狀態行與套件識別。它不是 App 日誌 |
deploy、status 與 logs 一定要有 --host(HOST_REQUIRED)。每個要交出去的 SPK 都要遞增 build。安裝、升級與發佈請見發佈。check、build、deploy 與 status 加上 --json 會輸出單一 JSON 物件。
| 項目 | 限制 |
|---|---|
| 套件名稱 | 2 到 64 個英文字母與數字,以字母開頭 |
build | 1 到 9999 的整數 |
| JSON 請求本文 | 1 MiB |
| 單一請求的時間 | 20 秒 |
| 架構 | x86_64、aarch64、armv7(armv7 的 jobs、delegate、root 操作與 daemon 尚未在 NAS 上驗證) |
| 最低 DSM 版本 | os_min_ver 預設為 7.1-42661 |
| 語言 | 九種,見上方 |
| 症狀或錯誤碼 | 原因 | 解法 |
|---|---|---|
一般使用者收到 admin_required | route 是 administrators,或未列在 permissions.json | 請使用管理員帳號開啟。若該方法應開放一般使用者,將它設為 "authenticated" 並重新建置。 |
unknown_method | 方法未在 routes.go 註冊 | 以 api.Handle 註冊 |
bad_request | decodeParams 拒絕了未知欄位,或 handler 拒絕了某個值 | 只傳 handler 會讀取的欄位 |
session_required | 沒有有效的 DSM 工作階段 | 重新登入 DSM |
app_privilege_denied | entry: app-privilege,而該帳號沒有此 App 的應用程式權限 | 請管理員到「控制台 > 應用程式權限」,選擇此 App 並按「編輯」,允許你的帳號或群組使用。 |
INIT_TARGET_NOT_EMPTY | init 發現已有檔案 | 使用空的目錄 |
| 新增 route 後 Go 測試失敗 | route 未加入 sampleParams | 加入有效參數 |
admin-only App 僅供 DSM administrators 群組的成員使用,請使用管理員帳號開啟;它不會列在應用程式權限中。遇到 app_privilege_denied 時,請依 App 的顯示名稱在應用程式權限清單中選擇。SDK 的拒絕畫面會區分這兩種錯誤碼,其他權限錯誤仍保留各自的說明。