Skip to content

Commit d618618

Browse files
Tonnodoubtclaude
andauthored
feat(task-hud): deferred completion, task restore, timeout, and thread-safe cache (#440)
* feat(task-hud): deferred completion, task restore, timeout, and thread-safe cache * fix(task-hud): resolve race condition, XSS vulnerability, and ghost tasks - Fix race condition in memo_reminder: add 5s buffer for bind_task completion - Improve HTTP error handling with detailed logging for task completion callbacks - Fix XSS vulnerability: use textContent instead of innerHTML for task type labels - Fix ghost tasks: replace task map instead of merge on page refresh - Add browser_use task type support with 🌐 icon - Add MCP task type icon (🔌) - Add typeBrowserUse i18n entries for all languages - Fix Chinese localization: "电脑控制" → "键鼠控制" for consistency Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * fix(task-hud): deferred timeout notification, validation, and UI fixes - Emit task_update when deferred task times out (frontend HUD now receives failed status) - Add validation to complete_deferred_task endpoint (only user_plugin with deferred_timeout) - Add deferred_bind_pending flag to distinguish deferred reminders from regular ones - Fix interval clearing on missing DOM (don't clear, just skip update) - Fix agent_status_update ghost tasks (replace map instead of merge) - Fix Japanese translation: "コンピュータ制御" → "キーボード/マウス操作" Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * fix(task-hud): comprehensive fixes for deferred tasks, session filtering, and retry logic - Add List import to agent_server.py (fixes NameError on startup) - Add lanlan_name to active_tasks snapshot for session filtering - Filter tasks by current lanlan_name in app.js (both restore paths) - Add deferred_bind_pending flag only for short-delay reminders (<1h) - Separate message delivery from callback retry in memo_reminder - Add 404 handling: abandon retry when task not found (server restart) - Add max retry limit (5) for callback failures - Preserve terminal tasks in linger window during agent_status_update - Fix Russian translation: "Управление компьютером" → "Управление клавиатурой/мышью" Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * fix(memo_reminder): transient state reset, deferred flag, and terminal_at - Clear transient fields (delivered, callback_*) on reschedule to allow repeated reminders to fire - Return needs_deferred instead of hardcoded True for deferred flag - Set terminal_at timestamp when task transitions to terminal state (for linger logic) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * fix(task-hud): HUD visibility, params merge, i18n, and code cleanup - Show HUD for any tasks (including terminal in linger), not just running/queued - Only start time-update interval when hasRunning=true - Fix params merge: use existing.params only when task.params is undefined - Fix Russian translation: cancelConfirm to match cancelAll scope - Remove duplicate import of ok/fail in memo_reminder bind_task Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * fix(task-hud): clear stale task fields on reschedule and improve linger window check - Clear agent_task_id and deferred_bind_pending when rescheduling recurring reminders - Include tasks in 10s linger window when checking hasActiveTasks to prevent premature HUD hide Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * fix(task-hud): unify linger timeout to 10000ms Change terminal task removal timeout from 8000ms to 10000ms to match LINGER_MS and MIN_DISPLAY_MS constants for consistent behavior. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 6f12289 commit d618618

11 files changed

Lines changed: 1135 additions & 179 deletions

File tree

agent_server.py

Lines changed: 289 additions & 26 deletions
Large diffs are not rendered by default.
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
# Task HUD System
2+
3+
Task HUD (Head-Up Display) 是一个悬浮在屏幕上的实时任务监控面板,用于显示 Agent 任务(包括插件任务)的执行状态和进度。
4+
5+
## 整体架构
6+
7+
```
8+
Backend Frontend
9+
┌────────────────┐ ┌────────────────────┐
10+
│ TaskExecutor │ │ app.js │
11+
│ │ │ WebSocket │ │ │
12+
│ │ task events│ ──────────────> │ │ _agentTaskMap │
13+
│ │ │ JSON messages │ │ │
14+
│ │ │ └─> AgentHUD │
15+
└────────────────┘ │ │ │
16+
│ ▼ │
17+
│ Task HUD Panel │
18+
└────────────────────┘
19+
```
20+
21+
## 数据流转
22+
23+
### 1. 任务创建
24+
25+
当用户触发一个 Agent 任务(包括插件任务)时,后端 Agent Server 会创建任务对象并分配唯一 ID。任务初始状态为 `queued`(队列中)。
26+
27+
### 2. 状态推送
28+
29+
后端通过 WebSocket 连接向前端推送任务状态消息,主要有两种类型:
30+
31+
| 消息类型 | 用途 |
32+
|---------|------|
33+
| `agent_task_snapshot` | 批量任务快照,初始化或重连时发送 |
34+
| `agent_task_update` | 单个任务状态更新,实时推送 |
35+
36+
### 3. 前端接收
37+
38+
前端 `app.js` 中的 WebSocket 处理器接收消息后:
39+
40+
1. 将任务数据存入 `window._agentTaskMap`(Map 结构)
41+
2. 计算各状态任务的数量统计
42+
3. 调用 `AgentHUD.updateAgentTaskHUD()` 更新界面
43+
44+
### 4. HUD 更新
45+
46+
`updateAgentTaskHUD()` 方法负责:
47+
48+
- 更新标题栏的统计数字(运行中、队列中)
49+
- 渲染或更新任务卡片
50+
- 控制空状态提示的显示/隐藏
51+
- 管理已完成/失败任务的短暂停留时间
52+
53+
## 任务状态
54+
55+
| 状态 | 显示文本 | 颜色 | 说明 |
56+
|------|---------|------|------|
57+
| `queued` | 队列中 | 灰色 | 任务已创建,等待执行 |
58+
| `running` | 运行中 | 蓝色 | 任务正在执行 |
59+
| `completed` | 已完成 | 绿色 | 任务执行成功 |
60+
| `failed` | 失败 | 红色 | 任务执行失败 |
61+
| `cancelled` | 已取消 | - | 任务被用户终止 |
62+
63+
## 任务数据结构
64+
65+
后端推送的任务对象包含以下字段:
66+
67+
| 字段 | 类型 | 说明 |
68+
|------|------|------|
69+
| `id` | string | 任务唯一标识 |
70+
| `type` | string | 任务类型(`user_plugin``plugin_direct``computer_use` 等) |
71+
| `status` | string | 任务状态 |
72+
| `start_time` | string | ISO 格式的开始时间 |
73+
| `params` | object | 任务参数 |
74+
75+
### params 字段
76+
77+
对于插件任务,`params` 包含:
78+
79+
| 字段 | 说明 |
80+
|------|------|
81+
| `plugin_id` | 插件 ID(内部标识) |
82+
| `plugin_name` | 插件友好名称(用于显示) |
83+
| `entry_id` | 入口点 ID |
84+
| `description` | 任务描述(用户原话,如"三分钟后提醒我吃饭") |
85+
86+
对于 Computer Use 任务,`params` 包含:
87+
88+
| 字段 | 说明 |
89+
|------|------|
90+
| `instruction` | 任务指令(用户原话) |
91+
92+
## 任务卡片布局
93+
94+
采用**紧凑多行**布局:
95+
96+
```
97+
┌──────────────────────────────────┐
98+
│ 🧩 提醒插件 ▏运行中▕ ✕ │ ← 第一行:图标 + 名称 + 状态 + 取消
99+
│ 三分钟后提醒我吃饭 │ ← 描述行:任务具体内容(可选)
100+
│ ⏱️ 1:23 ━━━━━━━━━━━━━━━━ 2/3 │ ← 进度行:倒计时 + 进度条 + 步数
101+
└──────────────────────────────────┘
102+
```
103+
104+
| 元素 | 位置 | 说明 |
105+
|------|------|------|
106+
| 类型图标 | 第一行左 | 插件 🧩 / Computer Use 🖱️ / 其他 ⚙️ |
107+
| 名称 | 第一行左 | 插件任务优先用 `plugin_name`,否则用 `plugin_id`,最后用类型翻译名 |
108+
| 状态徽章 | 第一行中 | 带背景色的状态文字 |
109+
| 取消按钮 | 第一行右 | 点击可终止单个任务 |
110+
| 任务描述 | 描述行 | 显示 `params.description``params.instruction`(用户原话),单行省略 |
111+
| 运行时间 | 进度行左 | 仅运行中任务显示,格式 `⏱️ 分:秒` |
112+
| 进度条 | 进度行中 | 运行中任务显示,支持确定性和动画两种模式 |
113+
| 步数 | 进度行右 |`step``step_total` 存在时显示,如 `2/3` |
114+
115+
> 已完成/失败的任务停留 10 秒后移除(透明度 0.6),只显示第一行和描述行。
116+
117+
### 任务类型
118+
119+
| 类型 ID | 显示名称 | 图标 |
120+
|---------|---------|------|
121+
| `user_plugin` | 用户插件 | 🧩 |
122+
| `plugin_direct` | 用户插件 | 🧩 |
123+
| `computer_use` | 电脑控制 | 🖱️ |
124+
| `mcp` | MCP工具 | ⚙️ |
125+
126+
## 交互功能
127+
128+
### 拖拽定位
129+
130+
- HUD 整体可拖拽,支持鼠标和触摸操作
131+
- 拖拽时自动进行边界检测,确保不超出屏幕
132+
- 位置保存到 `localStorage`,下次打开时恢复
133+
134+
### 折叠/展开
135+
136+
- 点击标题栏的最小化按钮(▼)可折叠为紧凑模式
137+
- 折叠状态下只显示统计数字,隐藏任务列表
138+
- 折叠状态保存到 `localStorage`
139+
140+
### 批量终止
141+
142+
- 标题栏右侧显示终止按钮(✕)
143+
- 点击后弹出确认对话框
144+
- 确认后调用 `/api/agent/admin/control` 接口终止所有任务
145+
146+
### 显示策略
147+
148+
- HUD 主要显示**运行中****队列中**的任务
149+
- 任务完成或失败后,卡片会**停留 10 秒**再从 HUD 移除(透明度降为 0.6),防止快速任务一闪而过
150+
- 停留期间使用 `setTimeout` 定时触发 re-render 清理过期卡片
151+
- 当没有活跃任务时,显示空状态提示
152+
153+
## 视觉设计
154+
155+
### 样式特点
156+
157+
- **毛玻璃效果**`backdrop-filter: blur(20px)` + 半透明背景
158+
- **圆角设计**:8px 圆角,卡片内部元素使用更小的圆角
159+
- **状态颜色**
160+
- 运行中:`#2a7bc4`(蓝色)
161+
- 已完成:`#16a34a`(绿色)
162+
- 失败:`#dc2626`(红色)
163+
- 队列中:`#666`(灰色)
164+
- **平滑动画**:状态变化和布局变化使用 CSS transition
165+
166+
### 尺寸限制
167+
168+
- 宽度:320px
169+
- 最大高度:60vh
170+
- 内容超出时内部滚动
171+
172+
## 技术细节
173+
174+
### 跨进程插件名称获取
175+
176+
由于 N.E.K.O 采用多进程架构,Agent Server(端口 48915)和 Main Server 是独立进程。插件状态(`plugin.core.state.state.plugins`)维护在 Main Server 进程中,Agent Server 无法直接访问。
177+
178+
**解决方案:**
179+
180+
Agent Server 通过 HTTP 调用嵌入式插件服务(端口 48916)的 `/plugins` 端点获取插件元数据:
181+
182+
```
183+
Agent Server (48915)
184+
185+
│ HTTP GET /plugins
186+
187+
User Plugin Server (48916) ──> 返回插件列表(含 name 字段)
188+
```
189+
190+
**缓存机制:**
191+
192+
为避免频繁 HTTP 请求,插件名称缓存 30 秒(`PLUGIN_NAME_CACHE_TTL`)。实现位于 `agent_server.py``_get_plugin_friendly_name()` 函数。
193+
194+
**数据流:**
195+
196+
1. 任务创建时,调用 `_get_plugin_friendly_name(plugin_id)` 获取友好名称
197+
2.`plugin_name` 添加到 `task_params`
198+
3. 通过 `task_update` WebSocket 消息推送到前端
199+
4. 前端从 `params.plugin_name` 读取并显示
200+
201+
### 前端显示逻辑
202+
203+
对于插件任务(`user_plugin``plugin_direct`),HUD 显示逻辑:
204+
205+
1. **名称行**:优先显示 `params.plugin_name`,回退到 `params.plugin_id`,最后显示翻译后的"用户插件"
206+
2. **描述行**:显示 `params.description`(插件任务)或 `params.instruction`(CU 任务),即用户原话
207+
3. **进度行**:倒计时 + 进度条(仅运行中任务)
208+
209+
### 任务停留与清理
210+
211+
**前端停留**:已完成/失败的任务在 HUD 中停留 10 秒后移除(`MIN_DISPLAY_MS = 10000`),通过 `_taskTerminalAt` 记录进入终态的时间戳,`setTimeout` 延迟触发 re-render 清理。
212+
213+
**后端内存清理**`task_registry` 中已完成/失败/取消的任务在 5 分钟后自动清理(`TASK_REGISTRY_CLEANUP_TTL = 300`),通过 `_cleanup_task_registry()` 在每次生成快照时触发(最多每 60 秒一次)。
214+
215+
### 样式主题适配
216+
217+
卡片文字颜色和背景全部使用 CSS 变量,自动适配暗色模式:
218+
219+
| 用途 | CSS 变量 | 亮色回退值 |
220+
|------|----------|-----------|
221+
| 名称文字 | `--neko-popup-text-sub` | `#666` |
222+
| 倒计时文字 | `--neko-popup-text-sub` | `#888` |
223+
| 取消按钮 | `--neko-popup-text-sub` | `#999` |
224+
| 运行中背景 | `--neko-popup-accent-bg` | `rgba(42, 123, 196, 0.08)` |
225+
| 进度条底色 | `--neko-popup-accent-bg` | `rgba(42, 123, 196, 0.15)` |
226+
| 进度条填充 | `--neko-popup-accent` | `#2a7bc4` |
227+
228+
## 相关文件
229+
230+
| 文件 | 说明 |
231+
|------|------|
232+
| `static/common-ui-hud.js` | HUD 组件核心实现 |
233+
| `static/app.js` | WebSocket 消息处理,调用 HUD 更新 |
234+
| `static/locales/*.json` | i18n 翻译文件 |
235+
| `agent_server.py` | `_get_plugin_friendly_name()` 跨进程获取插件名称 |
236+
237+
## 延迟完成机制(Deferred Completion)
238+
239+
部分插件任务在"调度成功"时并未真正完成,而是需要等待未来某个时间点才算真正结束(例如备忘提醒插件在提醒触发后才算完成)。
240+
241+
### 问题背景
242+
243+
默认流程下,`add_reminder()` 调度成功即返回 `ok()` → 任务立即标记为 `completed` → HUD 显示"已完成"。但用户期望在提醒实际触发后才看到完成状态。
244+
245+
### 实现流程
246+
247+
```
248+
add_reminder() → ok(data={deferred: true, reminder_id: "abc"})
249+
250+
agent_server 检测 deferred=true → 任务保持 "running",不发 completed 事件
251+
252+
HUD:🧩 备忘提醒 ▏运行中▕ / 15分钟后 起来活动 / ⏱️ 倒计时
253+
254+
agent_server 调用 bind_task 入口 → reminder 记录写入 agent_task_id
255+
256+
... 15分钟后 daemon 触发提醒 ...
257+
258+
_push_reminder() → HTTP POST /api/agent/tasks/{id}/complete
259+
260+
agent_server 标记 "completed" → 推送 task_update 事件 → HUD 10秒后消失
261+
```
262+
263+
### 插件侧实现
264+
265+
插件的 `add_reminder()` 在返回值中设置 `deferred: True`
266+
267+
```python
268+
return ok(data={
269+
"status": "scheduled",
270+
"deferred": True, # 通知 agent_server 任务尚未完成
271+
"reminder_id": rid,
272+
...
273+
})
274+
```
275+
276+
提醒记录中预留 `agent_task_id` 字段(由 `_bind_deferred_task` 回写):
277+
278+
```python
279+
reminder = {"id": rid, ..., "agent_task_id": None}
280+
```
281+
282+
`_push_reminder()` 在推送消息后调用回调端点:
283+
284+
```python
285+
agent_task_id = r.get("agent_task_id")
286+
if agent_task_id:
287+
httpx.post(f"http://127.0.0.1:48915/api/agent/tasks/{agent_task_id}/complete")
288+
```
289+
290+
插件还提供内部入口 `bind_task`,用于接收 agent_task_id 的绑定:
291+
292+
```python
293+
@plugin_entry(id="bind_task", description="内部接口:将 agent_task_id 关联到提醒记录")
294+
async def bind_task(self, reminder_id: str, agent_task_id: str, **kwargs): ...
295+
```
296+
297+
### Agent Server 侧实现
298+
299+
`_run_user_plugin_dispatch()` 检测 deferred 标志,跳过终态更新和事件推送:
300+
301+
```python
302+
is_deferred = isinstance(run_data, dict) and run_data.get("deferred") is True
303+
if up_result.success and is_deferred:
304+
# 不更新 registry 状态,不推 task_update(completed)
305+
loop.run_in_executor(None, _bind_deferred_task, plugin_id, reminder_id, task_id)
306+
return
307+
```
308+
309+
`_bind_deferred_task()` 在线程池中异步调用插件的 `bind_task` 入口(避免阻塞事件循环):
310+
311+
```python
312+
def _bind_deferred_task(plugin_id, reminder_id, agent_task_id):
313+
# 通过 /runs 接口调用 bind_task 入口,轮询等待完成
314+
...
315+
```
316+
317+
### 回调端点
318+
319+
daemon 触发提醒后,调用此端点标记任务完成并推送前端事件:
320+
321+
| 端点 | 方法 | 说明 |
322+
|------|------|------|
323+
| `/api/agent/tasks/{task_id}/complete` | POST | 将 deferred 任务标记为已完成并推送 HUD 更新 |
324+
325+
## API 端点
326+
327+
| 端点 | 方法 | 说明 |
328+
|------|------|------|
329+
| `/api/agent/tasks/{id}/cancel` | POST | 取消单个任务 |
330+
| `/api/agent/tasks/{id}/complete` | POST | 标记 deferred 任务为已完成(daemon 回调) |
331+
| `/api/agent/admin/control` | POST | 批量控制(`action: end_all`|
332+
333+
## 改进记录
334+
335+
### 2026-03-09:简洁双行布局 + 视觉统一
336+
337+
**背景**:原有卡片信息过多(描述行、阶段消息、独立进度条),任务完成后一闪即逝(5 秒)。
338+
339+
**改动**
340+
341+
1. **布局重构**:从多行布局精简为紧凑布局——第一行(图标+名称+状态+取消),描述行(任务具体内容),进度行(倒计时+进度条+步数)
342+
2. **任务描述**:后端将 `task_description` 存入插件任务的 `params.description`,前端显示用户原话(如"三分钟后提醒我吃饭")
343+
3. **停留策略**:已完成/失败的任务停留 10 秒后移除(`MIN_DISPLAY_MS = 10000`),防止快速任务一闪而过
344+
4. **内存清理**`task_registry` 自动清理 5 分钟前的已完成任务(`_cleanup_task_registry()`),防止长时间运行时内存堆积
345+
5. **视觉统一**:所有硬编码颜色(`#444``#666``#888``#999`)替换为 CSS 变量,支持暗色模式自动适配
346+
6. **透明度调整**:已完成任务透明度从 0.75 降至 0.6,视觉区分更明显

0 commit comments

Comments
 (0)