@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.
- package/README.md +36 -0
- package/lib/client.js +709 -285
- package/lib/index.js +114 -110
- package/package.json +1 -1
- package/plugin-src/client/channels/dingtalk/api.js +2 -0
- package/plugin-src/client/channels/dingtalk/index.js +67 -14
- package/plugin-src/client/channels/feishu/api.js +2 -0
- package/plugin-src/client/channels/feishu/index.js +70 -22
- package/plugin-src/client/channels/qq/api.js +2 -0
- package/plugin-src/client/channels/qq/index.js +45 -8
- package/plugin-src/client/channels/shared/token-api.js +2 -0
- package/plugin-src/client/channels/shared/token-channel.js +34 -8
- package/plugin-src/client/channels/wecom/api.js +2 -0
- package/plugin-src/client/channels/wecom/index.js +45 -8
- package/plugin-src/client/channels/weixin/api.js +2 -0
- package/plugin-src/client/channels/weixin/index.js +68 -8
- package/plugin-src/client/channels/whatsapp/api.js +2 -0
- package/plugin-src/client/channels/whatsapp/index.js +27 -5
- package/plugin-src/client/i18n.js +13 -0
- package/plugin-src/client/styles.js +13 -0
- package/plugin-src/client/workspace-editor.js +96 -0
- package/plugin-src/client/workspace-snapshot-fence.js +28 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +34 -11
- package/plugin-src/host/channels/dingtalk/rpc.mjs +17 -2
- package/plugin-src/host/channels/feishu/production.mjs +42 -5
- package/plugin-src/host/channels/feishu/rpc.mjs +17 -2
- package/plugin-src/host/channels/qq/production.mjs +48 -22
- package/plugin-src/host/channels/qq/rpc.mjs +16 -2
- package/plugin-src/host/channels/shared/production.mjs +48 -22
- package/plugin-src/host/channels/shared/rpc.mjs +15 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +25 -0
- package/plugin-src/host/channels/slack/production.mjs +48 -23
- package/plugin-src/host/channels/slack/rpc.mjs +16 -0
- package/plugin-src/host/channels/wecom/production.mjs +49 -23
- package/plugin-src/host/channels/wecom/rpc.mjs +16 -2
- package/plugin-src/host/channels/weixin/production.mjs +31 -11
- package/plugin-src/host/channels/weixin/rpc.mjs +21 -2
- package/plugin-src/host/channels/whatsapp/production.mjs +49 -23
- package/plugin-src/host/channels/whatsapp/rpc.mjs +16 -2
- package/src/channels/dingtalk/dingtalk-bridge.mjs +25 -11
- package/src/channels/dingtalk/dingtalk-controller.mjs +6 -1
- package/src/channels/dingtalk/harness-client.mjs +17 -3
- package/src/channels/dingtalk/state-store.mjs +5 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +39 -16
- package/src/channels/feishu/harness-client.mjs +19 -5
- package/src/channels/feishu/state-store.mjs +5 -0
- package/src/channels/qq/qq-bridge.mjs +28 -15
- package/src/channels/qq/qq-controller.mjs +8 -1
- package/src/channels/qq/state-store.mjs +5 -0
- package/src/channels/shared/bot-workspace-store.mjs +618 -0
- package/src/channels/shared/conversation-state-store.mjs +5 -0
- package/src/channels/shared/text-harness-bridge.mjs +26 -13
- package/src/channels/shared/workspace-command.mjs +115 -0
- package/src/channels/shared/workspace-session.mjs +44 -0
- package/src/channels/wecom/state-store.mjs +5 -0
- package/src/channels/wecom/wecom-bridge.mjs +25 -13
- package/src/channels/wecom/wecom-controller.mjs +8 -1
- package/src/channels/weixin/harness-client.mjs +19 -5
- package/src/channels/weixin/state-store.mjs +5 -0
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +19 -6
- package/src/channels/weixin/weixin-controller.mjs +6 -1
- 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:
|
|
69
|
+
workspace: defaultWorkspace,
|
|
53
70
|
agentPreset: config.agentPreset ?? 'standard',
|
|
54
71
|
autostart: false,
|
|
55
72
|
dshBin: config.dshBin ?? 'dsh',
|
|
56
73
|
});
|
|
57
|
-
const
|
|
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 }) =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
harness,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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')
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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:
|
|
54
|
+
workspace: defaultWorkspace,
|
|
39
55
|
agentPreset: config.agentPreset ?? 'standard',
|
|
40
56
|
autostart: false,
|
|
41
57
|
dshBin: config.dshBin ?? 'dsh',
|
|
42
58
|
});
|
|
43
|
-
const
|
|
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 }) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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')
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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:
|
|
78
|
+
workspace: defaultWorkspace,
|
|
62
79
|
agentPreset: config.agentPreset ?? 'standard',
|
|
63
80
|
autostart: false,
|
|
64
81
|
dshBin: config.dshBin ?? 'dsh',
|
|
65
82
|
});
|
|
66
|
-
const
|
|
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 }) =>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
harness,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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')
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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:
|
|
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:
|
|
78
|
+
workspace: defaultWorkspace,
|
|
62
79
|
agentPreset: config.agentPreset ?? 'standard',
|
|
63
80
|
autostart: false,
|
|
64
81
|
dshBin: config.dshBin ?? 'dsh',
|
|
65
82
|
});
|
|
66
|
-
const
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
}
|