feat(feishu): persistent session watches and completion pushes

- /watch resolves the target READ-ONLY: sessions are validated against
  registered workspace listings (current or any other) without binding the
  conversation or switching workspaces. /unwatch and /watchlist round out
  the command set; the watchlist card supports button and number-reply
  unwatching.
- Watches persist in the state store (sessionId + title + chatId + lastSeq)
  and resume at runtime start: the bridge starts the global event-mux
  watcher in its constructor when the harness supports it.
- Completion pushes fire on turn/end, deduped by sessionId + turn id, with
  the seq watermark persisted per watch. After a mux reconnect the bridge
  replays each watched session's recent history (session.history) through
  the normal handler, so missed turn/end events are compensated without
  duplicates.
- Bound-session push targets are persisted (chatTargets) instead of living
  only in memory, and refresh on every accepted message.
- Watch failures map to safe user-facing messages.
This commit is contained in:
liuwenbo00 2026-08-21 10:10:36 +08:00 • committed by xmanrui
parent 0579c24b13
commit beb269dc0e
6 changed files with 746 additions and 132 deletions

View file

@ -153,3 +153,32 @@ export function menuHelpText() {
'/workspace 绝对路径 切换工作区',
].join('\n');
}
/** The watch list for one conversation (unwatch buttons + reply fallback). */
export function watchListCard(entries) {
const elements = entries.length === 0
? [{ tag: 'div', text: markdown('当前没有关注的会话。\n`/watch <ID|序号>` 关注后,任务完成会自动推送。') }]
: [
{ tag: 'div', text: markdown('任务完成会自动推送,回复数字或点按钮取消关注:') },
...entries.map((entry, index) => button(
`${index + 1}. ${safeTitle(entry.title)}`,
`unwatch:${entry.sessionId}`,
)),
];
return cardWith('👁 关注列表', elements);
}
/**
* The completion push card. `title` is the session title, `reason` the
* turn-end kind (completed / stopped / aborted).
*/
export function completionCard(sessionId, title, reason) {
const reasonText = reason === 'stopped' ? '已停止' : reason === 'aborted' ? '已中止' : '已完成';
return cardWith('✅ 任务完成', [
{ tag: 'div', text: markdown(`**${safeTitle(title)}**\n\`${sessionId}\``) },
{ tag: 'div', text: markdown(`**状态**:${reasonText}`) },
button('打开会话列表', 'sessions'),
button('工作区', 'workspaces'),
{ tag: 'div', text: markdown('绑定该会话后可继续追问,输入文字即可。') },
]);
}