@xmanrui/dsh-im 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/lib/index.js +105 -103
- package/package.json +1 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +4 -1
- package/src/channels/dingtalk/harness-client.mjs +13 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +4 -1
- package/src/channels/feishu/harness-client.mjs +13 -0
- package/src/channels/qq/qq-bridge.mjs +4 -1
- package/src/channels/shared/bot-workspace-store.mjs +35 -1
- package/src/channels/shared/text-harness-bridge.mjs +4 -1
- package/src/channels/shared/workspace-command.mjs +95 -6
- package/src/channels/wecom/wecom-bridge.mjs +4 -1
- package/src/channels/weixin/harness-client.mjs +13 -0
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +4 -1
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@ const HELP_TEXT = [
|
|
|
15
15
|
'直接发送文字即可继续当前会话。',
|
|
16
16
|
'/new 开启一个全新会话',
|
|
17
17
|
'/workspace 工作区绝对路径 切换工作区',
|
|
18
|
+
'/workspacelist 列出工作区绝对路径',
|
|
18
19
|
'/status 检查连接状态',
|
|
19
20
|
'/help 显示本帮助',
|
|
20
21
|
].join('\n');
|
|
@@ -218,7 +219,9 @@ export class DingtalkHarnessBridge {
|
|
|
218
219
|
}
|
|
219
220
|
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
220
221
|
if (workspaceCommand) {
|
|
221
|
-
|
|
222
|
+
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
223
|
+
await this.#send(sessionWebhook, reply);
|
|
224
|
+
}
|
|
222
225
|
return;
|
|
223
226
|
}
|
|
224
227
|
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
4
|
+
|
|
5
|
+
function workspacePaths(value) {
|
|
6
|
+
if (!Array.isArray(value?.items)) return [];
|
|
7
|
+
return value.items.flatMap((item) => (
|
|
8
|
+
typeof item?.path === 'string' && isAbsolute(item.path) ? [item.path] : []
|
|
9
|
+
));
|
|
10
|
+
}
|
|
3
11
|
|
|
4
12
|
function sleep(ms, signal) {
|
|
5
13
|
return new Promise((resolve, reject) => {
|
|
@@ -216,6 +224,11 @@ export class HarnessClient {
|
|
|
216
224
|
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
|
217
225
|
}
|
|
218
226
|
|
|
227
|
+
async listWorkspaces(options = {}) {
|
|
228
|
+
await this.ensureRunning(options);
|
|
229
|
+
return workspacePaths(await this.rpc('workspace.list', {}, 30_000, options));
|
|
230
|
+
}
|
|
231
|
+
|
|
219
232
|
async workspaceId(options = {}) {
|
|
220
233
|
const { workspace = this.#workspace, ...rpcOptions } = options;
|
|
221
234
|
const { items } = await this.rpc('workspace.list', {}, 30_000, rpcOptions);
|
|
@@ -108,7 +108,7 @@ export class DiscordApi {
|
|
|
108
108
|
headers: {
|
|
109
109
|
authorization: `Bot ${this.#token}`,
|
|
110
110
|
'content-type': 'application/json',
|
|
111
|
-
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.
|
|
111
|
+
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.6.0)',
|
|
112
112
|
},
|
|
113
113
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
114
114
|
signal: requestSignal(signal, timeoutMs),
|
|
@@ -14,6 +14,7 @@ const HELP_TEXT = [
|
|
|
14
14
|
'直接发送问题即可继续当前会话。',
|
|
15
15
|
'/new 开启一个全新会话',
|
|
16
16
|
'/workspace 工作区绝对路径 切换工作区',
|
|
17
|
+
'/workspacelist 列出工作区绝对路径',
|
|
17
18
|
'/status 检查连接状态',
|
|
18
19
|
'/help 显示本帮助',
|
|
19
20
|
].join('\n');
|
|
@@ -111,7 +112,9 @@ export class FeishuHarnessBridge {
|
|
|
111
112
|
}
|
|
112
113
|
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
113
114
|
if (workspaceCommand) {
|
|
114
|
-
|
|
115
|
+
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
116
|
+
await this.#send(event.message.chat_id, reply);
|
|
117
|
+
}
|
|
115
118
|
return;
|
|
116
119
|
}
|
|
117
120
|
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
3
4
|
|
|
4
5
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
6
|
|
|
7
|
+
function workspacePaths(value) {
|
|
8
|
+
if (!Array.isArray(value?.items)) return [];
|
|
9
|
+
return value.items.flatMap((item) => (
|
|
10
|
+
typeof item?.path === 'string' && isAbsolute(item.path) ? [item.path] : []
|
|
11
|
+
));
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
function messageText(event) {
|
|
7
15
|
return (event?.data?.message?.content ?? [])
|
|
8
16
|
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
|
@@ -197,6 +205,11 @@ export class HarnessClient {
|
|
|
197
205
|
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
|
198
206
|
}
|
|
199
207
|
|
|
208
|
+
async listWorkspaces(options = {}) {
|
|
209
|
+
await this.ensureRunning();
|
|
210
|
+
return workspacePaths(await this.rpc('workspace.list', {}, 30000, options));
|
|
211
|
+
}
|
|
212
|
+
|
|
200
213
|
async workspaceId(options = {}) {
|
|
201
214
|
const workspace = options.workspace ?? this.#workspace;
|
|
202
215
|
const { items } = await this.rpc('workspace.list', {});
|
|
@@ -7,6 +7,7 @@ const HELP_TEXT = [
|
|
|
7
7
|
'直接发送文字即可继续当前会话。',
|
|
8
8
|
'/new 开启一个全新会话',
|
|
9
9
|
'/workspace 工作区绝对路径 切换工作区',
|
|
10
|
+
'/workspacelist 列出工作区绝对路径',
|
|
10
11
|
'/status 检查连接状态',
|
|
11
12
|
'/help 显示本帮助',
|
|
12
13
|
].join('\n');
|
|
@@ -127,7 +128,9 @@ export class QqHarnessBridge {
|
|
|
127
128
|
}
|
|
128
129
|
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
129
130
|
if (workspaceCommand) {
|
|
130
|
-
|
|
131
|
+
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
132
|
+
await this.#bot.sendText(target, reply);
|
|
133
|
+
}
|
|
131
134
|
await this.#state.markSeen(messageId);
|
|
132
135
|
return;
|
|
133
136
|
}
|
|
@@ -398,7 +398,41 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|
|
398
398
|
const sessionGenerations = new Map();
|
|
399
399
|
const scopedHarness = new Proxy(harness, {
|
|
400
400
|
get(target, property) {
|
|
401
|
-
if (property === 'currentWorkspace')
|
|
401
|
+
if (property === 'currentWorkspace') {
|
|
402
|
+
return () => {
|
|
403
|
+
if (!isCurrentScope()) {
|
|
404
|
+
const error = new Error('找不到要修改的机器人。');
|
|
405
|
+
error.code = 'workspace-bot-not-found';
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
return workspaces.workspaceFor(botId);
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (property === 'assertWorkspaceScope') {
|
|
412
|
+
return () => {
|
|
413
|
+
if (!isCurrentScope()) {
|
|
414
|
+
const error = new Error('找不到要修改的机器人。');
|
|
415
|
+
error.code = 'workspace-bot-not-found';
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (property === 'listWorkspaces' && typeof target.listWorkspaces === 'function') {
|
|
421
|
+
return async (...args) => {
|
|
422
|
+
if (!isCurrentScope()) {
|
|
423
|
+
const error = new Error('找不到要修改的机器人。');
|
|
424
|
+
error.code = 'workspace-bot-not-found';
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
const paths = await target.listWorkspaces(...args);
|
|
428
|
+
if (!isCurrentScope()) {
|
|
429
|
+
const error = new Error('找不到要修改的机器人。');
|
|
430
|
+
error.code = 'workspace-bot-not-found';
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
return paths;
|
|
434
|
+
};
|
|
435
|
+
}
|
|
402
436
|
if (property === 'switchWorkspace') {
|
|
403
437
|
return (workspace) => {
|
|
404
438
|
if (!isCurrentScope()) {
|
|
@@ -101,6 +101,7 @@ export class TextHarnessBridge {
|
|
|
101
101
|
'直接发送文字即可继续当前会话。',
|
|
102
102
|
'/new 开启一个全新会话',
|
|
103
103
|
'/workspace 工作区绝对路径 切换工作区',
|
|
104
|
+
'/workspacelist 列出工作区绝对路径',
|
|
104
105
|
'/status 检查连接状态',
|
|
105
106
|
'/help 显示本帮助',
|
|
106
107
|
].join('\n'));
|
|
@@ -115,7 +116,9 @@ export class TextHarnessBridge {
|
|
|
115
116
|
}
|
|
116
117
|
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
117
118
|
if (workspaceCommand) {
|
|
118
|
-
|
|
119
|
+
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
120
|
+
await this.#bot.sendText(target, reply);
|
|
121
|
+
}
|
|
119
122
|
await this.#state.markSeen(messageId);
|
|
120
123
|
return;
|
|
121
124
|
}
|
|
@@ -1,25 +1,114 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
|
|
1
4
|
const WORKSPACE_COMMAND = /^\/workspace(?:\s+([\s\S]+))?$/i;
|
|
5
|
+
const WORKSPACE_LIST_COMMAND = /^\/workspacelist(?:\s+([\s\S]+))?$/i;
|
|
6
|
+
const MAX_WORKSPACE_PATH_LENGTH = 4_096;
|
|
7
|
+
const MAX_COMMAND_MESSAGE_LENGTH = 1_800;
|
|
8
|
+
|
|
9
|
+
function commandResult(message, messages = [message]) {
|
|
10
|
+
return { handled: true, message, messages };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizedWorkspacePath(value) {
|
|
14
|
+
if (typeof value !== 'string' || value.length > MAX_WORKSPACE_PATH_LENGTH
|
|
15
|
+
|| !isAbsolute(value) || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value)) return null;
|
|
16
|
+
return resolve(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function existingWorkspacePaths(values) {
|
|
20
|
+
const unique = [...new Set(values.map(normalizedWorkspacePath).filter(Boolean))];
|
|
21
|
+
const checked = await Promise.all(unique.map(async (workspace) => {
|
|
22
|
+
try {
|
|
23
|
+
return (await stat(workspace)).isDirectory() ? workspace : null;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}));
|
|
28
|
+
return checked.filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function splitWorkspaceCommandMessage(message) {
|
|
32
|
+
const messages = [];
|
|
33
|
+
let offset = 0;
|
|
34
|
+
while (offset < message.length) {
|
|
35
|
+
let end = Math.min(offset + MAX_COMMAND_MESSAGE_LENGTH, message.length);
|
|
36
|
+
if (end < message.length) {
|
|
37
|
+
const lineBreak = message.lastIndexOf('\n', end - 1);
|
|
38
|
+
if (lineBreak >= offset) {
|
|
39
|
+
end = lineBreak + 1;
|
|
40
|
+
} else {
|
|
41
|
+
const trailing = message.charCodeAt(end - 1);
|
|
42
|
+
const leading = message.charCodeAt(end);
|
|
43
|
+
if (trailing >= 0xd800 && trailing <= 0xdbff
|
|
44
|
+
&& leading >= 0xdc00 && leading <= 0xdfff) end -= 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
messages.push(message.slice(offset, end));
|
|
48
|
+
offset = end;
|
|
49
|
+
}
|
|
50
|
+
return messages;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runWorkspaceListCommand(match, harness) {
|
|
54
|
+
if (match[1]?.trim()) return commandResult('用法:/workspacelist');
|
|
55
|
+
if (typeof harness?.listWorkspaces !== 'function') {
|
|
56
|
+
return commandResult('当前机器人暂不支持列出工作区。');
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const listed = await harness.listWorkspaces();
|
|
60
|
+
const current = typeof harness.currentWorkspace === 'function'
|
|
61
|
+
? normalizedWorkspacePath(harness.currentWorkspace())
|
|
62
|
+
: null;
|
|
63
|
+
const paths = await existingWorkspacePaths([
|
|
64
|
+
...(current ? [current] : []),
|
|
65
|
+
...(Array.isArray(listed) ? listed : []),
|
|
66
|
+
]);
|
|
67
|
+
harness.assertWorkspaceScope?.();
|
|
68
|
+
if (paths.length === 0) {
|
|
69
|
+
return commandResult('当前 Harness Host 上没有仍然存在的已登记工作区。');
|
|
70
|
+
}
|
|
71
|
+
const lines = [
|
|
72
|
+
`当前 Harness Host 上存在的工作区(${paths.length}):`,
|
|
73
|
+
...paths.map((workspace, index) => (
|
|
74
|
+
`${index + 1}. ${workspace}${workspace === current ? '(当前)' : ''}`
|
|
75
|
+
)),
|
|
76
|
+
'',
|
|
77
|
+
'切换用法:/workspace 工作区绝对路径',
|
|
78
|
+
];
|
|
79
|
+
const message = lines.join('\n');
|
|
80
|
+
return commandResult(message, splitWorkspaceCommandMessage(message));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error?.code === 'workspace-bot-not-found') {
|
|
83
|
+
return commandResult('机器人正在移除或已重新接入,无法列出原会话的工作区。');
|
|
84
|
+
}
|
|
85
|
+
return commandResult('暂时无法获取工作区列表,请稍后重试。');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
2
88
|
|
|
3
89
|
export async function runWorkspaceCommand(text, harness) {
|
|
4
90
|
if (typeof text !== 'string') return null;
|
|
5
|
-
const
|
|
91
|
+
const command = text.trim();
|
|
92
|
+
const listMatch = WORKSPACE_LIST_COMMAND.exec(command);
|
|
93
|
+
if (listMatch) return runWorkspaceListCommand(listMatch, harness);
|
|
94
|
+
const match = WORKSPACE_COMMAND.exec(command);
|
|
6
95
|
if (!match) return null;
|
|
7
96
|
const workspace = match[1]?.trim();
|
|
8
97
|
if (!workspace) {
|
|
9
|
-
return
|
|
98
|
+
return commandResult('用法:/workspace 工作区绝对路径');
|
|
10
99
|
}
|
|
11
100
|
if (typeof harness?.switchWorkspace !== 'function') {
|
|
12
|
-
return
|
|
101
|
+
return commandResult('当前机器人暂不支持切换工作区。');
|
|
13
102
|
}
|
|
14
103
|
try {
|
|
15
104
|
const current = await harness.switchWorkspace(workspace);
|
|
16
|
-
return
|
|
105
|
+
return commandResult(`工作区已切换为:${current}`);
|
|
17
106
|
} catch (error) {
|
|
18
107
|
if (['workspace-not-absolute', 'workspace-not-found', 'workspace-not-directory'].includes(error?.code)) {
|
|
19
|
-
return
|
|
108
|
+
return commandResult(`${error.message}\n用法:/workspace 工作区绝对路径`);
|
|
20
109
|
}
|
|
21
110
|
if (error?.code === 'workspace-bot-not-found') {
|
|
22
|
-
return
|
|
111
|
+
return commandResult('机器人正在移除或已重新接入,无法切换原会话的工作区。');
|
|
23
112
|
}
|
|
24
113
|
throw error;
|
|
25
114
|
}
|
|
@@ -8,6 +8,7 @@ const HELP_TEXT = [
|
|
|
8
8
|
'直接发送文字即可继续当前会话。',
|
|
9
9
|
'/new 开启一个全新会话',
|
|
10
10
|
'/workspace 工作区绝对路径 切换工作区',
|
|
11
|
+
'/workspacelist 列出工作区绝对路径',
|
|
11
12
|
'/status 检查连接状态',
|
|
12
13
|
'/help 显示本帮助',
|
|
13
14
|
].join('\n');
|
|
@@ -187,7 +188,9 @@ export class WecomHarnessBridge {
|
|
|
187
188
|
}
|
|
188
189
|
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
189
190
|
if (workspaceCommand) {
|
|
190
|
-
|
|
191
|
+
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
192
|
+
await this.#sendImmediate(frame, chatId, reply);
|
|
193
|
+
}
|
|
191
194
|
await this.#state.markSeen(messageId);
|
|
192
195
|
return;
|
|
193
196
|
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
3
4
|
|
|
4
5
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
6
|
|
|
7
|
+
function workspacePaths(value) {
|
|
8
|
+
if (!Array.isArray(value?.items)) return [];
|
|
9
|
+
return value.items.flatMap((item) => (
|
|
10
|
+
typeof item?.path === 'string' && isAbsolute(item.path) ? [item.path] : []
|
|
11
|
+
));
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
function assistantMessageText(event) {
|
|
7
15
|
return (event?.data?.message?.content ?? [])
|
|
8
16
|
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
|
@@ -187,6 +195,11 @@ export class HarnessClient {
|
|
|
187
195
|
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
|
188
196
|
}
|
|
189
197
|
|
|
198
|
+
async listWorkspaces(options = {}) {
|
|
199
|
+
await this.ensureRunning();
|
|
200
|
+
return workspacePaths(await this.rpc('workspace.list', {}, 30_000, options));
|
|
201
|
+
}
|
|
202
|
+
|
|
190
203
|
async workspaceId(options = {}) {
|
|
191
204
|
const workspace = options.workspace ?? this.#workspace;
|
|
192
205
|
const { items } = await this.rpc('workspace.list', {});
|
|
@@ -12,6 +12,7 @@ const HELP_TEXT = [
|
|
|
12
12
|
'直接发送文字或带文字识别结果的语音即可继续当前会话。',
|
|
13
13
|
'/new 开启一个全新会话',
|
|
14
14
|
'/workspace 工作区绝对路径 切换工作区',
|
|
15
|
+
'/workspacelist 列出工作区绝对路径',
|
|
15
16
|
'/status 检查连接状态',
|
|
16
17
|
'/help 显示本帮助',
|
|
17
18
|
].join('\n');
|
|
@@ -138,7 +139,9 @@ export class WeixinHarnessBridge {
|
|
|
138
139
|
}
|
|
139
140
|
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
140
141
|
if (workspaceCommand) {
|
|
141
|
-
|
|
142
|
+
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
143
|
+
await this.#send(sender, reply, contextToken, runId);
|
|
144
|
+
}
|
|
142
145
|
await this.#state.markSeen(messageId);
|
|
143
146
|
return;
|
|
144
147
|
}
|