# @synopackageland/app 中文文档

规范地址：https://npmcn.com/package/%40synopackageland/app/

版本：0\.47\.0
npm 元数据更新时间：2026-11-02T14:20:41.622Z
中文译文更新时间：2026-11-02T15:13:45.840Z
软件包许可：MIT

本文由上游 README 机器翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40synopackageland/app
原文对照：https://npmcn.com/package/%40synopackageland/app/?lang=original
项目主页：https://github.com/synopackageland/syno-pkg-land-sdk/tree/main/node#readme
源码仓库：https://github.com/synopackageland/syno-pkg-land-sdk

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：34.6 kB，19 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=22
- 运行依赖：0 个
- 首次发布：2026-10-13T09:47:17.668Z
- 最新版本发布：2026-11-02T14:20:41.458Z
- 已发布版本数：90（近 12 个月 90 个）

## 中文简介

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

## 安装

```sh
npm install @synopackageland/app
```

也可以使用：`pnpm add @synopackageland/app`、`yarn add @synopackageland/app`、`bun add @synopackageland/app`

## README

# @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`](https://npmcn.com/package/%40synopackageland/spec/bridge/v1/README.md))：在stdin和 标准输出，每个调用一个进程。应用程序永远不会以root身份运行，也永远不会看到 DSM会话;它向运行时请求的会话需要什么 `ctx.runtime`.

## 一个应用程序

```ts
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`](https://npmcn.com/package/%40synopackageland/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`](https://npmcn.com/package/%40synopackageland/spec/bridge/v1/README.md#services). 它们代表经过验证的调用者，在请求的DSM会话中 需要一个，然后以电话结束。

```ts
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`](https://npmcn.com/package/%40synopackageland/spec/bridge/v1/README.md#transfers)）。

### 截止日期和取消

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

```ts
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` 接受任何流，所以测试可以播放运行时：

```ts
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捆绑在一起 放到一个文件中。

## 开发

```sh
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` 和批准的遗嘱执行人档案，现有的 运行时服务接口支持配置文件授予和调用：

```ts
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 是什么？

用于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 和提交记录判断。

