@aion0/forge 0.10.20 → 0.10.23
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/RELEASE_NOTES.md +22 -4
- package/app/api/connectors/route.ts +1 -1
- package/app/api/watches/[id]/route.ts +25 -0
- package/app/api/watches/route.ts +17 -0
- package/app/chat/page.tsx +66 -4
- package/components/Dashboard.tsx +21 -5
- package/components/MonitorPanel.tsx +88 -0
- package/components/WatchesPanel.tsx +97 -0
- package/docs/forge-long-task-watch-design.md +223 -0
- package/docs/tp-automation-api.md +617 -0
- package/lib/browser-bridge-standalone.ts +13 -4
- package/lib/chat/agent-loop.ts +34 -4
- package/lib/chat/bridge-client.ts +2 -2
- package/lib/chat/protocols/ssh.ts +206 -0
- package/lib/chat/tool-dispatcher.ts +60 -5
- package/lib/chat-standalone.ts +12 -0
- package/lib/connectors/types.ts +118 -2
- package/lib/help-docs/21-build-connector.md +42 -0
- package/lib/help-docs/24-watch.md +77 -0
- package/lib/help-docs/CLAUDE.md +2 -0
- package/lib/watch/register.ts +108 -0
- package/lib/watch/start-watch-tool.ts +116 -0
- package/lib/watch/template.ts +40 -0
- package/lib/watch/watch-runner.ts +158 -0
- package/lib/watch/watch-store.ts +218 -0
- package/package.json +1 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Forge — Long-Task Watch(轻量独立后台轮询 + 回调编排)设计方案
|
|
2
|
+
|
|
3
|
+
> 状态:**后端已实现(分支 `feat/watch`,commit ad374ec,未 merge)**。
|
|
4
|
+
> 已建:types(AsyncWatchSpec)、lib/watch/{watch-store,watch-runner,register,template}、
|
|
5
|
+
> tool-dispatcher 注册钩子、chat-standalone 启动 runner(进度→watch_status 事件 /
|
|
6
|
+
> 完成→runTurn 回灌)、app/api/watches(list/cancel/delete)。tsc 通过。
|
|
7
|
+
> **待做**:① UI——extension + /chat 订阅 `watch_status` 渲染状态条 chip、Settings→Monitor
|
|
8
|
+
> 的 watch 列表+cancel/delete;② 给 nac.upgrade 等连接器加 `async:` 块(消费者);
|
|
9
|
+
> ③ 实测 + merge。
|
|
10
|
+
> 一句话定性(zliu):watch = **在 chat 里支持的一个轻量化异步回调机制**。
|
|
11
|
+
> 消费者:TP 升级、**NAC 直连升级(nac.upgrade → 轮询 nac.get_version 直到
|
|
12
|
+
> build 匹配)**、pytest 跑——凡是"发起后要等、完成再回 chat"的都用它。
|
|
13
|
+
> 起因:TP `upgrade_lab`(NAC 升级)同步阻塞 5-10 分钟。连接器已用
|
|
14
|
+
> `detach` 让它不卡死,但"发起后盯到完成"这步若靠 AI 在对话里轮询,
|
|
15
|
+
> **不可靠**。需要一个**轻量、独立、纯后台**的机制:发起后自动定期 poll,
|
|
16
|
+
> 完成时**回调一个工具**(而非通知用户),全程不依赖 AI / 不依赖对话存活。
|
|
17
|
+
|
|
18
|
+
## 0. 目标 & 非目标(按 zliu 最新意见收敛)
|
|
19
|
+
|
|
20
|
+
**目标**
|
|
21
|
+
- 发起即返回,不 hold 对话/标签页。
|
|
22
|
+
- **轻量、独立**:自带一个小 ticker + 一张表,**不挂靠 Schedules**(那是用户
|
|
23
|
+
预定任务,语义不同),也不新增 standalone 进程。
|
|
24
|
+
- 完成/失败 → **回灌到发起的那个 chat 会话**:把 poll 结果作为一条
|
|
25
|
+
tool-result/系统事件喂回该 session,**让助手吐一条消息**(并可顺势继续判断/
|
|
26
|
+
调用下一步工具)。这天然复用 chat:若该会话来自 telegram,消息就自然回到
|
|
27
|
+
telegram;来自 /chat 就在 /chat 冒出来——**不需要单独的 telegram/email 通道**。
|
|
28
|
+
- (可选)纯编排:也支持声明式 `on_done` 直接调一个工具做链式(升级完→自动
|
|
29
|
+
run_pytest),不吐消息。两种模式按 manifest 选。
|
|
30
|
+
- **每次 poll 可给轻量进度反馈,但不抢主对话**(zliu):进度走**独立的
|
|
31
|
+
状态通道**(`watch_status` push),在 chat 里渲染成一个**会就地更新的小状态条/
|
|
32
|
+
chip**——每 tick 替换、不追加,**不进消息历史、不触发 LLM**,完成即消失。
|
|
33
|
+
只有最终 `on_done`/`on_fail` 的终态消息才真正落进对话线程。即:
|
|
34
|
+
**过程 = 环境状态(ambient),结果 = 一条真消息**。
|
|
35
|
+
- **防死循环是一等需求**:后台自动调度最怕失控,必须有硬上限。
|
|
36
|
+
- **极简管理面**:不做常规 UI,但要**一个能看 active watch 列表 + 取消/删除**
|
|
37
|
+
的地方,避免 watch 堆积、占资源、跑飞。
|
|
38
|
+
|
|
39
|
+
**非目标**
|
|
40
|
+
- 不做"独立于 chat 之外"的新通知通道——完成消息走发起会话本身的 chat 流。
|
|
41
|
+
- 不做复杂表达式引擎(done 判定走安全的点路径)。
|
|
42
|
+
- 不进 Schedules UI、不算一种 Schedule 类型。
|
|
43
|
+
|
|
44
|
+
## 1. 形态:独立 `watch` 模块
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
AI(在 session S)─call─> upgrade_lab ─(detach 25s, 返回 fired_at)─┐
|
|
48
|
+
├─ dispatcher 见 async: registerWatch(..., session_id=S)
|
|
49
|
+
<── 立即返回 {dispatched, watch_id} ──────────────────┘
|
|
50
|
+
…………(对话可结束)…………
|
|
51
|
+
watch ticker(独立, ~30-60s)
|
|
52
|
+
└ 对每条 active watch,到点:
|
|
53
|
+
poll_tool ── done_path? ──┬─ 是 → 完成动作 → 终态 done
|
|
54
|
+
├─ fail_path? → 失败动作 → 终态 failed
|
|
55
|
+
└─ 否 → polls++;超 max_polls / 过 timeout → 终态 timed_out(走失败动作)
|
|
56
|
+
|
|
57
|
+
完成/失败动作(二选一,按 manifest):
|
|
58
|
+
• mode=chat(默认):把结果作为 tool-result 回灌 session S → resume 该会话 →
|
|
59
|
+
助手吐消息(走 chat-standalone 现有的 resume + bridgePush 流;telegram 会话则到 telegram)。
|
|
60
|
+
• mode=tool:派发声明的 on_done.tool(链式编排,不吐消息)。
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- ticker 是一个 `setInterval`(随 **chat-standalone** 起,单实例守卫),**不是新
|
|
64
|
+
进程**,也**不复用 Schedules tick**——逻辑独立、好推理、好限流。放 chat-standalone
|
|
65
|
+
里还顺手:回灌 session 用的就是它自己的 resume 能力。
|
|
66
|
+
- registerWatch 记下 `session_id`(发起工具的那个 chat 会话),完成时回灌。
|
|
67
|
+
- watch 是**短生命周期**:到达终态(done/failed/timed_out/cancelled)即停轮询。
|
|
68
|
+
|
|
69
|
+
## 2. Manifest 声明(连接器侧,新增 `async` 块)
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
upgrade_lab:
|
|
73
|
+
destructive: true
|
|
74
|
+
async:
|
|
75
|
+
poll: check_lab_upgrade_status # 同连接器内的查询工具
|
|
76
|
+
poll_args: # 用触发工具的 result/args 拼 poll 入参
|
|
77
|
+
deployinfo: "{args.deployinfo}"
|
|
78
|
+
lab: "{args.lab}"
|
|
79
|
+
since: "{result.fired_at}" # {result.*}=触发工具返回值;{args.*}=触发入参
|
|
80
|
+
# 完成判定:二选一。
|
|
81
|
+
done_path: done # (A) poll 结果该路径 truthy = 完成(TP:连接器自己比好返回 done:true)
|
|
82
|
+
# done_match: # (B) 值比较(NAC:get_version 返回 build 号,跟目标比)
|
|
83
|
+
# path: captured.build # poll 结果里的路径
|
|
84
|
+
# equals: "{args.target_build}" # equals / contains,期望值从触发 args/result 模板取
|
|
85
|
+
fail_path: any_failure # 可选:truthy = 失败
|
|
86
|
+
interval_sec: 60 # 轮询间隔(下限 30s)
|
|
87
|
+
timeout_sec: 1200 # 总时长上限(到点判 timed_out)
|
|
88
|
+
max_polls: 40 # 轮询次数硬上限(防失控)
|
|
89
|
+
on_done: # 完成动作
|
|
90
|
+
mode: chat # chat(默认,回灌发起会话让助手吐消息) | tool(链式) | none(仅落库)
|
|
91
|
+
message: "升级完成,核对结果。" # mode=chat 时注入会话的提示(可引用 {poll.*})
|
|
92
|
+
tool: "" # mode=tool 时要调的工具名
|
|
93
|
+
args: {} # mode=tool 入参,可引用 {poll.*}/{args.*}
|
|
94
|
+
on_fail: # 可选,同形(默认 mode=chat,把失败/超时也吐回会话)
|
|
95
|
+
mode: chat
|
|
96
|
+
message: "升级未在预期内完成,请手动核对。"
|
|
97
|
+
progress: # 每次 poll 的轻量反馈(不抢主对话)
|
|
98
|
+
show: true # 默认 true;false = 后台静默,只在终态出消息
|
|
99
|
+
message: "盯升级中… 第 {poll_count}/{max_polls} 次,build={poll.build}" # 可引用 {poll.*}/{poll_count}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**两条输出通道(关键:进度不抢主对话)**
|
|
103
|
+
- **进度通道**:每 tick 通过独立 push topic `watch_status` 推一条
|
|
104
|
+
`{watch_id, state, poll_count, text}`。chat 端渲染成一个**就地更新的小状态条/
|
|
105
|
+
chip**(按 watch_id 去重替换,不追加),**不写入消息历史、不触发 LLM、不计入
|
|
106
|
+
上下文**;watch 一到终态该状态条自动消失/收起。这样轮询再多也不污染对话。
|
|
107
|
+
- **结果通道**:只有 `on_done`/`on_fail`(mode=chat)产出**一条真消息**进对话
|
|
108
|
+
线程。`progress.show:false` 则全程静默,只留终态。
|
|
109
|
+
- 实现:进度走 `bridgePush('watch_status', …)`(extension/web 各加一个轻量
|
|
110
|
+
status-bar 组件订阅);结果走已有的 session resume 回灌(§1)。
|
|
111
|
+
- **终态清 chip(必须显式发,别靠超时)**:watch 到终态时,`on_done`/`on_fail`
|
|
112
|
+
只走结果通道(发完成消息),**不会再发常规进度**——若不补一条信号,chip 会
|
|
113
|
+
一直挂到 prune 超时(150s)才消失,出现"结果已给、进度条还在转"的割裂。
|
|
114
|
+
所以 `finish()` 额外补发一条**终态 `watch_status`**:`{watch_id, state, done:true, text}`。
|
|
115
|
+
前端收到 `done:true`(或 `state≠active`)就**立刻删除该 chip**;否则才更新。
|
|
116
|
+
即:进度通道既负责"更新",也负责"收尾删除"。(prune 150s 仅作兜底,正常
|
|
117
|
+
路径靠这条终态信号即时清除。)
|
|
118
|
+
|
|
119
|
+
- **完成回灌 chat**:默认 `mode=chat`——把最后一次 poll 结果 + `message` 作为
|
|
120
|
+
tool-result 喂回发起的 session,助手据此吐一条消息(还能继续调用下一步)。
|
|
121
|
+
`mode=tool` 则纯链式派发工具、不吐消息;`mode=none` 只落库置终态(AI 下次
|
|
122
|
+
需要时可查)。tool 模式走和 chat 同一条 dispatcher,可链式。
|
|
123
|
+
- **done 判定不用 eval**:`done_path`/`fail_path` 点路径取真值;`done_match`
|
|
124
|
+
做 equals/contains 比较(期望值模板取),都不跑 eval、零注入面。
|
|
125
|
+
|
|
126
|
+
NAC 直连升级的 async 块(第二个消费者,用 done_match):
|
|
127
|
+
```yaml
|
|
128
|
+
upgrade: # nac connector
|
|
129
|
+
async:
|
|
130
|
+
poll: get_version
|
|
131
|
+
poll_args: {} # get_version 无参,连 settings 里的 host 即可
|
|
132
|
+
done_match: { path: build, equals: "{args.target_build}" } # build6957 → "6957"
|
|
133
|
+
interval_sec: 60
|
|
134
|
+
timeout_sec: 900 # 含 reboot,放宽
|
|
135
|
+
on_done: { mode: chat, message: "NAC {settings.host} 已升级到 build {poll.build}。" }
|
|
136
|
+
```
|
|
137
|
+
注:`nac.upgrade` 目前参数没有 target_build——接 watch 时给它加一个(或从
|
|
138
|
+
镜像文件名 `build(\d+)` 自动解析),传给 watch 做比较。
|
|
139
|
+
|
|
140
|
+
## 3. 防死循环(纯后台调度的核心约束)
|
|
141
|
+
|
|
142
|
+
| 失控源 | 护栏 |
|
|
143
|
+
|---|---|
|
|
144
|
+
| 轮询永不停 | `max_polls`(默认 40)+ `timeout_sec`(默认 1200s),先到先终止 |
|
|
145
|
+
| 同一 watch 并发重入 | **单飞**:每条 watch 有 `running` 锁,上一次 poll 没回来不发下一次 |
|
|
146
|
+
| 回调再生回调(A→B→A) | 回调链 `chain_depth` 上限(默认 3)+ 已访问 watch 链路环检测 |
|
|
147
|
+
| 回调本身又是 async 工具 | 允许,但继承并递减 `chain_depth`;到 0 则只跑、不再登记新 watch |
|
|
148
|
+
| watch 越堆越多 | 全局 active watch 上限(默认 50);超了拒绝新登记并在触发工具结果里报错 |
|
|
149
|
+
| poll 报错刷屏 | 连续 N 次错误(默认 5)→ 终态 `errored`,不再重试 |
|
|
150
|
+
| 僵尸 watch | 任何 watch 硬性 `max_lifetime`(默认 2×timeout)兜底清理 |
|
|
151
|
+
|
|
152
|
+
所有上限都有**默认值**且 manifest 可调小,不可调到无界。
|
|
153
|
+
|
|
154
|
+
## 4. 重启不丢
|
|
155
|
+
|
|
156
|
+
- watch **持久化在 SQLite**(`connector_watches`),不是内存。
|
|
157
|
+
- Forge 重启/崩溃 → ticker 起来后扫 `state=active` 的 watch,凭存下的
|
|
158
|
+
`fired_at`/`next_poll_at`/`polls` 续轮询,**不靠 AI、不靠原对话**。
|
|
159
|
+
- 宕机超过 `max_lifetime` 的 watch → 标 `timed_out`、跑 `on_fail`(若有),
|
|
160
|
+
绝不静默丢。
|
|
161
|
+
|
|
162
|
+
## 5. 极简管理面(防资源占用 / 可取消)
|
|
163
|
+
|
|
164
|
+
不做常规 UI,但提供**最小可观测+可控**:
|
|
165
|
+
|
|
166
|
+
- **API**:`GET /api/watches`(列 active+近期终态)、`POST /api/watches/:id/cancel`、
|
|
167
|
+
`DELETE /api/watches/:id`。
|
|
168
|
+
- **UI 落点**(二选一,倾向 A):
|
|
169
|
+
- **A. Settings → Monitor** 加一个 "Background Watches" 折叠区:每行显示
|
|
170
|
+
connector·poll_tool·polls/max·下次轮询·状态,带 Cancel/Delete。和已有
|
|
171
|
+
进程监控同处,零新页面。
|
|
172
|
+
- **B.** /chat 侧栏一个折叠小列表。
|
|
173
|
+
- cancel = 立即置 `cancelled` 终态、停轮询;delete = 删行。
|
|
174
|
+
|
|
175
|
+
## 6. 改动清单
|
|
176
|
+
|
|
177
|
+
| 在哪 | 改什么 | 量 |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| 类型 | `lib/connectors/types.ts`:`ConnectorTool.async?: AsyncWatchSpec`(poll/poll_args/done_path/done_match/fail_path/interval/timeout/max_polls/on_done/on_fail/**progress**) | 小 |
|
|
180
|
+
| 存储 | `lib/watch/watch-store.ts`(新):SQLite 表 + CRUD + 单实例守卫 | ~100 行 |
|
|
181
|
+
| 轮询 | `lib/watch/watch-runner.ts`(新):独立 ticker,单飞、护栏、终态机、调 poll/回调、**每 tick + 终态各发一条 `watch_status`**(终态带 `done:true` 让前端立即清 chip) | ~170 行 |
|
|
182
|
+
| 派发 | `lib/chat/tool-dispatcher.ts`:跑完带 `async` 的工具 → `registerWatch`,返回附 `watch_id` | 小 |
|
|
183
|
+
| API | `app/api/watches/route.ts` + `[id]`:list / cancel / delete | 小 |
|
|
184
|
+
| UI(状态条) | extension + /chat 各加一个轻量组件,订阅 `watch_status` push → 渲染就地更新的 chip(不进消息流) | 小 |
|
|
185
|
+
| UI(管理面) | Settings Monitor 加 Background Watches 折叠区(只读+cancel/delete) | 小 |
|
|
186
|
+
| 连接器 | `upgrade_lab`/`upgrade_device` 加 `async:` 块 | 几行 |
|
|
187
|
+
|
|
188
|
+
**主体在 Forge**(类型+store+runner+API+UI);连接器只加声明块。**不复用
|
|
189
|
+
Schedules、不新增进程**——独立轻量,正合你要的形态。
|
|
190
|
+
|
|
191
|
+
## 7. TP 落地体验(后台轮询 + 完成回灌 chat)
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
用户:升级 AT16_Combined_FSW 到 build6957
|
|
195
|
+
AI :upgrade_lab(command=…) → {dispatched, watch_id, fired_at}
|
|
196
|
+
AI :「已发起,后台盯到完成会在这里告诉你。」← 对话可结束/去忙别的
|
|
197
|
+
(后台 ticker 每 60s poll check_lab_upgrade_status(since=fired_at),无 AI 参与)
|
|
198
|
+
done:true → 回灌 session S → 助手在原会话吐:
|
|
199
|
+
「✅ AT16_Combined_FSW 升级完成,FortiNAC(10.15.52.152)已到 build6957。」
|
|
200
|
+
(若该会话是 telegram,这条就到 telegram;是 /chat 就在 /chat 冒出来。
|
|
201
|
+
若配了 on_done.mode=tool,则改为自动跑 run_pytest,不吐消息。)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## 8. 工作量
|
|
205
|
+
|
|
206
|
+
- Forge:store + runner + dispatcher 钩子 + API + Monitor 区 ≈ **1~1.5 天**
|
|
207
|
+
(护栏/单飞/重启续跑要写稳)。
|
|
208
|
+
- 连接器:几行。
|
|
209
|
+
|
|
210
|
+
## 9. 待你决策
|
|
211
|
+
|
|
212
|
+
- [x] UI:不做常规 UI,但有**极简管理面**(列表+cancel/delete),落点倾向
|
|
213
|
+
Settings→Monitor 的折叠区。
|
|
214
|
+
- [x] 重启不丢:SQLite 持久化,续跑;超期回灌/回调,不静默丢。
|
|
215
|
+
- [x] 完成动作:默认 **mode=chat 回灌发起会话让助手吐消息**(telegram 会话→telegram,
|
|
216
|
+
/chat→/chat);可选 mode=tool 纯链式、mode=none 仅落库。**不**另起独立通知通道。
|
|
217
|
+
- [x] 进度反馈:每 tick 走独立 `watch_status` push,渲染成**就地更新的状态条/chip,
|
|
218
|
+
不进消息流、不触发 LLM**;只有终态出一条真消息。`progress.show` 可关。
|
|
219
|
+
- [ ] **是否实现?**
|
|
220
|
+
- [ ] 管理面落点 A(Settings→Monitor)还是 B(/chat 侧栏)? 右上角菜单下有个 watch 按钮
|
|
221
|
+
- [ ] 回调链深度 / active 上限 / max_polls 默认值是否认可(3 / 50 / 40)?
|
|
222
|
+
- [ ] mode=chat 回灌时,助手是"只吐一条总结消息"还是"可继续自主调用工具"(后者更强但要纳入防循环上限)?
|
|
223
|
+
- 只回总结吧,终止,然后由 chat决定吧。不用做嵌套,因为本身就是异步跟踪了,就可以一直获取。如果真的需要嵌套就设置一个嵌套数量吧,
|