@synopackageland/app

中文文档
0.47.0发布于 2026/11/02MITTypeScriptES Module零依赖

用于Synology Package Land Apps的Node SDK: synopkgland Go运行时(channel spec/bridge/v1)背后的后端方法

中文使用文档
文档目录16 节
  1. 一个应用程序
  2. API
  3. new App(options?)
  4. app.method(name, handler)
  5. app.run()
  6. app.serve(input, output)
  7. Context
  8. 错误
  9. 服务
  10. 转移
  11. 截止日期和取消
  12. 数字
  13. 测试应用
  14. 在一个包裹里
  15. 开发
  16. DSM文件会话

@synopackageland /应用程序

用于Synology Package Land应用程序后端的Node SDK TypeScript或JavaScript。节点22或更高版本(DSM软件包中心的) Node.js_v22),仅ESM,没有运行时 依赖关系。

包的CGI是synopkgland Go运行时(bridge/cmd/runtime). 它 检查请求的来源、DSM会话、入口门和方法 政策 permissions.json 和截止日期,然后才启动应用程序, 作为包帐户,并通过桥接通道将调用交给它 (spec/bridge/v1):在stdin和 标准输出,每个调用一个进程。应用程序永远不会以root身份运行,也永远不会看到 DSM会话;它向运行时请求的会话需要什么 ctx.runtime.

一个应用程序

import fs from 'node:fs'
import path from 'node:path'
import { App, AppError } from '@synopackageland/app'

const app = new App()

app.method('hello.greet', (ctx) => ({ message: `Hello, ${ctx.user}` }))

app.method('hello.save', (ctx, params: { note?: unknown } | null) => {
  if (typeof params?.note !== 'string') throw new AppError('bad_request', 'note must be a string')
  const notes = path.join(process.env.SYNOPKGLAND_DATA_DIR!, 'notes')
  fs.mkdirSync(notes, { recursive: true, mode: 0o700 })
  fs.writeFileSync(path.join(notes, `${ctx.requestId}.txt`), params.note, { flag: 'wx' })
  return { bytes: Buffer.byteLength(params.note) }
})

await app.run()

浏览器调用它 @synopackageland/sdk 和其他方法一样: {"method":"hello.greet"} 答案 {"success":true,"data":{"message":"Hello, alice"}}.

这些方法必须是包中的方法 bridge.json (methods); 运行时从那里注册它的路由,然后一个App用 其他方法被拒绝。他们的政策是 permissions.json,至于Go 应用程序。

API

new App(options?)

  • options.log(error):发生意外错误的地方。默认是这样写的 到stderr, DSM将其保存在包的日志中。浏览器只能看到 internal.

app.method(name, handler)

注册一个方法并返回App。 name 是 <group>.<name> 在较低的 例(^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$); 注册两次的名称 抛出。

handler(ctx, params) 返回数据,或者它的一个承诺:任何JSON值, undefined 是 null. params 请求是 params 当浏览器发送 它。的类型参数 method<P> 只注释它;检查值 在你使用之前。

app.run()

回答运行时在stdin上发送的一个调用,然后退出。它拒绝 以root身份启动(退出状态1) SYNOPKGLAND_BRIDGE 不是 stdio (退出状态2),因此它只在运行时下运行。当它服务的时候,什么 应用程序写入 process.stdout, console.log 包括,到stderr: Stdout是通道。开始的子进程 stdio: 'inherit' 仍然会分享它;给它 'ignore' 或管道。

node <entry> --describe 而是打印一行JSON, {"sdk":"node/…","versions":[1],"methods":[…]},因此包构建可以 对照检查方法 bridge.json 和 permissions.json. app.describe() 返回相同的对象。

app.serve(input, output)

在任意一对流上响应一个调用,并在结果为时进行解析 写的。 run 在标准输入和标准输出上使用它;测试将其用于 PassThrough 对,播放运行时的一边的通道。

Context

成员
user打来电话的DSM用户;对于公共方法为空
admin用户是否是DSM管理员
locale第一个 Accept-Language 标签,或者为空
requestId请求的ID, 32个十六进制数字,也在运行时日志中
deadline当运行时结束请求时,在 Date.now() 毫秒
deadlineMs运行时发送调用时剩余的时间,以毫秒为单位
signal一个 AbortSignal 请求结束时中止; signal.reason 是 'deadline' 或者 'canceled'
cancelled是否 signal 是流产
runtime(op, params?)代表调用者向运行时请求服务;解析到它的结果

所有这些都来自运行时经过验证的调用方。一个 Context 属于 一个电话。

错误

throw new AppError(code, message) 浏览器回答 {"success":false,"error":{"code":…,"message":…}}. code 要么是

  • 应用程序自己的代码, ^[a-z][a-z0-9_]{0,63}$,除了运行时 预约(session_required, admin_required, app_privilege_denied, auth_unavailable, unknown_method, cross_origin, origin_required, request_timeout, request_canceled, request_too_large, json_required, method_not_allowed, app_unavailable),或
  • 的大写拒绝 spec/protocol/v1/errors.json,例如 SCOPE_DENIED,这 @synopackageland/sdk 翻译。

任何其他代码抛出 TypeError,它和其他错误一样,回答 浏览器 internal 并写下为什么会出错。 message 被削减到1024字节 utf - 8。 AppError.badRequest(message) 是 new AppError('bad_request', message).

处理程序抛出的任何其他数据JSON都无法编码(a) BigInt,一个 周期),答案 internal; 错误会变成 log,永远不要切换到浏览器。

服务

ctx.runtime(op, params) 向运行时发送服务请求并解析为 结果,或者 null 当它一无所有时。当运行时拒绝时,它会拒绝 与 ServiceError (op, code, message): unknown_service 对于op来说 运行时不提供,否则服务的代码。一个 ServiceError 一个 当处理程序的代码是协议v1时,它不会捕获到达浏览器 拒绝,就是拒绝。 internal 否则。一个 op 这不是服务名称,或者 params JSON不能编码,拒绝使用 TypeError 在任何事情发生之前 发送。

运行时提供功能的服务 bridge.json (files.*, portal.status, delegate.*, bg.*, root.*),列于 它们的参数是 spec/bridge/v1. 它们代表经过验证的调用者,在请求的DSM会话中 需要一个,然后以电话结束。

app.method('notes.list', (ctx) => ctx.runtime('files.list', { path: '/home/notes' }))

转移

一个方法 transfers 名字 bridge.json 是HTTP上传或下载。 它的 params 是 {query, upload: {path, size}} 对于上传,使用假脱机文件 阅读,和 {query, download: {path}} 对于一个下载,一个假脱机文件 先写后回答 {name, content_type?} (spec/bridge/v1)。

截止日期和取消

运行时拥有截止日期。当它通过或浏览器退出时, 运行时发送 cancel 和 ctx.signal 中止;一秒钟后,它杀死了 App的进程组。通过 ctx.signal 来 fetch, fs 还有计时器 他们的请求是:

app.method('feed.fetch', async (ctx) => {
  const reply = await fetch('https://example.com/feed.json', { signal: ctx.signal })
  return reply.json()
})

一次 signal 中止,等待 ctx.runtime 电话,还有后来的电话 询问,拒绝 ServiceError 代码 request_canceled. 如果通道 早结束, signal 中止与 'canceled'.

数字

应用程序获取值,而不是JSON字面量: 2.50 到达, 2.5,以及整数 大于2^53会失去精度。携带64位的id和大小作为字符串。

测试应用

serve 接受任何流,所以测试可以播放运行时:

import { PassThrough } from 'node:stream'
import { createInterface } from 'node:readline'

const toApp = new PassThrough(), fromApp = new PassThrough()
const lines = createInterface({ input: fromApp })[Symbol.asyncIterator]()
const next = async () => JSON.parse((await lines.next()).value)
app.serve(toApp, fromApp)
toApp.write(JSON.stringify({ t: 'hello', versions: [1] }) + '\n')
await next() // {t: 'hello', version: 1, sdk: 'node/…', methods: [...]}
toApp.write(JSON.stringify({ t: 'call', id: '1', method: 'hello.greet', params: null,
  ctx: { user: 'alice', admin: false, locale: 'en', request_id: '0'.repeat(32), deadline_ms: 60000 } }) + '\n')
await next() // {t: 'result', id: '1', ok: true, data: {message: 'Hello, alice'}}

在一个包裹里

SPK的“App”目录为 /var/packages/<package>/target/app。这个 运行时开始 <entry> 有 /var/packages/<node.package>/target/usr/local/bin/node,所以SPK取决于 包裹中心的包裹(install_dep_packages). SDK没有 依赖项:包构建副本 package.json 和 dist/ 来 app/node_modules/@synopackageland/app/,或将应用程序和SDK捆绑在一起 放到一个文件中。

开发

npm ci
npm run generate   # src/frames.ts and src/refusals.ts from spec/
npm test           # type-checks src and test, then node:test
npm run build      # dist/: ESM and .d.ts

src/frames.ts (json-schema-to-typescript,从 spec/bridge/v1/frames.schema.json), src/refusals.ts (从 spec/protocol/v1/errors.json)生成并提交;测试失败 当他们不同于 spec/. scripts/gate.sh 还运行测试 bridge的golden runner和Docker中Node 22上的hello示例 (scripts/node22-test.sh)。

具有桥接能力 webapi:true 和批准的遗嘱执行人档案,现有的 运行时服务接口支持配置文件授予和调用:

const grant = await ctx.runtime('webapi.grant', {scope: 'calendar', lifetime: 'until-revoked'})
const result = await ctx.runtime('webapi.call', {delegation_id: (grant as {delegation_id:string}).delegation_id,
  api: 'SYNO.Cal.Event', method: 'list', version: 6, params: {}})

webapi.grants 列出呼叫者的授权; webapi.revoke 撤销。回复 包含完整的DSM信封。没有突变重试;调和未知的结果。 参见spec/bridge/v1和executor/README。Md为固定的权限和限制。

DSM文件会话

默认的Go运行时不再将浏览器DSM会话转发到File 站在HTTP上。其遗留 files.list, files.info 和 files.read 服务返回 unsupported; 包装器永远不会收到DSM证书。 使用前端SDK的同源文件客户端进行交互操作。 后端写入或授予需要独立的调用方绑定授权; 只有浏览器的应用程序不需要执行器。请参阅存储库的 docs/dsm-session-transport.md 查看App迁移检查表。

版本与发布记录

@synopackageland/app 共发布 90 个版本,首次发布于 2026/10/13;过去 12 个月发布了 90 个版本。

版本发布日期距今
0.47.0latest2026/11/02今天
0.46.22026/11/021 天前
0.46.12026/11/021 天前
0.44.1patch-0.442026/11/021 天前
0.46.02026/11/021 天前
0.44.02026/11/021 天前

关于 @synopackageland/app 的常见问题

@synopackageland/app 是什么?

用于Synology Package Land Apps的Node SDK: synopkgland Go运行时(channel spec/bridge/v1)背后的后端方法

如何安装 @synopackageland/app?

在项目目录运行 npm install @synopackageland/app,或使用 pnpm add / yarn add / bun add。

@synopackageland/app 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@synopackageland/app 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@synopackageland/app 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@synopackageland/app 可以商用吗?

@synopackageland/app 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@synopackageland/app 还在维护吗?

最新版本 0.47.0 发布于 2026/11/02(今天);至今共发布 90 个版本,其中过去 12 个月发布了 90 个;首次发布于 2026/10/13。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。