mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 02:53:20 +08:00
feat: add workspace list command
This commit is contained in:
parent
c44340accb
commit
1fe6180813
17 changed files with 570 additions and 118 deletions
|
|
@ -15,6 +15,7 @@ const HELP_TEXT = [
|
|||
'直接发送文字即可继续当前会话。',
|
||||
'/new 开启一个全新会话',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
@ -218,7 +219,9 @@ export class DingtalkHarnessBridge {
|
|||
}
|
||||
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
||||
if (workspaceCommand) {
|
||||
await this.#send(sessionWebhook, workspaceCommand.message);
|
||||
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
||||
await this.#send(sessionWebhook, reply);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { spawn } from 'node:child_process';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { isAbsolute } from 'node:path';
|
||||
|
||||
function workspacePaths(value) {
|
||||
if (!Array.isArray(value?.items)) return [];
|
||||
return value.items.flatMap((item) => (
|
||||
typeof item?.path === 'string' && isAbsolute(item.path) ? [item.path] : []
|
||||
));
|
||||
}
|
||||
|
||||
function sleep(ms, signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -216,6 +224,11 @@ export class HarnessClient {
|
|||
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
||||
}
|
||||
|
||||
async listWorkspaces(options = {}) {
|
||||
await this.ensureRunning(options);
|
||||
return workspacePaths(await this.rpc('workspace.list', {}, 30_000, options));
|
||||
}
|
||||
|
||||
async workspaceId(options = {}) {
|
||||
const { workspace = this.#workspace, ...rpcOptions } = options;
|
||||
const { items } = await this.rpc('workspace.list', {}, 30_000, rpcOptions);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const HELP_TEXT = [
|
|||
'直接发送问题即可继续当前会话。',
|
||||
'/new 开启一个全新会话',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
@ -111,7 +112,9 @@ export class FeishuHarnessBridge {
|
|||
}
|
||||
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
||||
if (workspaceCommand) {
|
||||
await this.#send(event.message.chat_id, workspaceCommand.message);
|
||||
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
||||
await this.#send(event.message.chat_id, reply);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import { spawn } from 'node:child_process';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { isAbsolute } from 'node:path';
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
function workspacePaths(value) {
|
||||
if (!Array.isArray(value?.items)) return [];
|
||||
return value.items.flatMap((item) => (
|
||||
typeof item?.path === 'string' && isAbsolute(item.path) ? [item.path] : []
|
||||
));
|
||||
}
|
||||
|
||||
function messageText(event) {
|
||||
return (event?.data?.message?.content ?? [])
|
||||
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
||||
|
|
@ -197,6 +205,11 @@ export class HarnessClient {
|
|||
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
||||
}
|
||||
|
||||
async listWorkspaces(options = {}) {
|
||||
await this.ensureRunning();
|
||||
return workspacePaths(await this.rpc('workspace.list', {}, 30000, options));
|
||||
}
|
||||
|
||||
async workspaceId(options = {}) {
|
||||
const workspace = options.workspace ?? this.#workspace;
|
||||
const { items } = await this.rpc('workspace.list', {});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const HELP_TEXT = [
|
|||
'直接发送文字即可继续当前会话。',
|
||||
'/new 开启一个全新会话',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
@ -127,7 +128,9 @@ export class QqHarnessBridge {
|
|||
}
|
||||
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
||||
if (workspaceCommand) {
|
||||
await this.#bot.sendText(target, workspaceCommand.message);
|
||||
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
||||
await this.#bot.sendText(target, reply);
|
||||
}
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -398,7 +398,41 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|||
const sessionGenerations = new Map();
|
||||
const scopedHarness = new Proxy(harness, {
|
||||
get(target, property) {
|
||||
if (property === 'currentWorkspace') return () => workspaces.workspaceFor(botId);
|
||||
if (property === 'currentWorkspace') {
|
||||
return () => {
|
||||
if (!isCurrentScope()) {
|
||||
const error = new Error('找不到要修改的机器人。');
|
||||
error.code = 'workspace-bot-not-found';
|
||||
throw error;
|
||||
}
|
||||
return workspaces.workspaceFor(botId);
|
||||
};
|
||||
}
|
||||
if (property === 'assertWorkspaceScope') {
|
||||
return () => {
|
||||
if (!isCurrentScope()) {
|
||||
const error = new Error('找不到要修改的机器人。');
|
||||
error.code = 'workspace-bot-not-found';
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
if (property === 'listWorkspaces' && typeof target.listWorkspaces === 'function') {
|
||||
return async (...args) => {
|
||||
if (!isCurrentScope()) {
|
||||
const error = new Error('找不到要修改的机器人。');
|
||||
error.code = 'workspace-bot-not-found';
|
||||
throw error;
|
||||
}
|
||||
const paths = await target.listWorkspaces(...args);
|
||||
if (!isCurrentScope()) {
|
||||
const error = new Error('找不到要修改的机器人。');
|
||||
error.code = 'workspace-bot-not-found';
|
||||
throw error;
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
}
|
||||
if (property === 'switchWorkspace') {
|
||||
return (workspace) => {
|
||||
if (!isCurrentScope()) {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export class TextHarnessBridge {
|
|||
'直接发送文字即可继续当前会话。',
|
||||
'/new 开启一个全新会话',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n'));
|
||||
|
|
@ -115,7 +116,9 @@ export class TextHarnessBridge {
|
|||
}
|
||||
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
||||
if (workspaceCommand) {
|
||||
await this.#bot.sendText(target, workspaceCommand.message);
|
||||
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
||||
await this.#bot.sendText(target, reply);
|
||||
}
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,114 @@
|
|||
import { 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 MAX_WORKSPACE_PATH_LENGTH = 4_096;
|
||||
const MAX_COMMAND_MESSAGE_LENGTH = 1_800;
|
||||
|
||||
function commandResult(message, messages = [message]) {
|
||||
return { handled: true, message, messages };
|
||||
}
|
||||
|
||||
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;
|
||||
return resolve(value);
|
||||
}
|
||||
|
||||
async function existingWorkspacePaths(values) {
|
||||
const unique = [...new Set(values.map(normalizedWorkspacePath).filter(Boolean))];
|
||||
const checked = await Promise.all(unique.map(async (workspace) => {
|
||||
try {
|
||||
return (await stat(workspace)).isDirectory() ? workspace : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
return checked.filter(Boolean);
|
||||
}
|
||||
|
||||
export function splitWorkspaceCommandMessage(message) {
|
||||
const messages = [];
|
||||
let offset = 0;
|
||||
while (offset < message.length) {
|
||||
let end = Math.min(offset + MAX_COMMAND_MESSAGE_LENGTH, message.length);
|
||||
if (end < message.length) {
|
||||
const lineBreak = message.lastIndexOf('\n', end - 1);
|
||||
if (lineBreak >= offset) {
|
||||
end = lineBreak + 1;
|
||||
} else {
|
||||
const trailing = message.charCodeAt(end - 1);
|
||||
const leading = message.charCodeAt(end);
|
||||
if (trailing >= 0xd800 && trailing <= 0xdbff
|
||||
&& leading >= 0xdc00 && leading <= 0xdfff) end -= 1;
|
||||
}
|
||||
}
|
||||
messages.push(message.slice(offset, end));
|
||||
offset = end;
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function runWorkspaceListCommand(match, harness) {
|
||||
if (match[1]?.trim()) return commandResult('用法:/workspacelist');
|
||||
if (typeof harness?.listWorkspaces !== 'function') {
|
||||
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?.();
|
||||
if (paths.length === 0) {
|
||||
return commandResult('当前 Harness Host 上没有仍然存在的已登记工作区。');
|
||||
}
|
||||
const lines = [
|
||||
`当前 Harness Host 上存在的工作区(${paths.length}):`,
|
||||
...paths.map((workspace, index) => (
|
||||
`${index + 1}. ${workspace}${workspace === current ? '(当前)' : ''}`
|
||||
)),
|
||||
'',
|
||||
'切换用法:/workspace 工作区绝对路径',
|
||||
];
|
||||
const message = lines.join('\n');
|
||||
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 match = WORKSPACE_COMMAND.exec(text.trim());
|
||||
const command = text.trim();
|
||||
const listMatch = WORKSPACE_LIST_COMMAND.exec(command);
|
||||
if (listMatch) return runWorkspaceListCommand(listMatch, harness);
|
||||
const match = WORKSPACE_COMMAND.exec(command);
|
||||
if (!match) return null;
|
||||
const workspace = match[1]?.trim();
|
||||
if (!workspace) {
|
||||
return { handled: true, message: '用法:/workspace 工作区绝对路径' };
|
||||
return commandResult('用法:/workspace 工作区绝对路径');
|
||||
}
|
||||
if (typeof harness?.switchWorkspace !== 'function') {
|
||||
return { handled: true, message: '当前机器人暂不支持切换工作区。' };
|
||||
return commandResult('当前机器人暂不支持切换工作区。');
|
||||
}
|
||||
try {
|
||||
const current = await harness.switchWorkspace(workspace);
|
||||
return { handled: true, message: `工作区已切换为:${current}` };
|
||||
return commandResult(`工作区已切换为:${current}`);
|
||||
} catch (error) {
|
||||
if (['workspace-not-absolute', 'workspace-not-found', 'workspace-not-directory'].includes(error?.code)) {
|
||||
return { handled: true, message: `${error.message}\n用法:/workspace 工作区绝对路径` };
|
||||
return commandResult(`${error.message}\n用法:/workspace 工作区绝对路径`);
|
||||
}
|
||||
if (error?.code === 'workspace-bot-not-found') {
|
||||
return { handled: true, message: '机器人正在移除或已重新接入,无法切换原会话的工作区。' };
|
||||
return commandResult('机器人正在移除或已重新接入,无法切换原会话的工作区。');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const HELP_TEXT = [
|
|||
'直接发送文字即可继续当前会话。',
|
||||
'/new 开启一个全新会话',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
@ -187,7 +188,9 @@ export class WecomHarnessBridge {
|
|||
}
|
||||
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
||||
if (workspaceCommand) {
|
||||
await this.#sendImmediate(frame, chatId, workspaceCommand.message);
|
||||
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
||||
await this.#sendImmediate(frame, chatId, reply);
|
||||
}
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import { spawn } from 'node:child_process';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { isAbsolute } from 'node:path';
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
function workspacePaths(value) {
|
||||
if (!Array.isArray(value?.items)) return [];
|
||||
return value.items.flatMap((item) => (
|
||||
typeof item?.path === 'string' && isAbsolute(item.path) ? [item.path] : []
|
||||
));
|
||||
}
|
||||
|
||||
function assistantMessageText(event) {
|
||||
return (event?.data?.message?.content ?? [])
|
||||
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
||||
|
|
@ -187,6 +195,11 @@ export class HarnessClient {
|
|||
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
||||
}
|
||||
|
||||
async listWorkspaces(options = {}) {
|
||||
await this.ensureRunning();
|
||||
return workspacePaths(await this.rpc('workspace.list', {}, 30_000, options));
|
||||
}
|
||||
|
||||
async workspaceId(options = {}) {
|
||||
const workspace = options.workspace ?? this.#workspace;
|
||||
const { items } = await this.rpc('workspace.list', {});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const HELP_TEXT = [
|
|||
'直接发送文字或带文字识别结果的语音即可继续当前会话。',
|
||||
'/new 开启一个全新会话',
|
||||
'/workspace 工作区绝对路径 切换工作区',
|
||||
'/workspacelist 列出工作区绝对路径',
|
||||
'/status 检查连接状态',
|
||||
'/help 显示本帮助',
|
||||
].join('\n');
|
||||
|
|
@ -138,7 +139,9 @@ export class WeixinHarnessBridge {
|
|||
}
|
||||
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
||||
if (workspaceCommand) {
|
||||
await this.#send(sender, workspaceCommand.message, contextToken, runId);
|
||||
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
||||
await this.#send(sender, reply, contextToken, runId);
|
||||
}
|
||||
await this.#state.markSeen(messageId);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue