dsh-session-superagent 0.8.20 → 0.8.29
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 +178 -2
- package/CHECKLIST.md +26 -0
- package/MAINTENANCE.md +35 -0
- package/README.md +31 -7
- package/UPDATING.md +43 -0
- package/client.js +2 -2
- package/lib/index.js +90 -20
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,174 @@
|
|
|
2
2
|
|
|
3
3
|
格式参照 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.8.29] - 2026-11-02 —— `watch_doctor` 现在会告诉你「面板接口死没死」
|
|
6
|
+
|
|
7
|
+
这一轮的真机事故(重复路由 → 六条接口全灭)有个共同特征:**工具还是好的,只有面板挂了**。
|
|
8
|
+
`watch_status` 看不出异常,用户只会觉得"面板空白"。而 `state.server` 里其实一直记着挂载结果。
|
|
9
|
+
|
|
10
|
+
### 新增
|
|
11
|
+
`watch_doctor` 第一行现在是挂载状态:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
OK 面板接口挂载:6 条,无失败
|
|
15
|
+
FAIL 面板接口挂载:webserver: duplicate exact route "/session-superagent/state"(工具仍可用,但面板会空白;重载一次即可)
|
|
16
|
+
WARN 面板接口部分失败(5/6 成功):…
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 测试
|
|
20
|
+
- 新增用例:doctor 输出里必须有挂载状态行。合计 **91 例**。
|
|
21
|
+
(写这个用例时踩到一个小坑:`cleanup` 用例会删掉临时状态目录,doctor 用例必须排在它前面 —— 否则看到的是"没有挂载记录"。)
|
|
22
|
+
|
|
23
|
+
## [0.8.28] - 2026-11-02 —— 旧实例还在吃 HTTP 请求(面板建的 watcher 一开就废)
|
|
24
|
+
|
|
25
|
+
### 真机复验发现
|
|
26
|
+
宿主在 19:49 挂载成功(`routes: 6, failed: []`),但**通过 HTTP(面板)建的 watcher 仍然报**:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
lastError: cannot get required service "agents" in inactive context
|
|
30
|
+
consecutiveFailures: 0 / nextAt: None
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
对照实验:用**工具**(`watch_start`)建的 watcher 同一时刻轮询正常、无错误。
|
|
34
|
+
差别在于请求被谁处理 —— 路由最初由 19:32 那个实例注册,重载后若没走 dispose,
|
|
35
|
+
它继续吃 HTTP 请求,而它的 `ctx` 早已 dispose;于是那些 watcher 的定时器跑在死上下文里。
|
|
36
|
+
|
|
37
|
+
### 修复
|
|
38
|
+
模块级 `activeRoutes`:每次 `apply()` 注册路由前,**先摘掉上一次注册的**(接管而不是并存)。
|
|
39
|
+
配合 0.8.25 的单条容错与 0.8.26 的 teardown,重载后的三条路径都覆盖了。
|
|
40
|
+
|
|
41
|
+
### 测试
|
|
42
|
+
- 新增用例:同一模块 `apply` 两次时,第二次会先 unregister 第一批(6 条)再注册新的 6 条。合计 **90 例**。
|
|
43
|
+
|
|
44
|
+
## [0.8.27] - 2026-11-02 —— 文案核对 + 把 0.8.21–0.8.27 一次发到 npm
|
|
45
|
+
|
|
46
|
+
### 文案
|
|
47
|
+
- 「已添加,30 秒内开始轮询」不准确:`startWatcher()` 会**立刻**跑一次 tick(不等 30 秒的下限),
|
|
48
|
+
改成「已添加,首轮立即开始轮询」。
|
|
49
|
+
|
|
50
|
+
### 发布
|
|
51
|
+
- 一次把 0.8.21–0.8.27 发出去(npm 上原来停在 0.8.20)。已确认 `latest = 0.8.27`,
|
|
52
|
+
tarball 直装验证:`version 0.8.27`、`routes 6`、`client.js` 语法通过、新文案在位。这一批里有三个真机事故修复:
|
|
53
|
+
`inactive context`(唤醒会失败)、未预期异常不退避、热重载后重复路由导致面板全灭。
|
|
54
|
+
|
|
55
|
+
### 测试
|
|
56
|
+
- 合计 **89 例**。
|
|
57
|
+
|
|
58
|
+
## [0.8.26] - 2026-11-02 —— 卸载时真的把路由摘掉(重复路由的根因)
|
|
59
|
+
|
|
60
|
+
0.8.25 让「一条重复不再全灭」,这一版处理根因:**卸载时要把旧路由摘掉**,否则宿主重载后
|
|
61
|
+
永远是新旧两套并存,面板读到的是旧 handler(改了密码却还是老数据那种感觉)。
|
|
62
|
+
|
|
63
|
+
### 修复
|
|
64
|
+
- 把清理逻辑抽成 `teardown()`,同时挂在 **`ctx.effect(() => teardown)`** 与 `ctx.on('dispose', …)` 两条路上:
|
|
65
|
+
effect 作用域销毁时一定调用,事件广播方式也能覆盖。`teardown()` 本身可重复调用(摘完就清空列表)。
|
|
66
|
+
|
|
67
|
+
### 测试
|
|
68
|
+
- 新增用例:卸载时六条路由都被 unregister;重复卸载不会重复 unregister。合计 **89 例**。
|
|
69
|
+
|
|
70
|
+
## [0.8.25] - 2026-11-02 —— 热重载时一条重复路由会让**全部**接口消失
|
|
71
|
+
|
|
72
|
+
### 真机现场
|
|
73
|
+
```
|
|
74
|
+
server = {at: 19:38:26Z, error: 'webserver: duplicate exact route "/session-superagent/state"'}
|
|
75
|
+
```
|
|
76
|
+
宿主热重载时,旧实例的路由还没被 dispose,新实例注册第一条就抛 —— 而注册是**一整块 try**,
|
|
77
|
+
于是六条路由全军覆没,面板一片空白(数据接口全 404),只有重启才能恢复。
|
|
78
|
+
|
|
79
|
+
### 修复
|
|
80
|
+
每条路由改成单独容错(`addRoute()`):重复的那条把原因记进 `state.server.failed`,
|
|
81
|
+
其余照常注册。单条重复不再等于全站不可用。
|
|
82
|
+
|
|
83
|
+
### 守门
|
|
84
|
+
新增 `test/route-resilience.test.mjs`:让宿主替身在注册 `client-error` 时抛重复路由,
|
|
85
|
+
断言其余 5 条仍然注册成功、`/state` 仍在、失败原因被记录下来。
|
|
86
|
+
|
|
87
|
+
### 测试
|
|
88
|
+
- 合计 **88 例**。
|
|
89
|
+
|
|
90
|
+
## [0.8.24] - 2026-11-02 —— 安装说明是错的(`npm i` 不会让 DSH 加载插件)
|
|
91
|
+
|
|
92
|
+
### 修复
|
|
93
|
+
README 原来写「方式一:npm,`npm i dsh-session-superagent`」,但 **DSH 插件必须装进 profile**
|
|
94
|
+
才会被加载(包里的 `dsh.bundle.patch` 由 profile 的安装流程读取);`npm i` 只是下载到本地
|
|
95
|
+
`node_modules`,什么都不会发生。另一条「`dsh plugin add < GitHub URL>`」本机也没有这个子命令。
|
|
96
|
+
|
|
97
|
+
对照同机的其它插件(`dsh-unlag` 用 `dsh plugin install dsh-unlag`、
|
|
98
|
+
`dsh-better-sidebar` 用 `dsh plugin --profile web add …@latest`)改成:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
dsh plugin install dsh-session-superagent
|
|
102
|
+
dsh plugin --profile desktop add dsh-session-superagent@latest
|
|
103
|
+
dsh plugin install /path/to/dsh-session-superagent # 本地开发
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
并写清「`npm i` 不会生效」与「装进 profile 后是硬链接、新增文件要 `sync:profile`」。
|
|
107
|
+
|
|
108
|
+
### 测试
|
|
109
|
+
- 合计 **87 例**。
|
|
110
|
+
|
|
111
|
+
## [0.8.23] - 2026-11-02 —— README 补「故障排查」,并把链接到的文档发进包
|
|
112
|
+
|
|
113
|
+
### 文档
|
|
114
|
+
- README 新增**故障排查**表,全部来自真实事故:新增文件没进 profile 导致插件不挂载、
|
|
115
|
+
硬链接断了导致「改了但没生效」、`inactive context`、幽灵会话不退避、
|
|
116
|
+
npm 刚发布时的 `ETARGET`、以及「有动静但没被叫醒」该看哪里(`watch_status` 的 `deferred=`、面板的「最近被过滤」)。
|
|
117
|
+
- README 底部链接的 `UPDATING.md` / `CHECKLIST.md` / `MAINTENANCE.md` 之前**不在 `files` 里**,
|
|
118
|
+
npm 页面上点了是 404。现在一起发出去(`npm pack` 10 → 13 个文件)。
|
|
119
|
+
|
|
120
|
+
### 测试
|
|
121
|
+
- 合计 **87 例**。
|
|
122
|
+
|
|
123
|
+
## [0.8.22] - 2026-11-02 —— 未预期异常也要退避(0.8.21 的同一类问题再补一刀)
|
|
124
|
+
|
|
125
|
+
0.8.21 修的是「读 `ctx.agents` 会抛」。修完回头看 tick 的兜底:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
} catch (error) {
|
|
129
|
+
entry.lastError = String(error.message) // 只记错误
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
也就是说 **任何**未预期异常都会让 watcher 按 `intervalSec` 无限重试 —— 正是 inactive-context
|
|
134
|
+
那次的症状(`consecutiveFailures: 0`、`nextAt: None`)。现在 catch 里也 `bumpFailure(entry, every)`,
|
|
135
|
+
坏掉的东西最多 30 分钟试一次,不会持续打 GitHub。
|
|
136
|
+
|
|
137
|
+
### 说明
|
|
138
|
+
这属于「同类问题系统性收口」,不是新功能:凡是走出 `pollOnce` 正常分支的失败,现在都会进退避窗口。
|
|
139
|
+
|
|
140
|
+
### 测试
|
|
141
|
+
- 合计 **87 例**(该分支需要真实定时器上下文,靠重载后的真机复验确认)。
|
|
142
|
+
|
|
143
|
+
## [0.8.21] - 2026-11-02 —— 真机复验抓到的**唤醒致命 bug**:inactive context
|
|
144
|
+
|
|
145
|
+
宿主重载后(`routes: 6`,0.8.14–0.8.20 全部生效)做了真机复验,造了一条绑定「不存在会话」的 watcher:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
lastError: cannot get required service "agents" in inactive context
|
|
149
|
+
consecutiveFailures: 0 ← 没进退避
|
|
150
|
+
nextAt: None ← 每 intervalSec 都会再打一次 GitHub
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 根因
|
|
154
|
+
定时器回调跑在**非活跃上下文**里,直接读 `ctx.agents` 会抛错。读 `goals` 一直用的是
|
|
155
|
+
`ctx.get('goals')`(不抛),唯独 `agents` 用的是 `ctx.agents` —— 这行代码在**每一次真唤醒**、
|
|
156
|
+
每一次「会话还在不在」判断上都会执行。
|
|
157
|
+
|
|
158
|
+
### 影响
|
|
159
|
+
- 幽灵会话判断(0.8.11)在本机其实从没跑到过:轮询在更早的一行就抛了。
|
|
160
|
+
- 更严重的是:**如果宿主对定时器上下文同样报错,真实唤醒也会失败**(看起来"在盯",其实一次都没成)。
|
|
161
|
+
|
|
162
|
+
### 修复
|
|
163
|
+
新增 `agentsService(ctx)`:优先 `ctx.get('agents')`,取不到再兜底直接读;`wake()`、
|
|
164
|
+
`pollOnce()` 与两个工具(`watch_start` / `watch_stop`)全部改走它。
|
|
165
|
+
|
|
166
|
+
### 守门
|
|
167
|
+
新增 `test/agents-context.test.mjs`:用一个**读 `ctx.agents` 就抛**的宿主替身,验证
|
|
168
|
+
`watch_start` / `watch_status` 仍然正常,并断言不会把宿主的上下文错误当成轮询错误。这条路以前完全没测过。
|
|
169
|
+
|
|
170
|
+
### 测试
|
|
171
|
+
- 合计 **87 例**。
|
|
172
|
+
|
|
5
173
|
## [0.8.20] - 2026-11-02 —— 冷启动演练:工具返回要能证明「选项生效了」
|
|
6
174
|
|
|
7
175
|
照 HANDOFF + README 走一遍「加 watcher → 观察 → 确认」(不看代码,只看文档),暴露的摩擦点:
|
|
@@ -16,6 +184,14 @@
|
|
|
16
184
|
HTTP 只在面板/调试时用 —— 并写清端口每次重启都会变、怎么找(会话上下文里的 GUI URL 或按 PID 查 netstat)。
|
|
17
185
|
冷启动演练时就是卡在「端口找不到」这一步。
|
|
18
186
|
|
|
187
|
+
### 发布
|
|
188
|
+
- 已发 `dsh-session-superagent@0.8.20`(一次性带上 0.8.17–0.8.20 的改动;`latest = 0.8.20`)。
|
|
189
|
+
- tarball 直装验证:`version 0.8.20`、**routes 6**、`client.js` 语法通过、
|
|
190
|
+
包内只有 7 项(CHANGELOG/LICENSE/README/client.js/cordis.patch.yml/lib/package.json)。
|
|
191
|
+
- **CI 已接入 `preflight --fast`**:GitHub Actions 在 Node 22 / 24 两个矩阵上跑
|
|
192
|
+
「单测 / 语法 / 版本一致 / 路由面 / 包内容」,本地无 profile 时该检查自动 SKIP。
|
|
193
|
+
运行记录:`gh run list` → `test (22)` 与 `test (24)` 均 success。
|
|
194
|
+
|
|
19
195
|
### 测试
|
|
20
196
|
- 合计 **85 例**。
|
|
21
197
|
|
|
@@ -465,7 +641,7 @@ OK .dsh-zhuli 下正好一个副本
|
|
|
465
641
|
|
|
466
642
|
## [0.8.0] - 2026-11-01 —— 移植官方 superagent 的 GUI
|
|
467
643
|
|
|
468
|
-
把官方 `dsh-superagent
|
|
644
|
+
把官方 `dsh-superagent`(本地 checkout 的 dsh-superagent-prototype)的**设计系统与信息架构**
|
|
469
645
|
搬进「主理鱼」标签页,数据接我们自己的 `/state`、`/watch`。
|
|
470
646
|
|
|
471
647
|
### 移植了什么
|
|
@@ -595,7 +771,7 @@ OK .dsh-zhuli 下正好一个副本
|
|
|
595
771
|
|
|
596
772
|
### 修复
|
|
597
773
|
- **中文路径被写成乱码**:Windows 下 argv 里的中文常是 GBK 字节,按 UTF-8 解得到一串 U+FFFD
|
|
598
|
-
|
|
774
|
+
(实测中文路径 `D:/某中文目录/...` 存成 `D:/??????/...`,worktree 建不出来)。新增 `decodeBody()`:
|
|
599
775
|
UTF-8 解码出现替换字符就用 `TextDecoder('gbk')` 重解。
|
|
600
776
|
- **同一个 watcher 并跑两轮**:首轮 tick 与手动「立刻」撞车会开两个隔离区。`entry.polling` 做并发闸,重复请求 409。
|
|
601
777
|
- `/state` 补 `isolated` / `workdir` / `lastWorktree`。
|
package/CHECKLIST.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# CHECKLIST —— 两张表,两个交付门
|
|
2
|
+
|
|
3
|
+
## A 表 · 代码交付前
|
|
4
|
+
|
|
5
|
+
- [ ] `node --test` 全绿,且新增行为有测试
|
|
6
|
+
- [ ] 所有返回字符串都过 `redact()`(新增输出路径重点查)
|
|
7
|
+
- [ ] 写操作仍要求 `confirm: true`
|
|
8
|
+
- [ ] 唤醒仍受 `agent.status === 'idle'` 约束
|
|
9
|
+
- [ ] 唤醒失败时不推进水位(`entry.seen` 不动)
|
|
10
|
+
- [ ] `package.json` 的 `files` 覆盖所有要发布的文件(`lib` + 文档 + patch)
|
|
11
|
+
- [ ] 没有把本地路径、个人 token、机器名写进仓库
|
|
12
|
+
|
|
13
|
+
## B 表 · 发版前
|
|
14
|
+
|
|
15
|
+
- [ ] 从最新 `main` 起功能分支
|
|
16
|
+
- [ ] `CHANGELOG.md` 增加条目,`package.json` bump 版本
|
|
17
|
+
- [ ] 本地 `node --test` 通过
|
|
18
|
+
- [ ] PR 合并到 `main`
|
|
19
|
+
- [ ] 打 annotated tag `vX.Y.Z` 并推送
|
|
20
|
+
- [ ] 创建 GitHub Release,说明取自 CHANGELOG
|
|
21
|
+
- [ ] 安装验证:`dsh plugin --profile <p> add github:MYCF711/dsh-session-superagent`
|
|
22
|
+
|
|
23
|
+
## 发完之后
|
|
24
|
+
|
|
25
|
+
- [ ] 在测试仓库 `watch_start` 跑一轮,确认唤醒可达
|
|
26
|
+
- [ ] 记录一次真实运行的观察结果到本仓库 issue
|
package/MAINTENANCE.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# MAINTENANCE —— 维护台账(给接手的 agent 看)
|
|
2
|
+
|
|
3
|
+
## 一、身份
|
|
4
|
+
|
|
5
|
+
- 包名:`dsh-session-superagent`
|
|
6
|
+
- 形态:DSH 插件(tools-only + 自带 `cordis.patch.yml`)
|
|
7
|
+
- 目标宿主:DeepSeek Harness ≥ 0.2.1-rc.5
|
|
8
|
+
- 依赖:宿主已安装并登录 `gh` CLI;Node ≥ 22.19
|
|
9
|
+
|
|
10
|
+
## 二、当前状态(2026-11-01)
|
|
11
|
+
|
|
12
|
+
- [x] 七个工具可用(github / repo_here / watch_start / watch_status / watch_stop / watch_ack / watch_doctor),`node --test` 88/88 通过
|
|
13
|
+
- [x] 自回声抑制(逐条水位)
|
|
14
|
+
- [x] 仅空闲注入(避免草稿回放)
|
|
15
|
+
- [x] 唤醒失败不推进水位
|
|
16
|
+
- [ ] webhook 代替轮询(未做)
|
|
17
|
+
- [ ] 多仓库单会话(未做,当前每会话一仓库)
|
|
18
|
+
|
|
19
|
+
## 三、下一步
|
|
20
|
+
|
|
21
|
+
1. 真实仓库连跑 24h,统计 `lastWake` 与误唤醒次数;
|
|
22
|
+
2. 把 `trigger.json` 从正式包移出(或加开关),它只是测试通道;
|
|
23
|
+
3. 评估 `steer()` 与 `followup()` 在不同场景下的取舍。
|
|
24
|
+
|
|
25
|
+
## 四、硬约束
|
|
26
|
+
|
|
27
|
+
1. **不注入正在运行的会话**——会污染客户端输入框,可能导致重复发送。
|
|
28
|
+
2. **凭证永不出插件**:所有返回必须过 `redact()`;新增输出路径要补测试。
|
|
29
|
+
3. **不改 profile 的 `bundles`**:那是操作者动作,包只带 patch。
|
|
30
|
+
4. **不删除用户文件**;状态文件写失败必须静默降级。
|
|
31
|
+
5. **没跑过的写成“未验证”**,不写“通过”。
|
|
32
|
+
|
|
33
|
+
## 五、最短接手路径
|
|
34
|
+
|
|
35
|
+
读 `README.md`(做什么)→ 本文件(现状与约束)→ `CHECKLIST.md`(交付门)→ `UPDATING.md`(怎么发版)→ `lib/watch.js`(核心逻辑,纯函数,测试齐全)。
|
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# dsh-session-superagent
|
|
2
2
|
|
|
3
|
+
<img src="assets/icon-180.png" width="88" height="88" alt="">
|
|
4
|
+
|
|
3
5
|
[](LICENSE)
|
|
4
|
-
[](https://github.com/MYCF711/dsh-session-superagent/releases)
|
|
5
7
|
[](https://www.npmjs.com/package/dsh-session-superagent)
|
|
6
8
|
[](https://github.com/topics/dsh-plugin)
|
|
7
9
|
|
|
@@ -51,15 +53,24 @@ GitHub 有新动态 → watcher 轮询 → Agent.followup(消息) → 会话跑
|
|
|
51
53
|
|
|
52
54
|
## 安装
|
|
53
55
|
|
|
56
|
+
**装进 profile**(DSH 插件不是 `npm i` 就生效,要装到 profile 里):
|
|
57
|
+
|
|
54
58
|
```bash
|
|
55
|
-
#
|
|
56
|
-
|
|
59
|
+
# 默认 profile
|
|
60
|
+
dsh plugin install dsh-session-superagent
|
|
57
61
|
|
|
58
|
-
#
|
|
59
|
-
dsh plugin add
|
|
62
|
+
# 指定 profile / 锁定版本
|
|
63
|
+
dsh plugin --profile desktop add dsh-session-superagent@latest
|
|
64
|
+
|
|
65
|
+
# 本地开发:直接装这个目录(会以 file: 硬链接方式挂进 profile)
|
|
66
|
+
dsh plugin install /path/to/dsh-session-superagent
|
|
60
67
|
```
|
|
61
68
|
|
|
62
|
-
|
|
69
|
+
> `npm i dsh-session-superagent` 只会把它下载到本地 node_modules,**不会**让 DSH 加载它 ——
|
|
70
|
+
> 包里的 `dsh.bundle.patch` 要由 profile 的安装流程来读。
|
|
71
|
+
> 也可以在应用内的插件市场里搜索安装。
|
|
72
|
+
|
|
73
|
+
装进 profile 后是 `file:` 硬链接:改源码即改线上;**但新增文件不会被自动带过去**,
|
|
63
74
|
加完文件要跑 `pnpm run sync:profile`(否则下次重启插件会整体不挂载,这坑踩过一次)。
|
|
64
75
|
|
|
65
76
|
## 一次真实运行长什么样
|
|
@@ -88,11 +99,24 @@ dsh plugin add https://github.com/MYCF711/dsh-session-superagent
|
|
|
88
99
|
## 开发
|
|
89
100
|
|
|
90
101
|
```sh
|
|
91
|
-
node --test #
|
|
102
|
+
node --test # 88 项单测
|
|
92
103
|
```
|
|
93
104
|
|
|
94
105
|
版本与发布流程见 [UPDATING.md](UPDATING.md),交付门见 [CHECKLIST.md](CHECKLIST.md),接手台账见 [MAINTENANCE.md](MAINTENANCE.md)。
|
|
95
106
|
|
|
107
|
+
## 故障排查(都是真实踩过的)
|
|
108
|
+
|
|
109
|
+
| 症状 | 原因 | 处理 |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| 面板空白、`watch_*` 工具消失、`/session-superagent/*` 全 404 | 新增的 `lib/*.js` 没同步进 profile(`file:` 安装只对**已有**文件建硬链接),宿主 import 失败 → 整个插件不挂载 | `pnpm run sync:profile`,然后重载插件 |
|
|
112
|
+
| 改了代码但线上行为没变 | 硬链接断了 —— **原子写、`git checkout/restore`、某些编辑器的保存**都会换 inode,profile 里还是旧文件 | `pnpm run sync:profile:check` 看漂移,`sync:profile:repair` 修 |
|
|
113
|
+
| 轮询报 `cannot get required service "agents" in inactive context` | 0.8.21 之前的 bug:定时器里直接读 `ctx.agents` | 升级到 ≥0.8.21 |
|
|
114
|
+
| 绑定的会话已删除,watcher 还在打 GitHub | 0.8.14 之前的 bug:该类失败没进退避 | 升级到 ≥0.8.14(0.8.22 起未预期异常也会退避) |
|
|
115
|
+
| `npm i dsh-session-superagent@<新版本>` 报 `ETARGET` | 刚发布,npm 客户端元数据缓存还没刷新 | 等 1-2 分钟,或 `npm i <tarball URL>` / `--prefer-online` |
|
|
116
|
+
| 有动静但会话没被叫醒 | 会话忙 / 时段外 / 每小时上限 / 被子过滤器刷掉 | `watch_status` 会显示 `deferred=` / `paused`;面板「总览」的「最近被过滤」给原因 |
|
|
117
|
+
|
|
118
|
+
`pnpm run preflight` 一条命令可自查:单测、语法、profile 一致性、版本一致、路由面、包内容、冒烟。
|
|
119
|
+
|
|
96
120
|
## 许可
|
|
97
121
|
|
|
98
122
|
MIT,见 [LICENSE](LICENSE)。
|
package/UPDATING.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 更新与发布
|
|
2
|
+
|
|
3
|
+
## 铁律
|
|
4
|
+
|
|
5
|
+
默认分支只接受通过门禁的 PR;`main` 永远可安装。
|
|
6
|
+
|
|
7
|
+
## 发一个版本
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git checkout main && git pull
|
|
11
|
+
git checkout -b feat/<主题>
|
|
12
|
+
|
|
13
|
+
# 1) 改代码 + 补测试
|
|
14
|
+
node --test
|
|
15
|
+
|
|
16
|
+
# 2) bump 版本 + 写 CHANGELOG
|
|
17
|
+
# 语义化:修 bug → patch;加能力 → minor;改默认行为/改 patch 行 id → major
|
|
18
|
+
|
|
19
|
+
# 3) 提交并推分支
|
|
20
|
+
git add -A && git commit -m "feat: ..."
|
|
21
|
+
git push -u origin feat/<主题>
|
|
22
|
+
|
|
23
|
+
# 4) PR 全绿后合并
|
|
24
|
+
gh pr create --fill && gh pr merge --squash --delete-branch
|
|
25
|
+
|
|
26
|
+
# 5) 回到 main 打 tag 并发 Release
|
|
27
|
+
git checkout main && git pull
|
|
28
|
+
git tag -a v0.1.1 -m "v0.1.1"
|
|
29
|
+
git push origin v0.1.1
|
|
30
|
+
gh release create v0.1.1 --title "v0.1.1" --notes-from-tag
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 用户怎么升级
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
dsh plugin --profile <p> update dsh-session-superagent
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
pnpm 依据 `github:` 规格重新解析;或重新 `add` 一次覆盖。
|
|
40
|
+
|
|
41
|
+
## 兼容性
|
|
42
|
+
|
|
43
|
+
插件依赖宿主内部 API(`ctx.tools`、`ctx.agents`)。升级到新的 DSH 内部 API 视为 **major**,因为旧宿主可能不再提供同样的服务。
|
package/client.js
CHANGED
|
@@ -14,7 +14,7 @@ window.__ModuleLoader__.load({
|
|
|
14
14
|
const { useCallback, useEffect, useRef, useState } = React
|
|
15
15
|
|
|
16
16
|
// 客户端版本探针:每次模块加载写一次,用来确认浏览器里跑的是哪一版
|
|
17
|
-
const CLIENT_VERSION = '0.8.
|
|
17
|
+
const CLIENT_VERSION = '0.8.29'
|
|
18
18
|
const BASE = '/session-superagent'
|
|
19
19
|
const ENDPOINT = BASE + '/state'
|
|
20
20
|
const POLL_MS = 5000
|
|
@@ -739,7 +739,7 @@ window.__ModuleLoader__.load({
|
|
|
739
739
|
}),
|
|
740
740
|
})
|
|
741
741
|
setForm({ repo: '', intervalSec: form.intervalSec, goal: '', labels: '', onlyAssigned: false, activeHours: form.activeHours, isolated: form.isolated, workdir: form.workdir, observe: form.observe })
|
|
742
|
-
}, '
|
|
742
|
+
}, '已添加,首轮立即开始轮询')
|
|
743
743
|
|
|
744
744
|
const saveCredentials = () => run(async () => {
|
|
745
745
|
const payload = {}
|
package/lib/index.js
CHANGED
|
@@ -41,6 +41,15 @@ const TRIGGER_FILE = join(STATE_DIR, 'trigger.json')
|
|
|
41
41
|
const CLIENT_NOTE_KEYS = new Set(['client-version', 'apply', 'desk-registered', 'desk-fail', 'orbit-registered', 'orbit-fail', 'orbit-mount', 'badge-registered', 'badge-mount', 'badge-state', 'error'])
|
|
42
42
|
const DEFAULT_COOLDOWN_MS = 180_000
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 当前挂在这个进程上的路由 disposer。
|
|
46
|
+
* 宿主重载时若没走 dispose,旧实例会继续吃 HTTP 请求,而它的 ctx 可能已经 dispose ——
|
|
47
|
+
* 真机表现:面板建的 watcher 第一轮就报
|
|
48
|
+
* `cannot get required service "agents" in inactive context`(2026-11-02 复验)。
|
|
49
|
+
* 所以每次 apply 注册路由前,先把上一次的摘掉。
|
|
50
|
+
*/
|
|
51
|
+
const activeRoutes = []
|
|
52
|
+
|
|
44
53
|
function readState() {
|
|
45
54
|
try { return JSON.parse(readFileSync(STATE_FILE, 'utf8')) } catch { return { watchers: {} } }
|
|
46
55
|
}
|
|
@@ -182,6 +191,22 @@ function ghEnv() {
|
|
|
182
191
|
return { ...process.env, GH_TOKEN: String(s.token) }
|
|
183
192
|
}
|
|
184
193
|
|
|
194
|
+
/**
|
|
195
|
+
* 安全地取 agents 服务。
|
|
196
|
+
* 定时器回调跑在「非活跃上下文」里,直接读 `ctx.agents` 会抛
|
|
197
|
+
* `cannot get required service "agents" in inactive context`(2026-11-02 真机复验踩到);
|
|
198
|
+
* `ctx.get(name)` 不抛,取不到就返回 undefined。
|
|
199
|
+
*/
|
|
200
|
+
function agentsService(ctx) {
|
|
201
|
+
try {
|
|
202
|
+
if (typeof ctx.get === 'function') {
|
|
203
|
+
const viaGet = ctx.get('agents')
|
|
204
|
+
if (viaGet) return viaGet
|
|
205
|
+
}
|
|
206
|
+
} catch {}
|
|
207
|
+
try { return ctx.agents } catch { return undefined }
|
|
208
|
+
}
|
|
209
|
+
|
|
185
210
|
/** gh CLI 可用性探针:5 分钟缓存,别让面板每 5 秒都 spawn 一次 gh。 */
|
|
186
211
|
const ghHealthCache = { at: 0, value: null }
|
|
187
212
|
async function ghHealth() {
|
|
@@ -339,7 +364,8 @@ function defineTool(options) {
|
|
|
339
364
|
* inbox alone only parks the message (the client shows it as a pending draft).
|
|
340
365
|
*/
|
|
341
366
|
function wake(ctx, sessionId, text) {
|
|
342
|
-
const
|
|
367
|
+
const agents = agentsService(ctx)
|
|
368
|
+
const agent = agents && typeof agents.get === 'function' ? agents.get(sessionId) : undefined
|
|
343
369
|
if (!agent) return { ok: false, reason: 'session not live' }
|
|
344
370
|
|
|
345
371
|
const allowed = decideWake(agent.status)
|
|
@@ -468,7 +494,8 @@ async function pollOnce(ctx, entry, baseMs) {
|
|
|
468
494
|
return
|
|
469
495
|
}
|
|
470
496
|
// 会话忙就别开隔离区了:白建一个没人用的 worktree
|
|
471
|
-
const
|
|
497
|
+
const liveAgents = agentsService(ctx)
|
|
498
|
+
const liveAgent = liveAgents && typeof liveAgents.get === 'function' ? liveAgents.get(entry.sessionId) : undefined
|
|
472
499
|
if (!liveAgent) {
|
|
473
500
|
// 会话已经不在了(被删/换机器):别开隔离区,也别假装在盯。
|
|
474
501
|
// 必须走 bumpFailure —— 否则这条会每 intervalSec 打一次 GitHub,永不退避(第四轮审核指出)。
|
|
@@ -540,7 +567,10 @@ export function apply(ctx) {
|
|
|
540
567
|
try {
|
|
541
568
|
await pollOnce(ctx, entry, every)
|
|
542
569
|
} catch (error) {
|
|
570
|
+
// 未预期的异常(比如宿主上下文/服务不可用)也必须退避:
|
|
571
|
+
// 之前只写 lastError,于是坏掉的 watcher 会按 intervalSec 无限重试(真机踩到过)。
|
|
543
572
|
entry.lastError = String((error && error.message) || error)
|
|
573
|
+
bumpFailure(entry, every)
|
|
544
574
|
} finally {
|
|
545
575
|
inFlight.delete(entry.id)
|
|
546
576
|
persistEntry(entry)
|
|
@@ -568,7 +598,22 @@ export function apply(ctx) {
|
|
|
568
598
|
// does not wait for it.
|
|
569
599
|
const registerRoutes = (httpServer) => {
|
|
570
600
|
if (!httpServer || typeof httpServer.register !== 'function') return
|
|
601
|
+
for (const dispose of activeRoutes) { try { dispose() } catch {} }
|
|
602
|
+
activeRoutes.length = 0
|
|
571
603
|
const routes = []
|
|
604
|
+
// 宿主热重载时旧实例的路由可能还没 dispose;一条重复会让整个 try 崩掉,
|
|
605
|
+
// 面板和所有接口一起 404(2026-11-02 真机踩到 webserver: duplicate exact route)。
|
|
606
|
+
// 改成每条路由单独容错:重复的那条记下来,其余照常注册。
|
|
607
|
+
const failedRoutes = []
|
|
608
|
+
const addRoute = (options) => {
|
|
609
|
+
try {
|
|
610
|
+
const dispose = httpServer['register'](options)
|
|
611
|
+
routes.push(dispose)
|
|
612
|
+
activeRoutes.push(dispose)
|
|
613
|
+
} catch (error) {
|
|
614
|
+
failedRoutes.push(String((error && error.message) || error).slice(0, 120))
|
|
615
|
+
}
|
|
616
|
+
}
|
|
572
617
|
const record = (kind, detail) => {
|
|
573
618
|
const state = readState()
|
|
574
619
|
state.client = state.client || {}
|
|
@@ -576,7 +621,7 @@ export function apply(ctx) {
|
|
|
576
621
|
writeState(state)
|
|
577
622
|
}
|
|
578
623
|
try {
|
|
579
|
-
|
|
624
|
+
addRoute({
|
|
580
625
|
kind: 'exact',
|
|
581
626
|
path: '/session-superagent/state',
|
|
582
627
|
handler: async (req, res) => {
|
|
@@ -627,8 +672,8 @@ export function apply(ctx) {
|
|
|
627
672
|
watchers: watchers.map((w) => ({ ...w, goal: goalView(ctx, w.sessionId) })),
|
|
628
673
|
}))
|
|
629
674
|
},
|
|
630
|
-
})
|
|
631
|
-
|
|
675
|
+
})
|
|
676
|
+
addRoute({
|
|
632
677
|
kind: 'exact',
|
|
633
678
|
path: '/session-superagent/client-note',
|
|
634
679
|
handler: (req, res) => {
|
|
@@ -640,12 +685,12 @@ export function apply(ctx) {
|
|
|
640
685
|
}
|
|
641
686
|
res.statusCode = 204; res.end()
|
|
642
687
|
},
|
|
643
|
-
})
|
|
644
|
-
|
|
688
|
+
})
|
|
689
|
+
addRoute({
|
|
645
690
|
kind: 'exact',
|
|
646
691
|
path: '/session-superagent/client-error',
|
|
647
692
|
handler: (req, res) => { record('error', String(req.url).slice(0, 400)); res.statusCode = 204; res.end() },
|
|
648
|
-
})
|
|
693
|
+
})
|
|
649
694
|
const readBody = (req) => new Promise((resolve) => {
|
|
650
695
|
const chunks = []
|
|
651
696
|
req.on('data', (chunk) => chunks.push(chunk))
|
|
@@ -666,7 +711,7 @@ export function apply(ctx) {
|
|
|
666
711
|
|
|
667
712
|
// Create or update a watcher from the panel. The panel is session-scoped,
|
|
668
713
|
// so it must send the session id it is bound to.
|
|
669
|
-
|
|
714
|
+
addRoute({
|
|
670
715
|
kind: 'exact',
|
|
671
716
|
path: '/session-superagent/watch',
|
|
672
717
|
handler: async (req, res) => {
|
|
@@ -747,11 +792,11 @@ export function apply(ctx) {
|
|
|
747
792
|
}
|
|
748
793
|
json(res, 200, { ok: true, id, repo, intervalSec, enabled: entry.enabled })
|
|
749
794
|
},
|
|
750
|
-
})
|
|
795
|
+
})
|
|
751
796
|
|
|
752
797
|
// Credentials typed by the operator. Stored 0600 under the state dir and
|
|
753
798
|
// never echoed back; the answer carries only a masked summary.
|
|
754
|
-
|
|
799
|
+
addRoute({
|
|
755
800
|
kind: 'exact',
|
|
756
801
|
path: '/session-superagent/credentials',
|
|
757
802
|
handler: async (req, res) => {
|
|
@@ -775,12 +820,12 @@ export function apply(ctx) {
|
|
|
775
820
|
writeSecrets(next)
|
|
776
821
|
json(res, 200, { credentials: secretsSummary() })
|
|
777
822
|
},
|
|
778
|
-
})
|
|
823
|
+
})
|
|
779
824
|
|
|
780
825
|
// Set / edit / clear the session's REAL goal through ctx.goals — the same
|
|
781
826
|
// service the /goal command drives, so the panel and the conversation are
|
|
782
827
|
// one source of truth (polling reads it back).
|
|
783
|
-
|
|
828
|
+
addRoute({
|
|
784
829
|
kind: 'exact',
|
|
785
830
|
path: '/session-superagent/goal',
|
|
786
831
|
handler: async (req, res) => {
|
|
@@ -812,23 +857,30 @@ export function apply(ctx) {
|
|
|
812
857
|
return json(res, 400, { error: String((error && error.message) || error) })
|
|
813
858
|
}
|
|
814
859
|
},
|
|
815
|
-
})
|
|
860
|
+
})
|
|
816
861
|
|
|
817
862
|
const state = readState()
|
|
818
|
-
state.server = { at: new Date().toISOString(), routes: routes.length }
|
|
863
|
+
state.server = { at: new Date().toISOString(), routes: routes.length, failed: failedRoutes }
|
|
819
864
|
writeState(state)
|
|
820
865
|
} catch (error) {
|
|
821
866
|
const state = readState()
|
|
822
|
-
state.server = { at: new Date().toISOString(), error: String((error && error.message) || error).slice(0, 300) }
|
|
867
|
+
state.server = { at: new Date().toISOString(), routes: routes.length, failed: failedRoutes, error: String((error && error.message) || error).slice(0, 300) }
|
|
823
868
|
writeState(state)
|
|
824
869
|
}
|
|
825
|
-
|
|
870
|
+
const teardown = () => {
|
|
826
871
|
for (const dispose of routes) { try { dispose() } catch {} }
|
|
872
|
+
routes.length = 0
|
|
873
|
+
activeRoutes.length = 0
|
|
827
874
|
// 插件卸载/热重载后旧定时器不能继续跑:它们还持有已 dispose 的 ctx
|
|
828
875
|
for (const handle of timers.values()) { try { clearInterval(handle) } catch {} }
|
|
829
876
|
timers.clear()
|
|
830
877
|
try { clearInterval(sweepHandle) } catch {}
|
|
831
|
-
}
|
|
878
|
+
}
|
|
879
|
+
// 两条路都挂上:ctx.effect 的清理函数在 effect 作用域销毁时一定被调用;
|
|
880
|
+
// ctx.on('dispose') 覆盖宿主走事件广播的卸载方式。真机踩过「路由没 dispose →
|
|
881
|
+
// 重载后重复路由 → 六条接口全灭」,多挂一条成本很低。
|
|
882
|
+
try { ctx.effect(() => teardown) } catch {}
|
|
883
|
+
ctx.on('dispose', teardown)
|
|
832
884
|
}
|
|
833
885
|
|
|
834
886
|
// Routes for the panel. `webServer` is the host's route registry (the same
|
|
@@ -934,6 +986,22 @@ export function apply(ctx) {
|
|
|
934
986
|
const all = Object.values(state.watchers || {})
|
|
935
987
|
const wanted = args.repo ? all.filter((w) => w && w.repo === String(args.repo)) : all
|
|
936
988
|
|
|
989
|
+
// 0. 挂载状态:面板/HTTP 靠路由,路由注册失败时工具仍然可用 —— 这种「半死」状态
|
|
990
|
+
// 光看工具是发现不了的(2026-11-02 真机踩过:duplicate exact route 让六条接口全灭)
|
|
991
|
+
if (state.server) {
|
|
992
|
+
const mounted = Number(state.server.routes) || 0
|
|
993
|
+
const failed = Array.isArray(state.server.failed) ? state.server.failed : []
|
|
994
|
+
if (state.server.error) {
|
|
995
|
+
lines.push(`FAIL 面板接口挂载:${state.server.error}(工具仍可用,但面板会空白;重载一次即可)`)
|
|
996
|
+
} else if (failed.length > 0) {
|
|
997
|
+
lines.push(`WARN 面板接口部分失败(${mounted}/6 成功):${failed.join(';')}`)
|
|
998
|
+
} else {
|
|
999
|
+
lines.push(`OK 面板接口挂载:${mounted} 条,无失败`)
|
|
1000
|
+
}
|
|
1001
|
+
} else {
|
|
1002
|
+
lines.push('WARN 还没有挂载记录(刚装上或从未加载过路由)')
|
|
1003
|
+
}
|
|
1004
|
+
|
|
937
1005
|
// 1. gh 是否登录
|
|
938
1006
|
const auth = await run('gh', ['auth', 'status'], { env: ghEnv() })
|
|
939
1007
|
const authed = auth.code === 0
|
|
@@ -1032,7 +1100,8 @@ export function apply(ctx) {
|
|
|
1032
1100
|
goal: { type: 'string', description: 'Optional per-watcher goal appended to the wake message.' },
|
|
1033
1101
|
},
|
|
1034
1102
|
async execute(args) {
|
|
1035
|
-
const
|
|
1103
|
+
const toolAgents = agentsService(ctx)
|
|
1104
|
+
const agent = toolAgents && typeof toolAgents.currentInitiator === 'function' ? toolAgents.currentInitiator() : undefined
|
|
1036
1105
|
const sessionId = agent && agent.id
|
|
1037
1106
|
if (!sessionId) return '[error: no live session — watch_start must be called from a session turn]'
|
|
1038
1107
|
|
|
@@ -1135,7 +1204,8 @@ export function apply(ctx) {
|
|
|
1135
1204
|
description: 'Stop watching a repository for the current session.',
|
|
1136
1205
|
parameters: { repo: { type: 'string', required: true, description: 'owner/name' } },
|
|
1137
1206
|
async execute(args) {
|
|
1138
|
-
const
|
|
1207
|
+
const toolAgents = agentsService(ctx)
|
|
1208
|
+
const agent = toolAgents && typeof toolAgents.currentInitiator === 'function' ? toolAgents.currentInitiator() : undefined
|
|
1139
1209
|
const sessionId = agent && agent.id
|
|
1140
1210
|
const id = `${args.repo}#${sessionId}`
|
|
1141
1211
|
const state = readState()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-superagent",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.29",
|
|
4
4
|
"description": "Session-level superagent for DeepSeek Harness: watch a GitHub repository and wake a session when something needs doing. Credentials never reach the model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
"README.md",
|
|
18
18
|
"CHANGELOG.md",
|
|
19
19
|
"LICENSE",
|
|
20
|
-
"client.js"
|
|
20
|
+
"client.js",
|
|
21
|
+
"UPDATING.md",
|
|
22
|
+
"CHECKLIST.md",
|
|
23
|
+
"MAINTENANCE.md"
|
|
21
24
|
],
|
|
22
25
|
"engines": {
|
|
23
26
|
"node": ">=22.19.0",
|