@xmanrui/dsh-im 0.4.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 (64) hide show
  1. package/README.md +30 -0
  2. package/lib/client.js +709 -285
  3. package/lib/index.js +112 -110
  4. package/package.json +1 -1
  5. package/plugin-src/client/channels/dingtalk/api.js +2 -0
  6. package/plugin-src/client/channels/dingtalk/index.js +67 -14
  7. package/plugin-src/client/channels/feishu/api.js +2 -0
  8. package/plugin-src/client/channels/feishu/index.js +70 -22
  9. package/plugin-src/client/channels/qq/api.js +2 -0
  10. package/plugin-src/client/channels/qq/index.js +45 -8
  11. package/plugin-src/client/channels/shared/token-api.js +2 -0
  12. package/plugin-src/client/channels/shared/token-channel.js +34 -8
  13. package/plugin-src/client/channels/wecom/api.js +2 -0
  14. package/plugin-src/client/channels/wecom/index.js +45 -8
  15. package/plugin-src/client/channels/weixin/api.js +2 -0
  16. package/plugin-src/client/channels/weixin/index.js +68 -8
  17. package/plugin-src/client/channels/whatsapp/api.js +2 -0
  18. package/plugin-src/client/channels/whatsapp/index.js +27 -5
  19. package/plugin-src/client/i18n.js +13 -0
  20. package/plugin-src/client/styles.js +13 -0
  21. package/plugin-src/client/workspace-editor.js +96 -0
  22. package/plugin-src/client/workspace-snapshot-fence.js +28 -0
  23. package/plugin-src/host/channels/dingtalk/production.mjs +34 -11
  24. package/plugin-src/host/channels/dingtalk/rpc.mjs +17 -2
  25. package/plugin-src/host/channels/feishu/production.mjs +42 -5
  26. package/plugin-src/host/channels/feishu/rpc.mjs +17 -2
  27. package/plugin-src/host/channels/qq/production.mjs +48 -22
  28. package/plugin-src/host/channels/qq/rpc.mjs +16 -2
  29. package/plugin-src/host/channels/shared/production.mjs +48 -22
  30. package/plugin-src/host/channels/shared/rpc.mjs +15 -0
  31. package/plugin-src/host/channels/shared/workspace-rpc.mjs +25 -0
  32. package/plugin-src/host/channels/slack/production.mjs +48 -23
  33. package/plugin-src/host/channels/slack/rpc.mjs +16 -0
  34. package/plugin-src/host/channels/wecom/production.mjs +49 -23
  35. package/plugin-src/host/channels/wecom/rpc.mjs +16 -2
  36. package/plugin-src/host/channels/weixin/production.mjs +31 -11
  37. package/plugin-src/host/channels/weixin/rpc.mjs +21 -2
  38. package/plugin-src/host/channels/whatsapp/production.mjs +49 -23
  39. package/plugin-src/host/channels/whatsapp/rpc.mjs +16 -2
  40. package/src/channels/dingtalk/dingtalk-bridge.mjs +22 -11
  41. package/src/channels/dingtalk/dingtalk-controller.mjs +6 -1
  42. package/src/channels/dingtalk/harness-client.mjs +4 -3
  43. package/src/channels/dingtalk/state-store.mjs +5 -0
  44. package/src/channels/discord/discord-api.mjs +1 -1
  45. package/src/channels/feishu/bridge.mjs +36 -16
  46. package/src/channels/feishu/harness-client.mjs +6 -5
  47. package/src/channels/feishu/state-store.mjs +5 -0
  48. package/src/channels/qq/qq-bridge.mjs +25 -15
  49. package/src/channels/qq/qq-controller.mjs +8 -1
  50. package/src/channels/qq/state-store.mjs +5 -0
  51. package/src/channels/shared/bot-workspace-store.mjs +584 -0
  52. package/src/channels/shared/conversation-state-store.mjs +5 -0
  53. package/src/channels/shared/text-harness-bridge.mjs +23 -13
  54. package/src/channels/shared/workspace-command.mjs +26 -0
  55. package/src/channels/shared/workspace-session.mjs +44 -0
  56. package/src/channels/wecom/state-store.mjs +5 -0
  57. package/src/channels/wecom/wecom-bridge.mjs +22 -13
  58. package/src/channels/wecom/wecom-controller.mjs +8 -1
  59. package/src/channels/weixin/harness-client.mjs +6 -5
  60. package/src/channels/weixin/state-store.mjs +5 -0
  61. package/src/channels/weixin/weixin-api.mjs +1 -1
  62. package/src/channels/weixin/weixin-bridge.mjs +16 -6
  63. package/src/channels/weixin/weixin-controller.mjs +6 -1
  64. package/src/channels/whatsapp/whatsapp-controller.mjs +8 -1
@@ -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.4.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';