@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
@@ -8,6 +8,12 @@ import { QqController } from '../../../../src/channels/qq/qq-controller.mjs';
8
8
  import { QqRuntime } from '../../../../src/channels/qq/qq-runtime.mjs';
9
9
  import { QqQrAuth } from '../../../../src/channels/qq/qr-auth.mjs';
10
10
  import { QqStateStore } from '../../../../src/channels/qq/state-store.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) {
@@ -25,6 +31,7 @@ function pluginPaths(config) {
25
31
  return {
26
32
  config: resolve(config.configPath ?? join(root, 'config.json')),
27
33
  bots: resolve(config.botsDir ?? join(root, 'bots')),
34
+ workspaces: resolve(config.workspacesPath ?? join(root, 'workspaces.json')),
28
35
  };
29
36
  }
30
37
 
@@ -42,6 +49,16 @@ export async function createProductionController(ctx, config = {}, internals = {
42
49
  const logger = typeof ctx.logger === 'function' ? ctx.logger('dsh-im:qq') : (ctx.logger ?? console);
43
50
  const paths = pluginPaths(config);
44
51
  const configStore = await new ConfigStore(paths.config).load();
52
+ const defaultWorkspace = resolve(config.workspace ?? process.cwd());
53
+ const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
54
+ const workspaces = internals.workspaces
55
+ ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
56
+ const configuredBots = configStore.list();
57
+ await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
58
+ await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
59
+ const observedConfigStore = typeof configStore.remove === 'function'
60
+ ? observeBotWorkspaceRemovals(configStore, { workspaces })
61
+ : configStore;
45
62
  const qrAuth = internals.qrAuth ?? new QrAuth({ source: config.qrSource ?? 'deepseek-harness' });
46
63
  const stateStores = new Map();
47
64
  const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
@@ -55,41 +72,50 @@ export async function createProductionController(ctx, config = {}, internals = {
55
72
  };
56
73
  const harness = new Harness({
57
74
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
58
- workspace: resolve(config.workspace ?? process.cwd()),
75
+ workspace: defaultWorkspace,
59
76
  agentPreset: config.agentPreset ?? 'standard',
60
77
  autostart: false,
61
78
  dshBin: config.dshBin ?? 'dsh',
62
79
  });
63
- const controller = new Controller({
80
+ const coreController = new Controller({
64
81
  qrAuth,
65
82
  credentials: ctx.credentials,
66
- configStore,
83
+ configStore: observedConfigStore,
67
84
  logger,
68
- createRuntime: async ({ botId, config: botConfig, appSecret }) => new Runtime({
69
- config: botConfig,
70
- appSecret,
71
- harness,
72
- state: await stateFor(botId),
73
- replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
74
- connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
75
- logger: {
76
- error: (...args) => logger.error?.(`[${botId}]`, ...args),
77
- warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
78
- info: (...args) => logger.info?.(`[${botId}]`, ...args),
79
- debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
80
- },
81
- }),
85
+ createRuntime: async ({ botId, config: botConfig, appSecret }) => {
86
+ const state = await stateFor(botId);
87
+ await workspaces.ensure(botId);
88
+ const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
89
+ return new Runtime({
90
+ config: botConfig,
91
+ appSecret,
92
+ harness: workspaceScope.harness,
93
+ state: workspaceScope.state,
94
+ replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
95
+ connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
96
+ logger: {
97
+ error: (...args) => logger.error?.(`[${botId}]`, ...args),
98
+ warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
99
+ info: (...args) => logger.info?.(`[${botId}]`, ...args),
100
+ debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
101
+ },
102
+ });
103
+ },
82
104
  deleteState: async ({ botId }) => {
83
105
  const state = stateStores.get(botId);
84
106
  stateStores.delete(botId);
85
- if (state && typeof state.remove === 'function') return state.remove();
86
- try {
87
- await unlink(statePath(botId));
88
- } catch (error) {
89
- if (error?.code !== 'ENOENT') throw error;
107
+ if (state && typeof state.remove === 'function') {
108
+ await state.remove();
109
+ } else {
110
+ try {
111
+ await unlink(statePath(botId));
112
+ } catch (error) {
113
+ if (error?.code !== 'ENOENT') throw error;
114
+ }
90
115
  }
91
116
  },
92
117
  });
118
+ const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
93
119
  const supervisor = createSupervisor({
94
120
  controller,
95
121
  harness,
@@ -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 QQ_RPC_CHANNEL = '/qq';
5
6
  export const QQ_ENDPOINTS = Object.freeze({
@@ -10,6 +11,7 @@ export const QQ_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 QQ_RPC_ENDPOINTS = Object.freeze(Object.values(QQ_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 === QQ_ENDPOINTS.setWorkspace) {
63
+ return validWorkspacePayload(payload)
64
+ ? null : '请输入工作区绝对路径。';
65
+ }
60
66
  return 'Unknown QQ endpoint.';
61
67
  }
62
68
 
@@ -120,16 +126,24 @@ export function createQqRpcHandler(controller, { encodeQr = qrDataUrl } = {}) {
120
126
  value = await publicStatus(await controller.bindCredentials(payload), cachedEncode);
121
127
  } else if (endpoint === QQ_ENDPOINTS.reconnectBot) {
122
128
  value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode);
129
+ } else if (endpoint === QQ_ENDPOINTS.setWorkspace) {
130
+ if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
131
+ value = await publicStatus(
132
+ await controller.updateWorkspace(payload.botId, payload.workspace),
133
+ cachedEncode,
134
+ );
123
135
  } else {
124
136
  value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
125
137
  }
126
138
  return signal?.aborted
127
139
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
128
140
  : { ok: true, value };
129
- } catch {
141
+ } catch (error) {
142
+ const workspaceError = publicWorkspaceError(error);
130
143
  return signal?.aborted
131
144
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
132
- : { ok: false, error: { code: 'qq-operation-failed', message: 'QQ 操作失败,请稍后重试。' } };
145
+ : { ok: false, error: workspaceError
146
+ ?? { code: 'qq-operation-failed', message: 'QQ 操作失败,请稍后重试。' } };
133
147
  }
134
148
  };
135
149
  }
@@ -3,6 +3,12 @@ import { homedir } from 'node:os';
3
3
  import { join, resolve } from 'node:path';
4
4
 
5
5
  import { createTokenConnectionSupervisor } from './connection-supervisor.mjs';
6
+ import {
7
+ BotWorkspaceStore,
8
+ createBotWorkspaceScope,
9
+ createWorkspaceAwareController,
10
+ observeBotWorkspaceRemovals,
11
+ } from '../../../../src/channels/shared/bot-workspace-store.mjs';
6
12
 
7
13
  export function harnessOrigin(webServer, configured) {
8
14
  if (configured !== undefined) return new URL(configured);
@@ -19,6 +25,7 @@ export function pluginPaths(config, channel) {
19
25
  return {
20
26
  config: resolve(config.configPath ?? join(root, 'config.json')),
21
27
  bots: resolve(config.botsDir ?? join(root, 'bots')),
28
+ workspaces: resolve(config.workspacesPath ?? join(root, 'workspaces.json')),
22
29
  };
23
30
  }
24
31
 
@@ -37,6 +44,16 @@ export async function createTokenProductionController(ctx, config, internals, de
37
44
  ? ctx.logger(`dsh-im:${channel}`) : (ctx.logger ?? console);
38
45
  const paths = pluginPaths(config, channel);
39
46
  const configStore = await new ResolvedConfigStore(paths.config).load();
47
+ const defaultWorkspace = resolve(config.workspace ?? process.cwd());
48
+ const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
49
+ const workspaces = internals.workspaces
50
+ ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
51
+ const configuredBots = configStore.list();
52
+ await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
53
+ await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
54
+ const observedConfigStore = typeof configStore.remove === 'function'
55
+ ? observeBotWorkspaceRemovals(configStore, { workspaces })
56
+ : configStore;
40
57
  const stateStores = new Map();
41
58
  const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
42
59
  const stateFor = async (botId) => {
@@ -49,41 +66,50 @@ export async function createTokenProductionController(ctx, config, internals, de
49
66
  };
50
67
  const harness = new ResolvedHarness({
51
68
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
52
- workspace: resolve(config.workspace ?? process.cwd()),
69
+ workspace: defaultWorkspace,
53
70
  agentPreset: config.agentPreset ?? 'standard',
54
71
  autostart: false,
55
72
  dshBin: config.dshBin ?? 'dsh',
56
73
  });
57
- const controller = new ResolvedController({
74
+ const coreController = new ResolvedController({
58
75
  credentials: ctx.credentials,
59
- configStore,
76
+ configStore: observedConfigStore,
60
77
  logger,
61
78
  ...(internals.inspectToken ? { inspectToken: internals.inspectToken } : {}),
62
- createRuntime: async ({ botId, config: botConfig, token }) => new ResolvedRuntime({
63
- config: botConfig,
64
- token,
65
- harness,
66
- state: await stateFor(botId),
67
- replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
68
- connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
69
- logger: {
70
- error: (...args) => logger.error?.(`[${botId}]`, ...args),
71
- warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
72
- info: (...args) => logger.info?.(`[${botId}]`, ...args),
73
- debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
74
- },
75
- }),
79
+ createRuntime: async ({ botId, config: botConfig, token }) => {
80
+ const state = await stateFor(botId);
81
+ await workspaces.ensure(botId);
82
+ const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
83
+ return new ResolvedRuntime({
84
+ config: botConfig,
85
+ token,
86
+ harness: workspaceScope.harness,
87
+ state: workspaceScope.state,
88
+ replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
89
+ connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
90
+ logger: {
91
+ error: (...args) => logger.error?.(`[${botId}]`, ...args),
92
+ warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
93
+ info: (...args) => logger.info?.(`[${botId}]`, ...args),
94
+ debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
95
+ },
96
+ });
97
+ },
76
98
  deleteState: async ({ botId }) => {
77
99
  const state = stateStores.get(botId);
78
100
  stateStores.delete(botId);
79
- if (state && typeof state.remove === 'function') return state.remove();
80
- try {
81
- await unlink(statePath(botId));
82
- } catch (error) {
83
- if (error?.code !== 'ENOENT') throw error;
101
+ if (state && typeof state.remove === 'function') {
102
+ await state.remove();
103
+ } else {
104
+ try {
105
+ await unlink(statePath(botId));
106
+ } catch (error) {
107
+ if (error?.code !== 'ENOENT') throw error;
108
+ }
84
109
  }
85
110
  },
86
111
  });
112
+ const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
87
113
  const supervisor = createSupervisor({
88
114
  channel,
89
115
  controller,
@@ -1,10 +1,16 @@
1
1
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
2
+ import {
3
+ publicWorkspaceError,
4
+ SET_WORKSPACE_ENDPOINT,
5
+ validWorkspacePayload,
6
+ } from './workspace-rpc.mjs';
2
7
 
3
8
  export const TOKEN_BOT_ENDPOINTS = Object.freeze({
4
9
  status: 'connection.status',
5
10
  bindCredentials: 'bot.bind-credentials',
6
11
  reconnectBot: 'bot.reconnect',
7
12
  deleteBot: 'bot.delete',
13
+ setWorkspace: SET_WORKSPACE_ENDPOINT,
8
14
  });
9
15
 
10
16
  const ENDPOINTS = Object.freeze(Object.values(TOKEN_BOT_ENDPOINTS));
@@ -45,6 +51,10 @@ function payloadFailure(endpoint, payload) {
45
51
  return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
46
52
  ? null : 'bot.delete requires a botId and confirm=true.';
47
53
  }
54
+ if (endpoint === TOKEN_BOT_ENDPOINTS.setWorkspace) {
55
+ return validWorkspacePayload(payload)
56
+ ? null : '请输入工作区绝对路径。';
57
+ }
48
58
  return 'Unknown bot endpoint.';
49
59
  }
50
60
 
@@ -59,6 +69,8 @@ function sanitizePublic(value) {
59
69
  }
60
70
 
61
71
  function operationError(channel, error) {
72
+ const workspaceError = publicWorkspaceError(error);
73
+ if (workspaceError) return workspaceError;
62
74
  if (error?.code === 'webhook-configured') {
63
75
  return { code: 'webhook-configured', message: error.message };
64
76
  }
@@ -93,6 +105,9 @@ export function createTokenBotRpcHandler(controller, { channel }) {
93
105
  value = await controller.bindCredentials(payload);
94
106
  } else if (endpoint === TOKEN_BOT_ENDPOINTS.reconnectBot) {
95
107
  value = await controller.reconnectBot(payload.botId);
108
+ } else if (endpoint === TOKEN_BOT_ENDPOINTS.setWorkspace) {
109
+ if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
110
+ value = await controller.updateWorkspace(payload.botId, payload.workspace);
96
111
  } else {
97
112
  value = await controller.deleteBot(payload.botId);
98
113
  }
@@ -0,0 +1,25 @@
1
+ import { isAbsolute } from 'node:path';
2
+
3
+ export const SET_WORKSPACE_ENDPOINT = 'bot.workspace.set';
4
+
5
+ export function validWorkspacePayload(payload) {
6
+ return payload !== null
7
+ && typeof payload === 'object'
8
+ && !Array.isArray(payload)
9
+ && Object.keys(payload).every((key) => ['botId', 'workspace'].includes(key))
10
+ && typeof payload.botId === 'string'
11
+ && /^[A-Za-z0-9_-]{1,128}$/.test(payload.botId)
12
+ && typeof payload.workspace === 'string'
13
+ && payload.workspace.length <= 4_096
14
+ && isAbsolute(payload.workspace.trim());
15
+ }
16
+
17
+ export function publicWorkspaceError(error) {
18
+ if (![
19
+ 'workspace-not-absolute',
20
+ 'workspace-not-found',
21
+ 'workspace-not-directory',
22
+ 'workspace-bot-not-found',
23
+ ].includes(error?.code)) return null;
24
+ return { code: error.code, message: error.message };
25
+ }
@@ -6,6 +6,12 @@ import { SlackController } from '../../../../src/channels/slack/slack-controller
6
6
  import { SlackHarnessClient } from '../../../../src/channels/slack/harness-client.mjs';
7
7
  import { SlackRuntime } from '../../../../src/channels/slack/slack-runtime.mjs';
8
8
  import { SlackStateStore } from '../../../../src/channels/slack/state-store.mjs';
9
+ import {
10
+ BotWorkspaceStore,
11
+ createBotWorkspaceScope,
12
+ createWorkspaceAwareController,
13
+ observeBotWorkspaceRemovals,
14
+ } from '../../../../src/channels/shared/bot-workspace-store.mjs';
9
15
  import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
10
16
  import { harnessOrigin, pluginPaths } from '../shared/production.mjs';
11
17
 
@@ -23,6 +29,16 @@ export async function createProductionController(ctx, config = {}, internals = {
23
29
  ? ctx.logger('dsh-im:slack') : (ctx.logger ?? console);
24
30
  const paths = pluginPaths(config, 'slack');
25
31
  const configStore = await new ResolvedConfigStore(paths.config).load();
32
+ const defaultWorkspace = resolve(config.workspace ?? process.cwd());
33
+ const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
34
+ const workspaces = internals.workspaces
35
+ ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
36
+ const configuredBots = configStore.list();
37
+ await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
38
+ await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
39
+ const observedConfigStore = typeof configStore.remove === 'function'
40
+ ? observeBotWorkspaceRemovals(configStore, { workspaces })
41
+ : configStore;
26
42
  const stateStores = new Map();
27
43
  const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
28
44
  const stateFor = async (botId) => {
@@ -35,42 +51,51 @@ export async function createProductionController(ctx, config = {}, internals = {
35
51
  };
36
52
  const harness = new ResolvedHarness({
37
53
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
38
- workspace: resolve(config.workspace ?? process.cwd()),
54
+ workspace: defaultWorkspace,
39
55
  agentPreset: config.agentPreset ?? 'standard',
40
56
  autostart: false,
41
57
  dshBin: config.dshBin ?? 'dsh',
42
58
  });
43
- const controller = new ResolvedController({
59
+ const coreController = new ResolvedController({
44
60
  credentials: ctx.credentials,
45
- configStore,
61
+ configStore: observedConfigStore,
46
62
  logger,
47
63
  ...(internals.inspectCredentials ? { inspectCredentials: internals.inspectCredentials } : {}),
48
- createRuntime: async ({ botId, config: botConfig, botToken, appToken }) => new ResolvedRuntime({
49
- config: botConfig,
50
- botToken,
51
- appToken,
52
- harness,
53
- state: await stateFor(botId),
54
- replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
55
- connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
56
- logger: {
57
- error: (...args) => logger.error?.(`[${botId}]`, ...args),
58
- warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
59
- info: (...args) => logger.info?.(`[${botId}]`, ...args),
60
- debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
61
- },
62
- }),
64
+ createRuntime: async ({ botId, config: botConfig, botToken, appToken }) => {
65
+ const state = await stateFor(botId);
66
+ await workspaces.ensure(botId);
67
+ const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
68
+ return new ResolvedRuntime({
69
+ config: botConfig,
70
+ botToken,
71
+ appToken,
72
+ harness: workspaceScope.harness,
73
+ state: workspaceScope.state,
74
+ replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
75
+ connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
76
+ logger: {
77
+ error: (...args) => logger.error?.(`[${botId}]`, ...args),
78
+ warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
79
+ info: (...args) => logger.info?.(`[${botId}]`, ...args),
80
+ debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
81
+ },
82
+ });
83
+ },
63
84
  deleteState: async ({ botId }) => {
64
85
  const state = stateStores.get(botId);
65
86
  stateStores.delete(botId);
66
- if (state && typeof state.remove === 'function') return state.remove();
67
- try {
68
- await unlink(statePath(botId));
69
- } catch (error) {
70
- if (error?.code !== 'ENOENT') throw error;
87
+ if (state && typeof state.remove === 'function') {
88
+ await state.remove();
89
+ } else {
90
+ try {
91
+ await unlink(statePath(botId));
92
+ } catch (error) {
93
+ if (error?.code !== 'ENOENT') throw error;
94
+ }
71
95
  }
72
96
  },
73
97
  });
98
+ const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
74
99
  const supervisor = createSupervisor({
75
100
  channel: 'slack',
76
101
  controller,
@@ -1,4 +1,9 @@
1
1
  import { resolveRpcAuthority } from '../../rpc-authority.mjs';
2
+ import {
3
+ publicWorkspaceError,
4
+ SET_WORKSPACE_ENDPOINT,
5
+ validWorkspacePayload,
6
+ } from '../shared/workspace-rpc.mjs';
2
7
 
3
8
  export const SLACK_RPC_CHANNEL = '/slack';
4
9
  export const SLACK_ENDPOINTS = Object.freeze({
@@ -6,6 +11,7 @@ export const SLACK_ENDPOINTS = Object.freeze({
6
11
  bindCredentials: 'bot.bind-credentials',
7
12
  reconnectBot: 'bot.reconnect',
8
13
  deleteBot: 'bot.delete',
14
+ setWorkspace: SET_WORKSPACE_ENDPOINT,
9
15
  });
10
16
  export const SLACK_RPC_ENDPOINTS = Object.freeze(Object.values(SLACK_ENDPOINTS));
11
17
 
@@ -54,6 +60,10 @@ function payloadFailure(endpoint, payload) {
54
60
  return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
55
61
  ? null : 'bot.delete requires a botId and confirm=true.';
56
62
  }
63
+ if (endpoint === SLACK_ENDPOINTS.setWorkspace) {
64
+ return validWorkspacePayload(payload)
65
+ ? null : '请输入工作区绝对路径。';
66
+ }
57
67
  return 'Unknown Slack endpoint.';
58
68
  }
59
69
 
@@ -68,6 +78,8 @@ function sanitizePublic(value) {
68
78
  }
69
79
 
70
80
  function operationError(error) {
81
+ const workspaceError = publicWorkspaceError(error);
82
+ if (workspaceError) return workspaceError;
71
83
  if (error?.code === 'slack-invalid-bot-token') {
72
84
  return { code: 'invalid-bot-token', message: 'Slack Bot Token 无效,请确认使用以 xoxb- 开头的令牌。' };
73
85
  }
@@ -103,6 +115,10 @@ export function createSlackRpcHandler(controller) {
103
115
  if (endpoint === SLACK_ENDPOINTS.status) value = await controller.status();
104
116
  else if (endpoint === SLACK_ENDPOINTS.bindCredentials) value = await controller.bindCredentials(payload);
105
117
  else if (endpoint === SLACK_ENDPOINTS.reconnectBot) value = await controller.reconnectBot(payload.botId);
118
+ else if (endpoint === SLACK_ENDPOINTS.setWorkspace) {
119
+ if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
120
+ value = await controller.updateWorkspace(payload.botId, payload.workspace);
121
+ }
106
122
  else value = await controller.deleteBot(payload.botId);
107
123
  return signal?.aborted
108
124
  ? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
@@ -8,6 +8,12 @@ import { WecomQrAuth } from '../../../../src/channels/wecom/qr-auth.mjs';
8
8
  import { WecomStateStore } from '../../../../src/channels/wecom/state-store.mjs';
9
9
  import { WecomController } from '../../../../src/channels/wecom/wecom-controller.mjs';
10
10
  import { WecomRuntime } from '../../../../src/channels/wecom/wecom-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) {
@@ -25,6 +31,7 @@ function pluginPaths(config) {
25
31
  return {
26
32
  config: resolve(config.configPath ?? join(root, 'config.json')),
27
33
  bots: resolve(config.botsDir ?? join(root, 'bots')),
34
+ workspaces: resolve(config.workspacesPath ?? join(root, 'workspaces.json')),
28
35
  };
29
36
  }
30
37
 
@@ -42,6 +49,16 @@ export async function createProductionController(ctx, config = {}, internals = {
42
49
  const logger = typeof ctx.logger === 'function' ? ctx.logger('dsh-im:wecom') : (ctx.logger ?? console);
43
50
  const paths = pluginPaths(config);
44
51
  const configStore = await new ConfigStore(paths.config).load();
52
+ const defaultWorkspace = resolve(config.workspace ?? process.cwd());
53
+ const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
54
+ const workspaces = internals.workspaces
55
+ ?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
56
+ const configuredBots = configStore.list();
57
+ await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
58
+ await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
59
+ const observedConfigStore = typeof configStore.remove === 'function'
60
+ ? observeBotWorkspaceRemovals(configStore, { workspaces })
61
+ : configStore;
45
62
  const qrAuth = internals.qrAuth ?? new QrAuth({
46
63
  source: config.qrSource ?? 'deepseek-harness',
47
64
  platform: config.qrPlatform,
@@ -58,42 +75,51 @@ 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
  qrAuth,
68
85
  credentials: ctx.credentials,
69
- configStore,
86
+ configStore: observedConfigStore,
70
87
  logger,
71
- createRuntime: async ({ botId, config: botConfig, secret }) => new Runtime({
72
- config: botConfig,
73
- secret,
74
- harness,
75
- state: await stateFor(botId),
76
- replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
77
- connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
78
- maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
79
- logger: {
80
- error: (...args) => logger.error?.(`[${botId}]`, ...args),
81
- warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
82
- info: (...args) => logger.info?.(`[${botId}]`, ...args),
83
- debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
84
- },
85
- }),
88
+ createRuntime: async ({ botId, config: botConfig, secret }) => {
89
+ const state = await stateFor(botId);
90
+ await workspaces.ensure(botId);
91
+ const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
92
+ return new Runtime({
93
+ config: botConfig,
94
+ secret,
95
+ harness: workspaceScope.harness,
96
+ state: workspaceScope.state,
97
+ replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
98
+ connectTimeoutMs: config.connectTimeoutMs ?? 20_000,
99
+ maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
100
+ logger: {
101
+ error: (...args) => logger.error?.(`[${botId}]`, ...args),
102
+ warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
103
+ info: (...args) => logger.info?.(`[${botId}]`, ...args),
104
+ debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
105
+ },
106
+ });
107
+ },
86
108
  deleteState: async ({ botId }) => {
87
109
  const state = stateStores.get(botId);
88
110
  stateStores.delete(botId);
89
- if (state && typeof state.remove === 'function') return state.remove();
90
- try {
91
- await unlink(statePath(botId));
92
- } catch (error) {
93
- if (error?.code !== 'ENOENT') throw error;
111
+ if (state && typeof state.remove === 'function') {
112
+ await state.remove();
113
+ } else {
114
+ try {
115
+ await unlink(statePath(botId));
116
+ } catch (error) {
117
+ if (error?.code !== 'ENOENT') throw error;
118
+ }
94
119
  }
95
120
  },
96
121
  });
122
+ const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
97
123
  const supervisor = createSupervisor({
98
124
  controller,
99
125
  harness,