@xmanrui/dsh-im 0.3.0 → 0.5.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.
Files changed (68) hide show
  1. package/README.md +64 -0
  2. package/assets/logo-icon.png +0 -0
  3. package/assets/logo.png +0 -0
  4. package/lib/client.js +765 -295
  5. package/lib/index.js +112 -110
  6. package/package.json +2 -1
  7. package/plugin-src/client/build.mjs +6 -1
  8. package/plugin-src/client/channels/dingtalk/api.js +2 -0
  9. package/plugin-src/client/channels/dingtalk/index.js +67 -14
  10. package/plugin-src/client/channels/feishu/api.js +2 -0
  11. package/plugin-src/client/channels/feishu/index.js +70 -22
  12. package/plugin-src/client/channels/qq/api.js +2 -0
  13. package/plugin-src/client/channels/qq/index.js +45 -8
  14. package/plugin-src/client/channels/shared/token-api.js +2 -0
  15. package/plugin-src/client/channels/shared/token-channel.js +34 -8
  16. package/plugin-src/client/channels/wecom/api.js +2 -0
  17. package/plugin-src/client/channels/wecom/index.js +45 -8
  18. package/plugin-src/client/channels/weixin/api.js +2 -0
  19. package/plugin-src/client/channels/weixin/index.js +68 -8
  20. package/plugin-src/client/channels/whatsapp/api.js +2 -0
  21. package/plugin-src/client/channels/whatsapp/index.js +27 -5
  22. package/plugin-src/client/i18n.js +15 -1
  23. package/plugin-src/client/index.js +28 -1
  24. package/plugin-src/client/styles.js +32 -9
  25. package/plugin-src/client/workspace-editor.js +96 -0
  26. package/plugin-src/client/workspace-snapshot-fence.js +28 -0
  27. package/plugin-src/host/channels/dingtalk/production.mjs +34 -11
  28. package/plugin-src/host/channels/dingtalk/rpc.mjs +17 -2
  29. package/plugin-src/host/channels/feishu/production.mjs +42 -5
  30. package/plugin-src/host/channels/feishu/rpc.mjs +17 -2
  31. package/plugin-src/host/channels/qq/production.mjs +48 -22
  32. package/plugin-src/host/channels/qq/rpc.mjs +16 -2
  33. package/plugin-src/host/channels/shared/production.mjs +48 -22
  34. package/plugin-src/host/channels/shared/rpc.mjs +15 -0
  35. package/plugin-src/host/channels/shared/workspace-rpc.mjs +25 -0
  36. package/plugin-src/host/channels/slack/production.mjs +48 -23
  37. package/plugin-src/host/channels/slack/rpc.mjs +16 -0
  38. package/plugin-src/host/channels/wecom/production.mjs +49 -23
  39. package/plugin-src/host/channels/wecom/rpc.mjs +16 -2
  40. package/plugin-src/host/channels/weixin/production.mjs +31 -11
  41. package/plugin-src/host/channels/weixin/rpc.mjs +21 -2
  42. package/plugin-src/host/channels/whatsapp/production.mjs +49 -23
  43. package/plugin-src/host/channels/whatsapp/rpc.mjs +16 -2
  44. package/src/channels/dingtalk/dingtalk-bridge.mjs +22 -11
  45. package/src/channels/dingtalk/dingtalk-controller.mjs +6 -1
  46. package/src/channels/dingtalk/harness-client.mjs +4 -3
  47. package/src/channels/dingtalk/state-store.mjs +5 -0
  48. package/src/channels/discord/discord-api.mjs +1 -1
  49. package/src/channels/feishu/bridge.mjs +36 -16
  50. package/src/channels/feishu/harness-client.mjs +6 -5
  51. package/src/channels/feishu/state-store.mjs +5 -0
  52. package/src/channels/qq/qq-bridge.mjs +25 -15
  53. package/src/channels/qq/qq-controller.mjs +8 -1
  54. package/src/channels/qq/state-store.mjs +5 -0
  55. package/src/channels/shared/bot-workspace-store.mjs +584 -0
  56. package/src/channels/shared/conversation-state-store.mjs +5 -0
  57. package/src/channels/shared/text-harness-bridge.mjs +23 -13
  58. package/src/channels/shared/workspace-command.mjs +26 -0
  59. package/src/channels/shared/workspace-session.mjs +44 -0
  60. package/src/channels/wecom/state-store.mjs +5 -0
  61. package/src/channels/wecom/wecom-bridge.mjs +22 -13
  62. package/src/channels/wecom/wecom-controller.mjs +8 -1
  63. package/src/channels/weixin/harness-client.mjs +6 -5
  64. package/src/channels/weixin/state-store.mjs +5 -0
  65. package/src/channels/weixin/weixin-api.mjs +1 -1
  66. package/src/channels/weixin/weixin-bridge.mjs +16 -6
  67. package/src/channels/weixin/weixin-controller.mjs +6 -1
  68. package/src/channels/whatsapp/whatsapp-controller.mjs +8 -1
@@ -1,3 +1,6 @@
1
+ import { runWorkspaceCommand } from './workspace-command.mjs';
2
+ import { askInWorkspaceSession } from './workspace-session.mjs';
3
+
1
4
  function cleanText(value) {
2
5
  return typeof value === 'string' ? value.trim() : '';
3
6
  }
@@ -97,6 +100,7 @@ export class TextHarnessBridge {
97
100
  '',
98
101
  '直接发送文字即可继续当前会话。',
99
102
  '/new 开启一个全新会话',
103
+ '/workspace 工作区绝对路径 切换工作区',
100
104
  '/status 检查连接状态',
101
105
  '/help 显示本帮助',
102
106
  ].join('\n'));
@@ -109,6 +113,12 @@ export class TextHarnessBridge {
109
113
  await this.#state.markSeen(messageId);
110
114
  return;
111
115
  }
116
+ const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
117
+ if (workspaceCommand) {
118
+ await this.#bot.sendText(target, workspaceCommand.message);
119
+ await this.#state.markSeen(messageId);
120
+ return;
121
+ }
112
122
  const conversationKey = `${message.kind}:${message.conversationId}`;
113
123
  if (command === '/new') {
114
124
  await this.#state.clearSession(conversationKey);
@@ -117,12 +127,6 @@ export class TextHarnessBridge {
117
127
  return;
118
128
  }
119
129
 
120
- let sessionId = this.#state.sessionFor(conversationKey);
121
- if (!sessionId || !(await this.#harness.sessionExists(sessionId))) {
122
- sessionId = await this.#harness.createSession();
123
- await this.#state.setSession(conversationKey, sessionId);
124
- }
125
-
126
130
  await this.#bot.sendTyping?.(target).catch((error) => {
127
131
  this.#logger.warn?.(`[dsh-im:${this.#descriptor.key}] typing indicator failed:`, error);
128
132
  });
@@ -138,13 +142,19 @@ export class TextHarnessBridge {
138
142
  );
139
143
  }
140
144
  }
141
- const answer = await this.#harness.ask(sessionId, text, {
142
- timeoutMs: this.#replyTimeoutMs,
143
- onUpdate: stream ? async (update) => {
144
- const progress = update.type === 'text' ? update.text
145
- : update.type === 'tool' ? `正在使用${update.name}…` : update.text;
146
- if (progress) await stream.update(progress);
147
- } : undefined,
145
+ const { answer } = await askInWorkspaceSession({
146
+ harness: this.#harness,
147
+ state: this.#state,
148
+ key: conversationKey,
149
+ text,
150
+ askOptions: {
151
+ timeoutMs: this.#replyTimeoutMs,
152
+ onUpdate: stream ? async (update) => {
153
+ const progress = update.type === 'text' ? update.text
154
+ : update.type === 'tool' ? `正在使用${update.name}…` : update.text;
155
+ if (progress) await stream.update(progress);
156
+ } : undefined,
157
+ },
148
158
  });
149
159
  if (stream) {
150
160
  try {
@@ -0,0 +1,26 @@
1
+ const WORKSPACE_COMMAND = /^\/workspace(?:\s+([\s\S]+))?$/i;
2
+
3
+ export async function runWorkspaceCommand(text, harness) {
4
+ if (typeof text !== 'string') return null;
5
+ const match = WORKSPACE_COMMAND.exec(text.trim());
6
+ if (!match) return null;
7
+ const workspace = match[1]?.trim();
8
+ if (!workspace) {
9
+ return { handled: true, message: '用法:/workspace 工作区绝对路径' };
10
+ }
11
+ if (typeof harness?.switchWorkspace !== 'function') {
12
+ return { handled: true, message: '当前机器人暂不支持切换工作区。' };
13
+ }
14
+ try {
15
+ const current = await harness.switchWorkspace(workspace);
16
+ return { handled: true, message: `工作区已切换为:${current}` };
17
+ } catch (error) {
18
+ if (['workspace-not-absolute', 'workspace-not-found', 'workspace-not-directory'].includes(error?.code)) {
19
+ return { handled: true, message: `${error.message}\n用法:/workspace 工作区绝对路径` };
20
+ }
21
+ if (error?.code === 'workspace-bot-not-found') {
22
+ return { handled: true, message: '机器人正在移除或已重新接入,无法切换原会话的工作区。' };
23
+ }
24
+ throw error;
25
+ }
26
+ }
@@ -0,0 +1,44 @@
1
+ export const WORKSPACE_SESSION_STALE = 'workspace-session-stale';
2
+
3
+ async function sessionExists(harness, sessionId, options) {
4
+ return options === undefined
5
+ ? harness.sessionExists(sessionId)
6
+ : harness.sessionExists(sessionId, options);
7
+ }
8
+
9
+ async function createSession(harness, options) {
10
+ return options === undefined
11
+ ? harness.createSession()
12
+ : harness.createSession(options);
13
+ }
14
+
15
+ /**
16
+ * Resolve, persist, and ask through a session that belongs to the bot's
17
+ * current workspace. A concurrent workspace switch invalidates the scoped
18
+ * session and retries before any prompt is sent to the stale session.
19
+ */
20
+ export async function askInWorkspaceSession({
21
+ harness,
22
+ state,
23
+ key,
24
+ text,
25
+ createOptions,
26
+ existsOptions,
27
+ askOptions,
28
+ }) {
29
+ while (true) {
30
+ let sessionId = state.sessionFor(key);
31
+ if (!sessionId || !(await sessionExists(harness, sessionId, existsOptions))) {
32
+ sessionId = await createSession(harness, createOptions);
33
+ if (await state.setSession(key, sessionId) === false) continue;
34
+ }
35
+ try {
36
+ return {
37
+ sessionId,
38
+ answer: await harness.ask(sessionId, text, askOptions),
39
+ };
40
+ } catch (error) {
41
+ if (error?.code !== WORKSPACE_SESSION_STALE) throw error;
42
+ }
43
+ }
44
+ }
@@ -54,6 +54,11 @@ export class WecomStateStore {
54
54
  await this.#persist();
55
55
  }
56
56
 
57
+ async clearSessions() {
58
+ this.#state.sessions = {};
59
+ await this.#persist();
60
+ }
61
+
57
62
  hasSeen(messageId) {
58
63
  return this.#state.seenMessageIds.includes(messageId);
59
64
  }
@@ -1,10 +1,13 @@
1
1
  import { generateReqId } from '@wecom/aibot-node-sdk';
2
+ import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
3
+ import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
2
4
 
3
5
  const HELP_TEXT = [
4
6
  '企业微信机器人已连接 DeepSeek Harness。',
5
7
  '',
6
8
  '直接发送文字即可继续当前会话。',
7
9
  '/new 开启一个全新会话',
10
+ '/workspace 工作区绝对路径 切换工作区',
8
11
  '/status 检查连接状态',
9
12
  '/help 显示本帮助',
10
13
  ].join('\n');
@@ -182,11 +185,11 @@ export class WecomHarnessBridge {
182
185
  await this.#state.markSeen(messageId);
183
186
  return;
184
187
  }
185
-
186
- let sessionId = this.#state.sessionFor(key);
187
- if (!sessionId || !(await this.#harness.sessionExists(sessionId))) {
188
- sessionId = await this.#harness.createSession();
189
- await this.#state.setSession(key, sessionId);
188
+ const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
189
+ if (workspaceCommand) {
190
+ await this.#sendImmediate(frame, chatId, workspaceCommand.message);
191
+ await this.#state.markSeen(messageId);
192
+ return;
190
193
  }
191
194
 
192
195
  streamId = this.#generateReqId('stream');
@@ -197,14 +200,20 @@ export class WecomHarnessBridge {
197
200
  this.#logger.warn?.('[dsh-im:wecom] unable to start a stream; using an active reply:', error);
198
201
  }
199
202
 
200
- const answer = await this.#harness.ask(sessionId, text, {
201
- timeoutMs: this.#replyTimeoutMs,
202
- onUpdate: streamStarted && typeof this.#client.replyStreamNonBlocking === 'function'
203
- ? async (update) => {
204
- const progress = splitUtf8(progressText(update))[0];
205
- if (progress) await this.#client.replyStreamNonBlocking(frame, streamId, progress, false);
206
- }
207
- : undefined,
203
+ const { answer } = await askInWorkspaceSession({
204
+ harness: this.#harness,
205
+ state: this.#state,
206
+ key,
207
+ text,
208
+ askOptions: {
209
+ timeoutMs: this.#replyTimeoutMs,
210
+ onUpdate: streamStarted && typeof this.#client.replyStreamNonBlocking === 'function'
211
+ ? async (update) => {
212
+ const progress = splitUtf8(progressText(update))[0];
213
+ if (progress) await this.#client.replyStreamNonBlocking(frame, streamId, progress, false);
214
+ }
215
+ : undefined,
216
+ },
208
217
  });
209
218
 
210
219
  const chunks = splitUtf8(answer || '任务已完成,但没有生成可显示的文本。');
@@ -400,7 +400,14 @@ export class WecomController {
400
400
  if (record.controller.signal.aborted) {
401
401
  await this.#stopRuntime(identity.botId);
402
402
  if (previousConfig) await this.#configStore.save(previousConfig).catch(() => undefined);
403
- else await this.#configStore.remove(identity.botId).catch(() => undefined);
403
+ else {
404
+ const removed = await this.#configStore.remove(identity.botId).catch(() => null);
405
+ if (removed) {
406
+ await this.#deleteState({ botId: identity.botId, config }).catch((cleanupError) => {
407
+ this.#logger.warn?.('[dsh-im:wecom] cancelled bot state cleanup failed:', cleanupError);
408
+ });
409
+ }
410
+ }
404
411
  await this.#restoreCredential(identity.secretRef, previousSecret);
405
412
  throw error;
406
413
  }
@@ -187,17 +187,18 @@ export class HarnessClient {
187
187
  throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
188
188
  }
189
189
 
190
- async workspaceId() {
190
+ async workspaceId(options = {}) {
191
+ const workspace = options.workspace ?? this.#workspace;
191
192
  const { items } = await this.rpc('workspace.list', {});
192
- const existing = items.find((item) => item.path === this.#workspace);
193
+ const existing = items.find((item) => item.path === workspace);
193
194
  if (existing) return existing.workspaceId;
194
- const created = await this.rpc('workspace.create', { path: this.#workspace });
195
+ const created = await this.rpc('workspace.create', { path: workspace });
195
196
  return created.workspace.workspaceId;
196
197
  }
197
198
 
198
- async createSession() {
199
+ async createSession(options = {}) {
199
200
  await this.ensureRunning();
200
- const workspaceId = await this.workspaceId();
201
+ const workspaceId = await this.workspaceId(options);
201
202
  const created = await this.rpc('session.create', {
202
203
  workspaceId,
203
204
  agentPreset: this.#agentPreset,
@@ -62,6 +62,11 @@ export class WeixinStateStore {
62
62
  await this.#persist();
63
63
  }
64
64
 
65
+ async clearSessions() {
66
+ this.#state.sessions = {};
67
+ await this.#persist();
68
+ }
69
+
65
70
  hasSeen(messageId) {
66
71
  return this.#state.seenMessageIds.includes(messageId);
67
72
  }
@@ -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.3.0',
95
+ bot_agent: 'DeepSeekHarness/0.5.0',
96
96
  };
97
97
  }
98
98
 
@@ -3,12 +3,15 @@ import {
3
3
  splitWeixinText,
4
4
  weixinMessageId,
5
5
  } from './weixin-api.mjs';
6
+ import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
7
+ import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
6
8
 
7
9
  const HELP_TEXT = [
8
10
  '微信已连接 DeepSeek Harness。',
9
11
  '',
10
12
  '直接发送文字或带文字识别结果的语音即可继续当前会话。',
11
13
  '/new 开启一个全新会话',
14
+ '/workspace 工作区绝对路径 切换工作区',
12
15
  '/status 检查连接状态',
13
16
  '/help 显示本帮助',
14
17
  ].join('\n');
@@ -133,14 +136,21 @@ export class WeixinHarnessBridge {
133
136
  await this.#state.markSeen(messageId);
134
137
  return;
135
138
  }
139
+ const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
140
+ if (workspaceCommand) {
141
+ await this.#send(sender, workspaceCommand.message, contextToken, runId);
142
+ await this.#state.markSeen(messageId);
143
+ return;
144
+ }
136
145
 
137
146
  const key = conversationKey(sender);
138
- let sessionId = this.#state.sessionFor(key);
139
- if (!sessionId || !(await this.#harness.sessionExists(sessionId))) {
140
- sessionId = await this.#harness.createSession();
141
- await this.#state.setSession(key, sessionId);
142
- }
143
- const answer = await this.#harness.ask(sessionId, text, { timeoutMs: this.#replyTimeoutMs });
147
+ const { answer } = await askInWorkspaceSession({
148
+ harness: this.#harness,
149
+ state: this.#state,
150
+ key,
151
+ text,
152
+ askOptions: { timeoutMs: this.#replyTimeoutMs },
153
+ });
144
154
  await this.#send(sender, answer, contextToken, runId);
145
155
  await this.#state.markSeen(messageId);
146
156
  this.#status.messagesReplied += 1;
@@ -468,7 +468,12 @@ export class WeixinController {
468
468
  await this.#stopRuntime(identity.botId);
469
469
  if (previousConfig) await this.#configStore.save(previousConfig).catch(() => undefined);
470
470
  else if (this.#configStore.get(identity.botId)) {
471
- await this.#configStore.remove(identity.botId).catch(() => undefined);
471
+ const removed = await this.#configStore.remove(identity.botId).catch(() => null);
472
+ if (removed) {
473
+ await this.#deleteState({ botId: identity.botId, config }).catch((cleanupError) => {
474
+ this.#logger.warn?.('[dsh-weixin] failed to clean up cancelled bot state:', cleanupError);
475
+ });
476
+ }
472
477
  }
473
478
  await this.#restoreCredential(identity.tokenRef, previousToken);
474
479
  if (previousConfig && previousToken?.value) {
@@ -311,7 +311,14 @@ export class WhatsappController {
311
311
  if (record.controller.signal.aborted || this.#closed || error?.name === 'AbortError') {
312
312
  await this.#stopRuntime(botId);
313
313
  if (previous) await this.#configStore.save(previous).catch(() => undefined);
314
- else await this.#configStore.remove(botId).catch(() => undefined);
314
+ else {
315
+ const removed = await this.#configStore.remove(botId).catch(() => null);
316
+ if (removed) {
317
+ await this.#deleteState({ botId, config }).catch((cleanupError) => {
318
+ this.#logger.warn?.('[dsh-im:whatsapp] cancelled bot state cleanup failed:', cleanupError);
319
+ });
320
+ }
321
+ }
315
322
  await this.#deleteAuth(record.authDirectory).catch(() => undefined);
316
323
  if (previous) await this.#startRuntime(previous).catch(() => undefined);
317
324
  record.state = 'cancelled';