mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 22:50:47 +08:00
feat: add workspace session list command
This commit is contained in:
parent
c51334ebc3
commit
28381468b3
17 changed files with 989 additions and 122 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue