@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,5 +1,6 @@
1
1
  import QRCode from 'qrcode';
2
2
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
+ import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
3
4
 
4
5
  export const WECOM_RPC_CHANNEL = '/wecom';
5
6
  export const WECOM_ENDPOINTS = Object.freeze({
@@ -10,6 +11,7 @@ export const WECOM_ENDPOINTS = Object.freeze({
10
11
  bindCredentials: 'bot.bind-credentials',
11
12
  reconnectBot: 'bot.reconnect',
12
13
  deleteBot: 'bot.delete',
14
+ setWorkspace: SET_WORKSPACE_ENDPOINT,
13
15
  });
14
16
  export const WECOM_RPC_ENDPOINTS = Object.freeze(Object.values(WECOM_ENDPOINTS));
15
17
 
@@ -57,6 +59,10 @@ function payloadFailure(endpoint, payload) {
57
59
  return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
58
60
  ? null : 'bot.delete requires a botId and confirm=true.';
59
61
  }
62
+ if (endpoint === WECOM_ENDPOINTS.setWorkspace) {
63
+ return validWorkspacePayload(payload)
64
+ ? null : '请输入工作区绝对路径。';
65
+ }
60
66
  return 'Unknown Enterprise WeChat endpoint.';
61
67
  }
62
68
 
@@ -125,16 +131,24 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
125
131
  value = await publicStatus(await controller.bindCredentials(payload), cachedEncode);
126
132
  } else if (endpoint === WECOM_ENDPOINTS.reconnectBot) {
127
133
  value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode);
134
+ } else if (endpoint === WECOM_ENDPOINTS.setWorkspace) {
135
+ if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
136
+ value = await publicStatus(
137
+ await controller.updateWorkspace(payload.botId, payload.workspace),
138
+ cachedEncode,
139
+ );
128
140
  } else {
129
141
  value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
130
142
  }
131
143
  return signal?.aborted
132
144
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
133
145
  : { ok: true, value };
134
- } catch {
146
+ } catch (error) {
147
+ const workspaceError = publicWorkspaceError(error);
135
148
  return signal?.aborted
136
149
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
137
- : { ok: false, error: { code: 'wecom-operation-failed', message: '企业微信操作失败,请稍后重试。' } };
150
+ : { ok: false, error: workspaceError
151
+ ?? { code: 'wecom-operation-failed', message: '企业微信操作失败,请稍后重试。' } };
138
152
  }
139
153
  };
140
154
  }
@@ -8,6 +8,12 @@ import { WeixinStateStore } from '../../../../src/channels/weixin/state-store.mj
8
8
  import { createWeixinApi } from '../../../../src/channels/weixin/weixin-api.mjs';
9
9
  import { WeixinController } from '../../../../src/channels/weixin/weixin-controller.mjs';
10
10
  import { WeixinRuntime } from '../../../../src/channels/weixin/weixin-runtime.mjs';
11
+ import {
12
+ BotWorkspaceStore,
13
+ createBotWorkspaceScope,
14
+ createWorkspaceAwareController,
15
+ observeBotWorkspaceRemovals,
16
+ } from '../../../../src/channels/shared/bot-workspace-store.mjs';
11
17
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
12
18
 
13
19
  function harnessOrigin(webServer, configured) {
@@ -26,6 +32,7 @@ function pluginPaths(config) {
26
32
  root,
27
33
  config: resolve(config.configPath ?? join(root, 'config.json')),
28
34
  accounts: resolve(config.accountsDir ?? join(root, 'accounts')),
35
+ workspaces: resolve(config.workspacesPath ?? join(root, 'workspaces.json')),
29
36
  };
30
37
  }
31
38
 
@@ -45,6 +52,16 @@ export async function createProductionController(ctx, config = {}, internals = {
45
52
  : (ctx.logger ?? console);
46
53
  const paths = pluginPaths(config);
47
54
  const configStore = await new ConfigStore(paths.config).load();
55
+ const defaultWorkspace = resolve(config.workspace ?? process.cwd());
56
+ const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
57
+ const workspaces = internals.workspaces
58
+ ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
59
+ const configuredBots = configStore.list();
60
+ await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
61
+ await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
62
+ const observedConfigStore = typeof configStore.remove === 'function'
63
+ ? observeBotWorkspaceRemovals(configStore, { workspaces })
64
+ : configStore;
48
65
  const stateStores = new Map();
49
66
 
50
67
  const statePath = (botId) => resolve(paths.accounts, botId, 'state.json');
@@ -58,24 +75,26 @@ export async function createProductionController(ctx, config = {}, internals = {
58
75
  };
59
76
  const harness = new Harness({
60
77
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
61
- workspace: resolve(config.workspace ?? process.cwd()),
78
+ workspace: defaultWorkspace,
62
79
  agentPreset: config.agentPreset ?? 'standard',
63
80
  autostart: false,
64
81
  dshBin: config.dshBin ?? 'dsh',
65
82
  });
66
- const controller = new Controller({
83
+ const coreController = new Controller({
67
84
  api,
68
85
  credentials: ctx.credentials,
69
- configStore,
86
+ configStore: observedConfigStore,
70
87
  logger,
71
88
  createRuntime: async ({ botId, config: accountConfig, token }) => {
72
89
  const state = await stateFor(botId);
90
+ await workspaces.ensure(botId);
91
+ const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
73
92
  return new Runtime({
74
93
  api,
75
94
  config: accountConfig,
76
95
  token,
77
- harness,
78
- state,
96
+ harness: workspaceScope.harness,
97
+ state: workspaceScope.state,
79
98
  replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
80
99
  maxMessageChars: config.maxMessageChars ?? 4_000,
81
100
  logger: {
@@ -91,15 +110,16 @@ export async function createProductionController(ctx, config = {}, internals = {
91
110
  stateStores.delete(botId);
92
111
  if (state && typeof state.remove === 'function') {
93
112
  await state.remove();
94
- return;
95
- }
96
- try {
97
- await unlink(statePath(botId));
98
- } catch (error) {
99
- if (error?.code !== 'ENOENT') throw error;
113
+ } else {
114
+ try {
115
+ await unlink(statePath(botId));
116
+ } catch (error) {
117
+ if (error?.code !== 'ENOENT') throw error;
118
+ }
100
119
  }
101
120
  },
102
121
  });
122
+ const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
103
123
  const supervisor = createSupervisor({
104
124
  controller,
105
125
  harness,
@@ -1,5 +1,10 @@
1
1
  import QRCode from 'qrcode';
2
2
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
3
+ import {
4
+ publicWorkspaceError,
5
+ SET_WORKSPACE_ENDPOINT,
6
+ validWorkspacePayload,
7
+ } from '../shared/workspace-rpc.mjs';
3
8
 
4
9
  export const WEIXIN_RPC_CHANNEL = '/weixin';
5
10
  export const WEIXIN_ENDPOINTS = Object.freeze({
@@ -10,6 +15,7 @@ export const WEIXIN_ENDPOINTS = Object.freeze({
10
15
  cancelProvisioning: 'provision.cancel',
11
16
  reconnectBot: 'bot.reconnect',
12
17
  deleteBot: 'bot.delete',
18
+ setWorkspace: SET_WORKSPACE_ENDPOINT,
13
19
  });
14
20
  export const WEIXIN_RPC_ENDPOINTS = Object.freeze(Object.values(WEIXIN_ENDPOINTS));
15
21
 
@@ -58,6 +64,10 @@ function payloadFailure(endpoint, payload) {
58
64
  ? null
59
65
  : 'bot.delete requires a botId and confirm=true.';
60
66
  }
67
+ if (endpoint === WEIXIN_ENDPOINTS.setWorkspace) {
68
+ return validWorkspacePayload(payload)
69
+ ? null : '请输入工作区绝对路径。';
70
+ }
61
71
  return 'Unknown Weixin endpoint.';
62
72
  }
63
73
 
@@ -156,12 +166,21 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
156
166
  if (!value) return badRequest('The provisioning attempt no longer exists.');
157
167
  } else if (endpoint === WEIXIN_ENDPOINTS.reconnectBot) {
158
168
  value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode);
169
+ } else if (endpoint === WEIXIN_ENDPOINTS.setWorkspace) {
170
+ if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
171
+ value = await publicStatus(
172
+ await controller.updateWorkspace(payload.botId, payload.workspace),
173
+ cachedEncode,
174
+ );
159
175
  } else {
160
176
  value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
161
177
  }
162
178
  return signal?.aborted ? cancelled() : { ok: true, value };
163
- } catch {
164
- return signal?.aborted ? cancelled() : internalFailure();
179
+ } catch (error) {
180
+ const workspaceError = publicWorkspaceError(error);
181
+ return signal?.aborted ? cancelled() : workspaceError
182
+ ? { ok: false, error: workspaceError }
183
+ : internalFailure();
165
184
  }
166
185
  };
167
186
  }
@@ -8,6 +8,12 @@ import { WhatsappStateStore } from '../../../../src/channels/whatsapp/state-stor
8
8
  import { WhatsappController } from '../../../../src/channels/whatsapp/whatsapp-controller.mjs';
9
9
  import { WhatsappRuntime } from '../../../../src/channels/whatsapp/whatsapp-runtime.mjs';
10
10
  import { createWhatsappWebSession } from '../../../../src/channels/whatsapp/whatsapp-web-session.mjs';
11
+ import {
12
+ BotWorkspaceStore,
13
+ createBotWorkspaceScope,
14
+ createWorkspaceAwareController,
15
+ observeBotWorkspaceRemovals,
16
+ } from '../../../../src/channels/shared/bot-workspace-store.mjs';
11
17
  import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
12
18
 
13
19
  const AUTH_DIRECTORY_PATTERN = /^[a-f0-9-]{36}$/;
@@ -32,6 +38,7 @@ function pluginPaths(config) {
32
38
  return {
33
39
  config: resolve(config.configPath ?? join(root, 'config.json')),
34
40
  bots: resolve(config.botsDir ?? join(root, 'bots')),
41
+ workspaces: resolve(config.workspacesPath ?? join(root, 'workspaces.json')),
35
42
  authPath,
36
43
  };
37
44
  }
@@ -49,6 +56,16 @@ export async function createProductionController(ctx, config = {}, internals = {
49
56
  const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
50
57
  const paths = pluginPaths(config);
51
58
  const configStore = await new ConfigStore(paths.config).load();
59
+ const defaultWorkspace = resolve(config.workspace ?? process.cwd());
60
+ const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
61
+ const workspaces = internals.workspaces
62
+ ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
63
+ const configuredBots = configStore.list();
64
+ await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
65
+ await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
66
+ const observedConfigStore = typeof configStore.remove === 'function'
67
+ ? observeBotWorkspaceRemovals(configStore, { workspaces })
68
+ : configStore;
52
69
  const stateStores = new Map();
53
70
  const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
54
71
  const stateFor = async (botId) => {
@@ -61,31 +78,36 @@ export async function createProductionController(ctx, config = {}, internals = {
61
78
  };
62
79
  const harness = new Harness({
63
80
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
64
- workspace: resolve(config.workspace ?? process.cwd()),
81
+ workspace: defaultWorkspace,
65
82
  agentPreset: config.agentPreset ?? 'standard',
66
83
  autostart: false,
67
84
  dshBin: config.dshBin ?? 'dsh',
68
85
  });
69
- const controller = new Controller({
70
- configStore,
86
+ const coreController = new Controller({
87
+ configStore: observedConfigStore,
71
88
  authPath: paths.authPath,
72
89
  createSession,
73
90
  logger,
74
- createRuntime: async ({ botId, config: botConfig, authDir }) => new Runtime({
75
- config: botConfig,
76
- authDir,
77
- harness,
78
- state: await stateFor(botId),
79
- replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
80
- connectTimeoutMs: config.connectTimeoutMs ?? 30_000,
81
- createSession,
82
- logger: {
83
- error: (...args) => logger.error?.(`[${botId}]`, ...args),
84
- warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
85
- info: (...args) => logger.info?.(`[${botId}]`, ...args),
86
- debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
87
- },
88
- }),
91
+ createRuntime: async ({ botId, config: botConfig, authDir }) => {
92
+ const state = await stateFor(botId);
93
+ await workspaces.ensure(botId);
94
+ const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
95
+ return new Runtime({
96
+ config: botConfig,
97
+ authDir,
98
+ harness: workspaceScope.harness,
99
+ state: workspaceScope.state,
100
+ replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
101
+ connectTimeoutMs: config.connectTimeoutMs ?? 30_000,
102
+ createSession,
103
+ logger: {
104
+ error: (...args) => logger.error?.(`[${botId}]`, ...args),
105
+ warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
106
+ info: (...args) => logger.info?.(`[${botId}]`, ...args),
107
+ debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
108
+ },
109
+ });
110
+ },
89
111
  deleteAuth: (authDirectory) => rm(paths.authPath(authDirectory), {
90
112
  recursive: true,
91
113
  force: true,
@@ -93,14 +115,18 @@ export async function createProductionController(ctx, config = {}, internals = {
93
115
  deleteState: async ({ botId }) => {
94
116
  const state = stateStores.get(botId);
95
117
  stateStores.delete(botId);
96
- if (state && typeof state.remove === 'function') return state.remove();
97
- try {
98
- await unlink(statePath(botId));
99
- } catch (error) {
100
- if (error?.code !== 'ENOENT') throw error;
118
+ if (state && typeof state.remove === 'function') {
119
+ await state.remove();
120
+ } else {
121
+ try {
122
+ await unlink(statePath(botId));
123
+ } catch (error) {
124
+ if (error?.code !== 'ENOENT') throw error;
125
+ }
101
126
  }
102
127
  },
103
128
  });
129
+ const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
104
130
  const supervisor = createSupervisor({
105
131
  channel: 'whatsapp',
106
132
  controller,
@@ -1,6 +1,7 @@
1
1
  import QRCode from 'qrcode';
2
2
 
3
3
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
4
+ import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
4
5
 
5
6
  export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
6
7
  export const WHATSAPP_ENDPOINTS = Object.freeze({
@@ -10,6 +11,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
10
11
  cancelProvisioning: 'provision.cancel',
11
12
  reconnectBot: 'bot.reconnect',
12
13
  deleteBot: 'bot.delete',
14
+ setWorkspace: SET_WORKSPACE_ENDPOINT,
13
15
  });
14
16
  export const WHATSAPP_RPC_ENDPOINTS = Object.freeze(Object.values(WHATSAPP_ENDPOINTS));
15
17
 
@@ -44,6 +46,10 @@ function payloadFailure(endpoint, payload) {
44
46
  return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId)
45
47
  && payload.confirm === true ? null : 'bot.delete requires a botId and confirm=true.';
46
48
  }
49
+ if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
50
+ return validWorkspacePayload(payload)
51
+ ? null : '请输入工作区绝对路径。';
52
+ }
47
53
  return 'Unknown WhatsApp endpoint.';
48
54
  }
49
55
 
@@ -114,16 +120,24 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
114
120
  value = sanitizePublic(await controller.cancelProvisioning(payload.attemptId));
115
121
  } else if (endpoint === WHATSAPP_ENDPOINTS.reconnectBot) {
116
122
  value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode);
123
+ } else if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
124
+ if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
125
+ value = await publicStatus(
126
+ await controller.updateWorkspace(payload.botId, payload.workspace),
127
+ cachedEncode,
128
+ );
117
129
  } else {
118
130
  value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
119
131
  }
120
132
  return signal?.aborted
121
133
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
122
134
  : { ok: true, value };
123
- } catch {
135
+ } catch (error) {
136
+ const workspaceError = publicWorkspaceError(error);
124
137
  return signal?.aborted
125
138
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
126
- : { ok: false, error: { code: 'whatsapp-operation-failed', message: 'WhatsApp 操作失败,请稍后重试。' } };
139
+ : { ok: false, error: workspaceError
140
+ ?? { code: 'whatsapp-operation-failed', message: 'WhatsApp 操作失败,请稍后重试。' } };
127
141
  }
128
142
  };
129
143
  }
@@ -3,6 +3,8 @@ import {
3
3
  splitDingtalkText,
4
4
  } from './dingtalk-api.mjs';
5
5
  import { createDingTalkCardStream } from './dingtalk-card-stream.mjs';
6
+ import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
7
+ import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
6
8
 
7
9
  const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
8
10
  const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
@@ -12,6 +14,7 @@ const HELP_TEXT = [
12
14
  '',
13
15
  '直接发送文字即可继续当前会话。',
14
16
  '/new 开启一个全新会话',
17
+ '/workspace 工作区绝对路径 切换工作区',
15
18
  '/status 检查连接状态',
16
19
  '/help 显示本帮助',
17
20
  ].join('\n');
@@ -213,12 +216,12 @@ export class DingtalkHarnessBridge {
213
216
  await this.#send(sessionWebhook, '已开启新会话。请发送你的问题。');
214
217
  return;
215
218
  }
216
-
217
- let sessionId = this.#state.sessionFor(key);
218
- if (!sessionId || !(await this.#harness.sessionExists(sessionId, { signal: this.#signal }))) {
219
- sessionId = await this.#harness.createSession({ signal: this.#signal });
220
- await this.#state.setSession(key, sessionId);
219
+ const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
220
+ if (workspaceCommand) {
221
+ await this.#send(sessionWebhook, workspaceCommand.message);
222
+ return;
221
223
  }
224
+
222
225
  if (typeof this.#api.createAiCard === 'function'
223
226
  && typeof this.#api.updateAiCard === 'function'
224
227
  && typeof this.#api.finishAiCard === 'function') {
@@ -232,12 +235,20 @@ export class DingtalkHarnessBridge {
232
235
  });
233
236
  cardStarted = await cardStream.start(CARD_INITIAL_TEXT);
234
237
  }
235
- const answer = await this.#harness.ask(sessionId, text, {
236
- timeoutMs: this.#replyTimeoutMs,
237
- signal: this.#signal,
238
- onUpdate: cardStarted
239
- ? (update) => cardStream.push(progressText(update))
240
- : undefined,
238
+ const { answer } = await askInWorkspaceSession({
239
+ harness: this.#harness,
240
+ state: this.#state,
241
+ key,
242
+ text,
243
+ createOptions: { signal: this.#signal },
244
+ existsOptions: { signal: this.#signal },
245
+ askOptions: {
246
+ timeoutMs: this.#replyTimeoutMs,
247
+ signal: this.#signal,
248
+ onUpdate: cardStarted
249
+ ? (update) => cardStream.push(progressText(update))
250
+ : undefined,
251
+ },
241
252
  });
242
253
  const streamed = cardStarted && await cardStream.finish(answer);
243
254
  if (!streamed) await this.#send(sessionWebhook, answer);
@@ -610,7 +610,12 @@ export class DingtalkController {
610
610
  await this.#stopRuntime(identity.botId);
611
611
  if (previousConfig) await this.#configStore.save(previousConfig).catch(() => undefined);
612
612
  else if (this.#configStore.get(identity.botId)) {
613
- await this.#configStore.remove(identity.botId).catch(() => undefined);
613
+ const removed = await this.#configStore.remove(identity.botId).catch(() => null);
614
+ if (removed) {
615
+ await this.#deleteState({ botId: identity.botId, config }).catch((cleanupError) => {
616
+ this.#logger.warn?.('[dsh-dingtalk] failed to clean up cancelled bot state:', cleanupError);
617
+ });
618
+ }
614
619
  }
615
620
  await this.#restoreCredential(identity.secretRef, previousSecret);
616
621
  if (previousConfig && cleanString(previousSecret?.value)) {
@@ -217,10 +217,11 @@ export class HarnessClient {
217
217
  }
218
218
 
219
219
  async workspaceId(options = {}) {
220
- const { items } = await this.rpc('workspace.list', {}, 30_000, options);
221
- const existing = items.find((item) => item.path === this.#workspace);
220
+ const { workspace = this.#workspace, ...rpcOptions } = options;
221
+ const { items } = await this.rpc('workspace.list', {}, 30_000, rpcOptions);
222
+ const existing = items.find((item) => item.path === workspace);
222
223
  if (existing) return existing.workspaceId;
223
- const created = await this.rpc('workspace.create', { path: this.#workspace }, 30_000, options);
224
+ const created = await this.rpc('workspace.create', { path: workspace }, 30_000, rpcOptions);
224
225
  return created.workspace.workspaceId;
225
226
  }
226
227
 
@@ -117,6 +117,11 @@ export class DingtalkStateStore {
117
117
  await this.#persist();
118
118
  }
119
119
 
120
+ async clearSessions() {
121
+ this.#state.sessions = {};
122
+ await this.#persist();
123
+ }
124
+
120
125
  hasSeen(messageId) {
121
126
  const id = nonEmptyString(messageId);
122
127
  return Boolean(id && this.#state.seenMessageIds.includes(id));
@@ -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.3.0)',
111
+ 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.5.0)',
112
112
  },
113
113
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
114
114
  signal: requestSignal(signal, timeoutMs),
@@ -5,12 +5,15 @@ import {
5
5
  isBotSender,
6
6
  splitText,
7
7
  } from './message-utils.mjs';
8
+ import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
9
+ import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
8
10
 
9
11
  const HELP_TEXT = [
10
12
  '北汇星河 AIOS 已连接 DeepSeek Harness。',
11
13
  '',
12
14
  '直接发送问题即可继续当前会话。',
13
15
  '/new 开启一个全新会话',
16
+ '/workspace 工作区绝对路径 切换工作区',
14
17
  '/status 检查连接状态',
15
18
  '/help 显示本帮助',
16
19
  ].join('\n');
@@ -106,25 +109,30 @@ export class FeishuHarnessBridge {
106
109
  await this.#send(event.message.chat_id, '飞书机器人与 DeepSeek Harness 连接正常。');
107
110
  return;
108
111
  }
109
-
110
- let sessionId = this.#state.sessionFor(key);
111
- if (!sessionId || !(await this.#harness.sessionExists(sessionId))) {
112
- sessionId = await this.#harness.createSession();
113
- await this.#state.setSession(key, sessionId);
112
+ const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
113
+ if (workspaceCommand) {
114
+ await this.#send(event.message.chat_id, workspaceCommand.message);
115
+ return;
114
116
  }
115
117
 
116
- console.info(`[bridge] processing ${event.message.chat_type} message ${messageId} in ${sessionId}`);
117
- await this.#answerWithStream(event, sessionId, text);
118
+ console.info(`[bridge] processing ${event.message.chat_type} message ${messageId}`);
119
+ await this.#answerWithStream(event, key, text);
118
120
  this.#status.messagesReplied += 1;
119
121
  this.#status.lastReplyAt = new Date().toISOString();
120
122
  this.#status.lastError = null;
121
123
  }
122
124
 
123
- async #answerWithStream(event, sessionId, text) {
125
+ async #answerWithStream(event, key, text) {
124
126
  const chatId = event.message.chat_id;
125
127
  const messageId = event.message.message_id;
126
128
  if (!this.#channel?.stream) {
127
- const answer = await this.#harness.ask(sessionId, text, { timeoutMs: this.#replyTimeoutMs });
129
+ const { answer } = await askInWorkspaceSession({
130
+ harness: this.#harness,
131
+ state: this.#state,
132
+ key,
133
+ text,
134
+ askOptions: { timeoutMs: this.#replyTimeoutMs },
135
+ });
128
136
  for (const chunk of splitText(answer)) await this.#send(chatId, chunk);
129
137
  this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1;
130
138
  return;
@@ -136,13 +144,19 @@ export class FeishuHarnessBridge {
136
144
  await this.#channel.stream(chatId, {
137
145
  markdown: async (controller) => {
138
146
  promptStarted = true;
139
- completedAnswer = await this.#harness.ask(sessionId, text, {
140
- timeoutMs: this.#replyTimeoutMs,
141
- onUpdate: async (update) => {
142
- await controller.setContent(this.#progressText(update));
143
- this.#status.streamUpdates = (this.#status.streamUpdates ?? 0) + 1;
147
+ ({ answer: completedAnswer } = await askInWorkspaceSession({
148
+ harness: this.#harness,
149
+ state: this.#state,
150
+ key,
151
+ text,
152
+ askOptions: {
153
+ timeoutMs: this.#replyTimeoutMs,
154
+ onUpdate: async (update) => {
155
+ await controller.setContent(this.#progressText(update));
156
+ this.#status.streamUpdates = (this.#status.streamUpdates ?? 0) + 1;
157
+ },
144
158
  },
145
- });
159
+ }));
146
160
  await controller.setContent(completedAnswer);
147
161
  },
148
162
  }, { replyTo: messageId });
@@ -158,7 +172,13 @@ export class FeishuHarnessBridge {
158
172
  if (promptStarted) throw error;
159
173
 
160
174
  console.warn('[bridge] native Feishu stream unavailable; using text fallback:', error.message);
161
- const answer = await this.#harness.ask(sessionId, text, { timeoutMs: this.#replyTimeoutMs });
175
+ const { answer } = await askInWorkspaceSession({
176
+ harness: this.#harness,
177
+ state: this.#state,
178
+ key,
179
+ text,
180
+ askOptions: { timeoutMs: this.#replyTimeoutMs },
181
+ });
162
182
  for (const chunk of splitText(answer)) await this.#send(chatId, chunk);
163
183
  this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1;
164
184
  }
@@ -197,17 +197,18 @@ export class HarnessClient {
197
197
  throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
198
198
  }
199
199
 
200
- async workspaceId() {
200
+ async workspaceId(options = {}) {
201
+ const workspace = options.workspace ?? this.#workspace;
201
202
  const { items } = await this.rpc('workspace.list', {});
202
- const existing = items.find((item) => item.path === this.#workspace);
203
+ const existing = items.find((item) => item.path === workspace);
203
204
  if (existing) return existing.workspaceId;
204
- const created = await this.rpc('workspace.create', { path: this.#workspace });
205
+ const created = await this.rpc('workspace.create', { path: workspace });
205
206
  return created.workspace.workspaceId;
206
207
  }
207
208
 
208
- async createSession() {
209
+ async createSession(options = {}) {
209
210
  await this.ensureRunning();
210
- const workspaceId = await this.workspaceId();
211
+ const workspaceId = await this.workspaceId(options);
211
212
  const created = await this.rpc('session.create', {
212
213
  workspaceId,
213
214
  agentPreset: this.#agentPreset,
@@ -41,6 +41,11 @@ export class StateStore {
41
41
  await this.#persist();
42
42
  }
43
43
 
44
+ async clearSessions() {
45
+ this.#state.sessions = {};
46
+ await this.#persist();
47
+ }
48
+
44
49
  hasSeen(messageId) {
45
50
  return this.#state.seenMessageIds.includes(messageId);
46
51
  }