@xmanrui/dsh-im 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +36 -0
  2. package/lib/client.js +709 -285
  3. package/lib/index.js +114 -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 +25 -11
  41. package/src/channels/dingtalk/dingtalk-controller.mjs +6 -1
  42. package/src/channels/dingtalk/harness-client.mjs +17 -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 +39 -16
  46. package/src/channels/feishu/harness-client.mjs +19 -5
  47. package/src/channels/feishu/state-store.mjs +5 -0
  48. package/src/channels/qq/qq-bridge.mjs +28 -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 +618 -0
  52. package/src/channels/shared/conversation-state-store.mjs +5 -0
  53. package/src/channels/shared/text-harness-bridge.mjs +26 -13
  54. package/src/channels/shared/workspace-command.mjs +115 -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 +25 -13
  58. package/src/channels/wecom/wecom-controller.mjs +8 -1
  59. package/src/channels/weixin/harness-client.mjs +19 -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 +19 -6
  63. package/src/channels/weixin/weixin-controller.mjs +6 -1
  64. package/src/channels/whatsapp/whatsapp-controller.mjs +8 -1
@@ -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,
@@ -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
  }