@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "通过扫码、App Manifest或机器人凭据把IM机器人接入DeepSeek Harness(支持飞书、微信、钉钉、企业微信、QQ、Slack、Telegram、Discord和WhatsApp)。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- await this.#send(sessionWebhook, workspaceCommand.message);
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.5.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
- await this.#send(event.message.chat_id, workspaceCommand.message);
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
- await this.#bot.sendText(target, workspaceCommand.message);
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') return () => workspaces.workspaceFor(botId);
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
- await this.#bot.sendText(target, workspaceCommand.message);
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 match = WORKSPACE_COMMAND.exec(text.trim());
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 { handled: true, message: '用法:/workspace 工作区绝对路径' };
98
+ return commandResult('用法:/workspace 工作区绝对路径');
10
99
  }
11
100
  if (typeof harness?.switchWorkspace !== 'function') {
12
- return { handled: true, message: '当前机器人暂不支持切换工作区。' };
101
+ return commandResult('当前机器人暂不支持切换工作区。');
13
102
  }
14
103
  try {
15
104
  const current = await harness.switchWorkspace(workspace);
16
- return { handled: true, message: `工作区已切换为:${current}` };
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 { handled: true, message: `${error.message}\n用法:/workspace 工作区绝对路径` };
108
+ return commandResult(`${error.message}\n用法:/workspace 工作区绝对路径`);
20
109
  }
21
110
  if (error?.code === 'workspace-bot-not-found') {
22
- return { handled: true, message: '机器人正在移除或已重新接入,无法切换原会话的工作区。' };
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
- await this.#sendImmediate(frame, chatId, workspaceCommand.message);
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', {});
@@ -92,7 +92,7 @@ function authenticatedHeaders(token) {
92
92
  function baseInfo() {
93
93
  return {
94
94
  channel_version: WEIXIN_PROTOCOL_VERSION,
95
- bot_agent: 'DeepSeekHarness/0.5.0',
95
+ bot_agent: 'DeepSeekHarness/0.6.0',
96
96
  };
97
97
  }
98
98
 
@@ -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
- await this.#send(sender, workspaceCommand.message, contextToken, runId);
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
  }