lakutata 3.1.3 → 3.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/README.zh-CN.md +5 -5
- package/dist/cjs/exports/NwjsAdapter.d.ts +1 -0
- package/dist/cjs/exports/NwjsAdapter.js +20 -0
- package/dist/esm/exports/NwjsAdapter.js +1 -0
- package/dist/types/exports/NwjsAdapter.d.ts +1 -0
- package/doc/en/Application.md +1 -1
- package/doc/en/Cache.md +2 -1
- package/doc/en/Command.md +10 -4
- package/doc/en/ContainerdIntegration.md +1 -1
- package/doc/en/DatabaseDrivers.md +3 -2
- package/doc/en/Entrypoint.md +3 -2
- package/doc/en/Exceptions.md +1 -1
- package/doc/en/FFI.md +1 -1
- package/doc/en/GRPC.md +9 -3
- package/doc/en/GettingStarted.md +15 -2
- package/doc/en/HTTP.md +35 -0
- package/doc/en/I18n.md +1 -1
- package/doc/en/Installers.md +34 -6
- package/doc/en/MCP.md +4 -2
- package/doc/en/Math.md +1 -1
- package/doc/en/NativeAddons.md +9 -2
- package/doc/en/Netif.md +261 -0
- package/doc/en/Overview.md +10 -8
- package/doc/en/Packaging.md +4 -1
- package/doc/en/ProjectStructure.md +8 -3
- package/doc/en/ServiceCalls.md +69 -6
- package/doc/en/UpgradingFrom1.md +2 -2
- package/doc/en/UpgradingFrom2.md +4 -4
- package/doc/en/WebRTC.md +4 -4
- package/doc/zh/Application.md +1 -1
- package/doc/zh/Cache.md +1 -1
- package/doc/zh/Command.md +7 -3
- package/doc/zh/ContainerdIntegration.md +1 -1
- package/doc/zh/DatabaseDrivers.md +1 -1
- package/doc/zh/Entrypoint.md +2 -1
- package/doc/zh/Exceptions.md +1 -1
- package/doc/zh/FFI.md +1 -1
- package/doc/zh/GRPC.md +6 -3
- package/doc/zh/GettingStarted.md +11 -2
- package/doc/zh/HTTP.md +30 -0
- package/doc/zh/I18n.md +1 -1
- package/doc/zh/Installers.md +16 -5
- package/doc/zh/MCP.md +3 -2
- package/doc/zh/Math.md +1 -1
- package/doc/zh/NativeAddons.md +3 -2
- package/doc/zh/Netif.md +207 -0
- package/doc/zh/Overview.md +5 -4
- package/doc/zh/Packaging.md +4 -1
- package/doc/zh/ProjectStructure.md +4 -2
- package/doc/zh/ServiceCalls.md +55 -5
- package/doc/zh/UpgradingFrom1.md +2 -2
- package/doc/zh/UpgradingFrom2.md +3 -3
- package/doc/zh/WebRTC.md +1 -1
- package/package.json +29 -15
package/doc/zh/Command.md
CHANGED
|
@@ -9,7 +9,7 @@ npx @lakutata/cli create --name my-app # 不安装,直接运行
|
|
|
9
9
|
npm install --global @lakutata/cli # 或者在本机安装一次,之后:lakutata create --name my-app
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
模板会把 `@lakutata/cli` 加入项目的 `devDependencies`,所以项目的脚本可以直接调用 `lakutata`,在项目里也可以用 `npx lakutata`。工具需要 Node.js
|
|
12
|
+
模板会把 `@lakutata/cli` 加入项目的 `devDependencies`,所以项目的脚本可以直接调用 `lakutata`,在项目里也可以用 `npx lakutata`。工具需要 Node.js 18.10 或更高版本(`>=18.10.0`)。
|
|
13
13
|
|
|
14
14
|
- `lakutata --help` 列出所有命令,`lakutata help <command>`(或 `lakutata <command> --help`)说明某个命令,`lakutata --version` 输出工具的版本。不带命令运行时,工具输出帮助,并以退出码 1 退出。
|
|
15
15
|
- 选项名用短横线连接(`--package-manager`)。默认开启的布尔选项用 `--no-` 关闭(`--no-git`、`--no-bail`)。
|
|
@@ -34,7 +34,7 @@ lakutata create --name tool --template cli --package-manager none --no-git
|
|
|
34
34
|
| `--path` | `.` | 在哪里创建项目目录(`<path>/<name>`) |
|
|
35
35
|
| `--init` | | 直接在 `--path` 这个已有目录中创建项目 |
|
|
36
36
|
| `--description`、`--author`、`--license` | 许可证为 `UNLICENSED` | 包的信息 |
|
|
37
|
-
| `--with` | | 模板的可选功能,用逗号分隔(`i18n`、`installer`) |
|
|
37
|
+
| `--with` | | 模板的可选功能,用逗号分隔(`i18n`、`installer`、`win7`) |
|
|
38
38
|
| `--package-manager` | `npm`;由 pnpm 或 yarn 启动工具时为对应的包管理器 | `npm`、`pnpm`、`yarn`,或 `none` 表示不安装依赖 |
|
|
39
39
|
| `--no-git` | | 不创建 git 仓库 |
|
|
40
40
|
| `--yes` | | 不询问可选功能,也不要求确认 |
|
|
@@ -67,6 +67,8 @@ lakutata create --name tool --template cli --package-manager none --no-git
|
|
|
67
67
|
|
|
68
68
|
`lakutata package bundle` 和 `package exe` 把编译好的应用打包成单个 JavaScript 文件,或者不需要安装 Node.js 就能运行的可执行文件,见 [Packaging](Packaging.md)。`lakutata package installer` 制作应用的安装包,安装包还可以把应用注册为服务,见 [Installers](Installers.md)。
|
|
69
69
|
|
|
70
|
+
NW.js 应用所用的 NW.js 版本由 `lakutata nwjs use <版本>` 设置(`latest`、`win7` 或某个版本,`lakutata nwjs versions` 会列出这些版本),版本会写进应用的 `package.json`。`lakutata nwjs run [args...]` 在这个 NW.js 的 sdk 版本(带 DevTools)中运行编译好的应用,原生插件会被处理成能在其中加载,并把参数传给应用。应用的安装包里是 normal 版本(见 [Installers](Installers.md) 第 9 节)。
|
|
71
|
+
|
|
70
72
|
## 6. 环境变量
|
|
71
73
|
|
|
72
74
|
| 变量 | 含义 |
|
|
@@ -74,7 +76,9 @@ lakutata create --name tool --template cli --package-manager none --no-git
|
|
|
74
76
|
| `LAKUTATA_LANG` | 工具的语言(`en`、`zh-CN`、`zh-TW`) |
|
|
75
77
|
| `LAKUTATA_NPM_REGISTRY` | 下载模板的仓库(否则取 `npm_config_registry`,再否则为 `https://registry.npmjs.org`) |
|
|
76
78
|
| `LAKUTATA_GITHUB_DOWNLOAD` | 下载 `github:` 模板的地址(默认 `https://github.com`) |
|
|
77
|
-
| `LAKUTATA_CACHE_DIR` |
|
|
79
|
+
| `LAKUTATA_CACHE_DIR` | 工具的缓存目录:模板、制作安装包所用的工具,以及 NW.js 的运行时(默认 `$XDG_CACHE_HOME/lakutata`,否则 `~/.cache/lakutata`) |
|
|
80
|
+
| `LAKUTATA_NWJS_MIRROR` | NW.js 运行时的下载地址(默认 `https://dl.nwjs.io`) |
|
|
81
|
+
| `LAKUTATA_NWJS_VERSIONS` | NW.js 的发布版本列表(默认 `https://nwjs.io/versions.json`) |
|
|
78
82
|
| `NO_COLOR` | 关闭颜色 |
|
|
79
83
|
|
|
80
84
|
工具运行的脚本和应用,拿到的是工具启动时的环境变量。打包后的应用所用的环境变量见 [Packaging](Packaging.md) 第 6 节。
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
| 功能 | 需要 |
|
|
10
10
|
|---|---|
|
|
11
|
-
| 全部功能 | Linux,带 runc 的 containerd 1.6 或更高版本,Node.js
|
|
11
|
+
| 全部功能 | Linux,带 runc 的 containerd 1.6 或更高版本,Node.js `>=18.10.0`;应用以 root 运行,或属于拥有 containerd 套接字的用户组 |
|
|
12
12
|
| CNI 网络(包括默认的 `bridge`)、端口发布 | CNI 插件(`bridge`、`portmap`、`host-local` 等,位于 `/opt/cni/bin`、`/usr/lib/cni` 或 `/usr/libexec/cni`,即 `cniPath` 选项)以及 iptables |
|
|
13
13
|
| 带 `stdin` 的 `exec()`、分开的 `stdout`/`stderr`、终端 | 主机上的 `mkfifo`(没有时,`exec()` 把两种输出都放在 `stdout` 中) |
|
|
14
14
|
| 构建 | BuildKit 守护进程(`buildkitd`),本地或远程 |
|
|
@@ -124,7 +124,7 @@ export const db = buildDatabaseOptions({
|
|
|
124
124
|
|
|
125
125
|
- 外键约束是生效的(`PRAGMA foreign_keys = ON`)。只有一个连接:不支持 `poolSize`。
|
|
126
126
|
- **`bigint` 列以数字返回**:超过 2^53 的值会丢失精度。这类值请存在文本列中。
|
|
127
|
-
- `better-sqlite3` 是原生模块,针对某个 Node.js 版本编译。Electron 应用需要针对它的 Electron 版本的构建(`electron-rebuild`,或 better-sqlite3 为 Electron 提供的预编译文件);Bun 无法加载它(在 Bun 上使用 `sqljs`)。
|
|
127
|
+
- `better-sqlite3` 是原生模块,针对某个 Node.js 版本编译。Electron 应用需要针对它的 Electron 版本的构建(`electron-rebuild`,或 better-sqlite3 为 Electron 提供的预编译文件);NW.js 应用会用 NW.js 的头文件重新构建它(需要 0.111.3 及以后的版本:更早的版本请使用 `sqljs`);Bun 无法加载它(在 Bun 上使用 `sqljs`)。
|
|
128
128
|
|
|
129
129
|
## SQLite:sql.js
|
|
130
130
|
|
package/doc/zh/Entrypoint.md
CHANGED
|
@@ -56,6 +56,7 @@ Application.run(() => ({
|
|
|
56
56
|
| `service` | `socketIOServiceEntrypoint()` | `lakutata/adapter/socket.io` | `socket.io` | [ServiceCalls](ServiceCalls.md) |
|
|
57
57
|
| `service` | `jsonRPCServiceEntrypoint()` | `lakutata/adapter/jsonrpc` | `ws`(WebSocket 时) | [ServiceCalls](ServiceCalls.md) |
|
|
58
58
|
| `service` | `electronIPCServiceEntrypoint()` | `lakutata/adapter/electron` | (Electron) | [ServiceCalls](ServiceCalls.md) |
|
|
59
|
+
| `service` | `nwjsServiceEntrypoint()` | `lakutata/adapter/nwjs` | (NW.js) | [ServiceCalls](ServiceCalls.md) |
|
|
59
60
|
| `service` | `grpcServiceEntrypoint()` | `@lakutata/grpc/service` | `@lakutata/grpc` | [GRPC](GRPC.md) |
|
|
60
61
|
| `service` | `natsServiceEntrypoint()` | `@lakutata/nats` | `@lakutata/nats` | [NATS](NATS.md) |
|
|
61
62
|
| `schedule` | `scheduleEntrypoint()` | `lakutata/com/entrypoint` | | [Schedules](Schedules.md) |
|
|
@@ -165,7 +166,7 @@ Application.run(() => ({
|
|
|
165
166
|
|
|
166
167
|
## 6. 向客户端推送事件
|
|
167
168
|
|
|
168
|
-
保持客户端连接的服务传输方式(Socket.IO、Electron、HTTP 服务的事件流、WebSocket 或 stdio 上的 JSON-RPC、gRPC)可以接收应用推送的事件。`entrypoint.notify(clientId, event, ...args)` 把事件发给一个客户端,即某个动作从 `this.context.clientId` 读到的那个客户端,返回客户端是否收到;`entrypoint.broadcast(event, ...args)` 发给所有已连接的客户端,返回客户端的数量。见 [GRPC](GRPC.md) 和 [ServiceCalls](ServiceCalls.md)。
|
|
169
|
+
保持客户端连接的服务传输方式(Socket.IO、Electron、NW.js、HTTP 服务的事件流、WebSocket 或 stdio 上的 JSON-RPC、gRPC)可以接收应用推送的事件。`entrypoint.notify(clientId, event, ...args)` 把事件发给一个客户端,即某个动作从 `this.context.clientId` 读到的那个客户端,返回客户端是否收到;`entrypoint.broadcast(event, ...args)` 发给所有已连接的客户端,返回客户端的数量。见 [GRPC](GRPC.md) 和 [ServiceCalls](ServiceCalls.md)。
|
|
169
170
|
|
|
170
171
|
## 7. 定制入口
|
|
171
172
|
|
package/doc/zh/Exceptions.md
CHANGED
|
@@ -113,7 +113,7 @@ export async function pay(orderId: number, charge: () => Promise<void>): Promise
|
|
|
113
113
|
| 传输方式 | 客户端收到 |
|
|
114
114
|
|---|---|
|
|
115
115
|
| HTTP | 状态码 `statusCode` 和 `{errno, errMsg}`(见 [HTTP](HTTP.md) 第 6 节) |
|
|
116
|
-
| Socket.IO、Electron、HTTP 服务、NATS、gRPC | 完整的错误:它所有的自身属性(见 [ServiceCalls](ServiceCalls.md) 第 3 节) |
|
|
116
|
+
| Socket.IO、Electron、NW.js、HTTP 服务、NATS、gRPC | 完整的错误:它所有的自身属性(见 [ServiceCalls](ServiceCalls.md) 第 3 节) |
|
|
117
117
|
| JSON-RPC | 一个错误:输入不合法时错误码为 `-32602`,没有匹配的动作时为 `-32601`,应用正在停止时为 `-32001`,其他情况为 `-32000`;完整的错误放在 `data` 中 |
|
|
118
118
|
| 命令行 | 标准错误中的 `error: <消息>`,退出码为 1(见 [CLI](CLI.md)) |
|
|
119
119
|
| 定时任务 | 没有客户端:异常交给 `onError`,或者写入日志(见 [Schedules](Schedules.md)) |
|
package/doc/zh/FFI.md
CHANGED
|
@@ -319,7 +319,7 @@ export class Vendor extends NativeLibrary {
|
|
|
319
319
|
|
|
320
320
|
## 12. 运行环境
|
|
321
321
|
|
|
322
|
-
Node.js
|
|
322
|
+
Node.js `>=18.10.0`、Electron 的主进程和 Bun;不支持 Electron 的渲染进程和浏览器。
|
|
323
323
|
|
|
324
324
|
这个包的测试(包括库的线程)在 macOS(arm64:Node.js 24、Bun 1.4、Electron 44)、Linux(arm64,glibc 和 musl:Node.js 24、Bun 1.4)和 Windows 11(arm64,以及模拟运行的 x64:Node.js 24、Bun 1.4)上都能通过。在 macOS 上,还用 `lakutata package` 把一个使用它的应用(它的库在自己的线程里执行)分别打成了 bundle、Node.js 和 Bun 的可执行文件以及 Electron 应用:每一个都能运行,库的线程也都从应用自己的资源目录里加载了自带的库。
|
|
325
325
|
|
package/doc/zh/GRPC.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lakutata gRPC
|
|
2
2
|
|
|
3
|
-
`@lakutata/grpc` 包让应用通过 gRPC 提供服务动作,也能从另一个应用调用这些动作;任何有 gRPC 库的语言都可以调用它们。它用 TypeScript 基于 Node.js
|
|
3
|
+
`@lakutata/grpc` 包让应用通过 gRPC 提供服务动作,也能从另一个应用调用这些动作;任何有 gRPC 库的语言都可以调用它们。它用 TypeScript 基于 Node.js(`>=18.10.0`)的 HTTP/2 模块实现,不需要原生插件,也不需要生成代码。`lakutata` 不包含这个包,需要另外安装:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install lakutata @lakutata/grpc
|
|
@@ -52,10 +52,13 @@ Application.run(() => ({
|
|
|
52
52
|
| `tls` | 无(明文 HTTP/2) | `{cert, key}` 启用 TLS;再加上 `ca` 时,客户端必须出示由它签发的证书(双向 TLS) |
|
|
53
53
|
| `maxMessageSize` | 4 MiB | 请求的最大大小(解压前后都算);超过时以 `RESOURCE_EXHAUSTED` 失败 |
|
|
54
54
|
| `compression` | `'identity'` | 设为 `'gzip'` 或 `'deflate'` 时,对支持的客户端压缩响应。压缩过的请求总是可以接收 |
|
|
55
|
-
| `listening` | | `(address) => void`,服务开始监听后调用;启动过程会等它完成 |
|
|
55
|
+
| `listening` | | `(address, listeners) => void`,服务开始监听后调用;启动过程会等它完成 |
|
|
56
|
+
| `interface`、`family`、`strict`、`onError` | | 按网口监听,而不是按地址监听,用法和 HTTP 适配器一样(见 [HTTP](HTTP.md) 第 2 节);不能和 `host` 或 `socketPath` 一起设置 |
|
|
56
57
|
| `middlewares` | | 通过 gRPC 调用的动作专用的中间件,在入口的中间件之后执行 |
|
|
57
58
|
| `name` | | 适配器的名字,用来区分多个适配器 |
|
|
58
59
|
|
|
60
|
+
设置 `interface` 时需要安装 `@lakutata/netif` 包。这时传给 `listening` 的 `address` 是第一个监听的地址;网口还不存在或还没有地址时是 `null`;`listeners` 是网口的监听器。
|
|
61
|
+
|
|
59
62
|
服务端不提供反射服务,也不提供健康检查服务。
|
|
60
63
|
|
|
61
64
|
## 2. 从另一个应用调用
|
|
@@ -223,4 +226,4 @@ print(json.loads(response.payload) if response.success else json.loads(response.
|
|
|
223
226
|
|
|
224
227
|
## 8. 底层组件
|
|
225
228
|
|
|
226
|
-
这个包也提供服务所依赖的底层组件,脱离 lakutata 也能使用:`GRPCConnection` 是可以调用任何 gRPC 服务端的客户端(一元调用、服务端流、双向流、元数据、TLS);`GRPCServer` 是服务端;`encode`/`decode` 是基于 schema 的 protobuf 编解码器。详见 `@lakutata/grpc` 的 README
|
|
229
|
+
这个包也提供服务所依赖的底层组件,脱离 lakutata 也能使用:`GRPCConnection` 是可以调用任何 gRPC 服务端的客户端(一元调用、服务端流、双向流、元数据、TLS);`GRPCServer` 是服务端;`encode`/`decode` 是基于 schema 的 protobuf 编解码器。详见 `@lakutata/grpc` 的 README。`GRPCServer.listenWith()` 也能像适配器一样,通过网口的监听器(`@lakutata/netif` 的 `listenOnAll()`)按网口监听。
|
package/doc/zh/GettingStarted.md
CHANGED
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
## 1. 环境要求
|
|
6
6
|
|
|
7
|
-
- Node.js
|
|
7
|
+
- Node.js 18.10 或更高版本(`>=18.10.0`)。少数功能需要较新的 Node.js:
|
|
8
|
+
- Fastify 适配器:Fastify 5 需要 Node.js 18.19、19.9、20 或更高版本(Express 适配器没有这个要求);
|
|
9
|
+
- `@lakutata/queue` 的 Kafka 传输:需要 Node.js 22.22、24.6 或更高版本,这是 `@platformatic/kafka` 的要求;
|
|
10
|
+
- Node.js 的可执行文件(`lakutata package exe` 和安装包):要用 Node.js 20.12、21.7 或更高版本来生成,或者用 `--node-program` 指定这样一个 Node.js;
|
|
11
|
+
- 对框架的对象使用 `await using`:需要 Node.js 18.18、20.4 或更高版本。
|
|
12
|
+
|
|
13
|
+
在 Node.js 18 和 19 上,应用要用 commander 13、@inquirer/prompts 7 和 better-sqlite3 11:它们再往后的大版本需要 Node.js 20 或更高版本。
|
|
14
|
+
|
|
15
|
+
`lakutata create` 生成的项目声明的是 Node.js `>=22.13.0`,Web 模板和桌面应用模板用到的工具需要它。
|
|
8
16
|
- TypeScript,开启它的装饰器(`experimentalDecorators`)及其元数据(`emitDecoratorMetadata`)。框架所需的编译选项以 `lakutata/tsconfig.json` 发布;模板使用 TypeScript 6。
|
|
9
17
|
- ES 模块(`"type": "module"`),和模板一样;包里同时也提供了 CommonJS 构建。
|
|
10
18
|
|
|
@@ -33,12 +41,13 @@ curl 'http://localhost:8080/example?name=you'
|
|
|
33
41
|
| `web-react`、`web-vue` | 提供 React 或 Vue 单页应用(Vite)的 HTTP API |
|
|
34
42
|
| `web-next`、`web-nuxt` | 在同一个进程中提供 Next.js 或 Nuxt 应用的 HTTP API |
|
|
35
43
|
| `electron-react`、`electron-vue` | 桌面应用:Electron,主进程通过 IPC 为 React 或 Vue 渲染进程提供服务 |
|
|
44
|
+
| `nwjs-react`、`nwjs-vue` | 桌面应用:NW.js,后台脚本为隔离在 webview 中的 React 或 Vue 用户界面提供服务 |
|
|
36
45
|
|
|
37
46
|
```bash
|
|
38
47
|
npx @lakutata/cli create --name shop --template service --with i18n,installer
|
|
39
48
|
```
|
|
40
49
|
|
|
41
|
-
`--with` 加入可选功能:所有模板都有 `i18n`(多语言消息),`cli`、`service` 和 `web-*` 模板还有 `installer
|
|
50
|
+
`--with` 加入可选功能:所有模板都有 `i18n`(多语言消息),`cli`、`service` 和 `web-*` 模板还有 `installer`(安装包和系统服务),`nwjs-*` 模板还有 `win7`(能在 Windows 7 上运行的 NW.js 0.72)。在终端里没有给出 `--with` 时,工具会列出这些功能供选择。模板中的文件见 [ProjectStructure](ProjectStructure.md),`lakutata create` 的选项见 [Command](Command.md)。
|
|
42
51
|
|
|
43
52
|
`default` 模板的脚本:
|
|
44
53
|
|
package/doc/zh/HTTP.md
CHANGED
|
@@ -101,6 +101,7 @@ HTTP/1.1 400 Bad Request
|
|
|
101
101
|
|---|---|---|
|
|
102
102
|
| 适配器 | `lakutata/adapter/express` 中的 `expressHTTPEntrypoint()` | `lakutata/adapter/fastify` 中的 `fastifyHTTPEntrypoint()` |
|
|
103
103
|
| 需要安装 | `express`(4 或 5) | `fastify`(5) |
|
|
104
|
+
| Node.js | 18.10 起 | 18.19、19.9、20 起(Fastify 5 需要 `diagnostics_channel.tracingChannel`) |
|
|
104
105
|
|
|
105
106
|
`expressHTTPEntrypoint()` 的选项:
|
|
106
107
|
|
|
@@ -119,6 +120,8 @@ HTTP/1.1 400 Bad Request
|
|
|
119
120
|
| `fallback` | | 响应动作路径之外的请求,比如 Web 界面的页面(第 7 节) |
|
|
120
121
|
| `stop` | | 适配器停止、服务器关闭之后调用 |
|
|
121
122
|
| `heartbeat` | `15000` | 每隔多久发送一条注释,让事件流保持连接,单位毫秒(第 5 节) |
|
|
123
|
+
| `listening` | | `(server, listeners) => …`:服务器开始监听后调用;按网口监听时,在网口的监听器建好后调用(见下文) |
|
|
124
|
+
| `interface` | | 按网口监听,而不是按地址监听,可以再配上 `family`、`strict` 和 `onError`(见下文) |
|
|
122
125
|
|
|
123
126
|
`fastifyHTTPEntrypoint()` 的选项相同,只是没有 `server` 和 `app`,取而代之的是 `fastify`:一个 Fastify 实例,或者要创建的实例的选项(`bodyLimit`,默认 1 MiB;`trustProxy`;`connectionTimeout`、`keepAliveTimeout`、`requestTimeout`;`logger`……)。它的 `body` 选项只有 `text`,大小上限是实例的 `bodyLimit`。
|
|
124
127
|
|
|
@@ -136,6 +139,33 @@ HTTP/1.1 400 Bad Request
|
|
|
136
139
|
|
|
137
140
|
应用停止时(见 [Entrypoint](Entrypoint.md) 第 4 节),适配器不再接受新的连接,并结束事件流;Fastify 还会对已打开的连接上仍然到达的请求返回 503。正在处理的请求可以在入口的 `shutdownTimeout` 内完成(超时后以 503 响应),之后最多再有一秒来发送响应,然后剩下的连接被关闭,WebSocket 连接也一样。
|
|
138
141
|
|
|
142
|
+
### 按网口监听
|
|
143
|
+
|
|
144
|
+
有多个网口的设备,常常只在其中一个网口上提供页面,而这个网口的地址来自 DHCP,会变。设置 `interface` 后,适配器监听的是这个网口,而不是某个地址:不管网口现在是什么地址,地址变化、网口拔掉再插回,都会继续监听。默认用的是 `@lakutata/netif` 包,需要另外安装,它会编译一个原生插件(见 [Netif](Netif.md))。
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
|
|
148
|
+
|
|
149
|
+
//页面只在 eth1 上提供,不管它是什么地址
|
|
150
|
+
export const http = expressHTTPEntrypoint({
|
|
151
|
+
port: 8080,
|
|
152
|
+
interface: 'eth1',
|
|
153
|
+
listening: (_server, listeners): void => console.log(listeners?.map(listener => listener.addresses))
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| 选项 | 默认值 | 含义 |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `interface` | | 网口的名字(`'eth1'`;Windows 上是网络连接的名字)、`{mac: '52:54:00:12:34:56'}` 或 `{index: 3}`;多个网口放在数组里,这时 `port` 不能是 `0`。不能和 `host` 一起设置 |
|
|
160
|
+
| `family` | `'both'` | `'IPv4'`、`'IPv6'` 或两者都监听 |
|
|
161
|
+
| `strict` | `false` | 如果系统挡不住从别的网口发往这个网口地址的连接,就让启动失败,并在挡不住的期间停止监听(见 [Netif](Netif.md) 第 2 节) |
|
|
162
|
+
| `onError` | 应用的日志 | 接收应用运行期间重新监听时的错误,比如端口被别的程序占用;适配器会自己重试 |
|
|
163
|
+
| `interfaceListening` | `@lakutata/netif` | 换用另一种按网口监听的实现:一个 `IInterfaceListening`,也就是 lakutata 定义、`@lakutata/netif` 实现的接口 |
|
|
164
|
+
|
|
165
|
+
- 应用启动时网口可能还不存在(比如 USB 网卡还没插上):启动照样成功,网口出现后适配器就开始监听。在那之前,监听器的 `addresses` 是空的。
|
|
166
|
+
- 服务器自己不监听,它的 `address()` 是 `null`:端口和地址从监听器上读取。
|
|
167
|
+
- 没有设置 `interface` 却设置了 `family`、`strict`、`onError` 或 `interfaceListening`,或者同时设置了 `interface` 和 `host`,创建适配器时就会报错。
|
|
168
|
+
|
|
139
169
|
## 3. 路由
|
|
140
170
|
|
|
141
171
|
| 装饰器 | 方法 |
|
package/doc/zh/I18n.md
CHANGED
|
@@ -164,7 +164,7 @@ Application.run(() => ({
|
|
|
164
164
|
| HTTP(Express、Fastify) | Query 参数 `?lang=`、Cookie `lang`,然后是请求头 `Accept-Language`(按权重) |
|
|
165
165
|
| Socket.IO | 连接握手时的 Query 参数 `lang`、Cookie `lang`、`Accept-Language` |
|
|
166
166
|
| HTTP 服务适配器、JSON-RPC(HTTP、WebSocket) | HTTP 请求(WebSocket 为建立连接的请求)的 Query 参数 `lang`、Cookie `lang`、`Accept-Language` |
|
|
167
|
-
| 命令行、Electron IPC、stdio 上的 JSON-RPC | `environmentVariable` 指定的环境变量(默认没有)、`LANGUAGE`、`LC_ALL`、`LC_MESSAGES`、`LANG`,最后是进程的语言 |
|
|
167
|
+
| 命令行、Electron IPC、NW.js、stdio 上的 JSON-RPC | `environmentVariable` 指定的环境变量(默认没有)、`LANGUAGE`、`LC_ALL`、`LC_MESSAGES`、`LANG`,最后是进程的语言 |
|
|
168
168
|
| 定时任务 | 无(默认语言) |
|
|
169
169
|
|
|
170
170
|
不是语言标签的 Query 参数或 Cookie 值会被跳过。
|
package/doc/zh/Installers.md
CHANGED
|
@@ -16,7 +16,7 @@ npx lakutata package installer --target pkg # 只制作其中几种
|
|
|
16
16
|
✔ dist/installer/greet-1.0.0-arm64.pkg (pkg, 36.0 MB)
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
安装包里是目录布局的可执行文件(`lakutata package exe --layout directory`):程序本身、它的原生插件(`native/`)和资源(`resources/`)。程序用 Node.js 制作;`lakutata.installer.runtime` 为 `"bun"` 时用 Bun
|
|
19
|
+
安装包里是目录布局的可执行文件(`lakutata package exe --layout directory`):程序本身、它的原生插件(`native/`)和资源(`resources/`)。程序用 Node.js 制作;`lakutata.installer.runtime` 为 `"bun"` 时用 Bun 制作。在低于 20.12 的 Node.js 上,和可执行文件一样,用 `--node-program` 指定一个更新的 Node.js。
|
|
20
20
|
|
|
21
21
|
| 平台 | 安装包(默认) | 文件名 | 制作工具 |
|
|
22
22
|
|---|---|---|---|
|
|
@@ -56,18 +56,18 @@ npx lakutata package installer --target pkg # 只制作其中几种
|
|
|
56
56
|
| `description`、`homepage`、`license` | 取自 `package.json` | 软件包的信息 |
|
|
57
57
|
| `maintainer` | `package.json` 的 `author` | 维护者(`Name <email>`);没有时工具会发出警告 |
|
|
58
58
|
| `vendor` | 无 | 供应商;在 Windows 上,发布者是供应商,没有时是维护者 |
|
|
59
|
-
| `runtime` | `node` | 可执行文件用 `node` 或 `bun`;Electron 应用用 `electron`(第 8 节) |
|
|
59
|
+
| `runtime` | `node` | 可执行文件用 `node` 或 `bun`;Electron 应用用 `electron`(第 8 节),NW.js 应用用 `nwjs`(第 9 节) |
|
|
60
60
|
| `addToPath` | `true` | 把程序加入 PATH:Linux 上是 `/usr/bin/<program>`,macOS 上是 `/usr/local/bin/<program>`,Windows 上是机器的 PATH |
|
|
61
61
|
| `service` | 无 | `true`,或服务的选项(第 4 节) |
|
|
62
62
|
| `linux.targets` | `deb`、`rpm`、`tar.gz`;Alpine 上是 `apk`、`tar.gz` | Linux 的安装包(`deb`、`rpm`、`archlinux`、`tar.gz`;Alpine 上是 `apk`、`tar.gz`) |
|
|
63
63
|
| `linux.depends`、`linux.recommends` | 无 | 依赖和推荐的系统软件包(总会加上 C++ 运行库) |
|
|
64
64
|
| `linux.section`、`linux.priority` | 无 | 分类和优先级(deb) |
|
|
65
|
-
| `mac.targets` | `pkg`、`tar.gz` | macOS
|
|
65
|
+
| `mac.targets` | `pkg`、`tar.gz` | macOS 的安装包(NW.js 应用还可以用 `dmg`) |
|
|
66
66
|
| `win.targets` | `nsis`、`msi`、`zip` | Windows 的安装包 |
|
|
67
67
|
| `linux.scripts`、`mac.scripts`、`win.scripts` 等 | 无 | 安装包执行的脚本(第 5 节) |
|
|
68
68
|
| `electron` | 无 | electron-builder 的配置(第 8 节) |
|
|
69
69
|
|
|
70
|
-
`lakutata.icon`(与 `lakutata.installer` 并列)是一张 256 像素或更大的正方形 PNG 图片:它是 Windows 可执行文件和安装程序的图标,也是 Electron 应用的图标。
|
|
70
|
+
`lakutata.icon`(与 `lakutata.installer` 并列)是一张 256 像素或更大的正方形 PNG 图片:它是 Windows 可执行文件和安装程序的图标,也是 Electron 和 NW.js 应用的图标。
|
|
71
71
|
|
|
72
72
|
配置在构建任何东西之前就会检查,不制作的那些安装包的选项也会检查,出错时会指出是哪个字段:
|
|
73
73
|
|
|
@@ -169,7 +169,7 @@ fi
|
|
|
169
169
|
|
|
170
170
|
## 6. 签名
|
|
171
171
|
|
|
172
|
-
- **macOS**:`pkg`
|
|
172
|
+
- **macOS**:`pkg` 安装包和 `dmg` 磁盘映像没有签名,它们安装的程序或应用也只有临时签名(ad hoc)。在组织以外的机器上分发所需要的 Developer ID 签名和公证,目前还不会制作。
|
|
173
173
|
- **Windows**:NSIS 和 MSI 安装包没有签名,分发前请用 signtool 签名。用 Node.js 制作的程序保留着 `node.exe` 的签名,而这个签名已经不再匹配,工具会对此发出警告。
|
|
174
174
|
- **Linux**:软件包没有签名。
|
|
175
175
|
|
|
@@ -230,3 +230,14 @@ jobs:
|
|
|
230
230
|
- **脚本**在 `deb` 和 `rpm` 软件包(全部四种)、`pkg`(`preinstall`、`postinstall`)和 `nsis` 安装程序(`preinstall`、`postinstall`、`preremove`)中执行;其他目标都不执行。
|
|
231
231
|
- **原生插件**按安装时的样子复制:electron-builder 不会重新构建它们。基于 Node-API 的插件可以直接运行;其他插件请在打包前安装它们为 Electron 构建的版本(`electron-rebuild`)(见 [NativeAddons](NativeAddons.md) 第 4 节)。
|
|
232
232
|
- 在 Linux arm64 上,deb 和 rpm 软件包需要系统中的 fpm(`gem install fpm`、`USE_SYSTEM_FPM=true`),Electron 模板的 `Dockerfile.installer` 就是这样安装它的。
|
|
233
|
+
|
|
234
|
+
## 9. NW.js 应用
|
|
235
|
+
|
|
236
|
+
使用 `"runtime": "nwjs"` 时,安装包里是由构建器制作的 NW.js 应用:应用所用 NW.js 版本(它的 `lakutata.nwjs`,由 `lakutata nwjs use` 设置)的 normal 版本,只下载一次到 lakutata 的缓存目录,并按其发布版本的校验和核对;应用放在其中,包括为这个 NW.js 的 Node.js 打包的代码、原生插件、资源(`lakutata.resources`,通过 `resourcesPath()` 读取)和 NW.js 的清单。`nwjs-react` 和 `nwjs-vue` 模板就是这样配置的。
|
|
237
|
+
|
|
238
|
+
- **运行时**以应用的名字命名:Windows 上是 `<name>.exe`,图标和版本信息都是应用的;Linux 上是 `<name>`;macOS 上是 `<productName>.app`,它的 `Info.plist` 给出应用的名称、标识(`appId`)、版本和图标,并重新做临时签名。
|
|
239
|
+
- **安装包**是桌面应用的安装包:默认 macOS 上是 `dmg`,Windows 上是 `nsis`,Linux 上是 `deb`(macOS 上还可以用 `pkg` 和 `tar.gz`,Windows 上可以用 `msi` 和 `zip`,Linux 上可以用 `rpm` 和 `tar.gz`)。Windows 的安装程序把应用加到开始菜单和桌面;`dmg` 磁盘映像里是应用和一个指向 /Applications 的链接,用户把应用拖过去即可,`pkg` 安装包则直接把它装到那里(Intel 版的应用也可以装到 Apple 芯片的 Mac 上);Linux 的软件包会添加应用的桌面入口和图标,并依赖 Chromium 所需的库。
|
|
240
|
+
- **架构**默认是本机的架构,也可以用 `--arch` 指定(`x64`、`ia32`、`arm64`):`ia32` 制作 32 位 Windows 的安装包,应用安装在 32 位程序的目录中。没有 Apple 芯片 Mac 构建的 NW.js 版本(如 0.72),会打包成 Intel 版。
|
|
241
|
+
- **NW.js 应用不是服务**,也不会加入 PATH(`addToPath` 默认为 `false`)。
|
|
242
|
+
- **脚本**和其他应用一样执行,只是 macOS 上的应用是直接删除的:那里不会执行卸载脚本。
|
|
243
|
+
- **原生插件**会被处理成能在 NW.js 中加载:为它的架构重新构建,使用 V8 API 的插件用它的头文件重新构建;在 Windows 上,插件加载的是 NW.js 的 Node.js(见 [NativeAddons](NativeAddons.md) 第 4 节)。
|
package/doc/zh/MCP.md
CHANGED
|
@@ -224,7 +224,8 @@ Application.run(() => ({
|
|
|
224
224
|
|---|---|---|
|
|
225
225
|
| `port`、`host`、`path` | `3000`、`'127.0.0.1'`、`'/mcp'` | 监听的位置 |
|
|
226
226
|
| `authenticate` | | `(request) => MCPAuthInfo \| undefined`:客户端的身份认证;抛出错误时以 401 响应,返回 `undefined` 时不认证,照常处理请求 |
|
|
227
|
-
| `allowedHosts`、`allowedOrigins` |
|
|
227
|
+
| `allowedHosts`、`allowedOrigins` | 在本机地址或回环网口上时为本机的名字 | 接受的 `Host` 和 `Origin` 请求头(否则为 403);`false` 表示不检查 |
|
|
228
|
+
| `interface`、`family`、`strict`、`onError` | | 按网口监听,代替 `host`,需要安装 `@lakutata/netif` 包(见 [HTTP](HTTP.md) 第 2 节) |
|
|
228
229
|
| `legacy` | `'stateless'` | 2025 年协议版本的客户端:以无会话的方式服务,或者用 `'reject'` 拒绝 |
|
|
229
230
|
| `maxRequestBodySize` | 4 MiB | 更大的请求体以 413 响应 |
|
|
230
231
|
| `bus` | 内存中 | 通知的去向,用于在应用的多个实例之间共享 |
|
|
@@ -232,7 +233,7 @@ Application.run(() => ({
|
|
|
232
233
|
| `serverName`、`serverVersion`、`instructions`、`server`、`listening`、`name`、`middlewares` | | 与 stdio 相同;`server` 是 `node:http` 服务器的选项 |
|
|
233
234
|
|
|
234
235
|
- 它通过 Streamable HTTP 提供协议:2026-07-28 版本,以及无会话方式的 2025 年版本。
|
|
235
|
-
- 在本机地址上,`Host` 和 `Origin` 请求头会与本机的名字比对,防止 DNS
|
|
236
|
+
- 在本机地址上,`Host` 和 `Origin` 请求头会与本机的名字比对,防止 DNS 重绑定攻击:在代理之后时,要列出客户端使用的名字。在其他地址上,除非列出名字,否则不做检查。设置 `interface` 时也一样:在回环网口上与本机的名字比对;在其他网口上,网络中的客户端都能访问这个服务器,所以要设置 `allowedHosts` 和 `authenticate`。
|
|
236
237
|
- 应用停止时,正在处理的调用会得到响应,然后端口关闭。
|
|
237
238
|
|
|
238
239
|
**与 HTTP 适配器共用端口。** 设置 `listen: false`,在 Express 应用的 `before` 钩子中挂载适配器的 `handler`:
|
package/doc/zh/Math.md
CHANGED
|
@@ -279,7 +279,7 @@ $ curl -X POST localhost:8080/solve -H 'Content-Type: application/json' -d '{"eq
|
|
|
279
279
|
- 公式的长度限制不了它的计算量:`(x + 1)^100000` 很短。能限制计算量的是时限。
|
|
280
280
|
- 这个包的值通过 `toJSON()` 转成 JSON:表达式、方程、十进制数和带单位的值转成它们的文本,解转成文本数组,矩阵转成它的各行。向量是 `Float64Array`,`JSON.stringify()` 会把它写成对象(`{"0": 1, "1": 1}`):请发送 `Array.from(vector)`。
|
|
281
281
|
- `lakutata package` 打出的单文件包、可执行文件和安装包都带着编译好的原生插件(`build/Release/math.node`)。请在每个目标平台上安装这个包之后再打包;原生插件的规则同样适用(见 [Packaging](Packaging.md) 和 [NativeAddons](NativeAddons.md))。
|
|
282
|
-
- 这个包可以在 Node.js
|
|
282
|
+
- 这个包可以在 Node.js `>=18.10.0`、Bun 和 Electron 的主进程中运行,后两者直接加载为 Node.js 编译的插件;不能在 Electron 的渲染进程和浏览器中运行。在 Bun 里,worker 线程里的插件运算还没结束就终止这个线程的话,整个进程会因为 Node-API 的致命错误退出(这是 Bun 1.4 的问题)。
|
|
283
283
|
|
|
284
284
|
## 13. 验证方式,以及不支持的情况
|
|
285
285
|
|
package/doc/zh/NativeAddons.md
CHANGED
|
@@ -61,14 +61,13 @@ pub fn greet(name: String) -> String {
|
|
|
61
61
|
```typescript
|
|
62
62
|
import {createRequire} from 'node:module'
|
|
63
63
|
import path from 'node:path'
|
|
64
|
-
import {fileURLToPath} from 'node:url'
|
|
65
64
|
|
|
66
65
|
type Binding = {
|
|
67
66
|
greet(name: string): string
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
/** The addon built into build/Release, loaded from the directory of the package */
|
|
71
|
-
const binding: Binding = createRequire(
|
|
70
|
+
const binding: Binding = createRequire(__filename)('node-gyp-build')(path.resolve(__dirname, '..'))
|
|
72
71
|
|
|
73
72
|
export function greet(name: string): string {
|
|
74
73
|
return binding.greet(name)
|
|
@@ -87,6 +86,7 @@ export function greet(name: string): string {
|
|
|
87
86
|
- **错误**:node-addon-api 的 C++ 异常会以同类错误抛到 JavaScript 中(`Napi::TypeError` 变成 `TypeError`)。在 Rust 中,类型不对的参数由 napi-rs 拒绝,抛出一个 `Error`,它的 `code` 说明原因(`StringExpected`)。
|
|
88
87
|
- **macOS**:C++ 插件在一个文件中同时为 Intel 和 Apple 芯片的 Mac 构建;Rust 插件只为本机的架构构建(用 `rustup target add` 添加其他架构)。
|
|
89
88
|
- **Node-API**:插件针对 Node-API 第 8 版构建,所有受支持的 Node.js 和 Electron 版本都提供这一版,所以不需要为它们分别重新构建。
|
|
89
|
+
- **CommonJS**:这个包是 CommonJS 包(`"type": "commonjs"`)。`lakutata package` 生成的文件是 CommonJS 格式,用 `require` 加载它,而 `require` 要到 Node.js 20.19、22.12 或更高版本才能加载 ES 模块。
|
|
90
90
|
- 和 `lakutata add` 创建的所有包一样,这个包是 `"private": true`:原生插件通常随应用一起发布,而不单独发布。
|
|
91
91
|
|
|
92
92
|
## 2. 链接库
|
|
@@ -177,5 +177,6 @@ lakutata native link libc --crate # 一个 crate(Rust:carg
|
|
|
177
177
|
- **单文件包和可执行文件**:`lakutata package bundle` 把插件包排除在单文件包之外,连同它们的文件(JavaScript、插件本身、它的动态库,但不包括源码和构建时产生的文件)写入 `dist/<name>.native/`,这个目录随单文件包一起发布;可执行文件则把它们嵌入其中,或者在目录布局中放在自己旁边(见 [Packaging](Packaging.md))。插件是为打包所在机器的平台构建的,所以每个平台的应用要在那个平台上打包(用 CI 的矩阵,Linux 也可以用对应架构的容器)。
|
|
178
178
|
- **基于框架的插件包**(它依赖 `lakutata` 或 `@lakutata/core`,比如为了提供一个组件)则和应用一起打进单文件包,这样框架只有一份,只有插件和它旁边的文件放进这个目录。插件要用下面几种方式之一加载,才会被放进去:像 `lakutata add` 生成的插件包那样,把包的目录交给 `node-gyp-build`;直接 `require` 插件文件;或者用从模块位置构造的路径(`path.join(__dirname, 'prebuilds', ...)`)。用其他方式加载的插件,`lakutata package` 会发出警告。
|
|
179
179
|
- **Electron**:插件按安装时的样子复制。`lakutata add` 创建的插件不需要重新构建就能在 Electron 中运行,所有基于 Node-API 的插件都是如此;基于 Node.js V8 API 的插件,打包前要安装它为 Electron 构建的版本(`electron-rebuild`)。
|
|
180
|
+
- **NW.js**:应用在 NW.js 中运行时(`lakutata nwjs run`,以及打包之后),插件会被处理成能在 NW.js 中加载。为运行时以外的架构构建的插件会从源码重新构建,使用 NW.js 内置 Node.js 的头文件;基于 Node.js V8 API 的插件也会重新构建,使用 NW.js 的头文件(需要 NW.js 0.111.3 及以后的版本:更早的版本,例如用于 Windows 7 的 0.72,只能运行基于 Node-API 的插件)。在 Windows 上,插件加载的是 NW.js 的 Node.js(`node.dll`)。项目里的插件保持原样,项目的测试仍然用 Node.js 运行。
|
|
180
181
|
- **Bun**:Bun 能加载 Node-API 的插件;Bun 可执行文件会对基于 Node.js V8 API 的插件发出警告。
|
|
181
182
|
- **Alpine(musl)**:Node.js 的可执行文件在那里不能加载原生插件,请用 Bun 制作。
|
package/doc/zh/Netif.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Lakutata Netif
|
|
2
|
+
|
|
3
|
+
`@lakutata/netif` 包让 Node.js 的服务器按网口监听,而不是按地址监听。有多个网口的设备,常常只在其中一个网口上提供管理页面:这个网口的地址来自 DHCP,会变,设备开机时可能还没有;而监听 `0.0.0.0` 又会让管理页面出现在所有网口上。用这个包,服务器监听的是“eth1 这个网口”,不管它现在是什么地址,地址变化、网口拔掉再插回,都会继续监听。它不属于 `lakutata`,也不需要应用。它的特点是:
|
|
4
|
+
|
|
5
|
+
- **任何服务器都能用**:`http`、`https`、`http2`、`tls`、Express 和 Fastify 都把这个网口的连接当成自己的连接处理;UDP 套接字也可以,适合只在一个网口上工作的服务(DHCP、mDNS、SSDP)。
|
|
6
|
+
- **lakutata 的入口**有 `interface` 选项,可以让应用的控制器只在一个网口上提供服务(第 5 节)。
|
|
7
|
+
- **系统能挡的,就让系统挡住别的网口**:在 Linux 上,套接字绑定在网口上,从别的网口发往这个网口地址的连接会被拒绝。在 Windows 和 macOS 上,监听的是这个网口的地址,`enforced` 告诉你系统会不会挡住那样的连接。
|
|
8
|
+
- **网口和它们的变化**:本机每个网口的状态,以及地址、连通状态、网口出现和消失时的事件,都来自系统的通知。
|
|
9
|
+
- **原生插件**:在安装这个包时编译,支持 Linux、macOS 和 Windows;也能在 Bun 和 Electron 的主进程里运行。
|
|
10
|
+
|
|
11
|
+
`@lakutata/netif` 的 README 介绍了每个函数及其选项;这篇指南按用途逐一说明这个包能做什么,每种用途都给出一个例子。
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @lakutata/netif
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
原生插件在安装这个包时编译,所以机器上要有 Python 3 和支持 C++17 的编译器(Alpine Linux 还要内核的头文件:`apk add python3 make g++ linux-headers`)。包管理器必须运行这个包的安装脚本:npm 会运行它,可能会提示这个脚本“not yet covered by allowScripts”(运行 `npm install-scripts approve @lakutata/netif` 记录你允许它运行);Bun 和 pnpm 只为被告知可以信任的包运行安装脚本(Bun 在 package.json 的 `trustedDependencies` 里设置,pnpm 用 `pnpm approve-builds`)。它的类型引用了 `@lakutata/core` 的类型,这是一个 peer 依赖,包管理器会一起安装;这个包运行时不会加载它。
|
|
18
|
+
|
|
19
|
+
## 1. 在网口上监听
|
|
20
|
+
|
|
21
|
+
`listenOn()` 让一个服务器在网口上监听。网口可以按名字、MAC 地址(`{mac: '52:54:00:12:34:56'}`,不随系统给网口起的名字变化)或序号指定;Windows 上的名字是网络连接的名字(`'以太网 2'`、`'Ethernet 2'`)。
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import {createServer} from 'node:http'
|
|
25
|
+
import express from 'express'
|
|
26
|
+
import {type InterfaceListener, listenOn} from '@lakutata/netif'
|
|
27
|
+
|
|
28
|
+
const app = express()
|
|
29
|
+
app.get('/', (request, response) => response.send(`served on ${request.socket.localAddress}`))
|
|
30
|
+
|
|
31
|
+
const listener: InterfaceListener = await listenOn('eth1', createServer(app), {port: 8080})
|
|
32
|
+
listener.on('error', error => console.error(error))
|
|
33
|
+
console.log(listener.port, listener.addresses)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- 监听器接受连接后交给服务器,服务器像处理自己的连接一样处理它们。服务器自己并没有在监听:它的 `address()` 是 `null`,`server.close()` 也不能让监听器停下;请用 `listener.port`、`listener.addresses` 和 `listener.close()`。
|
|
37
|
+
- 网口可以暂时不存在,也可以还没有地址:`listenOn()` 会立即返回,等网口能监听了就开始监听。网口拔掉再插回、地址变化时,会自动重新监听;已经建立的连接交给服务器处理。
|
|
38
|
+
- 第一次监听的错误(比如端口被占用)会直接抛出;之后的错误是 `error` 事件,过一会儿会重试。请给监听器加上 `error` 的监听函数,就像对 `net.Server` 那样。
|
|
39
|
+
- `family` 选择 `'IPv4'`、`'IPv6'` 或 `'both'`(默认);`port: 0` 让系统选一个端口,重新监听时保持同一个端口。
|
|
40
|
+
|
|
41
|
+
## 2. 哪些连接会被挡住
|
|
42
|
+
|
|
43
|
+
另一个网络的客户端,可以自己加一条经过设备的路由,把发往管理网口地址的包从设备的另一个网口送进来。系统会不会拒绝这样的连接,就是 `enforced` 回答的问题:
|
|
44
|
+
|
|
45
|
+
| | `enforced` |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Linux | `true`:套接字绑定在网口上(`SO_BINDTODEVICE`);Linux 5.7 以前这需要 `CAP_NET_RAW` 能力,没有的话就按地址监听,`enforced` 为 `false` |
|
|
48
|
+
| Windows | 其他网口都按强主机模型收包、都没开转发时为 `true`,默认就是这样 |
|
|
49
|
+
| macOS | 默认为 `false`;只有在系统设置为拒绝这样的包(`net.inet.ip.check_interface` 设为 2,IPv6 还有 `net.inet6.ip6.check_interface`)并且没开转发时才为 `true` |
|
|
50
|
+
|
|
51
|
+
默认按系统允许的最好方式监听,无论如何都不会比用 `listen(port, host)` 按地址监听差;`reason` 说明为什么没有保证,以及怎样才能有。如果管理网口是一道安全边界,用 `strict: true`:没有保证就拒绝监听,运行中保证丢了就停止监听:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import {createServer} from 'node:http'
|
|
55
|
+
import {type InterfaceListener, NetIfNotEnforcedException, NetworkInterface} from '@lakutata/netif'
|
|
56
|
+
|
|
57
|
+
const management: NetworkInterface = new NetworkInterface({mac: '52:54:00:12:34:56'})
|
|
58
|
+
try {
|
|
59
|
+
const listener: InterfaceListener = await management.listen(createServer(), {port: 443, strict: true})
|
|
60
|
+
listener.on('error', error => console.error(error))
|
|
61
|
+
listener.on('enforced', enforced => console.log(enforced ? 'guaranteed again' : 'stopped: no longer guaranteed'))
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof NetIfNotEnforcedException) console.error(error.message)
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 3. 跟踪一个网口
|
|
69
|
+
|
|
70
|
+
`NetworkInterface` 给出一个网口现在的状态(`present`、`running`、`addresses` 等),并在每次变化时发出事件。事件来自系统的通知,不靠定时轮询。
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import {type InterfaceAddress, listInterfaces, NetworkInterface} from '@lakutata/netif'
|
|
74
|
+
|
|
75
|
+
console.log(listInterfaces().map(entry => `${entry.name}: ${entry.running ? 'connected' : 'not connected'}`))
|
|
76
|
+
|
|
77
|
+
const wan: NetworkInterface = new NetworkInterface('eth0')
|
|
78
|
+
wan.on('change', ({added, removed}) => {
|
|
79
|
+
for (const address of added) console.log(`new address ${address.cidr}`)
|
|
80
|
+
for (const address of removed) console.log(`address gone ${address.cidr}`)
|
|
81
|
+
})
|
|
82
|
+
wan.on('down', () => console.log('the cable of eth0 is unplugged'))
|
|
83
|
+
wan.on('up', () => console.log('the cable of eth0 is plugged in'))
|
|
84
|
+
const address: InterfaceAddress = await wan.waitForAddress({family: 'IPv4', timeout: 60000})
|
|
85
|
+
console.log(`reachable at ${address.address}`)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- `listInterfaces()` 也列出没有地址的网口,并带上它们的状态;`os.networkInterfaces()` 只列出有地址的网口。
|
|
89
|
+
- IPv6 地址等它可用后才算:加上后大约一秒,重复地址检测结束时。
|
|
90
|
+
- `close()` 停止跟踪这个网口(它的监听器也会关闭)。
|
|
91
|
+
|
|
92
|
+
## 4. 网口上的 UDP
|
|
93
|
+
|
|
94
|
+
`createUDPSocket()` 返回这个网口的 UDP 套接字,方法和事件和 `dgram` 的套接字一样。下面是一个只回答管理网络里发现广播的设备:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import {type InterfaceUDPSocket, NetworkInterface} from '@lakutata/netif'
|
|
98
|
+
|
|
99
|
+
const management: NetworkInterface = new NetworkInterface('eth1')
|
|
100
|
+
const socket: InterfaceUDPSocket = await management.createUDPSocket({port: 30303, family: 'IPv4'})
|
|
101
|
+
socket.setBroadcast(true)
|
|
102
|
+
socket.on('message', (message, remote) => {
|
|
103
|
+
const answer: string = `device at ${management.addresses[0]?.address}`
|
|
104
|
+
if (message.toString() === 'who is there') socket.send(answer, remote.port, remote.address)
|
|
105
|
+
})
|
|
106
|
+
socket.on('error', error => console.error(error))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
在 Linux 上,这个套接字只接收从这个网口进来的数据,包括这个网口上的广播和用 `addMembership()` 加入的组播组,别的网口来的一概收不到。在 Windows 和 macOS 上,网口的每个地址各绑一个套接字;macOS 上这样的套接字收不到广播和组播。
|
|
110
|
+
|
|
111
|
+
## 5. 在应用中使用
|
|
112
|
+
|
|
113
|
+
lakutata 里自己监听端口的入口都有 `interface` 选项:`expressHTTPEntrypoint()`、`fastifyHTTPEntrypoint()`、`httpServiceEntrypoint()`、`socketIOServiceEntrypoint()`、`jsonRPCServiceEntrypoint()`、`@lakutata/grpc` 的 `grpcServiceEntrypoint()` 和 `@lakutata/mcp` 的 `httpMCPEntrypoint()`。设置后,控制器的动作就在这个网口上提供,不管它是什么地址:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import {Application} from 'lakutata'
|
|
117
|
+
import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
118
|
+
import {GET} from 'lakutata/decorator/ctrl'
|
|
119
|
+
import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
|
|
120
|
+
|
|
121
|
+
class ManagementController extends Controller {
|
|
122
|
+
@GET('/status')
|
|
123
|
+
public async status(): Promise<{ ok: boolean }> {
|
|
124
|
+
return {ok: true}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Application.run(() => ({
|
|
129
|
+
id: 'device.app',
|
|
130
|
+
name: 'Device',
|
|
131
|
+
components: {
|
|
132
|
+
entrypoint: buildEntrypoints({
|
|
133
|
+
controllers: [ManagementController],
|
|
134
|
+
http: expressHTTPEntrypoint({port: 8080, interface: 'eth1'})
|
|
135
|
+
})
|
|
136
|
+
},
|
|
137
|
+
bootstrap: ['entrypoint']
|
|
138
|
+
}))
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- `family`、`strict` 和 `onError` 与 `interface` 配合使用。没有设置 `onError` 时,重新监听的错误会写到应用的日志里。适配器的 `listening` 回调会拿到监听器。这些选项的说明见 [HTTP](HTTP.md) 第 2 节。
|
|
142
|
+
- 网口还不存在时应用也能启动,网口出现后就能访问它的动作。
|
|
143
|
+
- 入口通过 `IInterfaceListening` 按网口监听。这是 lakutata 定义的接口(来自 `@lakutata/core`),这个包实现了它,入口默认加载这个包。另一种实现可以用入口的 `interfaceListening` 选项给出;这个包的 `InterfaceListener` 就是一个 `IInterfaceListener`。
|
|
144
|
+
|
|
145
|
+
不属于入口的服务器由一个组件持有:在 `startup()` 里开始监听,在 `destroy()` 里停止。网口和端口作为组件的选项。
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import {createServer} from 'node:http'
|
|
149
|
+
import express from 'express'
|
|
150
|
+
import {Application, Component, DTO, type ILogger} from 'lakutata'
|
|
151
|
+
import {Configurable, Inject} from 'lakutata/decorator/di'
|
|
152
|
+
import {type InterfaceListener, listenOn} from '@lakutata/netif'
|
|
153
|
+
|
|
154
|
+
class ManagementPages extends Component {
|
|
155
|
+
@Inject('log')
|
|
156
|
+
protected readonly log: ILogger
|
|
157
|
+
|
|
158
|
+
@Configurable(DTO.String().required())
|
|
159
|
+
protected readonly interfaceName: string
|
|
160
|
+
|
|
161
|
+
@Configurable(DTO.Number().port().default(8080))
|
|
162
|
+
protected readonly port: number
|
|
163
|
+
|
|
164
|
+
protected listener: InterfaceListener
|
|
165
|
+
|
|
166
|
+
protected async startup(): Promise<void> {
|
|
167
|
+
const app = express()
|
|
168
|
+
app.get('/', (request, response) => response.send('management'))
|
|
169
|
+
this.listener = await listenOn(this.interfaceName, createServer(app), {port: this.port})
|
|
170
|
+
this.listener.on('error', (error: Error): void => this.log.error(error.message))
|
|
171
|
+
this.listener.on('listening', (): void => this.log.info('listening on %o', this.listener.addresses))
|
|
172
|
+
this.log.info('enforced: %s', this.listener.enforced)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
protected async destroy(): Promise<void> {
|
|
176
|
+
await this.listener?.close()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
Application.run(() => ({
|
|
181
|
+
id: 'device.app',
|
|
182
|
+
name: 'Device',
|
|
183
|
+
components: {
|
|
184
|
+
management: {class: ManagementPages, interfaceName: 'eth1', port: 8080}
|
|
185
|
+
},
|
|
186
|
+
bootstrap: ['management']
|
|
187
|
+
}))
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## 6. 系统、运行环境和打包
|
|
191
|
+
|
|
192
|
+
- **Linux** 把套接字绑定在网口上:系统会挡住别的网口,新加的地址立刻就能访问,UDP 能收到这个网口的广播和组播。Linux 5.7 以前,请给进程 `CAP_NET_RAW` 能力(`setcap cap_net_raw+ep`,或在 systemd 单元里写 `AmbientCapabilities=CAP_NET_RAW`)。
|
|
193
|
+
- **Windows** 监听网口的地址;它的强主机模型会挡住别的网口,除非有网口被设成了弱主机模型(`netsh interface ipv4 set interface "<名字>" weakhostreceive=enabled`,有些负载均衡软件会这样做)或者开了转发。`reason` 会说出是哪个网口。
|
|
194
|
+
- **macOS** 监听网口的地址,除非系统设置了,否则没有保证。macOS 27 对进程隐藏 MAC 地址(给出的都是 `02:00:00:00:00:00`):在那里请按名字指定网口。
|
|
195
|
+
- **Bun**:它的 `https` 服务器收到交过来的连接时不做 TLS,所以在 Bun 上 `listen()` 会拒绝它(用 `https` 选项创建的 Fastify 实例也一样);在 Bun 上请改用 `http2.createSecureServer({key, cert, allowHTTP1: true})` 或 `tls.createServer()`。Bun 的服务器不会关闭交给它的空闲长连接,入口会自己关闭它们。交给服务器的连接不能升级为 WebSocket(在 Bun 1.4 上观察到),所以在 Bun 上,入口按网口监听时不支持 WebSocket。
|
|
196
|
+
- **Electron**:在主进程里使用。
|
|
197
|
+
- `lakutata package` 生成的打包文件、可执行文件和安装包会带上编译好的原生插件(`build/Release/netif.node`):请在每个目标平台上先安装这个包,再在那里打包(见[打包](Packaging.md)和[原生插件](NativeAddons.md))。
|
|
198
|
+
|
|
199
|
+
## 7. 异常
|
|
200
|
+
|
|
201
|
+
| 异常 | 什么时候 |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `NetIfNotFoundException` | 指定的序号没有网口,指定的 MAC 有几个网口 |
|
|
204
|
+
| `NetIfNotEnforcedException` | 用了 `strict`,但系统挡不住别的网口;或者运行中保证丢了(`error` 事件) |
|
|
205
|
+
| `NetIfNotSupportedException` | 系统或运行环境做不到的操作:在 Linux 以外调用 `bindSocket()`,在 macOS 上给绑定在地址上的套接字加组播,在 Bun 上监听 `https` 服务器 |
|
|
206
|
+
| `NetIfTimeoutException` | `waitForAddress()` 超过了 `timeout` |
|
|
207
|
+
| `NetIfException` | 以上所有异常的基类;系统拒绝时,`errno` 是系统的错误码(`'EADDRINUSE'`、`'EACCES'`…) |
|
package/doc/zh/Overview.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lakutata 概述
|
|
2
2
|
|
|
3
|
-
**Lakutata** 是一个基于 IoC 的 Node.js / TypeScript 应用框架,让 Web 服务、命令行工具、桌面(Electron)应用和嵌入式设备共用同一套编程模型。应用是一棵由容器创建的对象树(组件、提供者、控制器):DTO 校验对象的选项和输入,各种传输方式把控制器暴露出去——HTTP(Express、Fastify)、CLI(Commander)、服务调用(HTTP、Socket.IO、JSON-RPC 2.0、gRPC、NATS、Electron IPC)、定时任务和消息队列。同一个控制器方法可以同时通过所有这些方式调用。
|
|
3
|
+
**Lakutata** 是一个基于 IoC 的 Node.js / TypeScript 应用框架,让 Web 服务、命令行工具、桌面(Electron、NW.js)应用和嵌入式设备共用同一套编程模型。应用是一棵由容器创建的对象树(组件、提供者、控制器):DTO 校验对象的选项和输入,各种传输方式把控制器暴露出去——HTTP(Express、Fastify)、CLI(Commander)、服务调用(HTTP、Socket.IO、JSON-RPC 2.0、gRPC、NATS、Electron IPC、NW.js)、定时任务和消息队列。同一个控制器方法可以同时通过所有这些方式调用。
|
|
4
4
|
|
|
5
5
|
`lakutata` 包通过各个子路径重新导出 `@lakutata/*` 系列包(见下表),并为每个应用预置两个组件:`log`(`Logger`,输出与 pino 兼容的 JSON 日志行)和 `entrypoint`(对外提供控制器的 `Entrypoint`)。
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
| `lakutata/decorator/ctrl` | `@HTTPAction`、`@GET`…`@OPTIONS`、`@CLIAction`、`@ServiceAction`、`@Schedule`、`@Prefix`、`@UseMiddlewares` |
|
|
22
22
|
| `lakutata/adapter/express`、`lakutata/adapter/fastify` | HTTP 传输 |
|
|
23
23
|
| `lakutata/adapter/commander` | CLI 传输 |
|
|
24
|
-
| `lakutata/adapter/socket.io`、`lakutata/adapter/jsonrpc`、`lakutata/adapter/electron` | 服务传输 |
|
|
24
|
+
| `lakutata/adapter/socket.io`、`lakutata/adapter/jsonrpc`、`lakutata/adapter/electron`、`lakutata/adapter/nwjs` | 服务传输 |
|
|
25
25
|
| `lakutata/com/logger` | `Logger` 组件,预置为 `log`(输出与 pino 兼容的 JSON 行) |
|
|
26
26
|
| `lakutata/orm`、`lakutata/decorator/orm` | TypeORM 及其装饰器 |
|
|
27
27
|
| `lakutata/com/database`、`lakutata/provider/database` | `Database` 组件和提供者 |
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
| `lakutata/security`、`lakutata/provider/passwordHash` | 哈希、加解密、签名、`PasswordHash` 提供者 |
|
|
36
36
|
| `lakutata/utils` | 辅助函数 |
|
|
37
37
|
|
|
38
|
-
不在本包中的:`@lakutata/cli`(`lakutata` 命令:创建项目、执行迁移、打包应用,见 [Command](Command.md))、`@lakutata/grpc`(gRPC 客户端、服务端和服务传输)、`@lakutata/nats`(NATS 组件和服务传输)、`@lakutata/dbus`(D-Bus)、`@lakutata/mcp`(Model Context Protocol)、`@lakutata/webrtc`(WebRTC:信令、TURN 服务器、媒体服务器、对等连接)、`@lakutata/ffi`(调用动态库中的 C
|
|
38
|
+
不在本包中的:`@lakutata/cli`(`lakutata` 命令:创建项目、执行迁移、打包应用,见 [Command](Command.md))、`@lakutata/grpc`(gRPC 客户端、服务端和服务传输)、`@lakutata/nats`(NATS 组件和服务传输)、`@lakutata/dbus`(D-Bus)、`@lakutata/mcp`(Model Context Protocol)、`@lakutata/webrtc`(WebRTC:信令、TURN 服务器、媒体服务器、对等连接)、`@lakutata/ffi`(调用动态库中的 C 函数)、`@lakutata/math`(符号计算和数值计算,直接作为函数使用,不需要 lakutata 应用)和 `@lakutata/netif`(服务器和 UDP 套接字按网口监听,不管网口的地址怎么变;本机的网口和它们的变化)。
|
|
39
39
|
|
|
40
40
|
## 指南
|
|
41
41
|
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
| [Middlewares](Middlewares.md) | 动作的中间件与访问控制 |
|
|
60
60
|
| [HTTP](HTTP.md) | HTTP 适配器:路由、输入、响应、错误,以及 Express 和 Fastify 的功能 |
|
|
61
61
|
| [CLI](CLI.md) | 命令行工具:命令、选项、提示输入、输出与退出码 |
|
|
62
|
-
| [ServiceCalls](ServiceCalls.md) | 通过 HTTP、Socket.IO、JSON-RPC 2.0 和
|
|
62
|
+
| [ServiceCalls](ServiceCalls.md) | 通过 HTTP、Socket.IO、JSON-RPC 2.0、Electron 和 NW.js 调用服务动作:模式、响应、客户端及其事件 |
|
|
63
63
|
| [GRPC](GRPC.md) | 通过 gRPC 提供服务动作(`@lakutata/grpc`),以及从其他应用和其他语言调用它们 |
|
|
64
64
|
| [NATS](NATS.md) | NATS 组件、通过 NATS 提供服务动作(`@lakutata/nats`)、服务代理及其事件 |
|
|
65
65
|
| [Schedules](Schedules.md) | 定时任务:间隔、cron 表达式、只在一个实例中执行的任务 |
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
| [WebRTC](WebRTC.md) | 点对点连接和媒体服务器(`@lakutata/webrtc`):信令、TURN 服务器、浏览器和应用、通过数据通道调用应用的动作、WHIP 和 WHEP |
|
|
82
82
|
| [FFI](FFI.md) | 调用动态库中的 C 函数(`@lakutata/ffi`):从头文件复制的声明、结构体、回调、内存、打包 |
|
|
83
83
|
| [Math](Math.md) | 数学计算(`@lakutata/math`):公式和精确结果、方程、任意精度的十进制数、矩阵、统计、数值解法、单位 |
|
|
84
|
+
| [Netif](Netif.md) | 网口(`@lakutata/netif`):服务器按网口监听,不管网口的地址怎么变;挡住别的网口的连接;网口和它们的变化;单个网口上的 UDP |
|
|
84
85
|
| [Command](Command.md) | `lakutata` 命令:创建项目、迁移、翻译、环境变量 |
|
|
85
86
|
| [Workspaces](Workspaces.md) | 由多个包组成的项目:运行各个包的脚本、统一版本号、添加包 |
|
|
86
87
|
| [NativeAddons](NativeAddons.md) | 用 C++ 或 Rust 编写的原生插件:创建、链接库、发布 |
|