feat: add workspace session list command

This commit is contained in:
xmanrui 2026-08-18 01:38:18 +08:00
parent c51334ebc3
commit 28381468b3
17 changed files with 989 additions and 122 deletions

View file

@ -1,10 +1,19 @@
import { stat } from 'node:fs/promises';
import { realpath, stat } from 'node:fs/promises';
import { isAbsolute, resolve } from 'node:path';
const WORKSPACE_COMMAND = /^\/workspace(?:\s+([\s\S]+))?$/i;
const WORKSPACE_LIST_COMMAND = /^\/workspacelist(?:\s+([\s\S]+))?$/i;
const SESSION_LIST_COMMAND = /^\/sessionlist(?:\s+([\s\S]+))?$/i;
const MAX_WORKSPACE_PATH_LENGTH = 4_096;
const MAX_COMMAND_MESSAGE_LENGTH = 1_800;
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_LIST_USAGE = [
'用法:',
'/sessionlist 列出当前工作区会话',
'/sessionlist 工作区序号 按 /workspacelist 序号列出会话',
'/sessionlist 工作区绝对路径 列出指定工作区会话',
].join('\n');
function commandResult(message, messages = [message]) {
return { handled: true, message, messages };
@ -12,20 +21,66 @@ function commandResult(message, messages = [message]) {
function normalizedWorkspacePath(value) {
if (typeof value !== 'string' || value.length > MAX_WORKSPACE_PATH_LENGTH
|| !isAbsolute(value) || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value)) return null;
|| !isAbsolute(value) || UNSAFE_DISPLAY_TEXT.test(value)) return null;
return resolve(value);
}
function safeDisplayText(value) {
if (typeof value !== 'string') return '';
return value.replace(UNSAFE_DISPLAY_TEXT_GLOBAL, ' ').replace(/\s+/gu, ' ').trim();
}
async function existingWorkspacePaths(values) {
const unique = [...new Set(values.map(normalizedWorkspacePath).filter(Boolean))];
const checked = await Promise.all(unique.map(async (workspace) => {
const checked = await Promise.all(values.map(async (value) => {
const workspace = normalizedWorkspacePath(value);
if (!workspace) return null;
try {
return (await stat(workspace)).isDirectory() ? workspace : null;
if (!(await stat(workspace)).isDirectory()) return null;
return normalizedWorkspacePath(await realpath(workspace));
} catch {
return null;
}
}));
return checked.filter(Boolean);
return [...new Set(checked.filter(Boolean))];
}
async function selectedWorkspacePath(value) {
if (typeof value !== 'string' || !isAbsolute(value.trim())) {
return { error: `工作区必须是绝对路径。\n${SESSION_LIST_USAGE}` };
}
const workspace = normalizedWorkspacePath(value.trim());
if (!workspace) {
return { error: `工作区路径包含不支持的字符或长度超过限制。\n${SESSION_LIST_USAGE}` };
}
let info;
try {
info = await stat(workspace);
} catch {
return { error: `工作区路径不存在。\n${SESSION_LIST_USAGE}` };
}
if (!info.isDirectory()) {
return { error: `工作区路径必须指向一个目录。\n${SESSION_LIST_USAGE}` };
}
try {
const canonical = normalizedWorkspacePath(await realpath(workspace));
return canonical
? { workspace: canonical }
: { error: `工作区路径包含不支持的字符或长度超过限制。\n${SESSION_LIST_USAGE}` };
} catch {
return { error: `工作区路径不存在。\n${SESSION_LIST_USAGE}` };
}
}
async function workspacePathSnapshot(harness) {
const listed = await harness.listWorkspaces();
const currentValue = typeof harness?.currentWorkspace === 'function'
? harness.currentWorkspace()
: null;
const [current] = currentValue ? await existingWorkspacePaths([currentValue]) : [];
const registered = await existingWorkspacePaths(Array.isArray(listed) ? listed : []);
const paths = [...new Set([...(current ? [current] : []), ...registered])];
harness.assertWorkspaceScope?.();
return { current: current ?? null, paths };
}
export function splitWorkspaceCommandMessage(message) {
@ -56,15 +111,7 @@ async function runWorkspaceListCommand(match, harness) {
return commandResult('当前机器人暂不支持列出工作区。');
}
try {
const listed = await harness.listWorkspaces();
const current = typeof harness.currentWorkspace === 'function'
? normalizedWorkspacePath(harness.currentWorkspace())
: null;
const paths = await existingWorkspacePaths([
...(current ? [current] : []),
...(Array.isArray(listed) ? listed : []),
]);
harness.assertWorkspaceScope?.();
const { current, paths } = await workspacePathSnapshot(harness);
if (paths.length === 0) {
return commandResult('当前 Harness Host 上没有仍然存在的已登记工作区。');
}
@ -75,6 +122,7 @@ async function runWorkspaceListCommand(match, harness) {
)),
'',
'切换用法:/workspace 工作区绝对路径',
'查看会话:/sessionlist 工作区序号或绝对路径',
];
const message = lines.join('\n');
return commandResult(message, splitWorkspaceCommandMessage(message));
@ -86,9 +134,80 @@ async function runWorkspaceListCommand(match, harness) {
}
}
async function resolveSessionListWorkspace(selector, harness) {
if (!selector) {
if (typeof harness?.currentWorkspace !== 'function') {
return { error: '当前机器人没有可用的工作区。' };
}
const selected = await selectedWorkspacePath(harness.currentWorkspace());
harness.assertWorkspaceScope?.();
return selected;
}
if (/^\d+$/u.test(selector)) {
if (typeof harness?.listWorkspaces !== 'function') {
return { error: '当前机器人暂不支持按序号选择工作区。' };
}
const { paths } = await workspacePathSnapshot(harness);
const position = Number(selector);
if (!Number.isSafeInteger(position) || position < 1 || position > paths.length) {
return { error: '工作区序号不存在,请先执行 /workspacelist。' };
}
return { workspace: paths[position - 1] };
}
const selected = await selectedWorkspacePath(selector);
harness.assertWorkspaceScope?.();
return selected;
}
function sessionListMessage(workspace, sessions) {
const rows = sessions.map((session) => {
const sessionId = safeDisplayText(session?.sessionId);
if (!sessionId) throw new TypeError('Harness returned an invalid session id');
const title = session?.summaryAvailable === false
? '标题暂不可用'
: safeDisplayText(session?.title) || '暂无标题';
return `${title}${session?.archived === true ? '(已归档)' : ''}\n ID: ${sessionId}`;
});
if (rows.length === 0) return `工作区:${workspace}\n该工作区暂无会话。`;
return [
`工作区:${workspace}`,
`会话(${rows.length}):`,
'',
...rows.map((row, index) => `${index + 1}. ${row}`),
].join('\n');
}
async function runSessionListCommand(match, harness) {
if (typeof harness?.listWorkspaceSessions !== 'function') {
return commandResult('当前机器人暂不支持列出工作区会话。');
}
const selector = match[1]?.trim() ?? '';
try {
const resolved = await resolveSessionListWorkspace(selector, harness);
if (resolved.error) return commandResult(resolved.error);
const listed = await harness.listWorkspaceSessions(resolved.workspace);
if (!listed || !Array.isArray(listed.sessions)) {
throw new TypeError('Harness returned an invalid workspace session list');
}
harness.assertWorkspaceScope?.();
const workspace = normalizedWorkspacePath(listed.workspace) ?? resolved.workspace;
const message = sessionListMessage(workspace, listed.sessions);
return commandResult(message, splitWorkspaceCommandMessage(message));
} catch (error) {
if (error?.code === 'workspace-bot-not-found') {
return commandResult('机器人正在移除或已重新接入,无法列出原会话的工作区会话。');
}
return commandResult('暂时无法获取工作区会话列表,请稍后重试。');
}
}
export async function runWorkspaceCommand(text, harness) {
if (typeof text !== 'string') return null;
const command = text.trim();
const sessionListMatch = SESSION_LIST_COMMAND.exec(command);
if (sessionListMatch) return runSessionListCommand(sessionListMatch, harness);
const listMatch = WORKSPACE_LIST_COMMAND.exec(command);
if (listMatch) return runWorkspaceListCommand(listMatch, harness);
const match = WORKSPACE_COMMAND.exec(command);