@bysir/herdr-web 0.1.0 → 0.2.0

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.
Files changed (2) hide show
  1. package/README.md +155 -16
  2. package/package.json +5 -5
package/README.md CHANGED
@@ -21,7 +21,16 @@ herdr-web # 只听 127.0.0.1
21
21
  curl -fsSL https://raw.githubusercontent.com/zbysir/herdr-web/master/install.sh | sh
22
22
  ```
23
23
 
24
- 装到 `~/.local/bin`,改地方用 `HERDR_WEB_INSTALL_DIR=`。装脚本**强制校验 sha256**,没有 `sha256sum`/`shasum` 就直接拒绝装 —— 这东西后面挂着一个登录 shell。
24
+ 装到 `~/.local/bin`。要换地方,变量得给 `sh` 而**不是** `curl`:
25
+
26
+ ```bash
27
+ curl -fsSL …/install.sh | HERDR_WEB_INSTALL_DIR=/opt/bin sh # 对
28
+ HERDR_WEB_INSTALL_DIR=/opt/bin curl -fsSL …/install.sh | sh # 错,变量给了 curl,脚本收不到
29
+ ```
30
+
31
+ 写错那条**不会报错**,它会安安静静装到默认目录去。同理 `HERDR_WEB_INSTALL_VER=v0.1.0` 装指定版本。
32
+
33
+ 装脚本**强制校验 sha256**,没有 `sha256sum`/`shasum` 就直接拒绝装 —— 这东西后面挂着一个登录 shell。
25
34
 
26
35
  其它几条路:
27
36
 
@@ -65,10 +74,42 @@ herdr-web unlock # 解开「失败太多」的全局熔断
65
74
 
66
75
  原来那把永不过期的 `~/.herdr-web/token` 降级成**只能引导**:旧书签第一次打开会自动换成设备凭据、并把 URL 里的 token 抹掉,之后就该 `rm ~/.herdr-web/token`。细节和为什么这么设计看 [SECURITY.md](SECURITY.md)。
67
76
 
68
- 连上之后**自动敲 `herdr`**。想敲别的、或者不想自动敲:`HERDR_WEB_ONCONNECT`(设成空串就留在 shell 里)。顶栏原来那个「敲 herdr」按钮去掉了 —— 自动敲之后它一天用不上一次,而软键条预设里有现成的「敲 herdr」键,要就自己放一个上去。
77
+ 连上之后**自动敲 `herdr`**。想敲别的、或者不想自动敲:`HERDR_WEB_ONCONNECT`(设成空串就留在 shell 里)。地址栏里加一段路径(`/work`)就是**另一个 herdr session**,见下面「[一个 URL 一个 session](#一个-url-一个-sessionname)」。顶栏原来那个「敲 herdr」按钮去掉了 —— 自动敲之后它一天用不上一次,而软键条预设里有现成的「敲 herdr」键,要就自己放一个上去。
69
78
 
70
79
  **管理页在 `http://127.0.0.1:<端口+1>/`**(启动横幅里有):看证书状态、点一下签发/续期、生成 DNS 的 `.env` 片段、出配对码、踢设备。它**只绑 loopback,公网上不存在**,所以不需要登录 —— 能连上它的东西已经有你的 shell 了。为什么不做成「主服务上一个需要认证的页面」:认证是会失效的控制,「碰不到」是个性质;而且**管理页不能依赖它自己要管的那个证书**(证书一坏就打不开修证书的页面)。
71
80
 
81
+ ## 一个 URL 一个 session(`/{name}`)
82
+
83
+ 地址栏里加一段就是**另一个 herdr**:
84
+
85
+ ```
86
+ https://herdr.bysir.top/ 默认 session(老行为,敲的是 `herdr`)
87
+ https://herdr.bysir.top/work 敲 `herdr --session work` —— 没有这个 session 就新建一个
88
+ https://herdr.bysir.top/scratch 再来一个,和上面那个互不相干
89
+ ```
90
+
91
+ `herdr --session <name>` 是 herdr 自己的**命名持久 session**:各有一个 server 进程、各有一套
92
+ workspace / tab / pane。所以书签存成 `/work` 和 `/scratch`,两个标签页就是两套工作现场,
93
+ 关掉浏览器再回来还在(session 是持久的,网页断开只是客户端断开)。
94
+
95
+ 要知道的几条:
96
+
97
+ - **发件箱和面板一览跟着 URL 走。** 命名 session 有自己的 socket
98
+ (`~/.config/herdr/sessions/<name>/herdr.sock`,`herdr session list --json` 里就是这个),
99
+ 所以页面上每个请求都带着 session 名。这是这个功能里唯一真正危险的地方 —— 拿默认 session 的
100
+ socket 去投一个 `/work` 页面上选的 pane,话会**静默进另一个 herdr**,而两边屏幕上都看不出
101
+ 异常。服务端因此**不给不合法的名字兜底**(不退回默认 session,直接报错)。
102
+ - **名字只能是 `[A-Za-z0-9._-]`、首字符字母数字、最长 40。** 因为它要被拼进一条敲进登录
103
+ shell 的命令行,也要被拼进 socket 路径。不合法时页面上会直接说,而不是给你开一个别的 session。
104
+ - **`HERDR_WEB_ONCONNECT` 不参与带 session 的 URL**(包括设成空串「什么都别敲」那种):
105
+ 地址栏里点名要哪个 session,比一个全局默认具体。`/` 还是老规矩。
106
+ - **顶栏左边会显示当前 session 名**(设置 →「终端」页底下也有一行,连着那个 session 的 socket
107
+ 路径)。默认 session 不显示这个标签 —— 没有标签就是默认那个。
108
+ - 一个进程最多同时盯 16 个 session(每个都带一条 agent 状态订阅)。超了会说,重启清空。
109
+ - 「添加到主屏幕」存的是 manifest 里的 `start_url`(`/`),所以从主屏图标进的是默认 session。
110
+ 要一个直达 `/work` 的图标,用浏览器书签。
111
+ - `herdr session list` / `stop` / `delete` 在终端里管这些 session,herdr-web 这边只负责「开/接上」。
112
+
72
113
  ## 只开本机 shell
73
114
 
74
115
  要连别的机器就在 herdr 里 ssh —— herdr 自己就能干这事,所以这一层不再实现主机管理和托管私钥(那还得管密钥落盘、`ssh-keygen`、`~/.ssh` 扫描、ssh_config 导入),连带把「浏览器能碰到私钥」这个安全面也一起去掉了。
@@ -150,15 +191,19 @@ herdr 那边一切焦点,画面自己就跟过来了;键盘那套操作一
150
191
 
151
192
  | 排序 | 规则 |
152
193
  |---|---|
153
- | **优先级**(默认)| 按「还需不需要你」分档:**等你回答 > 跑完了 > 在跑 / 闲着 > 认不出**,同一档里按最近动过排 |
154
- | **最近** | 不分状态,纯按最近动过排 |
155
- | **herdr 顺序** | workspace / tab / pane 的原顺序,按 workspace 分组 —— 和你在 herdr 里看到的一样 |
194
+ | **优先级**(默认)| 按「多想让你看一眼」分档:**等你回答 > 跑完了 > 在跑 > 闲着 > agent**,同一档里按最近动过排 |
195
+ | **分组** | workspace 分组,组里是 tab / pane 的原顺序 —— 和你在 herdr 里看到的一样 |
196
+
197
+ 状态点的颜色:**红 = 等你,绿 = 跑完了,黄 = 在跑**,闲着是灰点。「在跑」用黄不用绿,和 herdr
198
+ 自己 agents 栏那个黄点一致;绿留给「跑完了」(通用约定,一眼知道是好事)。只有闲着不给颜色 ——
199
+ 一列点全是彩的就没有重点了。
156
200
 
157
- `在跑` `闲着` 故意合成一档:两个都不需要你,谁该在前面没有客观答案,交给「刚动过」去定
158
- (一个刚开始跑的 agent 比一个闲了三天的更值得看一眼)。`等你` / `完成` 两档在行上额外挂一个
159
- 小标签,别的状态只有那个点的颜色 —— 每行都塞标签就没有重点了。
201
+ `在跑` 单独一档。一开始把它和 `闲着` 合成了一档(理由是两个都不需要你,谁在前面没有客观
202
+ 答案,交给「刚动过」去定),实际用起来不对:黄点那个正在跑的会被十几个闲着的埋掉,而列表里
203
+ 最想一眼看到的恰恰是它。`等你` / `完成` 两档在行上额外挂一个小标签,别的状态只有那个点的
204
+ 颜色 —— 每行都塞标签就没有重点了。
160
205
 
161
- **排序只认 `state_change_seq`**(herdr 里的全局递增计数,每次 agent 状态变化推高一格),
206
+ **同一档里认 `state_change_seq`**(herdr 里的全局递增计数,每次 agent 状态变化推高一格),
162
207
  不认时间。因为 herdr 的 API 里**一个时间戳都没有**:`agent.list` 只给这个计数,事件里也不带
163
208
  时间。计数一直是对的,所以排序一直是对的。
164
209
 
@@ -167,10 +212,14 @@ herdr 那边一切焦点,画面自己就跟过来了;键盘那套操作一
167
212
  时间是 **herdr-web 自己盯着状态变化打的**(`internal/agentwatch`:订一条
168
213
  `pane.agent_status_changed`,收到就记 `time.Now()`)。所以:
169
214
 
170
- - **herdr-web 刚起来时这一列是空的**,随着一次次状态变化填回来。空着是实话 —— 那个变化发生在
171
- 本进程起来之前,没法知道是什么时候,编一个时间比空着糟得多。
172
- - 不落盘。`pane_id` 在 herdr 重启之后会重新分配(这次的 `w1:p4K` 下次是别的 pane),而 herdr
173
- 一重启所有 agent 会话本来也都换了 —— 存下来的时间只会张冠李戴。
215
+ - **第一次跑的时候这一列是空的**,随着一次次状态变化填回来。空着是实话 —— 那个变化发生在
216
+ 开始盯之前,没法知道是什么时候,编一个时间比空着糟得多。列表底下会说明这一句。
217
+ - **按 `terminal_id` 存盘**(`~/.herdr-web/agent-seen.json`),所以 herdr-web 自己重启
218
+ (升级、改配置)不丢时间。不能按 `pane_id` 存:那是 herdr 里的位置编号,pane 一开一关就
219
+ 重新分配给别人了,会张冠李戴。herdr 重启之后终端 id 全是新的,旧记录自然对不上 —— 存盘时
220
+ 只写这会儿还在的终端,文件自己就不会长胖。
221
+ - **状态不存盘**,只存时间。存了状态的话,重启后拿旧状态一比就会把「停机期间变的」记成
222
+ 「刚刚变的」。
174
223
  - 订阅没连上(herdr server 没在跑)时,列表底下会说一句,免得空着的时间列看着像坏了。
175
224
  - 显示用 `3m` `2h` `4d` 这种紧凑写法(不到 45 秒算「刚刚」),完整时间在 title 里。手机上这一列
176
225
  只有几十像素,「3 分钟前」四个字放不下。
@@ -225,7 +274,7 @@ herdr 那边一切焦点,画面自己就跟过来了;键盘那套操作一
225
274
  - 预设分 8 组(前缀 / 标签 / Pane / 工作区 / 终端按键 / 文本 / Claude 命令 / 网页端动作),「载入预设」一下全进「我的按键」。herdr 那几组抄的是 `herdr --default-config` 的 `[keys]` 默认值,改过 keybinding 的人自己改;「Claude 命令」是 `/new` `/clear` `/compact` `/usage` `/context` `/model` `/resume` `/cost`,都带回车,一下点完。
226
275
  - 每个键有个**「两下」**勾选框:勾上的键要点两次才真发出去 —— 第一下只是举起来(键变红,文字不变,免得按键变宽把手指底下的键挪走),3 秒不点、或者点了别的键就放下。软键条上键挨得近,关 pane / 关标签这种误触没法撤销。预设里 `关 pane` `关标签` `关工作区` `断开` `/clear` 默认就带。
227
276
  - `Ctrl` / `Alt` 是**粘滞**的:点一下亮起,再敲一个字母就发出对应组合键,然后自动灭掉。手机虚拟键盘的 keydown 不可靠,所以这层是在数据流上做的,不依赖按键事件。写法是 `sticky:ctrl` / `sticky:alt`。
228
- - `act:` 是**网页端自己处理**的动作,不发任何字节:`act:kbd` 呼出 / 收起系统键盘,`act:img` 传图(弹相机 / 相册,路径按「发件箱开没开」决定去草稿还是直接敲进终端),`act:panes` 开「面板一览」(上面那节)。服务端只认白名单里这三个,写错了保存时就报错,不会下发一个点了没反应的键。
277
+ - `act:` 是**网页端自己处理**的动作,不发任何字节:`act:kbd` 呼出 / 收起系统键盘,`act:img` 传图(弹相机 / 相册,路径按「发件箱开没开」决定去草稿还是直接敲进终端),`act:panes` 开「面板一览」(上面那节),`act:clip` 把机器上的剪贴板取到手机剪贴板,`act:paste` 把手机剪贴板粘进终端(后两个见「[手机上怎么复制 / 粘贴](#手机上怎么复制--粘贴)」——**手机上这两条只能是点出来的**,浏览器不给定时器碰剪贴板)。服务端只认白名单里这几个,写错了保存时就报错,不会下发一个点了没反应的键。
229
278
  `act:panes` 放在软键条上是有讲究的:手机上键盘一弹起来顶栏整段就收掉了,那时候顶栏那个入口点不到,而软键条正好在拇指底下。
230
279
  - 按键谱在**服务端**解析成字节再下发,前端只管照发;写错了保存时会告诉你是第几个按键、哪里不认。回包里 `send` 是**解析好的字节**、`spec` 是你写的谱 —— 编辑器回传时两个字段都在,服务端**认 `spec`**。拿 `send` 当谱重解一次的话,Tab 的 `"\t"` 去掉空白就是空串,报「按键谱是空的」而用户什么都没改(踩过)。
231
280
 
@@ -250,6 +299,51 @@ xterm.js 的触屏支持基本只有「点一下聚焦隐藏 textarea」,剩
250
299
 
251
300
  端到端验过:在一个独立的 herdr session 里竖分屏,长按分界线**右边 3 格**再拖,分界从第 45/46 列移到 40/41 列;正好在贯穿竖线上竖划,发出去的仍然只有滚轮上报。
252
301
 
302
+ ### 手机上怎么复制 / 粘贴
303
+
304
+ 先说结论:手机上要**两个软键**(设置 →「软键条」→ 载入预设,「网页端动作」组里的
305
+ **📋 取** 和 **📥 粘**,拖到条上)。原因是下面两条,一条比一条反直觉。
306
+
307
+ **第一条:herdr 复制到的是「跑 herdr 那台机器」的剪贴板,不是手机的。**
308
+
309
+ 手机上长按拖选(那一下会被翻成鼠标拖动发给 herdr,配上 herdr 自己的 `copy_on_select` 就是
310
+ 一次复制),herdr 弹「copied 84 chars to clipboard」——**那 84 个字进的是 Mac 的剪贴板**
311
+ (`pbpaste` 读出来一字不差),浏览器一无所知,手机上哪儿都粘不出来。看着像「复制失败」,
312
+ 其实是复制成功了、只是落在另一台设备上。
313
+
314
+ 所以有了 **📋 取**(`act:clip`):点一下,服务端读机器上的剪贴板(`pbpaste` /
315
+ `wl-paste` / `xclip`,见 `internal/clip`)交给网页,网页写进手机剪贴板。之后在手机上随便哪儿
316
+ 长按粘贴都是它。
317
+
318
+ 反方向是 **📥 粘**(`act:paste`):点一下读手机剪贴板,按**括号粘贴**送进终端(多行不会被
319
+ 当成一行一次回车)。触屏上没法 `⌘V`,也没法长按呼出终端的粘贴菜单(单指手势被接管了),
320
+ 这个键是唯一的入口。
321
+
322
+ **为什么不能做成一个「自动同步」**:浏览器只在**用户手势**里给读写剪贴板,定时器里偷偷做
323
+ 一律被拒 —— 而且是静默的。所以两个方向各要一次点击,这一下是浏览器的硬要求,不是没做。
324
+
325
+ 触屏上**选不了字**(单指手势整个被上面那套接管了),所以另一条复制路径是 **herdr 自己的
326
+ COPY 模式**:`ctrl+b` 前缀进去,`hjkl` 选、`y` 复制。它走 **OSC 52** —— 终端里的程序把文本推给
327
+ 网页,网页再写系统剪贴板。
328
+
329
+ **但浏览器不一定让写,而且以前是静默失败的。** 两条限制叠在一起:`navigator.clipboard` 只在
330
+ 安全上下文里存在(局域网 http 上压根没这个对象),而且手机浏览器要求这一次写发生在**用户
331
+ 手势**里。COPY 模式和「选中即复制」的触发点都不是点击,于是在手机上被拒 —— 屏幕上选区好好的、
332
+ 一句提示都没有,剪贴板里还是上一次的东西。
333
+
334
+ 现在写不进去就在底部出一条**「点一下复制」**:那一下点击本身就是手势,按一下就进剪贴板。
335
+ 连 `execCommand` 也被拒的话,它把文本摊在一个**已经全选好**的框里,长按 → 「拷贝」。
336
+
337
+ 还有一个**桌面上也会踩**的坑:**标签页不可见时 Chrome 让 `writeText` 那个 promise 永远挂着**
338
+ (实测 26 秒既不 resolve 也不 reject,剪贴板也确实没变)。光 `await` 的话「写不进去」永远发现
339
+ 不了,所以那一步有 1.2 秒上限,超时就当失败、往后面两条路走。
340
+
341
+ 两条相关的:
342
+
343
+ - **「选中即复制」是鼠标那一档的设置**,触屏上没有选区,手机上开它不起作用。
344
+ - 想在电脑上看那条提示长什么样:URL 上加 **`?nocopy=1`**,两条写剪贴板的路都当成失败
345
+ (和 `?poll=` / `?push=` 一样是调试参数)。
346
+
253
347
  为什么要这么绕:xterm.js 只把 `wheel` 翻译成鼠标上报、完全不管 touch,所以 herdr 这种「占着备用屏幕(本地没 scrollback 可滚)+ 开了鼠标上报」的程序在手机上两头都不响应,彻底滚不动。而点击和长按又都会落到隐藏 textarea 上,浏览器顺手就把键盘顶出来 —— 在 TUI 里十次里有九次只是想点个 pane,不是想打字。
254
348
 
255
349
  做法是在 `touchstart` 上**无条件** `preventDefault`(单指手势),一次性掐掉聚焦、长按气泡、双击缩放和浏览器补发的兼容鼠标事件,然后自己在 `touchend` 里按位移和时长把手势分成上面几类。
@@ -348,9 +442,13 @@ internal/
348
442
  softkeys/ 软键条配置 + 按键谱解析(data.go 是从旧 JS 版生成的,不是手抄的;
349
443
  testdata/js-snapshot.json 存着当时的快照,测试比对前 6 组)
350
444
  uploads/ 图片落盘(按魔数认类型)
445
+ clip/ 读这台机器的剪贴板(pbpaste / wl-paste / xclip)—— herdr 的复制
446
+ 落在**跑 herdr 那台机器**上,手机要拿到只能由这一侧读出来
351
447
  server/ HTTP 路由 + PTY/WebSocket + 静态资源
352
448
  guard.go 是门卫(Host 白名单 / Origin / 安全响应头)
353
449
  authapi.go 是配对和设备管理的口
450
+ session.go 是「一个 URL 一个 herdr session」的分派(每个 session
451
+ 一个 socket、一份发件箱、一条状态订阅)
354
452
  webui/ embed 前端产物(dist 由 make build 拷进来)
355
453
  qr/ 启动时在终端画二维码
356
454
  version/ 版本号的唯一出处(goreleaser 用 ldflags 注进来)
@@ -388,10 +486,47 @@ make release V=v0.1.0 # 打 tag 并推上去,剩下的 GitHub Actions 干
388
486
 
389
487
  推上 tag 之后 `release.yml` 会:`make test` → goreleaser(交叉编译 4 个平台、出 archive 和 `checksums.txt`、建 GitHub Release)→ 把 archive 摊成 npm 包 → **先发 4 个平台子包、最后发根包**。顺序反了会有一段时间 `npm install` 装出一个没有二进制的壳。
390
488
 
391
- 需要一个仓库 secret:`NPM_TOKEN`(Automation 类型的 token,这样 2FA 不会挡住 CI)。
489
+ **Release 建好了但 npm 那步挂了**(发过一次,见下)用同一个 workflow 补发,不重新编译:
490
+
491
+ ```bash
492
+ gh workflow run release.yml -f tag=v0.1.0
493
+ ```
494
+
495
+ 它会去下已经发出去的那批 archive 再打包,所以补发的二进制和 Release 里的**逐字节相同**。
496
+
497
+ **发布 workflow 只能有一个,别再拆出去。** npm 的 Trusted Publisher(OIDC)一个包只能绑一个
498
+ workflow 文件名,绑的就是 `release.yml`;再开一个会发包的 workflow,从它发就对不上 OIDC。
499
+
500
+ 需要一个仓库 secret:`NPM_TOKEN`(**Automation** 类型 —— 另外两档在开了 2FA 的账号上发包会要交互式
501
+ 验证码,CI 里没人输)。配了 Trusted Publisher 之后可以去掉它,但**先发一版确认 OIDC 真的生效**再删。
502
+
503
+ Trusted Publisher 是**按包**配的,5 个包(根包 + 4 个平台子包)每个都要配一遍,都填 `release.yml`、
504
+ Environment name **留空**(我们的 workflow 没声明 environment,填了任何值 OIDC 都会对不上)。
505
+ 少配一个的表现是下次发版在「发 npm」那步中途失败。
506
+
507
+ **tag 要推到装着 `release.yml` 的那个远端**,也就是 GitHub。这个仓库有两个远端(`origin`
508
+ 是自建 git,`github` 才是 GitHub),所以 `make release` **不写死 origin** —— 它按 push URL 里的
509
+ `github.com` 认,认不出来就拒绝发版。推错远端是最难查的一种:tag 打上去了、命令也成功了,
510
+ Actions 那边一直没动静,而「没动静」和「还在排队」长得一模一样。要覆盖:
511
+ `make release V=vX.Y.Z RELEASE_REMOTE=xxx`。
392
512
 
393
513
  三处名字必须对得上,改一个就要改另外两个:`.goreleaser.yaml` 的 `name_template`、`internal/selfupdate.AssetName`(自更新下载)、`scripts/npm-build.mjs`。对不上的表现是 `herdr-web update` 下载 404。
394
514
 
515
+ `make release-dry` 跑完会**把工作区还回去**:`npm-build.mjs` 把版本号写进入库的
516
+ `npm/herdr-web/package.json`(干跑时是 `0.1.1-next` 这种快照号)。不还的话紧接着
517
+ `make release` 会说「工作区不干净」而你什么都没改,或者那个 `-next` 版本号被顺手提交进去。
518
+
519
+ 发版路上踩过、已经修掉的三个(都是**静默**失败):
520
+
521
+ - `web/tsconfig.tsbuildinfo` 曾经入库。它是 `tsc -b` 的增量缓存,`make test` 每跑一次就改写它,
522
+ 紧接着 goreleaser 判定 `git is in a dirty state` 直接拒绝发版。构建缓存一律不入库。
523
+ - `make web` 里那句 `rm -rf $(WEBDIST)` 会删掉入库的 `internal/webui/dist/.gitkeep`。那个文件是
524
+ 承重的:空目录上 `go:embed all:dist` 报 `cannot embed directory dist: contains no embeddable
525
+ files`,新 clone 连 `go build` 都过不了。所以 `web` 和 `clean` 两个目标都会把它写回来。
526
+ - 首发之后有几分钟,npm 的 packument 读路径还没物化(`version` 端点和 search 都查得到,packument
527
+ 却 404)。这时候 `npm i` 拿到 404 会**静默跳过** optional 依赖,装出一个没有二进制的壳。
528
+ 等几分钟重装就好,壳里那段报错会提示重装。
529
+
395
530
  **为什么终端那层不是 React 组件**:它要直接摸 xterm 的 parser、逐字节收 WebSocket、按 rAF 补重绘 —— 套上 React 的渲染周期只会碍事。React 那边只拿一个 ref 挂载它,再订阅几个状态回调。
396
531
 
397
532
  ### 配色(改界面之前先看这段)
@@ -468,7 +603,7 @@ export HERDR_WEB_TLS=auto
468
603
  | `HERDR_WEB_HOST` | `127.0.0.1` | 监听地址,`0.0.0.0` 开局域网 |
469
604
  | `HERDR_WEB_TOKEN` | 读 `~/.herdr-web/token` | **旧机制**,只够引导一次(换成设备凭据)。新装不再自动生成 |
470
605
  | `HERDR_WEB_SHELL` | `$SHELL` | PTY 里跑的 shell |
471
- | `HERDR_WEB_ONCONNECT` | `herdr` | 连上就自动往 PTY 里敲这一行(自带回车)。**显式设成空串就不敲**(`HERDR_WEB_ONCONNECT=`);也可以写成 `herdr --session work` 这种 |
606
+ | `HERDR_WEB_ONCONNECT` | `herdr` | 连上就自动往 PTY 里敲这一行(自带回车)。**显式设成空串就不敲**(`HERDR_WEB_ONCONNECT=`)。**地址栏里带 session 的 URL 不看这一项**(`/work` 一律敲 `herdr --session work`,见「[一个 URL 一个 session](#一个-url-一个-sessionname)」)—— 想固定进某个 session 就把 URL 存书签,别写在这儿 |
472
607
  | `HERDR_WEB_ONCONNECT_MS` | `250` | 上面那行等多久再敲。等的是「shell 吐出第一批输出之后」再加这么多 —— rc 里动 `stty` 或者补全插件初始化会**静默吞掉**早敲的字符。自动敲的那行没进去就调大它 |
473
608
  | `HERDR_WEB_DIR` | `~/.herdr-web` | 数据目录,分两层:配置和文件(`softkeys.json` / `tls/` / `uploads/`)在根上,**内部数据**(设备凭据、passkey 公钥)在 `data/` 里 —— 那两个用户不该手改,被改了会在终端告警。**路径别太深**:里面要开一个 unix socket(`ctl.sock`),全长超过 ~100 字节就 bind 不上,子命令会用不了 |
474
609
 
@@ -561,6 +696,10 @@ herdr-web service install --env-file .env
561
696
 
562
697
  抄进去的是所有 `HERDR_WEB_*`,加上 `PATH` / `SHELL` / `HOME` / `USER` / `LOGNAME` / `LANG` / `LC_ALL` / `TERM` / `HERDR_SOCKET_PATH`。`install` 会把这份清单全打出来 —— 以后「这台机器上服务到底在用哪套配置」只能靠 plist / unit 回答,装的时候看一眼最省事。
563
698
 
699
+ **签证书那条路(C / D 档)必须用 `--env-file`。** DNS provider 的凭据(`CLOUDFLARE_DNS_API_TOKEN`、`ALICLOUD_ACCESS_KEY` 这些)既不带 `HERDR_WEB_` 前缀、也不在上面那张白名单里,所以**从 shell 抄不进去**:你在 `.zshrc` 里 export 得再对,装出来的服务照样签不出证书,而且要等到第一次签发才炸。`--env-file` 里的 key 是**整份**进去的(还盖过当前环境),这是唯一能把 token 交给服务的路。文件只在 `install` 那一刻读,之后不再碰。
700
+
701
+ plist / unit 是 **0600** 的 —— 里面就是这份环境变量的明文。
702
+
564
703
  **抄 `PATH` 是必须的,这是装成服务后最常见的故障**:launchd 给的默认 `PATH` 只有 `/usr/bin:/bin:/usr/sbin:/sbin`,于是 `HERDR_WEB_ONCONNECT=herdr` 变成 `herdr: command not found`,而页面上只看到一个空 shell,完全看不出为什么。
565
704
 
566
705
  为什么是 user 级不是系统级:这个进程会开一个**你的** shell。跑成 root 的系统服务意味着浏览器里那个终端是 root 的,权限一步到位放到最大,而且 `~/.herdr-web`、`~/.config/herdr/herdr.sock` 这些路径全指到别人家去了。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bysir/herdr-web",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制,前端嵌在里面。",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/zbysir/herdr-web#readme",
@@ -29,9 +29,9 @@
29
29
  "node": ">=18"
30
30
  },
31
31
  "optionalDependencies": {
32
- "@bysir/herdr-web-darwin-arm64": "0.1.0",
33
- "@bysir/herdr-web-darwin-x64": "0.1.0",
34
- "@bysir/herdr-web-linux-arm64": "0.1.0",
35
- "@bysir/herdr-web-linux-x64": "0.1.0"
32
+ "@bysir/herdr-web-darwin-arm64": "0.2.0",
33
+ "@bysir/herdr-web-darwin-x64": "0.2.0",
34
+ "@bysir/herdr-web-linux-arm64": "0.2.0",
35
+ "@bysir/herdr-web-linux-x64": "0.2.0"
36
36
  }
37
37
  }