mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-08 22:00:50 +08:00
fix(feishu): streamline menu card layout and update help card
- Move preset/model/archive controls from settings card to main menu - Arrange 4 dropdowns in 2x2 grid with external icons - Align steer and archive side by side - Replace #showSettingsCard with #sendMenuCard for preset/model/archive updates - Add #sendMenuCard after 'new' button action for consistency - Update help card to reflect current menu layout (12 items) - Clean up duplicate sessionlist entries in menuHelpText - Add missing English dictionary entries
This commit is contained in:
parent
044ab74c28
commit
45f55f73eb
5 changed files with 300 additions and 194 deletions
258
lib/index.js
258
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -1259,6 +1259,7 @@ export class FeishuHarnessBridge {
|
|||
if (action === 'new') {
|
||||
await this.#state.clearSession(key);
|
||||
await this.#send(chatId, t('已开启全新 Harness 会话。'));
|
||||
await this.#sendMenuCard(key, chatId);
|
||||
return;
|
||||
}
|
||||
if (action === 'use:current') {
|
||||
|
|
@ -1385,8 +1386,8 @@ export class FeishuHarnessBridge {
|
|||
async #handleMenuPick(menu, number, { chatId, key, event }) {
|
||||
if (menu.kind === 'menu') {
|
||||
// Number fallback for the total menu:
|
||||
// 1=续写 2=新会话 3=会话列表 4=状态 5=修复 6=更多设置 7=帮助
|
||||
const actions = ['use:current', 'new', 'sessions', 'status', 'repair', 'settings', 'help'];
|
||||
// 1=工作区列表 2=新会话 3=会话列表 4=状态 5=修复 6=帮助
|
||||
const actions = ['workspaces', 'new', 'sessions', 'status', 'repair', 'help'];
|
||||
const action = actions[number - 1];
|
||||
if (!action) {
|
||||
await this.#send(chatId, t('菜单没有这个编号,回复 /m 重新打开。'));
|
||||
|
|
@ -1605,13 +1606,32 @@ export class FeishuHarnessBridge {
|
|||
currentSessionTitle = sessions[0].title ?? sessions[0].id;
|
||||
}
|
||||
const archiveVisible = this.#state?.includesArchivedSessions?.() ?? false;
|
||||
// 加载预设/模型目录供主卡内联配置使用
|
||||
let presetCatalog = null;
|
||||
let modelCatalog = null;
|
||||
try {
|
||||
const settings = await this.#harness.agentPresetSettings({ signal: this.#signal });
|
||||
presetCatalog = settings.agentPresetCatalog;
|
||||
presetCatalog._currentId = settings.agentPreset;
|
||||
} catch { /* preset section degrades to button */ }
|
||||
try {
|
||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||
const sessionId = this.#state?.sessionFor?.(key);
|
||||
let catalog;
|
||||
if (typeof sessionId === 'string' && sessionId) {
|
||||
const session = this.#harness.workspaceSession(sessionId);
|
||||
if (session?.models) catalog = await session.models({ signal: this.#signal });
|
||||
}
|
||||
if (!catalog) catalog = await this.#harness.listModels({ signal: this.#signal });
|
||||
modelCatalog = catalog;
|
||||
} catch { /* model section degrades to button */ }
|
||||
this.#rememberMenu(key, { kind: 'menu', chatId });
|
||||
await this.#sendCard(
|
||||
chatId,
|
||||
menuCard({
|
||||
workspaces, currentWorkspace,
|
||||
currentSession: currentSessionId ? { id: currentSessionId, title: currentSessionTitle } : null,
|
||||
sessions, archiveVisible,
|
||||
sessions, archiveVisible, presetCatalog, modelCatalog,
|
||||
}),
|
||||
{ key, update: !newCard },
|
||||
);
|
||||
|
|
@ -1883,7 +1903,7 @@ export class FeishuHarnessBridge {
|
|||
}
|
||||
await this.#harness.updateAgentPreset(null, { signal: this.#signal });
|
||||
await this.#send(chatId, '已恢复跟随 Host 默认预设。');
|
||||
await this.#showSettingsCard(key, chatId);
|
||||
await this.#sendMenuCard(key, chatId);
|
||||
} catch (error) {
|
||||
this.#logger.warn?.('[dsh-feishu] preset default failed:', error.message);
|
||||
await this.#send(chatId, '预设重置失败,请稍后重试。');
|
||||
|
|
@ -1901,7 +1921,7 @@ export class FeishuHarnessBridge {
|
|||
}
|
||||
await this.#harness.updateAgentPreset(presetId, { signal: this.#signal });
|
||||
await this.#send(chatId, `预设已切换为:${presetId}`);
|
||||
await this.#showSettingsCard(key, chatId);
|
||||
await this.#sendMenuCard(key, chatId);
|
||||
} catch (error) {
|
||||
this.#logger.warn?.('[dsh-feishu] preset select failed:', error.message);
|
||||
await this.#send(chatId, `预设切换失败:${error.message}`);
|
||||
|
|
@ -1927,7 +1947,7 @@ export class FeishuHarnessBridge {
|
|||
);
|
||||
const message = result?.message || '模型切换失败,请稍后重试。';
|
||||
await this.#send(chatId, message);
|
||||
await this.#showSettingsCard(key, chatId);
|
||||
await this.#sendMenuCard(key, chatId);
|
||||
} catch (error) {
|
||||
this.#logger.warn?.('[dsh-feishu] model select failed:', error.message);
|
||||
await this.#send(chatId, `模型切换失败:${error.message}`);
|
||||
|
|
|
|||
|
|
@ -113,15 +113,18 @@ function backButton() {
|
|||
* by operating flow:
|
||||
* 1. 会话 · 工作区(高频率)→ 会话下拉 · 工作区下拉 · 新会话 · 全部会话
|
||||
* 2. 任务控制 → 停止 · 压缩 · 补充指令下拉
|
||||
* 3. 系统 → 状态 · 帮助 · 更多设置
|
||||
* 3. 配置 → 预设下拉 · 模型下拉 · 归档切换
|
||||
* 4. 系统 → 状态 · 帮助
|
||||
* `ctx` bundle:
|
||||
* - workspaces: string[] (工作区下拉选项)
|
||||
* - currentWorkspace: string|null (当前工作区,下拉高亮)
|
||||
* - currentSession: {id,title}|null (当前绑定会话,续写目标)
|
||||
* - sessions: {id,title}[] (最近会话,供会话下拉切换)
|
||||
* - archiveVisible: boolean (归档显隐开关当前值)
|
||||
* - presetCatalog: object|null (预设目录, {items,defaultId,_currentId})
|
||||
* - modelCatalog: object|null (模型目录, {groups,current})
|
||||
* Number fallback: 1=续写 2=新会话 3=会话列表 4=状态
|
||||
* 5=修复 6=更多设置 7=帮助.
|
||||
* 5=修复 6=帮助.
|
||||
*/
|
||||
export function menuCard(ctx) {
|
||||
const {
|
||||
|
|
@ -130,26 +133,25 @@ export function menuCard(ctx) {
|
|||
currentSession = null,
|
||||
sessions = [],
|
||||
archiveVisible = false,
|
||||
presetCatalog = null,
|
||||
modelCatalog = null,
|
||||
} = ctx || {};
|
||||
|
||||
// currentSession 提供 id/title;为兼容旧调用,也接受 currentSessionTitle
|
||||
const currentSessionId = currentSession?.id ?? null;
|
||||
const currentSessionTitle = currentSession?.title ?? ctx?.currentSessionTitle ?? null;
|
||||
|
||||
// 状态摘要:一眼看"当前是什么"
|
||||
const wsLabel = currentWorkspace || '未设置工作区';
|
||||
const sessLabel = currentSessionTitle ? `「${safeTitle(currentSessionTitle)}」` : '无会话';
|
||||
const elements = [
|
||||
{ tag: 'div', text: markdown(`**📂 ${wsLabel}** · **💬 ${sessLabel}**`) },
|
||||
{ tag: 'hr' },
|
||||
];
|
||||
|
||||
// ── 会话 · 工作区(高频率,左右并排)────────────────────────
|
||||
const sessionOptions = (Array.isArray(sessions) ? sessions : []).slice(0, 20);
|
||||
const hasSessions = sessionOptions.length > 0;
|
||||
const hasWorkspaces = Array.isArray(workspaces) && workspaces.length > 0;
|
||||
const elements = [];
|
||||
|
||||
// 构建会话下拉(始终显示,无会话时显示占位)
|
||||
// ── 设置区 ──────────────────────────────────────────────────
|
||||
elements.push({ tag: 'div', text: markdown('**设置**') });
|
||||
|
||||
// ── 四个下拉菜单 2×2 网格 ────────────────────────────────────
|
||||
|
||||
// 第 1 行:会话 + 工作区
|
||||
let sessionDropdown = null;
|
||||
if (hasSessions) {
|
||||
const sessionPickOptions = sessionOptions.map((s) => ({
|
||||
|
|
@ -175,7 +177,6 @@ export function menuCard(ctx) {
|
|||
};
|
||||
}
|
||||
|
||||
// 构建工作区下拉
|
||||
let workspaceDropdown = null;
|
||||
if (hasWorkspaces) {
|
||||
const wsOptions = workspaces.slice(0, 20).map((path) => ({
|
||||
|
|
@ -192,25 +193,87 @@ export function menuCard(ctx) {
|
|||
};
|
||||
}
|
||||
|
||||
// 左右并排:column_set 两列,左会话右工作区
|
||||
if (sessionDropdown && workspaceDropdown) {
|
||||
elements.push({
|
||||
tag: 'column_set',
|
||||
flex_mode: 'none',
|
||||
columns: [
|
||||
{ tag: 'column', width: 'weighted', weight: 1, elements: [sessionDropdown] },
|
||||
{ tag: 'column', width: 'weighted', weight: 1, elements: [workspaceDropdown] },
|
||||
],
|
||||
// 第 2 行:预设 + 模型
|
||||
const presetItems = Array.isArray(presetCatalog?.items) ? presetCatalog.items : [];
|
||||
const curPresetId = presetCatalog?._currentId ?? null;
|
||||
const presetFollowDefault = curPresetId === null;
|
||||
let presetDropdown = null;
|
||||
if (presetItems.length >= 1) {
|
||||
const setPresetOptions = presetItems.slice(0, 30).map((item) => ({
|
||||
text: { tag: 'plain_text', content: `${item.id === curPresetId ? '✓ ' : ''}${item.label}` },
|
||||
value: item.id,
|
||||
}));
|
||||
const presetSelected = presetFollowDefault ? PRESET_FOLLOW_DEFAULT_SENTINEL : curPresetId;
|
||||
setPresetOptions.unshift({
|
||||
text: { tag: 'plain_text', content: `${presetFollowDefault ? '✓ ' : ''}跟随默认` },
|
||||
value: PRESET_FOLLOW_DEFAULT_SENTINEL,
|
||||
});
|
||||
} else {
|
||||
if (sessionDropdown) elements.push(sessionDropdown);
|
||||
if (workspaceDropdown) elements.push(workspaceDropdown);
|
||||
presetDropdown = {
|
||||
tag: 'select_static',
|
||||
name: 'preset_pick',
|
||||
placeholder: { tag: 'plain_text', content: '切换预设' },
|
||||
initial_index: initialIndex(setPresetOptions, presetSelected),
|
||||
options: setPresetOptions,
|
||||
behaviors: [{ type: 'callback', value: { action: 'preset_pick' } }],
|
||||
};
|
||||
}
|
||||
|
||||
// 新会话 + 全部会话按钮
|
||||
elements.push(buttonPair('🆕 新会话', 'new', '📋 全部会话', 'sessions'));
|
||||
const groups = Array.isArray(modelCatalog?.groups) ? modelCatalog.groups : [];
|
||||
const curModel = modelCatalog?.current;
|
||||
const curModelId = curModel ? `${curModel.provider}/${curModel.model}` : null;
|
||||
const flat = [];
|
||||
for (const group of groups) {
|
||||
for (const model of (group.models || [])) flat.push({ id: `${group.id}/${model.id}`, name: `${group.name} - ${model.name}` });
|
||||
}
|
||||
let modelDropdown = null;
|
||||
if (flat.length > 1) {
|
||||
const setModelOptions = flat.slice(0, 30).map((opt) => ({
|
||||
text: { tag: 'plain_text', content: `${opt.id === curModelId ? '✓ ' : ''}${opt.name}` },
|
||||
value: opt.id,
|
||||
}));
|
||||
modelDropdown = {
|
||||
tag: 'select_static',
|
||||
name: 'model_pick',
|
||||
placeholder: { tag: 'plain_text', content: '切换模型' },
|
||||
initial_index: initialIndex(setModelOptions, curModelId),
|
||||
options: setModelOptions,
|
||||
behaviors: [{ type: 'callback', value: { action: 'model_pick' } }],
|
||||
};
|
||||
}
|
||||
|
||||
// 如果既没有会话也没有工作区,显示工作区列表按钮
|
||||
// 渲染 2×2 网格:图标 + 下拉并列
|
||||
// 每一行用 4 列 column_set:图标 | 下拉 | 图标 | 下拉
|
||||
function iconCol(icon) {
|
||||
return { tag: 'column', width: 'weighted', weight: 0.1, vertical_align: 'center', elements: [{ tag: 'div', text: { tag: 'plain_text', content: icon } }] };
|
||||
}
|
||||
function dropdownCol(el) {
|
||||
return { tag: 'column', width: 'weighted', weight: 1, elements: [el] };
|
||||
}
|
||||
|
||||
const row1 = [];
|
||||
if (sessionDropdown) row1.push(sessionDropdown);
|
||||
if (workspaceDropdown) row1.push(workspaceDropdown);
|
||||
if (row1.length === 2) {
|
||||
elements.push({
|
||||
tag: 'column_set', flex_mode: 'none',
|
||||
columns: [iconCol('💬'), dropdownCol(row1[0]), iconCol('📂'), dropdownCol(row1[1])],
|
||||
});
|
||||
} else if (row1[0]) {
|
||||
elements.push(row1[0]);
|
||||
}
|
||||
|
||||
const row2 = [];
|
||||
const presetBtn = button('🤖 切换预设', 'presets');
|
||||
const modelBtn = button('🧠 切换模型', 'models');
|
||||
if (presetDropdown) row2.push(presetDropdown); else row2.push(presetBtn);
|
||||
if (modelDropdown) row2.push(modelDropdown); else row2.push(modelBtn);
|
||||
elements.push({
|
||||
tag: 'column_set', flex_mode: 'none',
|
||||
columns: [iconCol('🤖'), dropdownCol(row2[0]), iconCol('🧠'), dropdownCol(row2[1])],
|
||||
});
|
||||
|
||||
// 新会话 + 全部会话按钮
|
||||
elements.push(buttonPair('🆕 新会话', 'new', '📋 会话/关注', 'sessions'));
|
||||
if (!hasSessions && !hasWorkspaces) {
|
||||
elements.push(button('🗂 工作区列表', 'workspaces'));
|
||||
}
|
||||
|
|
@ -219,32 +282,51 @@ export function menuCard(ctx) {
|
|||
// ── 任务控制(对运行中任务的操作)────────────────────────
|
||||
elements.push({ tag: 'div', text: markdown('**任务控制**') });
|
||||
elements.push(buttonPair('⏹ 停止', 'stop', '📐 压缩', 'compact'));
|
||||
elements.push({ tag: 'div', text: markdown('**补充指令**(下拉选择,最后一项可自定义)') });
|
||||
elements.push({ tag: 'hr' });
|
||||
|
||||
// ── 补充指令 + 归档切换(并列)────────────────────────────
|
||||
elements.push({
|
||||
tag: 'select_static',
|
||||
name: 'steer_pick',
|
||||
placeholder: { tag: 'plain_text', content: '选择补充指令' },
|
||||
initial_index: 1,
|
||||
options: [
|
||||
...QUICK_STEER_OPTIONS.map((text) => ({
|
||||
text: { tag: 'plain_text', content: text },
|
||||
value: text,
|
||||
})),
|
||||
{ text: { tag: 'plain_text', content: '✏️ 更多 / 自定义…' }, value: 'custom' },
|
||||
tag: 'column_set', flex_mode: 'none',
|
||||
columns: [
|
||||
{
|
||||
tag: 'column', width: 'weighted', weight: 1,
|
||||
elements: [
|
||||
{ tag: 'div', text: markdown('**补充指令**') },
|
||||
{
|
||||
tag: 'select_static',
|
||||
name: 'steer_pick',
|
||||
placeholder: { tag: 'plain_text', content: '选择补充指令' },
|
||||
initial_index: 1,
|
||||
options: [
|
||||
...QUICK_STEER_OPTIONS.map((text) => ({
|
||||
text: { tag: 'plain_text', content: text },
|
||||
value: text,
|
||||
})),
|
||||
{ text: { tag: 'plain_text', content: '✏️ 更多 / 自定义…' }, value: 'custom' },
|
||||
],
|
||||
behaviors: [{ type: 'callback', value: { action: 'steer_pick' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tag: 'column', width: 'weighted', weight: 1,
|
||||
elements: [
|
||||
{ tag: 'div', text: markdown(`🗄 归档:${archiveVisible ? '已显示' : '已隐藏'}`) },
|
||||
button('切换归档显示', 'archive_toggle'),
|
||||
],
|
||||
},
|
||||
],
|
||||
behaviors: [{ type: 'callback', value: { action: 'steer_pick' } }],
|
||||
});
|
||||
elements.push({ tag: 'hr' });
|
||||
|
||||
// ── 底部操作(系统功能)────────────────────────────────────
|
||||
elements.push(buttonPair('📊 状态', 'status', '📖 帮助', 'help'));
|
||||
elements.push(button('⚙ 更多设置', 'settings'));
|
||||
|
||||
// 命令与数字兜底说明
|
||||
elements.push({ tag: 'div', text: markdown(
|
||||
'**数字兜底**\n'
|
||||
+ '**1**续写 · **2**新会话 · **3**会话列表 · **4**状态\n'
|
||||
+ '**5**🔧修复 · **6**更多设置 · **7**帮助',
|
||||
+ '**1**工作区列表 · **2**新会话 · **3**会话列表 · **4**状态\n'
|
||||
+ '**5**🔧修复 · **6**帮助',
|
||||
) });
|
||||
return cardWith('🤖 助手中心', elements);
|
||||
}
|
||||
|
|
@ -507,11 +589,11 @@ export function menuHelpText() {
|
|||
return [
|
||||
'🤖 助手菜单(回复数字即可,无需记命令)',
|
||||
'',
|
||||
'📋 会话',
|
||||
'/sessionlist / session N 查看/绑定当前工作区会话',
|
||||
'📋 会话 / 工作区',
|
||||
'/sessionlist 列出工作区会话',
|
||||
'/session ID 绑定已有会话',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/workspacelist 列出工作区',
|
||||
'/workspace 路径 切换工作区',
|
||||
'/new 开启全新会话',
|
||||
'',
|
||||
'📊 状态 / 压缩',
|
||||
|
|
@ -550,14 +632,16 @@ export function helpCard(extraTextLines = []) {
|
|||
{ tag: 'div', text: markdown('**📋 卡片功能**\n\n' +
|
||||
'1. 会话下拉 — 切换当前绑定会话\n' +
|
||||
'2. 工作区下拉 — 切换工作区\n' +
|
||||
'3. 🆕 新会话 — 开启全新会话\n' +
|
||||
'4. 📋 全部会话 — 查看/绑定会话\n' +
|
||||
'5. ⏹ 停止 — 停止当前任务\n' +
|
||||
'6. 📐 压缩 — 压缩当前会话上下文\n' +
|
||||
'7. 补充指令 — 给 Agent 发送指令\n' +
|
||||
'8. 📊 状态 — 查看系统连接状态\n' +
|
||||
'9. 📖 帮助 — 查看本帮助\n' +
|
||||
'10. ⚙ 更多设置 — 预设/模型/归档设置') },
|
||||
'3. 🤖 预设下拉 — 切换 Agent 预设\n' +
|
||||
'4. 🧠 模型下拉 — 切换模型\n' +
|
||||
'5. 🆕 新会话 — 开启全新会话\n' +
|
||||
'6. 📋 会话/关注 — 查看/绑定会话,管理关注\n' +
|
||||
'7. ⏹ 停止 — 停止当前任务\n' +
|
||||
'8. 📐 压缩 — 压缩当前会话上下文\n' +
|
||||
'9. 补充指令 — 给 Agent 发送指令\n' +
|
||||
'10. 🗄 归档切换 — 显示/隐藏归档会话\n' +
|
||||
'11. 📊 状态 — 查看系统连接状态\n' +
|
||||
'12. 📖 帮助 — 查看本帮助') },
|
||||
{ tag: 'hr' },
|
||||
{ tag: 'div', text: markdown('**⌨️ 文本命令**\n\n' +
|
||||
'`/m` — 打开菜单卡片\n' +
|
||||
|
|
@ -582,8 +666,8 @@ export function helpCard(extraTextLines = []) {
|
|||
'`/repair` — 修复卡片按钮') },
|
||||
{ tag: 'hr' },
|
||||
{ tag: 'div', text: markdown('**💡 数字兜底**\n回复数字快速操作:\n' +
|
||||
'**1**续写 · **2**新会话 · **3**全部会话\n' +
|
||||
'**4**状态 · **5**修复 · **6**更多设置 · **7**帮助') },
|
||||
'**1**工作区列表 · **2**新会话 · **3**会话/关注\n' +
|
||||
'**4**状态 · **5**修复 · **6**帮助') },
|
||||
{ tag: 'hr' },
|
||||
backButton(),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -2679,7 +2679,7 @@ test('card buttons from an allowed sender work', async () => {
|
|||
|
||||
await bridge.onCardAction(cardActionEvent('om_card_1', 'new', 'ou_owner'));
|
||||
await bridge.waitForIdle();
|
||||
assert.equal(sent.length, 2, 'allowed operator click should send a reply');
|
||||
assert.equal(sent.length, 3, 'allowed operator click should send a reply + menu card update');
|
||||
});
|
||||
|
||||
test('card buttons honor the wildcard sender allowlist', async () => {
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ test('menu exposes the increased command set and keeps repair number-only', () =
|
|||
button.behaviors?.map((behavior) => behavior?.value?.action) ?? []
|
||||
));
|
||||
assert.deepEqual(actions, [
|
||||
'new', 'sessions', 'workspaces', 'stop', 'compact',
|
||||
'status', 'help', 'settings',
|
||||
'presets', 'models', 'new', 'sessions', 'workspaces',
|
||||
'stop', 'compact', 'archive_toggle', 'status', 'help',
|
||||
]);
|
||||
// 修复不占位按钮:仅通过数字兜底「5🔧」触发(见 bridge)
|
||||
assert.equal(actions.includes('repair'), false);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue