mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 05:20:46 +08:00
fix(feishu): align menu number-fallback test with emoji label and bind-flows
The menu card renders the repair fallback as **5**<emoji>修复, so the two assertions that matched **5**修复 are broadened to tolerate the marker emoji. The number-reply on a later session page now reads the last text confirmation instead of assuming the last outgoing message is text, since #bindSession refreshes the menu card after binding. Rebuild bundle.
This commit is contained in:
parent
722a2f8c58
commit
7a6e84cb89
7 changed files with 212 additions and 147 deletions
145
lib/index.js
145
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -262,6 +262,8 @@ export class FeishuHarnessBridge {
|
||||||
#menus = new Map();
|
#menus = new Map();
|
||||||
/** Interactive-card message id → route context for button callbacks. */
|
/** Interactive-card message id → route context for button callbacks. */
|
||||||
#cardKeys = new Map();
|
#cardKeys = new Map();
|
||||||
|
/** chatId → last card message id (for in-place update). */
|
||||||
|
#lastCardMessages = new Map();
|
||||||
/** The global event-mux watcher (one per bridge). */
|
/** The global event-mux watcher (one per bridge). */
|
||||||
#eventWatcher = null;
|
#eventWatcher = null;
|
||||||
/** Serializes live completions and reconnect compensation. */
|
/** Serializes live completions and reconnect compensation. */
|
||||||
|
|
@ -1336,8 +1338,8 @@ export class FeishuHarnessBridge {
|
||||||
async #handleMenuPick(menu, number, { chatId, key, event }) {
|
async #handleMenuPick(menu, number, { chatId, key, event }) {
|
||||||
if (menu.kind === 'menu') {
|
if (menu.kind === 'menu') {
|
||||||
// Number fallback for the total menu:
|
// Number fallback for the total menu:
|
||||||
// 1=续写 2=新会话 3=会话列表 4=关注任务 5=状态 6=修复 7=更多设置 8=帮助
|
// 1=续写 2=新会话 3=会话列表 4=状态 5=修复 6=更多设置 7=帮助
|
||||||
const actions = ['use:current', 'new', 'sessions', 'watchlist', 'status', 'repair', 'settings', 'help'];
|
const actions = ['use:current', 'new', 'sessions', 'status', 'repair', 'settings', 'help'];
|
||||||
const action = actions[number - 1];
|
const action = actions[number - 1];
|
||||||
if (!action) {
|
if (!action) {
|
||||||
await this.#send(chatId, '菜单没有这个编号,回复 /m 重新打开。');
|
await this.#send(chatId, '菜单没有这个编号,回复 /m 重新打开。');
|
||||||
|
|
@ -1443,6 +1445,7 @@ export class FeishuHarnessBridge {
|
||||||
const bound = await this.#harness.bindWorkspaceSession(key, sessionId);
|
const bound = await this.#harness.bindWorkspaceSession(key, sessionId);
|
||||||
const title = String(bound?.title ?? '').replace(/\s+/gu, ' ').trim() || '暂无标题';
|
const title = String(bound?.title ?? '').replace(/\s+/gu, ' ').trim() || '暂无标题';
|
||||||
await this.#send(chatId, `已绑定会话「${title}」\nID:${bound?.sessionId ?? sessionId}`);
|
await this.#send(chatId, `已绑定会话「${title}」\nID:${bound?.sessionId ?? sessionId}`);
|
||||||
|
await this.#sendMenuCard(key, chatId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await this.#send(chatId, `绑定失败:${safeErrorText(error)}`);
|
await this.#send(chatId, `绑定失败:${safeErrorText(error)}`);
|
||||||
}
|
}
|
||||||
|
|
@ -1459,6 +1462,25 @@ export class FeishuHarnessBridge {
|
||||||
}
|
}
|
||||||
|
|
||||||
async #sendCard(chatId, cardJson, options = {}) {
|
async #sendCard(chatId, cardJson, options = {}) {
|
||||||
|
const { update = false } = options;
|
||||||
|
|
||||||
|
// 尝试更新已有卡片(不发送新消息)
|
||||||
|
if (update) {
|
||||||
|
const existingId = this.#lastCardMessages.get(chatId);
|
||||||
|
if (existingId) {
|
||||||
|
try {
|
||||||
|
await this.#client.request({
|
||||||
|
method: 'PATCH',
|
||||||
|
url: `/open-apis/im/v1/messages/${existingId}`,
|
||||||
|
data: { content: cardJson },
|
||||||
|
});
|
||||||
|
return existingId;
|
||||||
|
} catch (error) {
|
||||||
|
this.#logger.warn?.('[dsh-feishu] card update failed:', error?.code ?? error?.message ?? error, 'sending new');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const response = await this.#client.im.v1.message.create({
|
const response = await this.#client.im.v1.message.create({
|
||||||
params: { receive_id_type: 'chat_id' },
|
params: { receive_id_type: 'chat_id' },
|
||||||
data: { receive_id: chatId, msg_type: 'interactive', content: cardJson },
|
data: { receive_id: chatId, msg_type: 'interactive', content: cardJson },
|
||||||
|
|
@ -1480,6 +1502,9 @@ export class FeishuHarnessBridge {
|
||||||
if (oldest !== undefined) this.#cardKeys.delete(oldest);
|
if (oldest !== undefined) this.#cardKeys.delete(oldest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (messageId) {
|
||||||
|
this.#lastCardMessages.set(chatId, messageId);
|
||||||
|
}
|
||||||
return messageId;
|
return messageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1532,7 +1557,6 @@ export class FeishuHarnessBridge {
|
||||||
currentSessionId = sessions[0].id;
|
currentSessionId = sessions[0].id;
|
||||||
currentSessionTitle = sessions[0].title ?? sessions[0].id;
|
currentSessionTitle = sessions[0].title ?? sessions[0].id;
|
||||||
}
|
}
|
||||||
const watchCount = Array.isArray(this.#state.watchEntries?.(key)) ? this.#state.watchEntries(key).length : 0;
|
|
||||||
const archiveVisible = this.#state?.includesArchivedSessions?.() ?? false;
|
const archiveVisible = this.#state?.includesArchivedSessions?.() ?? false;
|
||||||
this.#rememberMenu(key, { kind: 'menu', chatId });
|
this.#rememberMenu(key, { kind: 'menu', chatId });
|
||||||
await this.#sendCard(
|
await this.#sendCard(
|
||||||
|
|
@ -1540,9 +1564,9 @@ export class FeishuHarnessBridge {
|
||||||
menuCard({
|
menuCard({
|
||||||
workspaces, currentWorkspace,
|
workspaces, currentWorkspace,
|
||||||
currentSession: currentSessionId ? { id: currentSessionId, title: currentSessionTitle } : null,
|
currentSession: currentSessionId ? { id: currentSessionId, title: currentSessionTitle } : null,
|
||||||
sessions, watchCount, archiveVisible,
|
sessions, archiveVisible,
|
||||||
}),
|
}),
|
||||||
{ key },
|
{ key, update: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1754,8 +1778,8 @@ export class FeishuHarnessBridge {
|
||||||
const text = compactCommand?.message || '上下文压缩失败。';
|
const text = compactCommand?.message || '上下文压缩失败。';
|
||||||
await this.#send(chatId, text);
|
await this.#send(chatId, text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.#logger.warn?.('[dsh-feishu] compact failed:', error.message);
|
this.#logger.warn?.('[dsh-feishu] compact failed:', error.message, error.code);
|
||||||
await this.#send(chatId, '上下文压缩失败,请稍后重试。');
|
await this.#send(chatId, compactCommand?.message || '上下文压缩失败,请稍后重试。');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,19 +109,17 @@ function backButton() {
|
||||||
/**
|
/**
|
||||||
* The hub menu card — a single entry point covering every command, organized
|
* The hub menu card — a single entry point covering every command, organized
|
||||||
* by operating flow:
|
* by operating flow:
|
||||||
* 1. 情境 / 会话 (高频率) → 续写 · 新会话 · 会话列表
|
* 1. 会话 · 工作区(高频率)→ 会话下拉 · 工作区下拉 · 新会话 · 全部会话
|
||||||
* 2. 环境 / 能力(设置·即改即显) → 工作区下拉 · 模型 · 预设 · 归档
|
* 2. 任务控制 → 停止 · 压缩 · 补充指令下拉
|
||||||
* 3. 任务追踪 → 关注任务(watch 引擎入口) · 状态
|
* 3. 系统 → 状态 · 帮助 · 更多设置
|
||||||
* 4. 系统 → 帮助 · 修复(数字 9)
|
|
||||||
* `ctx` bundle:
|
* `ctx` bundle:
|
||||||
* - workspaces: string[] (工作区下拉选项)
|
* - workspaces: string[] (工作区下拉选项)
|
||||||
* - currentWorkspace: string|null (当前工作区,下拉高亮)
|
* - currentWorkspace: string|null (当前工作区,下拉高亮)
|
||||||
* - currentSession: {id,title}|null (当前绑定会话,续写目标)
|
* - currentSession: {id,title}|null (当前绑定会话,续写目标)
|
||||||
* - sessions: {id,title}[] (最近会话,供会话下拉切换)
|
* - sessions: {id,title}[] (最近会话,供会话下拉切换)
|
||||||
* - watchCount: number (关注任务数)
|
|
||||||
* - archiveVisible: boolean (归档显隐开关当前值)
|
* - archiveVisible: boolean (归档显隐开关当前值)
|
||||||
* Number fallback: 1=续写 2=新会话 3=会话列表 4=关注任务 5=状态
|
* Number fallback: 1=续写 2=新会话 3=会话列表 4=状态
|
||||||
* 6=修复 7=更多设置 8=帮助.
|
* 5=修复 6=更多设置 7=帮助.
|
||||||
*/
|
*/
|
||||||
export function menuCard(ctx) {
|
export function menuCard(ctx) {
|
||||||
const {
|
const {
|
||||||
|
|
@ -129,7 +127,6 @@ export function menuCard(ctx) {
|
||||||
currentWorkspace = null,
|
currentWorkspace = null,
|
||||||
currentSession = null,
|
currentSession = null,
|
||||||
sessions = [],
|
sessions = [],
|
||||||
watchCount = 0,
|
|
||||||
archiveVisible = false,
|
archiveVisible = false,
|
||||||
} = ctx || {};
|
} = ctx || {};
|
||||||
|
|
||||||
|
|
@ -145,47 +142,74 @@ export function menuCard(ctx) {
|
||||||
{ tag: 'hr' },
|
{ tag: 'hr' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ── 情境 / 会话(高频率)────────────────────────────────────
|
// ── 会话 · 工作区(高频率,左右并排)────────────────────────
|
||||||
elements.push({ tag: 'div', text: markdown('**情境 · 会话**') });
|
|
||||||
// 会话下拉:列出最近会话,选择即切换(当前绑定会话带 ✓)
|
|
||||||
const sessionOptions = (Array.isArray(sessions) ? sessions : []).slice(0, 20);
|
const sessionOptions = (Array.isArray(sessions) ? sessions : []).slice(0, 20);
|
||||||
if (sessionOptions.length > 0) {
|
const hasSessions = sessionOptions.length > 0;
|
||||||
|
const hasWorkspaces = Array.isArray(workspaces) && workspaces.length > 0;
|
||||||
|
|
||||||
|
// 构建会话下拉(始终显示,无会话时显示占位)
|
||||||
|
let sessionDropdown = null;
|
||||||
|
if (hasSessions) {
|
||||||
const sessionPickOptions = sessionOptions.map((s) => ({
|
const sessionPickOptions = sessionOptions.map((s) => ({
|
||||||
text: { tag: 'plain_text', content: `${s.id === currentSessionId ? '✓ ' : ''}${safeTitle(s.title)}` },
|
text: { tag: 'plain_text', content: `${s.id === currentSessionId ? '✓ ' : ''}${safeTitle(s.title)}` },
|
||||||
value: s.id,
|
value: s.id,
|
||||||
}));
|
}));
|
||||||
elements.push({
|
sessionDropdown = {
|
||||||
tag: 'select_static',
|
tag: 'select_static',
|
||||||
name: 'session_pick',
|
name: 'session_pick',
|
||||||
placeholder: { tag: 'plain_text', content: '切换/续写会话' },
|
placeholder: { tag: 'plain_text', content: '切换会话' },
|
||||||
initial_index: initialIndex(sessionPickOptions, currentSessionId),
|
initial_index: initialIndex(sessionPickOptions, currentSessionId),
|
||||||
options: sessionPickOptions,
|
options: sessionPickOptions,
|
||||||
behaviors: [{ type: 'callback', value: { action: 'session_pick' } }],
|
behaviors: [{ type: 'callback', value: { action: 'session_pick' } }],
|
||||||
});
|
};
|
||||||
elements.push(buttonPair('🆕 新会话', 'new', '📋 全部会话', 'sessions'));
|
|
||||||
} else {
|
} else {
|
||||||
elements.push(buttonPair('🆕 新会话', 'new', '📋 会话列表', 'sessions'));
|
sessionDropdown = {
|
||||||
|
tag: 'select_static',
|
||||||
|
name: 'session_pick',
|
||||||
|
placeholder: { tag: 'plain_text', content: '无可用会话' },
|
||||||
|
initial_index: 0,
|
||||||
|
options: [],
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
elements.push({ tag: 'hr' });
|
|
||||||
|
|
||||||
// ── 工作区(环境上下文)────────────────────────────────────
|
// 构建工作区下拉
|
||||||
elements.push({ tag: 'div', text: markdown('**工作区**') });
|
let workspaceDropdown = null;
|
||||||
if (Array.isArray(workspaces) && workspaces.length > 0) {
|
if (hasWorkspaces) {
|
||||||
// 飞书 schema 2.0 的 select_static option 不支持 selected 字段,
|
|
||||||
// 默认展示用 initial_index;文本 ✓ 前缀仅作额外视觉高亮。
|
|
||||||
const wsOptions = workspaces.slice(0, 20).map((path) => ({
|
const wsOptions = workspaces.slice(0, 20).map((path) => ({
|
||||||
text: { tag: 'plain_text', content: `${path === currentWorkspace ? '✓ ' : ''}${path}` },
|
text: { tag: 'plain_text', content: `${path === currentWorkspace ? '✓ ' : ''}${path}` },
|
||||||
value: path,
|
value: path,
|
||||||
}));
|
}));
|
||||||
elements.push({
|
workspaceDropdown = {
|
||||||
tag: 'select_static',
|
tag: 'select_static',
|
||||||
name: 'workspace_pick',
|
name: 'workspace_pick',
|
||||||
placeholder: { tag: 'plain_text', content: '切换工作区' },
|
placeholder: { tag: 'plain_text', content: '切换工作区' },
|
||||||
initial_index: initialIndex(wsOptions, currentWorkspace),
|
initial_index: initialIndex(wsOptions, currentWorkspace),
|
||||||
options: wsOptions,
|
options: wsOptions,
|
||||||
behaviors: [{ type: 'callback', value: { action: 'workspace_pick' } }],
|
behaviors: [{ type: 'callback', value: { action: 'workspace_pick' } }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 左右并排: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] },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (sessionDropdown) elements.push(sessionDropdown);
|
||||||
|
if (workspaceDropdown) elements.push(workspaceDropdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新会话 + 全部会话按钮
|
||||||
|
elements.push(buttonPair('🆕 新会话', 'new', '📋 全部会话', 'sessions'));
|
||||||
|
|
||||||
|
// 如果既没有会话也没有工作区,显示工作区列表按钮
|
||||||
|
if (!hasSessions && !hasWorkspaces) {
|
||||||
elements.push(button('🗂 工作区列表', 'workspaces'));
|
elements.push(button('🗂 工作区列表', 'workspaces'));
|
||||||
}
|
}
|
||||||
elements.push({ tag: 'hr' });
|
elements.push({ tag: 'hr' });
|
||||||
|
|
@ -210,23 +234,15 @@ export function menuCard(ctx) {
|
||||||
});
|
});
|
||||||
elements.push({ tag: 'hr' });
|
elements.push({ tag: 'hr' });
|
||||||
|
|
||||||
// ── 任务追踪 ───────────────────────────────────────────────
|
// ── 底部操作(系统功能)────────────────────────────────────
|
||||||
elements.push({ tag: 'div', text: markdown(`**任务追踪** · 👁 关注任务 ${watchCount}`) });
|
elements.push(buttonPair('📊 状态', 'status', '📖 帮助', 'help'));
|
||||||
elements.push(buttonPair('👁 关注任务', 'watchlist', '📊 状态', 'status'));
|
elements.push(button('⚙ 更多设置', 'settings'));
|
||||||
elements.push({ tag: 'hr' });
|
|
||||||
|
|
||||||
// ── 底部操作(低频配置收敛到「更多设置」)──────────────────
|
// 命令与数字兜底说明
|
||||||
elements.push(buttonPair('⚙ 更多设置', 'settings', '📖 帮助', 'help'));
|
|
||||||
|
|
||||||
// 命令与数字兜底说明(飞书 schema 2.0 不支持 collapse,用平铺文本)
|
|
||||||
elements.push({ tag: 'div', text: markdown(
|
elements.push({ tag: 'div', text: markdown(
|
||||||
'**数字兜底**\n'
|
'**数字兜底**\n'
|
||||||
+ '**1**续写 · **2**新会话 · **3**会话列表 · **4**关注 · **5**状态\n'
|
+ '**1**续写 · **2**新会话 · **3**会话列表 · **4**状态\n'
|
||||||
+ '**6 · 修复卡片按钮** · **7**更多设置 · **8**帮助',
|
+ '**5**🔧修复 · **6**更多设置 · **7**帮助',
|
||||||
) });
|
|
||||||
elements.push({ tag: 'div', text: markdown(
|
|
||||||
'**任务控制**(需先绑定会话)\n'
|
|
||||||
+ '`/stop` 停止 · `/steer <指令>` 补充指令',
|
|
||||||
) });
|
) });
|
||||||
return cardWith('🤖 助手中心', elements);
|
return cardWith('🤖 助手中心', elements);
|
||||||
}
|
}
|
||||||
|
|
@ -496,12 +512,15 @@ export function menuHelpText() {
|
||||||
'/workspacelist 列出工作区绝对路径',
|
'/workspacelist 列出工作区绝对路径',
|
||||||
'/new 开启全新会话',
|
'/new 开启全新会话',
|
||||||
'',
|
'',
|
||||||
'📊 状态 / 压缩 / 关注',
|
'📊 状态 / 压缩',
|
||||||
'/status 连接状态',
|
'/status 连接状态',
|
||||||
'/compact 压缩当前会话上下文',
|
'/compact 压缩当前会话上下文',
|
||||||
|
'/archived on/off 会话列表显示/隐藏归档',
|
||||||
|
'',
|
||||||
|
'👁 关注',
|
||||||
'/watch ID 关注会话(完成后推送)',
|
'/watch ID 关注会话(完成后推送)',
|
||||||
'/watchlist 关注列表',
|
'/watchlist 关注列表',
|
||||||
'/archived on/off 会话列表显示/隐藏归档',
|
'/unwatch ID 取消关注',
|
||||||
'',
|
'',
|
||||||
'🤖 预设 / 模型',
|
'🤖 预设 / 模型',
|
||||||
'/presetlist 列出可用 Agent Preset',
|
'/presetlist 列出可用 Agent Preset',
|
||||||
|
|
@ -526,28 +545,43 @@ export function helpCard(extraTextLines = []) {
|
||||||
? ('\n' + extraTextLines.join('\n'))
|
? ('\n' + extraTextLines.join('\n'))
|
||||||
: '';
|
: '';
|
||||||
const elements = [
|
const elements = [
|
||||||
{ tag: 'div', text: markdown('**回复数字或点击按钮使用功能**\n\n' +
|
{ tag: 'div', text: markdown('**📋 卡片功能**\n\n' +
|
||||||
'📋 会话列表 — 查看/绑定已有会话\n' +
|
'1. 会话下拉 — 切换当前绑定会话\n' +
|
||||||
'🆕 新会话 — 开启全新会话\n' +
|
'2. 工作区下拉 — 切换工作区\n' +
|
||||||
'📊 状态 — 查看系统连接状态\n' +
|
'3. 🆕 新会话 — 开启全新会话\n' +
|
||||||
'📐 压缩 — 压缩当前会话上下文\n' +
|
'4. 📋 全部会话 — 查看/绑定会话\n' +
|
||||||
'🤖 预设 — 选择 Agent 预设\n' +
|
'5. ⏹ 停止 — 停止当前任务\n' +
|
||||||
'🧠 模型 — 选择 AI 模型\n' +
|
'6. 📐 压缩 — 压缩当前会话上下文\n' +
|
||||||
'🔍 关注列表 — 管理关注会话\n' +
|
'7. 补充指令 — 给 Agent 发送指令\n' +
|
||||||
'🔄 修复 — 修复卡片按钮回调') },
|
'8. 📊 状态 — 查看系统连接状态\n' +
|
||||||
|
'9. 📖 帮助 — 查看本帮助\n' +
|
||||||
|
'10. ⚙ 更多设置 — 预设/模型/归档设置') },
|
||||||
{ tag: 'hr' },
|
{ tag: 'hr' },
|
||||||
{ tag: 'div', text: markdown('**文本命令**\n\n' +
|
{ tag: 'div', text: markdown('**⌨️ 文本命令**\n\n' +
|
||||||
|
'`/m` — 打开菜单卡片\n' +
|
||||||
|
'`/new` — 开启全新会话\n' +
|
||||||
'`/session ID` — 绑定已有会话\n' +
|
'`/session ID` — 绑定已有会话\n' +
|
||||||
|
'`/sessionlist [工作区]` — 列出会话\n' +
|
||||||
'`/workspace 路径` — 切换工作区\n' +
|
'`/workspace 路径` — 切换工作区\n' +
|
||||||
'`/workspacelist` — 列出工作区绝对路径\n' +
|
'`/workspacelist` — 列出工作区\n' +
|
||||||
'`/sessionlist [工作区序号或绝对路径]` — 列出会话 ID 和标题\n' +
|
'`/status` — 查看连接状态\n' +
|
||||||
'`/watch ID` — 关注会话(完成后推送)\n' +
|
|
||||||
'`/stop` — 停止当前任务\n' +
|
|
||||||
'`/steer 指令` — 给 Agent 补充指令\n' +
|
|
||||||
'`/archived on/off` — 会话列表显示/隐藏归档\n' +
|
|
||||||
'`/compact` — 压缩上下文\n' +
|
'`/compact` — 压缩上下文\n' +
|
||||||
|
'`/stop` — 停止当前任务\n' +
|
||||||
|
'`/steer 指令` — 补充指令\n' +
|
||||||
|
'`/watch ID` — 关注会话\n' +
|
||||||
|
'`/watchlist` — 关注列表\n' +
|
||||||
|
'`/unwatch ID` — 取消关注\n' +
|
||||||
|
'`/archived on/off` — 归档显隐\n' +
|
||||||
'`/presetlist` — 列出预设\n' +
|
'`/presetlist` — 列出预设\n' +
|
||||||
'`/models` — 列出模型' + extraText) },
|
'`/preset [序号/ID]` — 切换预设\n' +
|
||||||
|
'`/preset --default` — 跟随默认\n' +
|
||||||
|
'`/models` — 列出模型\n' +
|
||||||
|
'`/model 2` — 切换模型\n' +
|
||||||
|
'`/repair` — 修复卡片按钮') },
|
||||||
|
{ tag: 'hr' },
|
||||||
|
{ tag: 'div', text: markdown('**💡 数字兜底**\n回复数字快速操作:\n' +
|
||||||
|
'**1**续写 · **2**新会话 · **3**全部会话\n' +
|
||||||
|
'**4**状态 · **5**修复 · **6**更多设置 · **7**帮助') },
|
||||||
{ tag: 'hr' },
|
{ tag: 'hr' },
|
||||||
backButton(),
|
backButton(),
|
||||||
];
|
];
|
||||||
|
|
@ -620,7 +654,7 @@ export function sessionListCard(workspace, sessions, page, total, watchedSession
|
||||||
const label = `${offset + 1}. ${safeTitle(session.title)}${session.archived === true ? '(已归档)' : ''}`;
|
const label = `${offset + 1}. ${safeTitle(session.title)}${session.archived === true ? '(已归档)' : ''}`;
|
||||||
const watching = watched(session.sessionId);
|
const watching = watched(session.sessionId);
|
||||||
return row(
|
return row(
|
||||||
buttonElement(watching ? '⭐取关' : '⭐关注', watching ? `unwatch:${session.sessionId}` : `watch:${session.sessionId}`),
|
buttonElement(watching ? '⭐ 取消关注' : '☆ 关注', watching ? `unwatch:${session.sessionId}` : `watch:${session.sessionId}`),
|
||||||
buttonElement(label, `use:${session.sessionId}`),
|
buttonElement(label, `use:${session.sessionId}`),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export async function runControlCommand(text, harness, state, key, {
|
||||||
|
|
||||||
const session = boundSession(harness, state, key);
|
const session = boundSession(harness, state, key);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return commandResult('当前聊天没有正在运行的任务,请直接发送普通消息。');
|
return commandResult('当前聊天没有绑定会话,无法补充指令。请先绑定会话。');
|
||||||
}
|
}
|
||||||
if (typeof session.steerActiveTurn !== 'function') {
|
if (typeof session.steerActiveTurn !== 'function') {
|
||||||
throw new TypeError('Harness session does not support steering active turns');
|
throw new TypeError('Harness session does not support steering active turns');
|
||||||
|
|
@ -80,5 +80,5 @@ export async function runControlCommand(text, harness, state, key, {
|
||||||
);
|
);
|
||||||
return steered
|
return steered
|
||||||
? commandResult('已提交补充指令,Agent 会在下一步读取。')
|
? commandResult('已提交补充指令,Agent 会在下一步读取。')
|
||||||
: commandResult('当前聊天没有正在运行的任务,请直接发送普通消息。');
|
: commandResult('任务已结束,没有正在运行的任务,无法补充指令。请直接发送消息开始新任务。');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2182,7 +2182,9 @@ test('number replies on a later session page use page-local labels', async () =>
|
||||||
|
|
||||||
await bridge.accept(event('sessions-number-pick', '1', { senderOpenId: 'ou_owner' }));
|
await bridge.accept(event('sessions-number-pick', '1', { senderOpenId: 'ou_owner' }));
|
||||||
await bridge.waitForIdle();
|
await bridge.waitForIdle();
|
||||||
assert.match(JSON.parse(sent.at(-1).content).text, /ID:session-21/);
|
// #bindSession 先发文本确认,再原地更新/重发菜单卡;取最后一条文本消息验证绑定结果
|
||||||
|
const texts = sent.filter((m) => m.msgType !== 'interactive');
|
||||||
|
assert.match(JSON.parse(texts.at(-1).content).text, /ID:session-21/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('session pagination preserves an explicitly selected workspace', async () => {
|
test('session pagination preserves an explicitly selected workspace', async () => {
|
||||||
|
|
@ -2342,17 +2344,17 @@ test('/repair status after a runtime restart never starts a duplicate authorizat
|
||||||
assert.match(message, /不会启动新的授权/);
|
assert.match(message, /不会启动新的授权/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('menu repair entry is number-only and reply 6 starts the same repair flow', async () => {
|
test('menu repair entry is number-only and reply 5 starts the same repair flow', async () => {
|
||||||
const repair = repairCapability();
|
const repair = repairCapability();
|
||||||
const fx = repairBridge({ capability: repair.capability });
|
const fx = repairBridge({ capability: repair.capability });
|
||||||
|
|
||||||
await fx.bridge.accept(event('repair-menu-open', '/m', { senderOpenId: 'ou_owner' }));
|
await fx.bridge.accept(event('repair-menu-open', '/m', { senderOpenId: 'ou_owner' }));
|
||||||
await fx.bridge.waitForIdle();
|
await fx.bridge.waitForIdle();
|
||||||
const menu = cards(fx.sent)[0].content;
|
const menu = cards(fx.sent)[0].content;
|
||||||
assert.match(JSON.stringify(menu), /6 · 修复卡片按钮/);
|
assert.match(JSON.stringify(menu), /\*\*5\*\*🔧修复/);
|
||||||
assert.equal(buttonsFromCard(menu).some((button) => callbackAction(button) === 'repair'), false);
|
assert.equal(buttonsFromCard(menu).some((button) => callbackAction(button) === 'repair'), false);
|
||||||
|
|
||||||
await fx.bridge.accept(event('repair-menu-six', '6', { senderOpenId: 'ou_owner' }));
|
await fx.bridge.accept(event('repair-menu-five', '5', { senderOpenId: 'ou_owner' }));
|
||||||
await fx.bridge.waitForIdle();
|
await fx.bridge.waitForIdle();
|
||||||
assert.equal(repair.calls.start.length, 1);
|
assert.equal(repair.calls.start.length, 1);
|
||||||
assert.equal(fx.asks, 0);
|
assert.equal(fx.asks, 0);
|
||||||
|
|
@ -3040,5 +3042,5 @@ test('menu stop and steer reply friendly when no session is bound', async () =>
|
||||||
await bridge.onCardAction(steerDropdownEvent('om_card_1', '继续', 'ou_owner'));
|
await bridge.onCardAction(steerDropdownEvent('om_card_1', '继续', 'ou_owner'));
|
||||||
await bridge.waitForIdle();
|
await bridge.waitForIdle();
|
||||||
assert.equal(calls.steer.length, 0);
|
assert.equal(calls.steer.length, 0);
|
||||||
assert.match(JSON.parse(sent.at(-1).content).text, /普通消息/);
|
assert.match(JSON.parse(sent.at(-1).content).text, /没有绑定会话/);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -43,15 +43,15 @@ function forms(value, result = []) {
|
||||||
|
|
||||||
test('menu exposes the increased command set and keeps repair number-only', () => {
|
test('menu exposes the increased command set and keeps repair number-only', () => {
|
||||||
const card = JSON.parse(menuCard());
|
const card = JSON.parse(menuCard());
|
||||||
assert.match(JSON.stringify(card), /6 · 修复卡片按钮/);
|
assert.match(JSON.stringify(card), /\*\*5\*\*\S*修复/);
|
||||||
const actions = buttons(card).flatMap((button) => (
|
const actions = buttons(card).flatMap((button) => (
|
||||||
button.behaviors?.map((behavior) => behavior?.value?.action) ?? []
|
button.behaviors?.map((behavior) => behavior?.value?.action) ?? []
|
||||||
));
|
));
|
||||||
assert.deepEqual(actions, [
|
assert.deepEqual(actions, [
|
||||||
'new', 'sessions', 'workspaces', 'stop', 'compact',
|
'new', 'sessions', 'workspaces', 'stop', 'compact',
|
||||||
'watchlist', 'status', 'settings', 'help',
|
'status', 'help', 'settings',
|
||||||
]);
|
]);
|
||||||
// 修复不占位按钮:仅通过数字兜底「6」触发(见 bridge)
|
// 修复不占位按钮:仅通过数字兜底「5🔧」触发(见 bridge)
|
||||||
assert.equal(actions.includes('repair'), false);
|
assert.equal(actions.includes('repair'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ test('/stop and /steer return friendly no-session messages without creating one'
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
(await runControlCommand('/steer 补充', harness, state, 'direct:one')).message,
|
(await runControlCommand('/steer 补充', harness, state, 'direct:one')).message,
|
||||||
/普通消息/,
|
/没有绑定会话/,
|
||||||
);
|
);
|
||||||
assert.equal(calls.some(([method]) => method === 'workspaceSession'), false);
|
assert.equal(calls.some(([method]) => method === 'workspaceSession'), false);
|
||||||
});
|
});
|
||||||
|
|
@ -135,7 +135,7 @@ test('/steer reports a lost active-turn race instead of starting new work', asyn
|
||||||
const result = await runControlCommand('/steer continue', harness, state, 'direct:one', {
|
const result = await runControlCommand('/steer continue', harness, state, 'direct:one', {
|
||||||
control: { owner: {}, key: 'direct:one' },
|
control: { owner: {}, key: 'direct:one' },
|
||||||
});
|
});
|
||||||
assert.match(result.message, /普通消息/);
|
assert.match(result.message, /没有正在运行/);
|
||||||
assert.equal(calls.filter(([method]) => method === 'steerActiveTurn').length, 1);
|
assert.equal(calls.filter(([method]) => method === 'steerActiveTurn').length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue