@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.
- package/README.md +64 -0
- package/assets/logo-icon.png +0 -0
- package/assets/logo.png +0 -0
- package/lib/client.js +765 -295
- package/lib/index.js +112 -110
- package/package.json +2 -1
- package/plugin-src/client/build.mjs +6 -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 +15 -1
- package/plugin-src/client/index.js +28 -1
- package/plugin-src/client/styles.js +32 -9
- 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 +22 -11
- package/src/channels/dingtalk/dingtalk-controller.mjs +6 -1
- package/src/channels/dingtalk/harness-client.mjs +4 -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 +36 -16
- package/src/channels/feishu/harness-client.mjs +6 -5
- package/src/channels/feishu/state-store.mjs +5 -0
- package/src/channels/qq/qq-bridge.mjs +25 -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 +584 -0
- package/src/channels/shared/conversation-state-store.mjs +5 -0
- package/src/channels/shared/text-harness-bridge.mjs +23 -13
- package/src/channels/shared/workspace-command.mjs +26 -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 +22 -13
- package/src/channels/wecom/wecom-controller.mjs +8 -1
- package/src/channels/weixin/harness-client.mjs +6 -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 +16 -6
- package/src/channels/weixin/weixin-controller.mjs +6 -1
- 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:
|
|
75
|
+
workspace: defaultWorkspace,
|
|
59
76
|
agentPreset: config.agentPreset ?? 'standard',
|
|
60
77
|
autostart: false,
|
|
61
78
|
dshBin: config.dshBin ?? 'dsh',
|
|
62
79
|
});
|
|
63
|
-
const
|
|
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 }) =>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
harness,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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')
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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:
|
|
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:
|
|
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,
|