mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +08:00
feat(feishu): session-list rows as V2 column pairs (fixed star toggle + session button)
Each session row is a column_set with a fixed 90px watch-toggle column (⭐关注/⭐取关 for already-watched sessions) and a weighted session button carrying the number label; number replies bind the session. Replaces the stacked button pair; also honors the per-bot archived-session policy.
This commit is contained in:
parent
1790ec21ac
commit
4e325c62e0
4 changed files with 195 additions and 145 deletions
|
|
@ -1048,7 +1048,15 @@ export class FeishuHarnessBridge {
|
|||
if (!action) return Promise.resolve();
|
||||
const messageId = nonEmptyString(event?.context?.open_message_id);
|
||||
const entry = messageId ? this.#cardKeys.get(messageId) : null;
|
||||
if (!entry) return Promise.resolve();
|
||||
if (!entry) {
|
||||
// The card predates this process (the in-memory mapping resets on
|
||||
// restart) or never came from us: nudge instead of staying silent.
|
||||
const chatId = nonEmptyString(event?.context?.open_chat_id);
|
||||
if (chatId) {
|
||||
this.#send(chatId, '这个菜单已过期,请回复 /m 重新打开。').catch(() => undefined);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
// The promise is returned so tests (and future callers) can await the
|
||||
// action; the runtime dispatcher ignores it.
|
||||
return this.#handleCardAction(action, entry).catch((error) => {
|
||||
|
|
@ -1140,6 +1148,7 @@ export class FeishuHarnessBridge {
|
|||
await this.#send(chatId, `本页只有 ${menu.sessions.length} 个会话,回复 /sessionlist 重新查看。`);
|
||||
return;
|
||||
}
|
||||
// The number label sits on the session (bind) button of the row.
|
||||
await this.#handleCardAction(`use:${session.sessionId}`, { chatId, key });
|
||||
return;
|
||||
}
|
||||
|
|
@ -1186,16 +1195,24 @@ export class FeishuHarnessBridge {
|
|||
}
|
||||
const pageCount = Math.ceil(sessions.length / MENU_PAGE_SIZE);
|
||||
const safePage = Number.isSafeInteger(page) && page > 0 ? Math.min(page, pageCount - 1) : 0;
|
||||
const watchedSet = new Set(
|
||||
(this.#state.watchEntries?.(key) ?? []).map((entry) => entry.sessionId),
|
||||
);
|
||||
const pageSlice = sessions.slice(safePage * MENU_PAGE_SIZE, (safePage + 1) * MENU_PAGE_SIZE);
|
||||
this.#rememberMenu(key, {
|
||||
kind: 'sessions',
|
||||
sessions: sessions.slice(safePage * MENU_PAGE_SIZE, (safePage + 1) * MENU_PAGE_SIZE),
|
||||
});
|
||||
await this.#sendCard(chatId, sessionListCard(workspace, sessions, safePage, sessions.length), {
|
||||
key,
|
||||
// Keep the canonical selector result for later page callbacks. The
|
||||
// list response's workspace is display data and is not authoritative.
|
||||
sessionWorkspace: resolved.workspace,
|
||||
sessions: pageSlice.map((session) => ({ ...session, watched: watchedSet.has(session.sessionId) })),
|
||||
});
|
||||
await this.#sendCard(
|
||||
chatId,
|
||||
sessionListCard(workspace, sessions, safePage, sessions.length, watchedSet),
|
||||
{
|
||||
key,
|
||||
// Keep the canonical selector result for later page callbacks. The
|
||||
// list response's workspace is display data and is not authoritative.
|
||||
sessionWorkspace: resolved.workspace,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
this.#logger.warn?.('[dsh-feishu] session list failed:', error.message);
|
||||
await this.#send(chatId, '暂时无法获取会话列表,请稍后重试。');
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@
|
|||
* `im.message.create` API expects as `content` for `msg_type: interactive`
|
||||
* (card schema 2.0; callback buttons live inside a column_set/column layout).
|
||||
*
|
||||
* Buttons carry a small `{ action }` callback behavior that
|
||||
* `card.action.trigger` events echo back (when the app subscribes that
|
||||
* callback); every button also carries a numeric label so the number-reply
|
||||
* fallback stays usable without button callbacks.
|
||||
* The session list lays each row out as a `column_set` (fixed-width ⭐
|
||||
* watch-toggle column + weighted session-button column), which is how V2
|
||||
* expresses a row of buttons.
|
||||
*
|
||||
* Buttons carry a small `{ action }` value object that `card.action.trigger`
|
||||
* events echo back (when the app subscribes that event); every numbered
|
||||
* button also has a numeric label so the number-reply fallback stays usable
|
||||
* without button callbacks.
|
||||
*/
|
||||
|
||||
export const MENU_PAGE_SIZE = 10;
|
||||
|
|
@ -39,6 +43,17 @@ function button(content, actionValue) {
|
|||
};
|
||||
}
|
||||
|
||||
/** The raw button element (without the full-width column_set wrapper). */
|
||||
function buttonElement(content, actionValue) {
|
||||
return {
|
||||
tag: 'button',
|
||||
text: plainText(content),
|
||||
type: 'default',
|
||||
width: 'fill',
|
||||
behaviors: [{ type: 'callback', value: { action: actionValue } }],
|
||||
};
|
||||
}
|
||||
|
||||
function safeTitle(value) {
|
||||
const title = String(value ?? '').replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu, ' ').replace(/\s+/gu, ' ').trim();
|
||||
return title || '暂无标题';
|
||||
|
|
@ -102,29 +117,43 @@ export function cardActionProbeCard(nonce) {
|
|||
}
|
||||
|
||||
/**
|
||||
* One page of the workspace's sessions. Each session renders as a bind
|
||||
* button plus a watch button (schema 2.0: buttons are top-level body
|
||||
* elements, so the pair is two adjacent buttons); the number label equals
|
||||
* the reply-number for the bind action (fallback). Archived sessions are
|
||||
* marked in the label.
|
||||
* One page of the workspace's sessions. Each row is a `column_set` pair:
|
||||
* the fixed-width ⭐ watch toggle (`⭐关注` / `⭐取关` for already-watched
|
||||
* sessions) followed by the session button that carries the page-local
|
||||
* number label (reply-number fallback = bind). Archived sessions are marked
|
||||
* in the label. `watchedSessionIds` is a Set-like of ids this conversation
|
||||
* already watches.
|
||||
*/
|
||||
export function sessionListCard(workspace, sessions, page, total) {
|
||||
export function sessionListCard(workspace, sessions, page, total, watchedSessionIds = new Set()) {
|
||||
const start = page * MENU_PAGE_SIZE;
|
||||
const slice = sessions.slice(start, start + MENU_PAGE_SIZE);
|
||||
const pageCount = Math.max(1, Math.ceil(total / MENU_PAGE_SIZE));
|
||||
const watched = (id) => typeof watchedSessionIds?.has === 'function' && watchedSessionIds.has(id);
|
||||
/** One row: fixed 90px watch toggle + the session button filling the rest. */
|
||||
const row = (watchButton, sessionButton) => ({
|
||||
tag: 'column_set',
|
||||
flex_mode: 'none',
|
||||
horizontal_spacing: 'default',
|
||||
columns: [
|
||||
{ tag: 'column', width: '90px', vertical_align: 'center', elements: [watchButton] },
|
||||
{ tag: 'column', width: 'weighted', weight: 1, vertical_align: 'center', elements: [sessionButton] },
|
||||
],
|
||||
});
|
||||
const elements = [
|
||||
{ tag: 'div', text: markdown(`**工作区**:\`${workspace}\`\n共 **${total}** 个会话${total > MENU_PAGE_SIZE ? `(第 ${page + 1}/${pageCount} 页)` : ''}`) },
|
||||
...slice.flatMap((session, offset) => {
|
||||
...slice.map((session, offset) => {
|
||||
// Page-local numbering: number replies resolve against this page.
|
||||
const label = `${offset + 1}. ${safeTitle(session.title)}${session.archived === true ? '(已归档)' : ''}`;
|
||||
return [
|
||||
button(label, `use:${session.sessionId}`),
|
||||
button('👁 关注', `watch:${session.sessionId}`),
|
||||
];
|
||||
const watching = watched(session.sessionId);
|
||||
return row(
|
||||
buttonElement(watching ? '⭐取关' : '⭐关注', watching ? `unwatch:${session.sessionId}` : `watch:${session.sessionId}`),
|
||||
buttonElement(label, `use:${session.sessionId}`),
|
||||
);
|
||||
}),
|
||||
];
|
||||
if (page > 0) elements.push(button('◀ 上一页', `sessions:${page - 1}`));
|
||||
if (page + 1 < pageCount) elements.push(button('下一页 ▶', `sessions:${page + 1}`));
|
||||
elements.push({ tag: 'div', text: markdown('回复数字(1~N)同样可以绑定本页会话。') });
|
||||
elements.push({ tag: 'div', text: markdown('回复数字(1~N)绑定本页会话。') });
|
||||
return cardWith('📂 会话列表', elements);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue