|
@@ -330,25 +330,55 @@ export function agentLayers(agent: Agent | null | undefined, now = Date.now()):
|
|
|
* 🔴 两边的标识格式不同,必须容错匹配:
|
|
* 🔴 两边的标识格式不同,必须容错匹配:
|
|
|
*
|
|
*
|
|
|
* ```
|
|
* ```
|
|
|
- * App 会话 id $approot_mw7l1ezj_wnuym1jt:fmode.cn ← Matrix thread 根 event_id
|
|
|
|
|
- * 容器 sessionKey agent:main:matrix:dm:!roomId:threadId ← build_session_key()
|
|
|
|
|
|
|
+ * App 会话 id $approot_mw7l1ezj_wnuym1jt:fmode.cn ← Matrix thread 根 event_id
|
|
|
|
|
+ * 容器 sessionKey agent:main:matrix:dm:!roomId:threadId ← build_session_key()
|
|
|
* ```
|
|
* ```
|
|
|
*
|
|
*
|
|
|
- * 所以判据是「容器的 sessionKey 里**包含** App 的会话 id 主体」——
|
|
|
|
|
- * 容器把 thread_id 拼在 key 的末尾,而 thread_id 就是 App 造的根 event_id。
|
|
|
|
|
|
|
+ * ## 三级匹配(2026-11-11 实测定稿)
|
|
|
*
|
|
*
|
|
|
- * 🔴 这条匹配是否成立**必须实测**(见方案文档「待确认」)。
|
|
|
|
|
- * 匹配不到就返回 undefined,调用方回落到全局状态,不显示「未知」。
|
|
|
|
|
|
|
+ * | 优先级 | 依据 | 说明 |
|
|
|
|
|
+ * |---|---|---|
|
|
|
|
|
+ * | ① | sessionKey 含 **thread 根**(`$approot_xxx`) | 最精确,一房多会话也不会串 |
|
|
|
|
|
+ * | ② | sessionKey 含 **roomId**(`!xxx`) | 退路:DM 通道 `thread_id` 常为 NULL(实测 46%),只能靠房间认 |
|
|
|
|
|
+ * | ③ | 都不中 | 返回 undefined,调用方回落全局状态 |
|
|
|
|
|
+ *
|
|
|
|
|
+ * ## 为什么需要 roomId 这条退路
|
|
|
|
|
+ *
|
|
|
|
|
+ * 实测(lj-test,2026-11-11):`state.db` 里 Matrix 会话的 `thread_id` **大量为 NULL**
|
|
|
|
|
+ * (本月 DM 通道进入时没有 `thread.relates_to`),于是容器只能报内部 id,
|
|
|
|
|
+ * App 永远匹配不上 → 会话级状态形同虚设。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 而 `chat_id`(roomId)实测 **100% 稳定**:212 条会话全是 `!` 开头,
|
|
|
|
|
+ * 同一房间跨 22 天、83 条会话不变。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ⚠️ 代价:roomId 粒度比 thread 粗。同一房间下多条会话时,
|
|
|
|
|
+ * 「有任意一条在跑」会被认为「这条在跑」。
|
|
|
|
|
+ * 但相比「完全匹配不上、状态永远不显示」,这个误判是可接受的取舍。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param roomIdOf 可选:把 App 会话 id 换算成 roomId(调用方从会话目录查)。
|
|
|
|
|
+ * 不传则只用 thread 根匹配(向后兼容)。
|
|
|
*/
|
|
*/
|
|
|
export function sessionStateOf(
|
|
export function sessionStateOf(
|
|
|
sessions: AgentSessionState[] | undefined,
|
|
sessions: AgentSessionState[] | undefined,
|
|
|
appSessionId: string | null | undefined,
|
|
appSessionId: string | null | undefined,
|
|
|
|
|
+ roomIdOf?: string | null,
|
|
|
): AgentSessionState | undefined {
|
|
): AgentSessionState | undefined {
|
|
|
if (!sessions?.length || !appSessionId) return undefined;
|
|
if (!sessions?.length || !appSessionId) return undefined;
|
|
|
// 去掉 App 会话 id 的域名后缀,只留主体($approot_xxx)
|
|
// 去掉 App 会话 id 的域名后缀,只留主体($approot_xxx)
|
|
|
const core = appSessionId.split(':')[0];
|
|
const core = appSessionId.split(':')[0];
|
|
|
if (!core) return undefined;
|
|
if (!core) return undefined;
|
|
|
- return sessions.find((s) => s.sessionKey.includes(core) || s.sessionKey.includes(appSessionId));
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /* ① 精确:thread 根命中 */
|
|
|
|
|
+ const byThread = sessions.find(
|
|
|
|
|
+ (s) => s.sessionKey.includes(core) || s.sessionKey.includes(appSessionId),
|
|
|
|
|
+ );
|
|
|
|
|
+ if (byThread) return byThread;
|
|
|
|
|
+
|
|
|
|
|
+ /* ② 退路:roomId 命中(DM 无 thread 的场景) */
|
|
|
|
|
+ if (roomIdOf) {
|
|
|
|
|
+ return sessions.find((s) => s.sessionKey.includes(roomIdOf));
|
|
|
|
|
+ }
|
|
|
|
|
+ return undefined;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -360,7 +390,8 @@ export function sessionStateOf(
|
|
|
export function sessionRunning(
|
|
export function sessionRunning(
|
|
|
sessions: AgentSessionState[] | undefined,
|
|
sessions: AgentSessionState[] | undefined,
|
|
|
appSessionId: string | null | undefined,
|
|
appSessionId: string | null | undefined,
|
|
|
|
|
+ roomIdOf?: string | null,
|
|
|
): boolean | undefined {
|
|
): boolean | undefined {
|
|
|
- const s = sessionStateOf(sessions, appSessionId);
|
|
|
|
|
|
|
+ const s = sessionStateOf(sessions, appSessionId, roomIdOf);
|
|
|
return s ? s.status === 'running' : undefined;
|
|
return s ? s.status === 'running' : undefined;
|
|
|
}
|
|
}
|