|
|
@@ -32,6 +32,18 @@ const FRESH_MS = HEARTBEAT_STALE_SEC * 1000;
|
|
|
*/
|
|
|
const REPLY_QUIET_MS = QUIET_MS;
|
|
|
|
|
|
+/**
|
|
|
+ * 本地「刚发出」标记的有效期(2026-11-11)。
|
|
|
+ *
|
|
|
+ * 它只是**过渡用**:覆盖「用户点发送 → 容器上报 running」之间的空档。
|
|
|
+ * 一旦超过这个时间,就该由容器上报的真实状态说了算 ——
|
|
|
+ * 否则某个界面忘了清理(比如左侧会话树不走 `describe()`)就会永久转圈。
|
|
|
+ *
|
|
|
+ * 取 60 秒:足够覆盖容器首轮响应的最慢情况(DNS 修复后实测首字 2~9 秒),
|
|
|
+ * 又不至于让误留的标记长期作妖。
|
|
|
+ */
|
|
|
+const SESSION_LOCAL_START_TTL_MS = 60_000;
|
|
|
+
|
|
|
/** 超过这么久的事件视为「历史重放」,不据此判断"正在回复" */
|
|
|
const STALE_MSG_MS = 60_000;
|
|
|
|
|
|
@@ -191,16 +203,30 @@ export class TaskPulseService {
|
|
|
this._localSessionStart.set(next);
|
|
|
}
|
|
|
|
|
|
- /** 该会话是否处在「刚发出、容器还没确认」的窗口内 */
|
|
|
- sessionLocalStartAt = (sessionId: string): number | undefined =>
|
|
|
- this._localSessionStart().get(sessionId);
|
|
|
+ /**
|
|
|
+ * 该会话是否处在「刚发出、容器还没确认」的窗口内。
|
|
|
+ *
|
|
|
+ * 🔴 **带 TTL**(2026-11-11 用户实测:左侧一直转圈不停)。
|
|
|
+ *
|
|
|
+ * 原实现直接返回标记、不看过期时间。而清除逻辑只写在 `describe()` 里,
|
|
|
+ * **左侧会话树根本不走 `describe()`** —— 它直接调 `sessionRunningOf()`。
|
|
|
+ * 于是标记一直留在 Map 里,左侧永远转圈,右侧却已正常。
|
|
|
+ *
|
|
|
+ * 现在给标记加窗口期:超过它就不再算「刚发出」,
|
|
|
+ * 由容器上报的真实状态接管。这样即使某个界面忘了清,也不会永久卡住。
|
|
|
+ */
|
|
|
+ sessionLocalStartAt = (sessionId: string, now = Date.now()): number | undefined => {
|
|
|
+ const at = this._localSessionStart().get(sessionId);
|
|
|
+ if (at === undefined) return undefined;
|
|
|
+ return now - at < SESSION_LOCAL_START_TTL_MS ? at : undefined;
|
|
|
+ };
|
|
|
|
|
|
/**
|
|
|
* 容器确认该会话结束后清掉本地标记。
|
|
|
*
|
|
|
* 只在「确实存在」时才写信号,避免 compute 里无谓地触发重算。
|
|
|
*/
|
|
|
- private clearSessionLocalStart(sessionId: string): void {
|
|
|
+ clearSessionLocalStart(sessionId: string): void {
|
|
|
const cur = this._localSessionStart();
|
|
|
if (!cur.has(sessionId)) return;
|
|
|
const next = new Map(cur);
|
|
|
@@ -268,6 +294,27 @@ export class TaskPulseService {
|
|
|
if (resolved.state === 'offline') {
|
|
|
return { text: '已离线', tone: 'danger' };
|
|
|
}
|
|
|
+ /*
|
|
|
+ * 🔴 会话级上报**排在最前**(2026-11-11 第二次修正)。
|
|
|
+ *
|
|
|
+ * 用户实测:回复完了,右上角一直挂「正在处理…」。
|
|
|
+ * 排查发现是 `local && isBusy`(用户刚发消息的本地标记)排在前面,
|
|
|
+ * 它只认「容器整体 status」,不看「**本会话**是否已结束」——
|
|
|
+ * 只要容器还在跑别的活,本地标记就一直让它显示在忙。
|
|
|
+ *
|
|
|
+ * 容器用**会话级**数据说「这条空闲」,这是最高优先级的事实:
|
|
|
+ * 比「用户刚发过消息」这个本地推测更准。
|
|
|
+ */
|
|
|
+ const sessionAware = sessionId !== undefined && sessionId !== null;
|
|
|
+ const hasSessionReport = (agent.currentSessions?.length ?? 0) > 0;
|
|
|
+ const mine = sessionAware
|
|
|
+ ? sessionRunning(agent.currentSessions, sessionId, roomId, myUserId, now)
|
|
|
+ : undefined;
|
|
|
+ if (sessionAware && mine === false) {
|
|
|
+ if (sessionId) this.clearSessionLocalStart(sessionId);
|
|
|
+ return { text: '在线', tone: 'success' };
|
|
|
+ }
|
|
|
+
|
|
|
/*
|
|
|
* 🔴 2026-11-09 调整顺序(C3):
|
|
|
*
|
|
|
@@ -305,33 +352,20 @@ export class TaskPulseService {
|
|
|
*
|
|
|
* 用户原话:「右上角仅显示当前会话的进度,如果当前会话没有就显示在线状态」。
|
|
|
*/
|
|
|
- const sessionAware = sessionId !== undefined && sessionId !== null;
|
|
|
/*
|
|
|
- * 容器**有**会话级上报,我才敢拿它下结论。
|
|
|
- * `currentSessions` 为空 = 老容器从没报过 → `mine` 只能是 undefined,
|
|
|
- * 此时回落到下面的全局口径,行为与改造前完全一致。
|
|
|
+ * 会话级上报已在函数开头处理(`mine === false` 优先返回「在线」)。
|
|
|
+ * 这里只剩「容器报了列表但没有本会话」这一种情况 ——
|
|
|
+ * 刚发出时容器可能还没写入记录,保留 25 秒兜底。
|
|
|
*/
|
|
|
- const hasSessionReport = (agent.currentSessions?.length ?? 0) > 0;
|
|
|
- const mine = sessionAware ? sessionRunning(agent.currentSessions, sessionId, roomId, myUserId) : undefined;
|
|
|
- if (sessionAware && hasSessionReport && mine !== true) {
|
|
|
+ if (sessionAware && hasSessionReport && mine === undefined) {
|
|
|
/*
|
|
|
- * 本会话没在跑 —— 两种情况都归到这里:
|
|
|
- * · `mine === false`:容器明确报了本会话 idle
|
|
|
- * · `mine === undefined`:容器报了别的会话、**没报本会话**
|
|
|
- * (`sessionRunning` 约定 undefined = 不知道;但在「页面已指定会话」
|
|
|
- * 的语义下,容器报了一堆会话却唯独没有我这条,就是**我没有在跑**)
|
|
|
- *
|
|
|
- * 但上报是分钟级的,刚发完消息时容器可能还没更新 —— 用「最近有消息进来」
|
|
|
- * 兜住,避免正文还在流式输出就显示「在线」。
|
|
|
+ * 容器报了会话列表,但**没有本会话** → 说明本会话不在跑。
|
|
|
+ * 仍保留 25 秒兜底:刚发出时容器可能还没写入记录。
|
|
|
*/
|
|
|
const quietMs = now - this.lastActivity(agent.id);
|
|
|
if (quietMs < REPLY_QUIET_MS) {
|
|
|
return { text: '正在回复…', tone: 'info', active: true };
|
|
|
}
|
|
|
- /*
|
|
|
- * 容器已确认结束 → 清掉本地「刚发出」标记(2026-11-11)。
|
|
|
- * 不清的话左侧会话树的旋转图标会一直转下去。
|
|
|
- */
|
|
|
if (sessionId) this.clearSessionLocalStart(sessionId);
|
|
|
return { text: '在线', tone: 'success' };
|
|
|
}
|