mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 01:53:21 +08:00
fix: clarify session index binding scope
This commit is contained in:
parent
7b369187f8
commit
33d1cd43a7
10 changed files with 204 additions and 124 deletions
205
lib/index.js
205
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -24,7 +24,7 @@ const HELP_TEXT = [
|
|||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 将当前聊天绑定到指定会话',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const HELP_TEXT = [
|
|||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 将当前聊天绑定到指定会话',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const HELP_TEXT = [
|
|||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 将当前聊天绑定到指定会话',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ export class TextHarnessBridge {
|
|||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 将当前聊天绑定到指定会话',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n'));
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const MAX_COMMAND_MESSAGE_LENGTH = 1_800;
|
|||
const MAX_SESSION_ID_LENGTH = 256;
|
||||
const UNSAFE_DISPLAY_TEXT = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
||||
const UNSAFE_DISPLAY_TEXT_GLOBAL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
|
||||
const SESSION_BIND_USAGE = '用法:/session Session ID 或 序号(/session N)';
|
||||
const SESSION_BIND_USAGE = '用法:/session Session ID 或当前工作区序号(/session N)';
|
||||
const SESSION_LIST_USAGE = [
|
||||
'用法:',
|
||||
'/sessionlist 列出当前工作区会话',
|
||||
|
|
@ -194,7 +194,7 @@ function formatSessionRelativeTime(value) {
|
|||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
}
|
||||
|
||||
function sessionListMessage(workspace, sessions) {
|
||||
function sessionListMessage(workspace, sessions, { currentWorkspace = false } = {}) {
|
||||
const rows = sessions.map((session) => {
|
||||
const sessionId = safeDisplayText(session?.sessionId);
|
||||
if (!sessionId) throw new TypeError('Harness returned an invalid session id');
|
||||
|
|
@ -212,10 +212,19 @@ function sessionListMessage(workspace, sessions) {
|
|||
'',
|
||||
...rows.map((row, index) => `${index + 1}. ${row}`),
|
||||
'',
|
||||
'绑定用法:/session Session ID 或 序号(/session N)',
|
||||
currentWorkspace
|
||||
? '绑定用法:/session Session ID 或当前工作区序号(/session N)'
|
||||
: '绑定用法:/session Session ID\n提示:/session N 只按机器人当前工作区的序号绑定。',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function currentSessionListWorkspace(harness) {
|
||||
if (typeof harness?.currentWorkspace !== 'function') return null;
|
||||
const [current] = await existingWorkspacePaths([harness.currentWorkspace()]);
|
||||
harness.assertWorkspaceScope?.();
|
||||
return current ?? null;
|
||||
}
|
||||
|
||||
async function runSessionListCommand(match, harness) {
|
||||
if (typeof harness?.listWorkspaceSessions !== 'function') {
|
||||
return commandResult('当前机器人暂不支持列出工作区会话。');
|
||||
|
|
@ -230,7 +239,10 @@ async function runSessionListCommand(match, harness) {
|
|||
}
|
||||
harness.assertWorkspaceScope?.();
|
||||
const workspace = normalizedWorkspacePath(listed.workspace) ?? resolved.workspace;
|
||||
const message = sessionListMessage(workspace, listed.sessions);
|
||||
const currentWorkspace = await currentSessionListWorkspace(harness);
|
||||
const message = sessionListMessage(workspace, listed.sessions, {
|
||||
currentWorkspace: workspace === currentWorkspace,
|
||||
});
|
||||
return commandResult(message, splitWorkspaceCommandMessage(message));
|
||||
} catch (error) {
|
||||
if (error?.code === 'workspace-bot-not-found') {
|
||||
|
|
@ -275,21 +287,30 @@ async function runSessionBindCommand(command, harness, conversationKey) {
|
|||
|| typeof harness?.currentWorkspace !== 'function') {
|
||||
return commandResult('当前机器人暂不支持按序号绑定,请使用 /session Session ID。');
|
||||
}
|
||||
const selected = await selectedWorkspacePath(harness.currentWorkspace());
|
||||
if (selected.error) return commandResult(selected.error);
|
||||
let listed;
|
||||
try {
|
||||
listed = await harness.listWorkspaceSessions(selected.workspace);
|
||||
const selected = await selectedWorkspacePath(harness.currentWorkspace());
|
||||
if (selected.error) return commandResult(selected.error);
|
||||
const listed = await harness.listWorkspaceSessions(selected.workspace);
|
||||
if (!listed || !Array.isArray(listed.sessions)) {
|
||||
throw new TypeError('Harness returned an invalid workspace session list');
|
||||
}
|
||||
harness.assertWorkspaceScope?.();
|
||||
} catch {
|
||||
const position = Number(sessionId);
|
||||
if (!Number.isSafeInteger(position) || position < 1
|
||||
|| position > listed.sessions.length) {
|
||||
return commandResult('会话序号不存在,请先执行 /sessionlist 查看序号。');
|
||||
}
|
||||
const selectedSessionId = listed.sessions[position - 1]?.sessionId;
|
||||
if (!validSessionId(selectedSessionId)) {
|
||||
throw new TypeError('Harness returned an invalid session id');
|
||||
}
|
||||
sessionId = selectedSessionId;
|
||||
} catch (error) {
|
||||
if (error?.code === 'workspace-bot-not-found') {
|
||||
return commandResult(sessionBindErrorMessage(error));
|
||||
}
|
||||
return commandResult('暂时无法获取会话列表,请稍后重试。');
|
||||
}
|
||||
const position = Number(sessionId);
|
||||
if (!Number.isSafeInteger(position) || position < 1
|
||||
|| position > (listed?.sessions?.length ?? 0)) {
|
||||
return commandResult('会话序号不存在,请先执行 /sessionlist 查看序号。');
|
||||
}
|
||||
sessionId = listed.sessions[position - 1].sessionId;
|
||||
}
|
||||
if (!validSessionId(sessionId)) return commandResult(SESSION_BIND_USAGE);
|
||||
if (typeof harness?.bindWorkspaceSession !== 'function') {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const HELP_TEXT = [
|
|||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 将当前聊天绑定到指定会话',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const HELP_TEXT = [
|
|||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/session Session ID 将当前聊天绑定到指定会话',
|
||||
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -28,6 +28,61 @@ test('/session binds exactly one safe Session ID to the current conversation', a
|
|||
assert.match(result.message, /归档:是/);
|
||||
assert.equal(result.messages.join(''), result.message);
|
||||
});
|
||||
|
||||
test('/session N binds the selected position from the current workspace', async () => {
|
||||
const workspace = process.cwd();
|
||||
const calls = [];
|
||||
const harness = {
|
||||
currentWorkspace() { return workspace; },
|
||||
async listWorkspaceSessions(requestedWorkspace) {
|
||||
assert.equal(requestedWorkspace, workspace);
|
||||
return {
|
||||
workspace,
|
||||
sessions: [
|
||||
{ sessionId: 'session-first' },
|
||||
{ sessionId: 'session-second' },
|
||||
],
|
||||
};
|
||||
},
|
||||
async bindWorkspaceSession(key, sessionId) {
|
||||
calls.push({ key, sessionId });
|
||||
return { workspace, sessionId, title: 'Selected session' };
|
||||
},
|
||||
};
|
||||
|
||||
const result = await runWorkspaceCommand('/session 2', harness, 'direct:conversation-1');
|
||||
assert.deepEqual(calls, [{ key: 'direct:conversation-1', sessionId: 'session-second' }]);
|
||||
assert.match(result.message, /ID:session-second/);
|
||||
|
||||
const missing = await runWorkspaceCommand('/session 3', harness, 'direct:conversation-1');
|
||||
assert.match(missing.message, /会话序号不存在/);
|
||||
assert.equal(calls.length, 1);
|
||||
});
|
||||
|
||||
test('/session N maps position lookup failures to safe messages', async () => {
|
||||
const stale = new Error('private old bot lifecycle');
|
||||
stale.code = 'workspace-bot-not-found';
|
||||
const staleCurrent = await runWorkspaceCommand('/session 1', {
|
||||
currentWorkspace() { throw stale; },
|
||||
async listWorkspaceSessions() { throw new Error('must not be called'); },
|
||||
}, 'direct:conversation-1');
|
||||
assert.match(staleCurrent.message, /正在移除或已重新接入/);
|
||||
assert.doesNotMatch(staleCurrent.message, /private old bot lifecycle/);
|
||||
|
||||
const staleList = await runWorkspaceCommand('/session 1', {
|
||||
currentWorkspace() { return process.cwd(); },
|
||||
async listWorkspaceSessions() { throw stale; },
|
||||
}, 'direct:conversation-1');
|
||||
assert.match(staleList.message, /正在移除或已重新接入/);
|
||||
|
||||
const unavailable = await runWorkspaceCommand('/session 1', {
|
||||
currentWorkspace() { return process.cwd(); },
|
||||
async listWorkspaceSessions() { throw new Error('private Harness detail'); },
|
||||
}, 'direct:conversation-1');
|
||||
assert.match(unavailable.message, /暂时无法获取会话列表/);
|
||||
assert.doesNotMatch(unavailable.message, /private Harness detail/);
|
||||
});
|
||||
|
||||
test('/session strictly rejects missing, multiple, oversized, and unsafe IDs', async () => {
|
||||
let bindCalls = 0;
|
||||
const harness = {
|
||||
|
|
@ -156,7 +211,7 @@ test('all nine channel bridges advertise /session and pass their current convers
|
|||
];
|
||||
for (const [file, key] of bridgeFamilies) {
|
||||
const source = await readFile(new URL(file, import.meta.url), 'utf8');
|
||||
assert.match(source, /\/session Session ID 将当前聊天绑定到指定会话/);
|
||||
assert.match(source, /\/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话/);
|
||||
assert.ok(
|
||||
source.includes(`runWorkspaceCommand(text, this.#harness, ${key})`),
|
||||
`${file} must pass ${key} to the shared command`,
|
||||
|
|
|
|||
|
|
@ -907,11 +907,14 @@ test('/sessionlist supports the current workspace, list numbers, and absolute pa
|
|||
assert.doesNotMatch(current.message, /\u202e|\n4\. injected/);
|
||||
assert.match(current.message, /2\. 暂无标题(已归档)\n ID: session-archived/);
|
||||
assert.match(current.message, /3\. 标题暂不可用\n ID: session-missing-summary/);
|
||||
assert.match(current.message, /绑定用法:\/session Session ID 或当前工作区序号(\/session N)/);
|
||||
assert.equal(current.messages.join(''), current.message);
|
||||
|
||||
const numbered = await runWorkspaceCommand('/sessionlist 2', harness);
|
||||
assert.match(numbered.message, new RegExp(`工作区:${alternateWorkspace.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
|
||||
assert.match(numbered.message, /Alternate session/);
|
||||
assert.match(numbered.message, /绑定用法:\/session Session ID\n提示:\/session N 只按机器人当前工作区的序号绑定/);
|
||||
assert.doesNotMatch(numbered.message, /Session ID 或当前工作区序号/);
|
||||
|
||||
const absolute = await runWorkspaceCommand(`/sessionlist ${thirdWorkspace}`, harness);
|
||||
assert.match(absolute.message, /该工作区暂无会话/);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue