pinomad 0.1.0 → 0.1.1
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 +48 -3
- package/dist/{chunk-srbefzgp.js → chunk-8h3wrn84.js} +2 -2
- package/dist/{chunk-jew71ysz.js → chunk-9z7echn7.js} +2 -2
- package/dist/{chunk-v7yf5752.js → chunk-fpb6f9n5.js} +1 -1
- package/dist/{chunk-hach8mdw.js → chunk-mqbg2cbw.js} +24 -11
- package/dist/{chunk-pkxr5my9.js → chunk-ne6a5q2j.js} +1 -1
- package/dist/{chunk-c3v7vhny.js → chunk-tpd3trvd.js} +1 -1
- package/dist/{chunk-928atme2.js → chunk-zx77w1nm.js} +1 -1
- package/dist/pinomad.js +6 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,7 +138,39 @@ Restart=always
|
|
|
138
138
|
WantedBy=multi-user.target
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
#### 容器平台(Zeabur 等)
|
|
142
|
+
|
|
143
|
+
镜像在 `deploy/relay/Dockerfile`,装的是锁版本的 npm 发布包(`ARG PINOMAD_VERSION`)。平台终结 TLS 并转发到 `$PORT`(默认 8080),不需要 Caddy。中继把转接状态放在内存里,**只能跑一个实例**,不要开扩容。
|
|
144
|
+
|
|
145
|
+
| 环境变量 | 含义 |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `PINOMAD_PUBLIC_ORIGIN` | 客户端打开的 https origin,比如 `https://nomad.example.com` |
|
|
148
|
+
| `PINOMAD_ALLOW_HOST` | 允许登记的 hostId,多个用空格分隔(每台宿主上跑 `pinomad relay-id`) |
|
|
149
|
+
| `ZBPACK_DOCKERFILE_PATH=Dockerfile` | **Zeabur 必加**:目录里只有一个 Dockerfile 时,Zeabur 会把它识别成静态站点、用 Caddy 托管文件,中继根本没启动;这个变量强制按 Dockerfile 构建(部署日志里应是 `PLANTYPE=docker`) |
|
|
150
|
+
|
|
151
|
+
Zeabur 上的步骤(在 Dedicated Server 上部署,用 CLI 上传目录,不接 GitHub):
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
cd deploy/relay
|
|
155
|
+
zeabur deploy --project-id <项目 id> --create --name pinomad-relay # 首次:在项目里建出服务
|
|
156
|
+
# 之后重新部署同一个服务:
|
|
157
|
+
zeabur deploy --service-id <服务 id>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
然后在服务上配好上面的环境变量(`PORT` 由平台注入,不用自己设),在 Domains 里绑定自定义域名,到 DNS 加一条记录:Dedicated Server 用 **A 记录指向服务器 IP**;Cloudflare 上建议**关掉代理(灰云)**,橙云会让国内访问多绕一圈,证书也由 Zeabur 自己签。域名状态变成 `PROVISIONED` 后,验证:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
curl -sI https://nomad.example.com/ # 200,中继托管的 Web 客户端
|
|
164
|
+
pinomad service install --relay https://nomad.example.com … # 宿主接上中继
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
宿主登记成功会在日志里写一行 `{"event":"relay","status":"registered",…}`,掉线重连时写 `"status":"connecting"`(`pinomad service logs` 查看);也可以从外面连设备入口 `wss://<origin>/c/<hostId>` 确认:宿主在线时连接保持打开(等握手),没登记会立刻以 `4604`(hostOffline)关闭。
|
|
168
|
+
|
|
169
|
+
也可以从 GitHub 仓库部署:Root Directory 设 `deploy/relay`、Watch Paths 限 `deploy/relay/**`,其余同上。
|
|
170
|
+
|
|
171
|
+
#### 升级
|
|
172
|
+
|
|
173
|
+
中继托管的 Web 客户端和宿主版本不一致时,客户端会显示需要更新;两边一起升级——宿主 `pinomad upgrade`,VPS 上 `npm i -g pinomad@latest` 后重启中继;容器部署则是改 Dockerfile 里的 `PINOMAD_VERSION`,再在 `deploy/relay` 下重新 `zeabur deploy --service-id <服务 id>`(从 GitHub 部署的话合进 main 即触发)。宿主不在中继的 `--allow-host` 里时,宿主日志和 Web 警告会带着要补的 `--allow-host <hostId>`。
|
|
142
174
|
|
|
143
175
|
npm 装的实例和本机任何开发检出互不相干(各自的数据目录、进程、包路径都独立),所以在同一台机器上开发 PiNomad 不会碰到正在跑的服务。
|
|
144
176
|
|
|
@@ -160,8 +192,9 @@ pinomad upgrade [--force]
|
|
|
160
192
|
- `restart` 先连上宿主读任务图,有未结束的任务就等它们跑完再重启(Ctrl-C 取消);`--force` 立即重启,交给 Durable 恢复。打包安装时 `upgrade` 把 `pinomad@latest` 装进运行包所在的 npm 全局前缀(非全局安装会拒绝执行)+ 同样的 restart;源码检出里(`bun run upgrade`)依次 `git pull --ff-only`、`bun install`、`bun run build`,工作区有未提交改动时拒绝执行。前面任何一步失败都直接停下,不重启,服务继续跑旧代码。
|
|
161
193
|
- `uninstall` 只卸载服务,不动数据目录。
|
|
162
194
|
- macOS:宿主以登录会话运行,机器重启后要能自动起来需在系统设置里打开自动登录,并为这台机器关掉睡眠。日志写到 `<数据目录>/logs/host.log`。
|
|
163
|
-
- Linux:要在未登录时也运行需自己执行 `loginctl enable-linger`(install 检测到没开会提示);日志走 `journalctl --user
|
|
164
|
-
-
|
|
195
|
+
- Linux:要在未登录时也运行需自己执行 `loginctl enable-linger`(install 检测到没开会提示);日志走 `journalctl --user`。开着 ufw 时,局域网设备访问 `--remote-port` 要先放行,比如 `sudo ufw allow from 192.168.0.0/24 to any port 7422 proto tcp`(只走中继就不用开)。
|
|
196
|
+
- **服务绑定安装时的 node 路径**:单元里写的是 node 和包的绝对路径。用 mise / nvm 管理 Node 时,路径里带着具体版本号(`…/node/26.8.2/…`);换了 Node 版本、旧目录被删后服务就起不来,要用新版本重新 `npm i -g pinomad`,再跑一次 `pinomad service install …`。`pinomad upgrade` 只升 pinomad,装回原来的前缀,不受影响。
|
|
197
|
+
- 服务模式下**回环端口也用 HTTP 提供构建好的 Web 客户端**。npm 安装时 `pinomad link` 总是指向宿主自己提供的 Web;源码检出里按顺序探测:正在跑的 vite 开发服务器(5199)优先,否则宿主提供 Web 的端口,都没有则仍是 5199。开发时 `bun run start` + vite 照旧。
|
|
165
198
|
- 服务在跑时 `bun run start` 会因为目录锁拿不到而失败(同一数据目录只有一个宿主);开发时用另一个 `--data-dir`,或先 `uninstall`。
|
|
166
199
|
|
|
167
200
|
Windows 不在支持范围;其他平台跑 `service` 会直接报错退出。
|
|
@@ -173,3 +206,15 @@ bun run typecheck
|
|
|
173
206
|
bun run test # vitest + Playwright,首次跑前先 bunx playwright install chromium
|
|
174
207
|
bun run build
|
|
175
208
|
```
|
|
209
|
+
|
|
210
|
+
## 发布(ADR-0016)
|
|
211
|
+
|
|
212
|
+
推 `v*` tag 触发 `.github/workflows/release.yml`:跑完 typecheck / test / build、打包、`npm pack`、装包冒烟后才 `npm publish`。带 `-` 的预发布 tag(`v0.2.0-beta.1`)发到 `next`,正式版发到 `latest`。发布用 npm trusted publishing(OIDC),仓库里没有 npm token。
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
git tag v0.1.1
|
|
216
|
+
git push origin v0.1.1
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- 手动发布(流水线不可用时):`bun run build && bun run package -- --version <v>`,再在 `apps/cli/out` 里 `npm publish`。终端里的 npm 要先 `npm login`(`npm whoami` 能报出用户名);没登录时 npm 回的是 `404 Not Found` 而不是 401,容易误判成包名问题。
|
|
220
|
+
- 发布后几分钟内 registry 可能还没处理完(新包还会先出现一个 `0.0.0-stage` 占位版本);这期间查过的机器缓存了旧元数据,安装报 `ETARGET` / "No matching version" 时加 `--prefer-online`。
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
restartService,
|
|
4
4
|
serviceManager
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-zx77w1nm.js";
|
|
6
6
|
import {
|
|
7
7
|
globalPrefixOf,
|
|
8
8
|
packageDir,
|
|
9
9
|
packagedVersion
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-fpb6f9n5.js";
|
|
11
11
|
import"./chunk-y73t2jmc.js";
|
|
12
12
|
import"./chunk-wgn3cx5q.js";
|
|
13
13
|
import"./chunk-cjfmfyjb.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// apps/host/src/distribution.ts
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
var packagedVersion = "0.1.
|
|
3
|
+
var packagedVersion = "0.1.1";
|
|
4
4
|
var webRoot = fileURLToPath(new URL(packagedVersion === undefined ? "../../web/dist" : "../web/", import.meta.url));
|
|
5
5
|
var hostEntry = packagedVersion === undefined ? { script: fileURLToPath(new URL("./main.ts", import.meta.url)), args: [] } : { script: fileURLToPath(new URL("./pinomad.js", import.meta.url)), args: ["host"] };
|
|
6
6
|
var commandHint = (name) => packagedVersion === undefined ? `bun run ${name}` : `pinomad ${name}`;
|
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
serveWebClient
|
|
4
4
|
} from "./chunk-r6w5aqkw.js";
|
|
5
5
|
import {
|
|
6
|
+
packagedVersion,
|
|
6
7
|
webRoot
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-fpb6f9n5.js";
|
|
8
9
|
import {
|
|
9
10
|
PROTOCOL_VERSION,
|
|
10
11
|
UNAUTHORIZED_CLOSE_CODE,
|
|
@@ -1446,6 +1447,12 @@ function startRelayLink(options) {
|
|
|
1446
1447
|
let delay = minDelay;
|
|
1447
1448
|
let retryTimer;
|
|
1448
1449
|
let pendingAccepts = 0;
|
|
1450
|
+
function setState(next) {
|
|
1451
|
+
const changed = state.status !== next.status || state.status === "rejected" && next.status === "rejected" && state.reason !== next.reason;
|
|
1452
|
+
state = next;
|
|
1453
|
+
if (changed)
|
|
1454
|
+
options.onStateChange?.(next);
|
|
1455
|
+
}
|
|
1449
1456
|
const watched = new Set;
|
|
1450
1457
|
const ponged = new WeakMap;
|
|
1451
1458
|
const pingIntervalMs = options.pingIntervalMs ?? 30000;
|
|
@@ -1505,7 +1512,7 @@ function startRelayLink(options) {
|
|
|
1505
1512
|
break;
|
|
1506
1513
|
case "registered":
|
|
1507
1514
|
delay = minDelay;
|
|
1508
|
-
|
|
1515
|
+
setState({ status: "registered" });
|
|
1509
1516
|
break;
|
|
1510
1517
|
case "incoming": {
|
|
1511
1518
|
if (pendingAccepts >= maxPendingAccepts)
|
|
@@ -1532,14 +1539,12 @@ function startRelayLink(options) {
|
|
|
1532
1539
|
const rejection = REJECTION_REASONS[code];
|
|
1533
1540
|
if (rejection !== undefined) {
|
|
1534
1541
|
const reason = `relay ${origin}: ${rejection(hostId)} (${code})`;
|
|
1535
|
-
|
|
1536
|
-
options.onRejected?.(reason);
|
|
1537
|
-
state = { status: "rejected", reason };
|
|
1542
|
+
setState({ status: "rejected", reason });
|
|
1538
1543
|
delay = maxDelay;
|
|
1539
1544
|
reconnect(maxDelay);
|
|
1540
1545
|
return;
|
|
1541
1546
|
}
|
|
1542
|
-
|
|
1547
|
+
setState({ status: "connecting" });
|
|
1543
1548
|
reconnect(delay);
|
|
1544
1549
|
delay = Math.min(maxDelay, delay * 2);
|
|
1545
1550
|
}
|
|
@@ -1627,17 +1632,25 @@ async function startGateway(options) {
|
|
|
1627
1632
|
remote = await startRemote(options, register);
|
|
1628
1633
|
const secure = options.secure;
|
|
1629
1634
|
if (secure?.relay !== undefined) {
|
|
1630
|
-
|
|
1635
|
+
let link;
|
|
1636
|
+
link = startRelayLink({
|
|
1631
1637
|
origin: secure.relay.origin,
|
|
1632
1638
|
signingKey: secure.relay.signingKey,
|
|
1633
1639
|
accept: (socket) => secureHandshake(socket, secure, options.harness, register),
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1640
|
+
onStateChange: (state) => {
|
|
1641
|
+
if (state.status === "registered") {
|
|
1642
|
+
console.log(JSON.stringify({ event: "relay", status: "registered", origin: link.origin, hostId: link.hostId }));
|
|
1643
|
+
} else if (state.status === "connecting") {
|
|
1644
|
+
console.log(JSON.stringify({ event: "relay", status: "connecting", origin: link.origin }));
|
|
1645
|
+
} else {
|
|
1646
|
+
broadcast("warning", state.reason);
|
|
1647
|
+
console.error(state.reason);
|
|
1648
|
+
}
|
|
1637
1649
|
},
|
|
1638
1650
|
...secure.relay.reconnectDelayMs === undefined ? {} : { reconnectDelayMs: secure.relay.reconnectDelayMs },
|
|
1639
1651
|
...secure.relay.pingIntervalMs === undefined ? {} : { pingIntervalMs: secure.relay.pingIntervalMs }
|
|
1640
1652
|
});
|
|
1653
|
+
relay = link;
|
|
1641
1654
|
}
|
|
1642
1655
|
return {
|
|
1643
1656
|
url: `ws://127.0.0.1:${port}`,
|
|
@@ -2656,7 +2669,7 @@ async function main(argv) {
|
|
|
2656
2669
|
const options = values.faux !== undefined ? fauxOptions(() => () => fauxAssistantMessage(values.faux)) : await piOptions();
|
|
2657
2670
|
const host = await openHost(options);
|
|
2658
2671
|
const model = options.initialModel === undefined ? null : `${options.initialModel.provider}/${options.initialModel.modelId}`;
|
|
2659
|
-
const web = "http://127.0.0.1:5199/";
|
|
2672
|
+
const web = packagedVersion === undefined ? "http://127.0.0.1:5199/" : new URL(host.url.replace(/^ws/, "http")).href;
|
|
2660
2673
|
console.log(JSON.stringify({
|
|
2661
2674
|
event: "ready",
|
|
2662
2675
|
url: host.url,
|
package/dist/pinomad.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
packagedVersion
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-fpb6f9n5.js";
|
|
5
5
|
import {
|
|
6
6
|
__require
|
|
7
7
|
} from "./chunk-df32avw6.js";
|
|
8
8
|
|
|
9
9
|
// apps/cli/src/pinomad.ts
|
|
10
10
|
var COMMANDS = {
|
|
11
|
-
host: () => import("./chunk-
|
|
12
|
-
relay: () => import("./chunk-
|
|
13
|
-
service: () => import("./chunk-
|
|
14
|
-
upgrade: () => import("./chunk-
|
|
15
|
-
link: () => import("./chunk-
|
|
11
|
+
host: () => import("./chunk-mqbg2cbw.js"),
|
|
12
|
+
relay: () => import("./chunk-ne6a5q2j.js"),
|
|
13
|
+
service: () => import("./chunk-9z7echn7.js"),
|
|
14
|
+
upgrade: () => import("./chunk-8h3wrn84.js"),
|
|
15
|
+
link: () => import("./chunk-tpd3trvd.js"),
|
|
16
16
|
pair: () => import("./chunk-hsdzb73r.js"),
|
|
17
17
|
"relay-id": () => import("./chunk-7fqn69ys.js")
|
|
18
18
|
};
|