dsh-im-ops/src/channels/feishu/feishu-cards.mjs

118 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Feishu interactive-card builders for the dsh-im menu / session-list /
* workspace-list UX. All builders return the JSON string the
* `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.
*/
export const MENU_PAGE_SIZE = 10;
function plainText(content) {
return { tag: 'plain_text', content: String(content) };
}
function markdown(content) {
return { tag: 'lark_md', content: String(content) };
}
function button(content, actionValue) {
return {
tag: 'column_set',
flex_mode: 'none',
columns: [{
tag: 'column',
width: 'weighted',
weight: 1,
elements: [{
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 || '暂无标题';
}
function cardWith(headerText, elements) {
return JSON.stringify({
schema: '2.0',
header: { title: plainText(headerText), template: 'blue' },
body: { elements },
});
}
/** The main command menu (buttons + number-reply fallback). */
export function menuCard() {
return cardWith('🤖 助手菜单', [
{ tag: 'div', text: markdown('**点击按钮或直接回复数字**') },
button('1 · 会话列表', 'sessions'),
button('2 · 工作区', 'workspaces'),
button('3 · 新会话', 'new'),
button('4 · 状态', 'status'),
button('5 · 帮助', 'help'),
]);
}
/**
* One page of the workspace's sessions. Each row is a bind button; the
* number label equals the reply-number for the same action (fallback).
*/
export function sessionListCard(workspace, sessions, page, total) {
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 elements = [
{ tag: 'div', text: markdown(`**工作区**:\`${workspace}\`\n共 **${total}** 个会话${total > MENU_PAGE_SIZE ? `(第 ${page + 1}/${pageCount} 页)` : ''}`) },
...slice.map((session, offset) => button(
`${offset + 1}. ${safeTitle(session.title)}`,
`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)同样可以绑定本页会话。') });
return cardWith('📂 会话列表', elements);
}
/** The workspace list card (switch-workspace buttons + reply fallback). */
export function workspaceListCard(paths, current) {
const elements = paths.length === 0
? [{ tag: 'div', text: markdown('当前 Host 上没有已登记的工作区。') }]
: [
{ tag: 'div', text: markdown(`回复数字切换工作区,或点击按钮:`) },
...paths.map((path, index) => button(
`${index + 1}. ${path}${path === current ? '(当前)' : ''}`,
`workspace:${path}`,
)),
];
return cardWith('🗂 工作区', elements);
}
/** The card-menu help text (number-driven, no command memorization). */
export function menuHelpText() {
return [
'🤖 助手菜单(回复数字即可,无需记命令)',
'',
'1 · /sessionlist 列出会话(回复数字绑定)',
'2 · /workspacelist 列出工作区(回复数字切换)',
'3 · /new 开启新会话',
'4 · /status 连接状态',
'5 · /help 本帮助',
'',
'直接发送文字/图片即继续当前会话。',
'/session ID 或序号 绑定已有会话',
'/compact 压缩上下文',
'/workspace 绝对路径 切换工作区',
].join('\n');
}