nx-kv 0.9.0 → 0.10.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/CHANGELOG.md +109 -0
- package/README.md +43 -1
- package/assets/groups.json +4 -0
- package/assets/kv-timegoal/SKILL.md +160 -0
- package/assets/kv-timegoal/references/workflow.md +151 -0
- package/package.json +1 -1
- package/src/core/serve-port.js +139 -0
- package/src/modules/localtask/service.js +105 -22
- package/src/modules/localtask/view.jsx +454 -5
- package/src/modules/system/index.js +4 -1
- package/src/modules/timegoal/index.js +157 -0
- package/src/modules/timegoal/service.js +313 -0
- package/src/runtime/cli.js +33 -4
- package/src/runtime/registry.js +2 -1
- package/src/web/public/assets/{CliHints-CGOWc046.js → CliHints-B98Zw4uK.js} +1 -1
- package/src/web/public/assets/{index-BVbLNACS.js → index-57QX-j8y.js} +2 -2
- package/src/web/public/assets/{view-BlGIceud.js → view-BskvoBME.js} +1 -1
- package/src/web/public/assets/{view-BBdEL07x.js → view-BzY577lO.js} +1 -1
- package/src/web/public/assets/view-Cn7mYuiU.js +1 -0
- package/src/web/public/assets/{view-DUy79STG.js → view-_UA707kn.js} +1 -1
- package/src/web/public/index.html +1 -1
- package/src/web/public/assets/view-CwFWDigR.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,115 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录对外可见的变更。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.10.1] - 2026-10-20
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **端口被占用时不再报错退出:自动复用或顺延,并把浏览器指过去**。`serve` 绑定端口
|
|
12
|
+
遇到 `EADDRINUSE` 时,先探测该端口上的 `/api/bootstrap`:占用者**就是本项目的面板**
|
|
13
|
+
(`app.name` 相同)就直接复用它——不再起第二个进程——并打开浏览器指向它;占用者是
|
|
14
|
+
别的服务(或探测不出来)则顺延到**下一个空闲端口**起自己的面板,并打开浏览器指向新
|
|
15
|
+
面板,不会把浏览器指向别人的服务。
|
|
16
|
+
- **跨项目统一的关闭开关 `NX_SERVE_NO_OPEN`**:设成 `1` / `true` / `yes` / `on` 时
|
|
17
|
+
`serve` 完全不开浏览器(所有 nx-xx 项目共用这一个变量名);`--no-open` 仍然只关
|
|
18
|
+
当前这一次。
|
|
19
|
+
|
|
20
|
+
## [0.10.0] - 2026-10-20
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **新增 `timegoal` 模块:把"持续运行 N 小时"任务固化到 nx-kv。** 完全独立的命令族
|
|
25
|
+
/ 独立 store / 独立循环链路——**与 localtask wait 没有任何关系**(不调用、不被调用、
|
|
26
|
+
store 也不共享),可以同时跑也可以分开跑。UI 上嵌在「本地任务」主题卡里只是为了查
|
|
27
|
+
看便利(数据/逻辑完全独立——拔掉 localtask 本模块照样跑)。
|
|
28
|
+
|
|
29
|
+
- 命令族 `nx-kv timegoal <...>`:
|
|
30
|
+
- **`set`** 新建/替换本目录 timegoal(`--duration 60-86400`、`--prompt "<...>"`),
|
|
31
|
+
重置起点;
|
|
32
|
+
- **`update`** 改提示词,可选**同时改时长**(`--duration`)——**保留起点**
|
|
33
|
+
(`deadline = startedAt + 新时长`,已用时间仍算数);agent 自我迭代提示词、
|
|
34
|
+
用户 Web 端编辑都用这条,避免一次小改动把整套 3 小时清零;
|
|
35
|
+
- **`show`** 仅查询;**`check`** 时间运算 + **原文回显 prompt**
|
|
36
|
+
(让 agent 永远以 store 为准,避免长对话里漂移),到点返回 `reached:true`;
|
|
37
|
+
- **`clear`** 清空(无则 `no_goal`,面板按钮可无脑调用);
|
|
38
|
+
- **`list`** 总览所有有 timegoal 的目录。
|
|
39
|
+
- **典型流程**:开头 `set` 一次 → 每轮 `check` 拿 `prompt` 当本轮根指令干活 →
|
|
40
|
+
调研发现 prompt 漏条件就 `update` 替换 → 到 `reached:true` 那轮立刻停手。
|
|
41
|
+
- 独立 store `~/.nx-kv/timegoals.json`(`NX_KV_TIMEGOAL_STORE` 可覆盖),
|
|
42
|
+
与 localtask 的 `localtasks.json` 完全分离——两套循环并行不冲突。
|
|
43
|
+
- **UI 完整编辑入口**(`localtask/view.jsx`,任务看板):
|
|
44
|
+
- 页面顶部新增「时间目标」汇总卡:列出**所有目录**的 timegoal(含还没在
|
|
45
|
+
看板里出现过的目录——CLI 先设、agent 后 wait),支持**新建**(填目录 +
|
|
46
|
+
时长 + 提示词)、**编辑**(改时长/提示词;默认保留起点,勾「重置起点」
|
|
47
|
+
才从头计)、**清空**;状态列显示 `剩 mm:ss` / `已到点`,每 1.5s 刷新。
|
|
48
|
+
- 主题卡里 timegoal 行**恒显示**:已设置 → 倒计时 + 查看/编辑/清空;
|
|
49
|
+
未设置 → 「未设置时间目标 + 设置」(直接为该目录新建)。
|
|
50
|
+
- 时长输入带快捷按钮(30 分钟 / 1 / 2 / 3 / 8 小时),不用手打秒数。
|
|
51
|
+
- 编辑一律走 `PATCH /api/timegoals`(保留起点);只有勾「重置起点」才走
|
|
52
|
+
`POST`(= `set`,计时清零)。
|
|
53
|
+
- 模块边界:timegoal service 不读 localtask service;`topicKeyFor / stampNow
|
|
54
|
+
/ parseStampMs` 是 4-6 行的纯工具,本地复制避免跨模块耦合;纯函数
|
|
55
|
+
`evalTimegoal(goal, nowMs)` 不读时钟、便于单测钉边界。
|
|
56
|
+
- 单测 `tests/unit/timegoal.test.mjs`:duration / prompt 边界、`evalTimegoal`
|
|
57
|
+
四时段、set → show → check 链路、update 保留起点、不同 cwd 隔离、模拟到点、
|
|
58
|
+
store 损坏容忍、环境变量覆盖、prompt 含换行/Unicode 原样回显。
|
|
59
|
+
- 配套 skill `kv-timegoal`:`nx-kv skill install kv-timegoal`。`SKILL.md` 与
|
|
60
|
+
`references/workflow.md` 强调 timegoal 与 localtask wait 等其它命令族完全独立
|
|
61
|
+
——skill 文字里不出现"`localtask wait` + `timegoal check`"的搭配示例。
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **短时限等待被秒级截断「吃掉」:`waitingUntil` 只存到秒。** `stampNow` 是**截断**
|
|
66
|
+
不是四舍五入——真实 deadline `S+1.97s` 存成 `"S+1"` 后解析回来是 `S+1.000`,比真实
|
|
67
|
+
时刻早了近 1 秒。等待时长只有 1~2 秒时这点误差能吃掉**整个窗口**:wait 若恰好在
|
|
68
|
+
某秒最后 ~40ms 启动,事件流 100ms 后轮询时判定「不在等待」→ `waiting` 事件永远推
|
|
69
|
+
不出来(`tests/unit/localtask-sse.test.mjs` 由此带 ~4% 的 flake,实测 flake 率与
|
|
70
|
+
40ms/1000ms 的理论值吻合)。
|
|
71
|
+
- **修法**:`setWaiting` 额外写毫秒精度的 `waitingUntilMs`(epoch ms);判定一律走
|
|
72
|
+
`deadlineMsOf()`(优先毫秒值,老数据回退解析字符串)——`isLiveWaiting` /
|
|
73
|
+
`watchEvents` / 面板倒计时全部改用它。`watchEvents` 判「新一轮等待」也从比较
|
|
74
|
+
秒级字符串改成比较毫秒 deadline(同一秒内开始的两次等待字符串相同,靠它判会漏)。
|
|
75
|
+
- 真实影响:默认 180s 超时下截断误差无感(相对误差太小),但**判据正确性**不应
|
|
76
|
+
依赖时长够长;面板倒计时也不再提前 1 秒归零。
|
|
77
|
+
- 回归测试 4 例:字符串已过期但毫秒值未到 → 仍算等待中;毫秒值也过期 → 不算;
|
|
78
|
+
老数据无 `waitingUntilMs` 回退字符串;真实 wait 写入毫秒值且与字符串同秒。
|
|
79
|
+
|
|
80
|
+
- **「等待中」永远不结束:僵尸 `waiting` 标志残留。** 面板上某个主题永远显示
|
|
81
|
+
「等待中 已等 760:35 · 剩 0:00」,点「关闭」也没反应。
|
|
82
|
+
- **根因**:`waiting` 标志由 wait 进程写入、结束时在 `finally` 里复位;但 wait
|
|
83
|
+
被**强杀**(SIGKILL / 任务管理器杀 / 终端关掉 / 机器崩了)时 `finally` 不会执行,
|
|
84
|
+
标志就永久留在 store 里。曾实测:gs-ac 的 wait 在 22:29 开始、应在 22:32 超时
|
|
85
|
+
收尾,但进程没了 —— 于是标志卡住,用户 23:14 点「关闭」也**没有活进程**来消费
|
|
86
|
+
`cancelRequested`。
|
|
87
|
+
- **修复**:判定「是否真的在等」统一走新函数 `isLiveWaiting(t)` ——
|
|
88
|
+
`waiting` 为真**且**截止时刻还没过。截止已过 = 那个 wait 本来也该收尾了 =
|
|
89
|
+
没人真在等。此前 `countWaitingTopics()` 与 `watchEvents()` 各有一份内联判断,
|
|
90
|
+
`topicView()` 却只看裸标志,于是面板看到的是假状态。
|
|
91
|
+
- 改动点:`topicView()` 对外把 `waiting` 抹成假值、另给 `staleWaiting` 标记;
|
|
92
|
+
`pruneTopics()` / `removeTopic()` / `removeTopics()` / `cancelWait()` /
|
|
93
|
+
`countWaitingTopics()` 全部改用 `isLiveWaiting`。
|
|
94
|
+
- **连带修好的三处卡死**:僵尸等待的主题过去永远躲过 `prune`(空主题也清不掉)、
|
|
95
|
+
被「等待中不能删」挡住、也阻塞面板空闲自释放。
|
|
96
|
+
- 面板显示改为「**已结束(上次等待超时,进程已退出)**」,如实说明发生了什么,
|
|
97
|
+
而不是让倒计时一直转。
|
|
98
|
+
- 单测 `tests/unit/localtask-stale.test.mjs`(7 例):`isLiveWaiting` 四个分支 +
|
|
99
|
+
僵尸等待在 list / count / prune / remove / cancel 各路径的表现 + 正常等待不受影响。
|
|
100
|
+
|
|
101
|
+
- **`localtask wait` 会重复打开多个浏览器窗口**。根因是「有没有活标签」这条判据失效:
|
|
102
|
+
服务端 `panelPresence()` 统计的是 `/api/localtasks/events` 上的活动 SSE 连接,
|
|
103
|
+
但面板页改成 1.5s 轮询后**再也没有建立过 SSE**,presence 恒为 0 ——
|
|
104
|
+
于是每次 wait 都以为没人在看、都开一个新窗口,等待期间还每 20s 再开一个。
|
|
105
|
+
- **面板补上常驻 SSE**(`localtask/view.jsx`):既是 presence 的信号,也顺手把
|
|
106
|
+
waiting / reminder / resolved 变成即时刷新,轮询退化为兜底。
|
|
107
|
+
- **wait 开窗加三道闸**:初次开页即计入冷却(页面加载到 SSE 连上之间有空窗,
|
|
108
|
+
否则 5s 后第一次复查就会再开一个);单次探测为 0 不算标签被关,要**连续两次**
|
|
109
|
+
(≈10s)探不到才重开 —— 后台标签被冻结、SSE 自动重连、网络抖动都会短暂掉 0;
|
|
110
|
+
一次 wait 最多开 3 个窗口,避免浏览器拦脚本时刷成窗口风暴。
|
|
111
|
+
- 单测 `tests/unit/localtask-window.test.mjs`:`shouldReopenWindow` 四个分支 +
|
|
112
|
+
`panelPresence` 的连接计数与断开归零。
|
|
113
|
+
|
|
5
114
|
## [0.9.0] - 2026-10-16
|
|
6
115
|
|
|
7
116
|
### Added
|
package/README.md
CHANGED
|
@@ -28,6 +28,13 @@ pnpm test
|
|
|
28
28
|
|
|
29
29
|
冒烟测试**对着假后端跑**(`tests/fake-backend.mjs`),不会碰生产服务器上的真实数据。
|
|
30
30
|
|
|
31
|
+
`nx-kv serve` 默认会打开浏览器;**端口被占时不会报错退出**:
|
|
32
|
+
|
|
33
|
+
- 占用者**就是本项目的面板** → 直接复用它(不重复起进程),并打开它;
|
|
34
|
+
- 占用者是别的服务 → 顺延到**下一个空闲端口**起自己的面板,并打开它。
|
|
35
|
+
|
|
36
|
+
`--no-open` 只关这一次;`NX_SERVE_NO_OPEN=1`(`true` / `yes` / `on` 同义)则是**所有 nx-xx 项目共用**的全局开关,设为真值后 `serve` 完全不开浏览器。
|
|
37
|
+
|
|
31
38
|
## 登录
|
|
32
39
|
|
|
33
40
|
```bash
|
|
@@ -141,6 +148,39 @@ nx-kv skill install kv-submit # 提交侧:总结上下文、提炼需求
|
|
|
141
148
|
nx-kv skill install kv-localget # 领取侧:主动领取排给本目录的任务
|
|
142
149
|
```
|
|
143
150
|
|
|
151
|
+
## 时间目标(timegoal,不需登录)
|
|
152
|
+
|
|
153
|
+
把「AI 持续运行 N 小时」这类长任务**固化到工具里**,而不是散落在外部脚本:
|
|
154
|
+
|
|
155
|
+
每个**工作目录**绑定一个 timegoal = 「持续秒数 + 一段完整全局提示词」。
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# 开头设一次:3 小时 + 完整提示词(提示词会被原文固化到 store)
|
|
159
|
+
nx-kv timegoal set --cwd "$PWD" --duration 10800 --prompt "<完整全局提示词>" --json
|
|
160
|
+
|
|
161
|
+
# 每轮调一次:算出还剩多少时间,并把提示词【原文】回显给你
|
|
162
|
+
nx-kv timegoal check --cwd "$PWD" --json
|
|
163
|
+
# → { reached:false, remainingSec:10780, prompt:"<完整提示词原文>", deadline, ... }
|
|
164
|
+
|
|
165
|
+
# 提示词要改(agent 自我迭代 / 用户在面板上改)——只换提示词,保住计时起点
|
|
166
|
+
nx-kv timegoal update --cwd "$PWD" --prompt "<新版>" [--duration <秒>] --json
|
|
167
|
+
|
|
168
|
+
# 其余:show(仅查)/ clear(清空)/ list(总览所有目录)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**为什么需要它**:长对话里 agent 一定会把自己手里的提示词改写、漂移、漏条件。
|
|
172
|
+
`check` 每次把 store 里的**原文**回显出来,agent 就没有"凭记忆发挥"的空间。
|
|
173
|
+
到点时 `reached:true`,agent 据此停手、收尾。
|
|
174
|
+
|
|
175
|
+
**完全独立的模块**:独立 store(`~/.nx-kv/timegoals.json`)、独立命令族、
|
|
176
|
+
独立循环——不调用 `localtask wait`、不被它调用,两者可以同时跑。
|
|
177
|
+
面板上嵌在「本地任务」页(顶部「时间目标」汇总卡 + 每个主题卡里一行),
|
|
178
|
+
纯粹是为了查看与编辑方便。
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
nx-kv skill install kv-timegoal # 持续 N 小时任务的时序与提示词模板
|
|
182
|
+
```
|
|
183
|
+
|
|
144
184
|
## ⚠️ 定位一律按内容,不按 id
|
|
145
185
|
|
|
146
186
|
待办的 id **非常容易重复**,两条独立的原因:
|
|
@@ -202,13 +242,14 @@ nx-kv todo get --ref "旧记录" --topic fr # 或按主题收窄
|
|
|
202
242
|
|
|
203
243
|
```
|
|
204
244
|
src/core/ 零业务语义:kvapi / fileapi(后端客户端) / attachstore(附件本地镜像) / config / errors / fstree
|
|
205
|
-
src/modules/ 功能域:system / setting / auth / group / localtask / todo / file / bundled
|
|
245
|
+
src/modules/ 功能域:system / setting / auth / group / localtask / timegoal / todo / file / bundled
|
|
206
246
|
src/runtime/ 装配层:registry / spec / cli / api / server
|
|
207
247
|
src/web/frontend/ React 壳
|
|
208
248
|
assets/nx-kv/ 内置 skill:KV 清单操作手册(远程)
|
|
209
249
|
assets/kv-waittask/ 内置 skill:对话内本地任务接力(等待侧)
|
|
210
250
|
assets/kv-submit/ 内置 skill:需求提炼与委托提交(提交侧)
|
|
211
251
|
assets/kv-localget/ 内置 skill:领取本目录本地任务(领取侧)
|
|
252
|
+
assets/kv-timegoal/ 内置 skill:持续 N 小时任务的时间目标与提示词回显
|
|
212
253
|
```
|
|
213
254
|
|
|
214
255
|
分层由 `eslint.config.js` 的 `no-restricted-imports` 强制;
|
|
@@ -221,6 +262,7 @@ nx-kv skill install # 装到 ~/.claude/skills/nx-kv(KV 清单操作手册
|
|
|
221
262
|
nx-kv skill install kv-waittask # 本地任务接力:最后一步阻塞等用户喂任务
|
|
222
263
|
nx-kv skill install kv-submit # 提交侧:总结上下文、提炼需求,委托别的 agent
|
|
223
264
|
nx-kv skill install kv-localget # 领取侧:主动领取排给本目录的任务
|
|
265
|
+
nx-kv skill install kv-timegoal # 持续 N 小时任务:每轮回显提示词原文、到点自动停手
|
|
224
266
|
nx-kv skill list # 列出可装 skill、默认安装项与可装 group
|
|
225
267
|
nx-kv skill groups # 列出 group → 包含哪些 skill
|
|
226
268
|
nx-kv skill install --group=<key> # 一键装整组(与 <name> 二选一)
|
package/assets/groups.json
CHANGED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kv-timegoal
|
|
3
|
+
description: 把"AI 持续运行 N 小时(如 3 小时)"任务固化到 nx-kv 里——用 CLI 设置时间目标 + 完整全局提示词,每轮自动回显提示词原文避免长对话里 agent 漂移(防偏移),到点返回 reached:true,agent 据此停手收尾。timegoal 是独立模块(独立 store、独立循环、独立命令族),与 localtask wait 等任何其它机制都没有关系;UI 上嵌入在「本地任务」主题卡里只是为了方便查看,逻辑上完全不依赖。agent 可通过 `timegoal update` 自己迭代提示词、用户也可在 web 端动态编辑(都不重置计时器)。典型用法:设置 3 小时"调研开源 Redis 差距并逼近生产级数据库"任务;每轮调 subagent 找差距、写 CHANGELOG、调 update 改 prompt,调 check 看是否到点。触发词:timegoal、time goal、时间目标、3 小时、两小时、持续运行、定时任务、跑一晚上、强制 N 小时、N 小时任务、self-improve prompt、自我迭代提示词、agent 改自己的 prompt、到点自动结束、到点停手、循环任务、跑完为止、UX prompt 防漂移、防偏移。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# kv-timegoal — 把"持续 N 小时任务"固化到 nx-kv
|
|
7
|
+
|
|
8
|
+
一个带"到期硬出口"的循环:开头一次性 `set`,**完整全局提示词固化进 store**;
|
|
9
|
+
每轮调 `check`,**拿到的 `prompt` 字段就是这轮的根指令**(永远从 store 原文回显,
|
|
10
|
+
不让 agent 在长对话里漂移);`reached:true` 那轮**自动停手**,写最终总结退出。
|
|
11
|
+
|
|
12
|
+
## 怎么用
|
|
13
|
+
|
|
14
|
+
### 1. 开头 set 一次
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
nx-kv timegoal set --cwd "$PWD" \
|
|
18
|
+
--duration 10800 \
|
|
19
|
+
--prompt "<完整全局提示词>" --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `--duration` 持续秒数,**60-86400**(1 分钟到 24 小时)。例:3 小时 = `10800`
|
|
23
|
+
- `--prompt` 完整全局提示词,≤20000 字
|
|
24
|
+
- `--cwd` 绑定的目录(缺省用当前目录);**一个目录一个 timegoal**
|
|
25
|
+
|
|
26
|
+
返回:
|
|
27
|
+
|
|
28
|
+
```jsonc
|
|
29
|
+
{
|
|
30
|
+
"status": "ok",
|
|
31
|
+
"replaced": false, // 之前没设过;已有则 true(会重置起点)
|
|
32
|
+
"goal": {
|
|
33
|
+
"durationSec": 10800,
|
|
34
|
+
"startedAt": "2026-10-19 20:31:12",
|
|
35
|
+
"deadline": "2026-10-19 23:31:12", // = startedAt + duration
|
|
36
|
+
"prompt": "<完整提示词原文>" // 已固化
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. 每轮循环
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
每轮开始:
|
|
45
|
+
nx-kv timegoal check --cwd "$PWD" --json
|
|
46
|
+
→ { remainingSec, prompt, ... }
|
|
47
|
+
→ 把 prompt 字段当作本轮根指令(不要从对话历史里重新拼)
|
|
48
|
+
|
|
49
|
+
干活(按 prompt 的指令:调 subagent、写代码、改 CHANGELOG 等)
|
|
50
|
+
|
|
51
|
+
写一行本轮总结到 .timegoal-log.md(或 git CHANGELOG.md)
|
|
52
|
+
|
|
53
|
+
如本轮调研发现 prompt 漏条件:
|
|
54
|
+
nx-kv timegoal update --cwd "$PWD" --prompt "<新版>" --json
|
|
55
|
+
(只换 prompt,截止时间不动)
|
|
56
|
+
|
|
57
|
+
回到本轮开头
|
|
58
|
+
|
|
59
|
+
拿到的 {reached: true} 那一轮:
|
|
60
|
+
写完本轮总结 → 不再开新轮 → 退出本循环
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. check 的返回形状
|
|
64
|
+
|
|
65
|
+
```jsonc
|
|
66
|
+
{
|
|
67
|
+
"hasGoal": true,
|
|
68
|
+
"reached": false, // true 即停手信号
|
|
69
|
+
"elapsedSec": 120, // 已用秒
|
|
70
|
+
"remainingSec": 10680, // 还剩秒
|
|
71
|
+
"startedAt": "2026-10-19 20:31:12",
|
|
72
|
+
"deadline": "2026-10-19 23:31:12",
|
|
73
|
+
"now": "2026-10-19 20:33:12",
|
|
74
|
+
"prompt": "<完整提示词原文>", // ← 这才是该轮的根指令
|
|
75
|
+
"cwd": "/path/to/workdir",
|
|
76
|
+
"key": "<目录 key>",
|
|
77
|
+
"name": "<目录名>"
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
边界:
|
|
82
|
+
|
|
83
|
+
- 没设过 timegoal → `{hasGoal:false, reached:false, prompt:""}`(需先 `set`)
|
|
84
|
+
- 到点后 `reached:true`,goal 对象仍在;可继续 `check` 看 prompt,直到 `clear`
|
|
85
|
+
|
|
86
|
+
## 四条硬规则
|
|
87
|
+
|
|
88
|
+
1. **一次 `set`,整个循环只看 `check` 的返回。** 不要再从对话历史里拼"原始 prompt"
|
|
89
|
+
—— 长对话里 agent 一定会自己改写、漂移、漏条件。**永远把 `check` 返回的
|
|
90
|
+
`prompt` 字段当作本轮根指令**。
|
|
91
|
+
2. **想改 prompt 必须用 `timegoal update`,不要 `set`。** `set` 会**重置起点**
|
|
92
|
+
(一次小迭代把 3 小时清零!);`update` 保留起点——只换 prompt,或顺带
|
|
93
|
+
`--duration` 调整时长(deadline 相应平移,已用时间仍算数)。
|
|
94
|
+
3. **`reached:true` 立刻停手。** 干完手头这步、最后写一遍最终总结到
|
|
95
|
+
`.timegoal-log.md`(或 git CHANGELOG.md),**不再开新轮**;这次循环结束。
|
|
96
|
+
4. **每轮结尾必写一行总结**到 `<workdir>/.timegoal-log.md`(或 git CHANGELOG),
|
|
97
|
+
格式自定,目的是"失败也能复盘":时间 / 干了什么 / 下一步计划。
|
|
98
|
+
|
|
99
|
+
## 完整命令清单
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
nx-kv timegoal set --cwd <dir> --duration <秒> --prompt "<...>" # 设/换(新起点)
|
|
103
|
+
nx-kv timegoal update --cwd <dir> --prompt "<新版>" [--duration <秒>] # 改提示词/时长(保起点)
|
|
104
|
+
nx-kv timegoal show --cwd <dir> # 仅查(不算时间)
|
|
105
|
+
nx-kv timegoal check --cwd <dir> # 算时间 + 回显 prompt
|
|
106
|
+
nx-kv timegoal clear --cwd <dir> # 清空
|
|
107
|
+
nx-kv timegoal list # 所有有 timegoal 的目录
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
全部命令都支持 `--json`(推荐 agent 用)。
|
|
111
|
+
|
|
112
|
+
`update` 与 `set` 的区别:
|
|
113
|
+
|
|
114
|
+
| | `set` | `update` |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| 起点 `startedAt` | **重置**(计时从头算) | **保留**(已用时间仍算数) |
|
|
117
|
+
| 时长 | 必填 | 可选(省略=不变) |
|
|
118
|
+
| prompt | 必填 | 必填 |
|
|
119
|
+
| 用途 | 开一局新的 | 迭代提示词 / 调整时长 |
|
|
120
|
+
|
|
121
|
+
`update` 改时长时:`deadline = startedAt + 新时长`。
|
|
122
|
+
|
|
123
|
+
## Web 端操作
|
|
124
|
+
|
|
125
|
+
面板「本地任务」页有两处入口(都是**纯 UI 便利**,数据与逻辑仍属 timegoal 模块):
|
|
126
|
+
|
|
127
|
+
**1. 顶部「时间目标」汇总卡** —— 列出所有目录的 timegoal(包括还没在看板里出现过
|
|
128
|
+
的目录):
|
|
129
|
+
|
|
130
|
+
- **新建**:填目录 + 时长 + 提示词 → 创建
|
|
131
|
+
- **编辑**:改时长 / 提示词;默认**保留计时起点**,勾「重置起点」才从头计
|
|
132
|
+
- **清空**:删除该目录的 timegoal
|
|
133
|
+
- 状态列显示 `剩 mm:ss` 或 `已到点`,每 1.5s 自动刷新
|
|
134
|
+
|
|
135
|
+
**2. 主题卡里的 timegoal 行**:
|
|
136
|
+
|
|
137
|
+
- 已设置 → `时间目标剩余 mm:ss` / `已到点(用时 ...)` +「查看提示词 / 编辑 / 清空」
|
|
138
|
+
- 未设置 → 「未设置时间目标」+「设置」(为该目录新建)
|
|
139
|
+
|
|
140
|
+
时长输入带快捷按钮(30 分钟 / 1 / 2 / 3 / 8 小时)。
|
|
141
|
+
|
|
142
|
+
## 与其它机制的关系
|
|
143
|
+
|
|
144
|
+
**没有依赖关系。** timegoal 只做两件事:算时间、回显提示词原文。
|
|
145
|
+
|
|
146
|
+
- 不调用 `localtask wait`:等不到任务是别的事,与是否到点无关
|
|
147
|
+
- 不阻塞任何东西:可以同时跑人机接力、派任务给别的 agent、外部 cron
|
|
148
|
+
- 不需要登录 KV 后端:纯本机
|
|
149
|
+
|
|
150
|
+
你可以把它当成一个独立计时器 + 便签本:`set` 把便签写进文件,`check` 读出来
|
|
151
|
+
顺便告诉你还剩多少时间。
|
|
152
|
+
|
|
153
|
+
## 安装
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
nx-kv skill install kv-timegoal
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
要详细时序与一份可抄走的 prompt 模板("调研开源 Redis 差距并逼近生产级数据库"
|
|
160
|
+
那种 3 小时任务的范例),读 `references/workflow.md`(按需加载,不查时序不必读)。
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# kv-timegoal · 时序与完整示例
|
|
2
|
+
|
|
3
|
+
按需加载:只是想看命令的话回到 [[kv-timegoal|SKILL.md]],不必读这份。
|
|
4
|
+
下面给一份能复刻的"3 小时调研 Redis 差距并自迭代实现生产级数据库"任务
|
|
5
|
+
完整时序,含可直接抄走的 prompt 模板。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 完整时序(一次 timegoal 循环长什么样)
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌──────────────────┐
|
|
13
|
+
│ ~/.nx-kv/ │
|
|
14
|
+
│ timegoals.json │
|
|
15
|
+
│ (独立存储) │
|
|
16
|
+
└──────────────────┘
|
|
17
|
+
▲
|
|
18
|
+
│ set/update/check/clear
|
|
19
|
+
│ 都只读写这一个文件
|
|
20
|
+
│
|
|
21
|
+
┌─────────┐ ▼
|
|
22
|
+
│ user │ cli: nx-kv timegoal <...> ──────►─┐
|
|
23
|
+
│ (人) │ │
|
|
24
|
+
│ │ web: 时间目标 主题卡里内嵌显示 │ ┌──────────────┐
|
|
25
|
+
│ │ (逻辑上无关) │ │ nx-kv │
|
|
26
|
+
└─────────┘ │ │ timegoal │
|
|
27
|
+
│ │ module │
|
|
28
|
+
│ │ (独立模块) │
|
|
29
|
+
└──────┬───────┘
|
|
30
|
+
│
|
|
31
|
+
┌─────────────┐ ▼
|
|
32
|
+
│ agent │ cli: nx-kv timegoal set/update/check (事实源)
|
|
33
|
+
│ (LLM) │ ★ agent 唯一读取指令来源 = check 返回的 prompt 字段
|
|
34
|
+
└──────┬──────┘
|
|
35
|
+
│
|
|
36
|
+
│ 每一轮:
|
|
37
|
+
▼
|
|
38
|
+
┌───────────────────────────────────────────────────────┐
|
|
39
|
+
│ 1. nx-kv timegoal check --cwd "$PWD" --json │
|
|
40
|
+
│ ↓ 拿到 {remainingSec, prompt} │
|
|
41
|
+
│ 2. 把 prompt 字段当本轮根指令 │
|
|
42
|
+
│ 3. 干活(按 prompt 指令): │
|
|
43
|
+
│ · 调 subagent 调研差距、产出最小补丁 + 测试 │
|
|
44
|
+
│ · 把本轮成果追加到 CHANGELOG.md 与 .timegoal-log.md │
|
|
45
|
+
│ 4. 如本轮调研发现 prompt 漏条件: │
|
|
46
|
+
│ nx-kv timegoal update --prompt "<V2>" --json │
|
|
47
|
+
│ (**只换 prompt、截止时间不动**) │
|
|
48
|
+
│ 5. 回到第 1 步 │
|
|
49
|
+
└───────────────────────────────────────────────────────┘
|
|
50
|
+
│
|
|
51
|
+
▼
|
|
52
|
+
┌───────────────────────────────────────────────────────┐
|
|
53
|
+
│ 拿到的 {reached: true} 那一轮: │
|
|
54
|
+
│ · 写完本轮总结 │
|
|
55
|
+
│ · 不再开新轮、不再 check(除非写最终总结查 prompt) │
|
|
56
|
+
│ · 退出本循环 │
|
|
57
|
+
└───────────────────────────────────────────────────────┘
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**关键**:timegoal 循环里**只有 `timegoal` 命令族**,没有 `localtask wait`、
|
|
61
|
+
没有别的 nx-kv 命令。可以把它单独跑、可以并行跑——互不感知。
|
|
62
|
+
|
|
63
|
+
## 完整可抄走的 prompt 模板("调研 Redis 差距并逼近生产级数据库")
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
你是「Redis-compatible 生产级数据库」的迭代研发 agent;目标是用 N 小时
|
|
67
|
+
(到 set 的 deadline 为止)持续调研当前项目与开源 Redis 的能力差距,
|
|
68
|
+
逐轮修复差距、把改进写到 CHANGELOG.md,并自我迭代本 prompt 让下一轮更高效。
|
|
69
|
+
|
|
70
|
+
每轮你必须做:
|
|
71
|
+
1. 调 subagent(general-purpose)对比「当前项目 + 上一轮改进」与「开源
|
|
72
|
+
Redis 最新稳定版」的能力清单,输出 1-3 个本轮最高 ROI 的差距修复项。
|
|
73
|
+
2. 对每项写出最小可工作补丁 + 测试,写完跑测试;失败的子任务要在下一轮
|
|
74
|
+
优先重试。
|
|
75
|
+
3. 把本轮成果追加一行到 CHANGELOG.md(格式:「- [YYYY-MM-DD HH:mm] 干了什么」)。
|
|
76
|
+
4. 把本轮总结写到 .timegoal-log.md 的同一行尾(每轮一行)。
|
|
77
|
+
5. 如果 subagent 给出"prompt 优化建议":调 `nx-kv timegoal update --cwd "$PWD"
|
|
78
|
+
--prompt "<新版 V(N+1)>"` 把下一轮的 prompt 替换掉。**不要调 set**——
|
|
79
|
+
set 会把已用时间清零。
|
|
80
|
+
|
|
81
|
+
硬性约束:
|
|
82
|
+
- 严禁超过本轮剩余时间预算(remainingSec < 30 时立刻停手、写总结、不开新轮)
|
|
83
|
+
- 严禁自己改 deadline;想缩短就 `timegoal clear` 再 `timegoal set`
|
|
84
|
+
- 每轮开始时先 `nx-kv timegoal check --cwd "$PWD" --json`,把返回里的
|
|
85
|
+
`prompt` 字段当作本轮根指令;**不要从对话历史里重新拼**——以 store 为准
|
|
86
|
+
- 写代码必须随附测试;测试失败的修补不计入本轮成果
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
把这个 prompt 一次性 `timegoal set` 进 store,agent 之后每一轮照着干到点为止。
|
|
90
|
+
|
|
91
|
+
## 实际跑(手工模拟)
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
WORKDIR=/path/to/your-project
|
|
95
|
+
cd "$WORKDIR"
|
|
96
|
+
|
|
97
|
+
# 1. set
|
|
98
|
+
nx-kv timegoal set --cwd "$PWD" --duration 10800 \
|
|
99
|
+
--prompt "..." --json
|
|
100
|
+
|
|
101
|
+
# 2. 立刻 check:reached:false
|
|
102
|
+
nx-kv timegoal check --cwd "$PWD" --json
|
|
103
|
+
|
|
104
|
+
# 3. sleep 6s
|
|
105
|
+
sleep 6
|
|
106
|
+
|
|
107
|
+
# 4. 再 check:注意 elapsedSec 增加
|
|
108
|
+
nx-kv timegoal check --cwd "$PWD" --json
|
|
109
|
+
|
|
110
|
+
# 5. update:只换 prompt,deadline 不动
|
|
111
|
+
nx-kv timegoal update --cwd "$PWD" --prompt "V2 改得更简洁" --json
|
|
112
|
+
|
|
113
|
+
# 6. check:prompt 已是 V2,deadline 仍是 1 小时后
|
|
114
|
+
nx-kv timegoal check --cwd "$PWD" --json
|
|
115
|
+
|
|
116
|
+
# 7. 等到点(或手动改 store)后:reached:true
|
|
117
|
+
# (实操:把 ~/.nx-kv/timegoals.json 里 startedAt 改成 4 小时前)
|
|
118
|
+
|
|
119
|
+
# 8. clear
|
|
120
|
+
nx-kv timegoal clear --cwd "$PWD" --json
|
|
121
|
+
|
|
122
|
+
# 全程不曾、也不需调过 localtask / todo / 等任何其它命令族
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## 常见问题
|
|
126
|
+
|
|
127
|
+
| 现象 | 原因 / 做法 |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `reached` 没翻 | 检查客户端时区或 set 时是否给了 `--duration` 过短(默认下限 60s) |
|
|
130
|
+
| update 失败"主题「X」尚未设置 timegoal" | 先 `set` 再 `update`;`update` 不创建 |
|
|
131
|
+
| clear 返回"无需清空" | 本来就没设过;这是 info 不是 error |
|
|
132
|
+
| 想换目录 | clear 旧的 → set 新的;目录(cwd)是绑定的 |
|
|
133
|
+
| 想中途换 task queue | **与 timegoal 无关**——本模块不参与队列,那是其它事 |
|
|
134
|
+
| 想同时用 localtask 等用户 | **可以同时跑**——两套独立循环、各管各的;timegoal 只关心自身 deadline |
|
|
135
|
+
|
|
136
|
+
## timegoal 的运行边界
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
timegoal 循环里只有 timegoal 命令族——没有别的 nx-kv 命令参与。
|
|
140
|
+
|
|
141
|
+
跑 100 轮 timegoal 不需要任何其它机制配合;
|
|
142
|
+
反过来,跑别的东西也不需要 timegoal 配合。
|
|
143
|
+
|
|
144
|
+
· timegoal 只做两件事:算时间、回显提示词原文
|
|
145
|
+
· 不阻塞任何东西——可以同时跑人机接力、派任务给别的 agent、外部 cron
|
|
146
|
+
· 不需要登录 KV 后端——纯本机
|
|
147
|
+
· 数据落在本机文件(timegoals.json),可由环境变量 NX_KV_TIMEGOAL_STORE 改路径
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**唯一的交互面**是 CLI 与 web 主题卡上的三个按钮(查看 / 编辑 / 清空),
|
|
151
|
+
没有任何隐式联动。
|
package/package.json
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// serve 的端口协商 + 浏览器打开策略。
|
|
2
|
+
//
|
|
3
|
+
// 零业务语义:所有 nx-xx 面板共用这一份,靠环境变量 NX_SERVE_NO_OPEN 统一关闭自动开浏览器。
|
|
4
|
+
// 放 core/ 是因为它只依赖 Node 内置模块与「本项目的 app 名」,不涉及任何功能域。
|
|
5
|
+
//
|
|
6
|
+
// 这份文件在所有 nx-xx 项目之间**逐字节相同**——改一处就要全量同步,
|
|
7
|
+
// 否则「口径统一」这件事本身会悄悄分叉。
|
|
8
|
+
import net from 'node:net';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 所有 nx-xx 项目共享的关闭开关。
|
|
12
|
+
* 设成 1 / true / yes / on 即禁止 `serve` 自动打开浏览器(正常启动与端口复用都算)。
|
|
13
|
+
* 命令行 `--no-open` 只影响这一次;这个环境变量对整个 shell 生效。
|
|
14
|
+
*/
|
|
15
|
+
export const NO_OPEN_ENV = 'NX_SERVE_NO_OPEN';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 探测面板时给的应答窗口。
|
|
19
|
+
*
|
|
20
|
+
* 1500ms 而不是更短:面板的 bootstrap 不一定是纯本机计算——nx-kv 登录后要去后端
|
|
21
|
+
* 拉分组,链路一慢就超过 800ms。窗口太窄的后果不是「探测失败」这么轻,
|
|
22
|
+
* 而是**把自己的面板误判成别人的服务**,于是白起一个实例、占掉第二个端口——
|
|
23
|
+
* 恰好是这套机制要消灭的那种体验。
|
|
24
|
+
*/
|
|
25
|
+
export const PROBE_TIMEOUT_MS = 1500;
|
|
26
|
+
|
|
27
|
+
/** 真值判定:1 / true / yes / on(大小写不敏感)算开,其他一律算关。 */
|
|
28
|
+
export function isEnvTruthy(value) {
|
|
29
|
+
if (value === undefined || value === null) return false;
|
|
30
|
+
const s = String(value).trim().toLowerCase();
|
|
31
|
+
return s === '1' || s === 'true' || s === 'yes' || s === 'on';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 是否允许自动打开浏览器。
|
|
36
|
+
* 优先级:`--no-open` 命令行开关 > NX_SERVE_NO_OPEN 环境变量 > 默认允许。
|
|
37
|
+
*/
|
|
38
|
+
export function autoOpenEnabled(flagNoOpen) {
|
|
39
|
+
if (flagNoOpen) return false;
|
|
40
|
+
return !isEnvTruthy(process.env[NO_OPEN_ENV]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 探一次端口,返回 { data, timedOut }。
|
|
45
|
+
*
|
|
46
|
+
* 把「超时」和「不是面板」分开,是因为两者的后续动作不同:
|
|
47
|
+
* 超时说明端口上可能正是一个应答慢的面板,值得再等一次;
|
|
48
|
+
* 明确不是面板就没有再等的必要。
|
|
49
|
+
*/
|
|
50
|
+
async function probePanelOnce(port, host, timeoutMs, headers) {
|
|
51
|
+
try {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
54
|
+
let res;
|
|
55
|
+
try {
|
|
56
|
+
res = await fetch(`http://${host}:${port}/api/bootstrap`, {
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
headers: headers || undefined,
|
|
59
|
+
});
|
|
60
|
+
} finally {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
}
|
|
63
|
+
if (!res.ok) return { data: null, timedOut: false };
|
|
64
|
+
const body = await res.json();
|
|
65
|
+
return { data: (body && body.data) || null, timedOut: false };
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return { data: null, timedOut: !!(err && err.name === 'AbortError') };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 探测端口上有没有面板在跑:GET /api/bootstrap,短超时。
|
|
73
|
+
*
|
|
74
|
+
* 所有 nx-xx 面板都暴露 /api/bootstrap,响应里带 app.name / app.version ——
|
|
75
|
+
* 这就是跨项目唯一的认领签名(别的服务不会恰好返回它)。
|
|
76
|
+
* 不是面板、无响应、超时一律返回 null。
|
|
77
|
+
*
|
|
78
|
+
* headers 形参给需要鉴权的项目用(nx-as 的管理面要 Bearer 密钥);
|
|
79
|
+
* 默认不传,匿名探测。
|
|
80
|
+
*/
|
|
81
|
+
export async function probePanel(port, host = '127.0.0.1', timeoutMs = PROBE_TIMEOUT_MS, headers = null) {
|
|
82
|
+
const { data } = await probePanelOnce(port, host, timeoutMs, headers);
|
|
83
|
+
return data;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 从 start 起逐个试,返回第一个能监听的端口(最多试 50 个)。
|
|
88
|
+
* 用临时 net.Server 探测、立刻关闭;与 startServer 走同一套 EADDRINUSE 判定。
|
|
89
|
+
*/
|
|
90
|
+
export function findFreePort(start, host = '127.0.0.1') {
|
|
91
|
+
return new Promise((resolve_, reject) => {
|
|
92
|
+
const tryPort = (p) => {
|
|
93
|
+
if (p > start + 50) {
|
|
94
|
+
reject(new Error(`找不到空闲端口(从 ${start} 起连续 50 个被占)`));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const probe = net.createServer();
|
|
98
|
+
probe.once('error', (err) => {
|
|
99
|
+
if (err && err.code === 'EADDRINUSE') tryPort(p + 1);
|
|
100
|
+
else reject(err);
|
|
101
|
+
});
|
|
102
|
+
probe.listen(p, host, () => probe.close(() => resolve_(p)));
|
|
103
|
+
};
|
|
104
|
+
tryPort(start);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 端口冲突的统一决策。
|
|
110
|
+
*
|
|
111
|
+
* reuse —— 端口上跑的就是本项目的面板:复用它,不再起第二个进程。
|
|
112
|
+
* 起第二个会让「切项目」变成「切端口」,而用户想看的其实是已经开着的那一个。
|
|
113
|
+
* shift —— 端口被别的东西占着:顺延到下一个空闲端口,再起自己的面板。
|
|
114
|
+
*
|
|
115
|
+
* 返回 { action, port, occupant, existing }:
|
|
116
|
+
* action 'reuse' | 'shift'
|
|
117
|
+
* port reuse 时是原端口;shift 时是下一个空闲端口
|
|
118
|
+
* occupant 占用者的 app 名(探测不到就是 null)
|
|
119
|
+
*/
|
|
120
|
+
export async function resolvePortConflict(port, appName, host = '127.0.0.1', headers = null) {
|
|
121
|
+
let probe = await probePanelOnce(port, host, PROBE_TIMEOUT_MS, headers);
|
|
122
|
+
if (probe.timedOut) {
|
|
123
|
+
// 慢应答的面板也认:再等一次。别把「自己的面板」误判成「别人的服务」,
|
|
124
|
+
// 白白再起一个实例——那正是这套机制要消灭的行为。
|
|
125
|
+
probe = await probePanelOnce(port, host, PROBE_TIMEOUT_MS, headers);
|
|
126
|
+
}
|
|
127
|
+
const existing = probe.data;
|
|
128
|
+
const occupant = existing && existing.app ? existing.app.name || null : null;
|
|
129
|
+
if (existing && occupant === appName) {
|
|
130
|
+
return { action: 'reuse', port, occupant, existing };
|
|
131
|
+
}
|
|
132
|
+
const next = await findFreePort(port + 1, host);
|
|
133
|
+
return { action: 'shift', port: next, occupant, existing };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 占用者的一句话描述,用于终端提示。探测不到就是「未知服务」。 */
|
|
137
|
+
export function describeOccupant(occupant) {
|
|
138
|
+
return occupant || '未知服务';
|
|
139
|
+
}
|