mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 11:40:45 +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
|
|
@ -16,6 +16,7 @@ const HELP_TEXT = [
|
|||
'/new 开启一个全新会话',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -9,6 +9,47 @@ function workspacePaths(value) {
|
|||
));
|
||||
}
|
||||
|
||||
function workspaceFromList(workspacePath, workspaceList) {
|
||||
if (!Array.isArray(workspaceList?.items)
|
||||
|| !Array.isArray(workspaceList?.archivedSessionIds)) {
|
||||
throw new Error('Harness returned an invalid response for workspace.list');
|
||||
}
|
||||
|
||||
const workspace = workspaceList.items.find((item) => item?.path === workspacePath);
|
||||
if (!workspace) return null;
|
||||
if (!Array.isArray(workspace.sessionIds)
|
||||
|| workspace.sessionIds.some((sessionId) => typeof sessionId !== 'string')) {
|
||||
throw new Error('Harness returned invalid session IDs for workspace.list');
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
function workspaceSessions(workspace, archivedSessionIds, sessionList) {
|
||||
if (!Array.isArray(sessionList?.items)) {
|
||||
throw new Error('Harness returned an invalid response for session.list');
|
||||
}
|
||||
|
||||
const archived = new Set(archivedSessionIds);
|
||||
const summaries = new Map(sessionList.items.flatMap((item) => (
|
||||
typeof item?.sessionId === 'string' ? [[item.sessionId, item]] : []
|
||||
)));
|
||||
return {
|
||||
workspace: workspace.path,
|
||||
sessions: workspace.sessionIds.map((sessionId) => {
|
||||
const summary = summaries.get(sessionId);
|
||||
const title = summary?.projections?.values?.title;
|
||||
return {
|
||||
sessionId,
|
||||
title: typeof title === 'string' ? title : null,
|
||||
archived: archived.has(sessionId),
|
||||
blank: summary?.blank === true,
|
||||
origin: summary?.origin === 'subagent' ? 'subagent' : null,
|
||||
summaryAvailable: summary !== undefined,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function sleep(ms, signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (signal?.aborted) {
|
||||
|
|
@ -229,6 +270,15 @@ export class HarnessClient {
|
|||
return workspacePaths(await this.rpc('workspace.list', {}, 30_000, options));
|
||||
}
|
||||
|
||||
async listWorkspaceSessions(workspacePath, options = {}) {
|
||||
await this.ensureRunning(options);
|
||||
const workspaceList = await this.rpc('workspace.list', {}, 30_000, options);
|
||||
const workspace = workspaceFromList(workspacePath, workspaceList);
|
||||
if (!workspace) return { workspace: workspacePath, sessions: [] };
|
||||
const sessionList = await this.rpc('session.list', {}, 30_000, options);
|
||||
return workspaceSessions(workspace, workspaceList.archivedSessionIds, sessionList);
|
||||
}
|
||||
|
||||
async workspaceId(options = {}) {
|
||||
const { workspace = this.#workspace, ...rpcOptions } = options;
|
||||
const { items } = await this.rpc('workspace.list', {}, 30_000, rpcOptions);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const HELP_TEXT = [
|
|||
'/new 开启一个全新会话',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -11,6 +11,47 @@ function workspacePaths(value) {
|
|||
));
|
||||
}
|
||||
|
||||
function workspaceFromList(workspacePath, workspaceList) {
|
||||
if (!Array.isArray(workspaceList?.items)
|
||||
|| !Array.isArray(workspaceList?.archivedSessionIds)) {
|
||||
throw new Error('Harness returned an invalid response for workspace.list');
|
||||
}
|
||||
|
||||
const workspace = workspaceList.items.find((item) => item?.path === workspacePath);
|
||||
if (!workspace) return null;
|
||||
if (!Array.isArray(workspace.sessionIds)
|
||||
|| workspace.sessionIds.some((sessionId) => typeof sessionId !== 'string')) {
|
||||
throw new Error('Harness returned invalid session IDs for workspace.list');
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
function workspaceSessions(workspace, archivedSessionIds, sessionList) {
|
||||
if (!Array.isArray(sessionList?.items)) {
|
||||
throw new Error('Harness returned an invalid response for session.list');
|
||||
}
|
||||
|
||||
const archived = new Set(archivedSessionIds);
|
||||
const summaries = new Map(sessionList.items.flatMap((item) => (
|
||||
typeof item?.sessionId === 'string' ? [[item.sessionId, item]] : []
|
||||
)));
|
||||
return {
|
||||
workspace: workspace.path,
|
||||
sessions: workspace.sessionIds.map((sessionId) => {
|
||||
const summary = summaries.get(sessionId);
|
||||
const title = summary?.projections?.values?.title;
|
||||
return {
|
||||
sessionId,
|
||||
title: typeof title === 'string' ? title : null,
|
||||
archived: archived.has(sessionId),
|
||||
blank: summary?.blank === true,
|
||||
origin: summary?.origin === 'subagent' ? 'subagent' : null,
|
||||
summaryAvailable: summary !== undefined,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function messageText(event) {
|
||||
return (event?.data?.message?.content ?? [])
|
||||
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
||||
|
|
@ -210,6 +251,15 @@ export class HarnessClient {
|
|||
return workspacePaths(await this.rpc('workspace.list', {}, 30000, options));
|
||||
}
|
||||
|
||||
async listWorkspaceSessions(workspacePath, options = {}) {
|
||||
await this.ensureRunning();
|
||||
const workspaceList = await this.rpc('workspace.list', {}, 30000, options);
|
||||
const workspace = workspaceFromList(workspacePath, workspaceList);
|
||||
if (!workspace) return { workspace: workspacePath, sessions: [] };
|
||||
const sessionList = await this.rpc('session.list', {}, 30000, options);
|
||||
return workspaceSessions(workspace, workspaceList.archivedSessionIds, sessionList);
|
||||
}
|
||||
|
||||
async workspaceId(options = {}) {
|
||||
const workspace = options.workspace ?? this.#workspace;
|
||||
const { items } = await this.rpc('workspace.list', {});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const HELP_TEXT = [
|
|||
'/new 开启一个全新会话',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -417,20 +417,21 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|||
}
|
||||
};
|
||||
}
|
||||
if (property === 'listWorkspaces' && typeof target.listWorkspaces === 'function') {
|
||||
if ((property === 'listWorkspaces' || property === 'listWorkspaceSessions')
|
||||
&& typeof target[property] === 'function') {
|
||||
return async (...args) => {
|
||||
if (!isCurrentScope()) {
|
||||
const error = new Error('找不到要修改的机器人。');
|
||||
error.code = 'workspace-bot-not-found';
|
||||
throw error;
|
||||
}
|
||||
const paths = await target.listWorkspaces(...args);
|
||||
const result = await target[property](...args);
|
||||
if (!isCurrentScope()) {
|
||||
const error = new Error('找不到要修改的机器人。');
|
||||
error.code = 'workspace-bot-not-found';
|
||||
throw error;
|
||||
}
|
||||
return paths;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
if (property === 'switchWorkspace') {
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export class TextHarnessBridge {
|
|||
'/new 开启一个全新会话',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n'));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const HELP_TEXT = [
|
|||
'/new 开启一个全新会话',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -11,6 +11,47 @@ function workspacePaths(value) {
|
|||
));
|
||||
}
|
||||
|
||||
function workspaceFromList(workspacePath, workspaceList) {
|
||||
if (!Array.isArray(workspaceList?.items)
|
||||
|| !Array.isArray(workspaceList?.archivedSessionIds)) {
|
||||
throw new Error('Harness returned an invalid response for workspace.list');
|
||||
}
|
||||
|
||||
const workspace = workspaceList.items.find((item) => item?.path === workspacePath);
|
||||
if (!workspace) return null;
|
||||
if (!Array.isArray(workspace.sessionIds)
|
||||
|| workspace.sessionIds.some((sessionId) => typeof sessionId !== 'string')) {
|
||||
throw new Error('Harness returned invalid session IDs for workspace.list');
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
function workspaceSessions(workspace, archivedSessionIds, sessionList) {
|
||||
if (!Array.isArray(sessionList?.items)) {
|
||||
throw new Error('Harness returned an invalid response for session.list');
|
||||
}
|
||||
|
||||
const archived = new Set(archivedSessionIds);
|
||||
const summaries = new Map(sessionList.items.flatMap((item) => (
|
||||
typeof item?.sessionId === 'string' ? [[item.sessionId, item]] : []
|
||||
)));
|
||||
return {
|
||||
workspace: workspace.path,
|
||||
sessions: workspace.sessionIds.map((sessionId) => {
|
||||
const summary = summaries.get(sessionId);
|
||||
const title = summary?.projections?.values?.title;
|
||||
return {
|
||||
sessionId,
|
||||
title: typeof title === 'string' ? title : null,
|
||||
archived: archived.has(sessionId),
|
||||
blank: summary?.blank === true,
|
||||
origin: summary?.origin === 'subagent' ? 'subagent' : null,
|
||||
summaryAvailable: summary !== undefined,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function assistantMessageText(event) {
|
||||
return (event?.data?.message?.content ?? [])
|
||||
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
||||
|
|
@ -200,6 +241,15 @@ export class HarnessClient {
|
|||
return workspacePaths(await this.rpc('workspace.list', {}, 30_000, options));
|
||||
}
|
||||
|
||||
async listWorkspaceSessions(workspacePath, options = {}) {
|
||||
await this.ensureRunning();
|
||||
const workspaceList = await this.rpc('workspace.list', {}, 30_000, options);
|
||||
const workspace = workspaceFromList(workspacePath, workspaceList);
|
||||
if (!workspace) return { workspace: workspacePath, sessions: [] };
|
||||
const sessionList = await this.rpc('session.list', {}, 30_000, options);
|
||||
return workspaceSessions(workspace, workspaceList.archivedSessionIds, sessionList);
|
||||
}
|
||||
|
||||
async workspaceId(options = {}) {
|
||||
const workspace = options.workspace ?? this.#workspace;
|
||||
const { items } = await this.rpc('workspace.list', {});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const HELP_TEXT = [
|
|||
'/new 开启一个全新会话',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue