text = data.decode("latin-1", errors="replace")
entry["buffer"].append(text)
entry["buflen"] += len(text)
+ entry["last_output_ts"] = time.time()
_trim_buffer(entry)
ws = entry.get("ws")
if ws is not None:
"name": task["name"], "cwd": cwd, "model": model,
"started_at": time.time(), "buffer": [], "buflen": 0,
"ws": None, "done": False, "exit_code": None,
+ "last_output_ts": time.time(),
"send_lock": asyncio.Lock(),
}
RUNNING[task["id"]] = entry
})
+# 进程活着但无 WS 时,距上次 PTY 输出超过该秒数即视为“后台等待中”(已输出完、等下一条指令)
+IDLE_THRESHOLD = 2.5
+
+
@app.get("/api/tasks")
async def api_tasks(request: Request):
if not _auth_ok(request):
elif t["id"] in RUNNING:
e = RUNNING[t["id"]]
if e["proc"].poll() is None:
- tt["live_status"] = "background"
+ last_ts = e.get("last_output_ts") or 0
+ if (time.time() - last_ts) < IDLE_THRESHOLD:
+ tt["live_status"] = "background"
+ else:
+ tt["live_status"] = "background_idle"
else:
# 后台跑完了但当时没人在看 → 提示用户回来确认
tt["live_status"] = "done_detached"
.task-card.tc-connected .tc-status { color: #ffe66d; }
.task-card.tc-background { border-color: #4ecca3; }
.task-card.tc-background .tc-status { color: #4ecca3; }
+.task-card.tc-waiting { border-color: #74c0fc; }
+.task-card.tc-waiting .tc-status { color: #74c0fc; }
.task-card.tc-paused .tc-status { color: #ffd866; }
.task-card.tc-idle .tc-status { color: #777; }
.task-card.tc-done { border-color: #ffa94d; }
let statusLine, statusClass;
if (ls === 'connected') { statusLine = '● 前台已登录'; statusClass = 'tc-connected'; }
else if (ls === 'background') { statusLine = '⚙ 后台运行中'; statusClass = 'tc-background'; }
+ else if (ls === 'background_idle') { statusLine = '⏸ 后台等待中'; statusClass = 'tc-waiting'; }
else if (ls === 'done_detached') { statusLine = '⏳ 后台任务待确认' + (t.exit_code != null ? '(退出码 ' + t.exit_code + ')' : ''); statusClass = 'tc-done'; }
else if (isPaused) { statusLine = '⏸ 已暂停'; statusClass = 'tc-paused'; }
else { statusLine = '○ 空闲'; statusClass = 'tc-idle'; }