@xmanrui/dsh-im 0.4.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 +30 -0
- package/lib/client.js +709 -285
- package/lib/index.js +112 -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 +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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { h } from './i18n.js';
|
|
4
|
+
|
|
5
|
+
function looksLikeAbsolutePath(value) {
|
|
6
|
+
return value.startsWith('/') || /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function WorkspaceEditor({ workspace, disabled = false, onSave }) {
|
|
10
|
+
const [editing, setEditing] = React.useState(false);
|
|
11
|
+
const [draft, setDraft] = React.useState(workspace ?? '');
|
|
12
|
+
const [saving, setSaving] = React.useState(false);
|
|
13
|
+
const [error, setError] = React.useState(null);
|
|
14
|
+
const inputRef = React.useRef(null);
|
|
15
|
+
const editButtonRef = React.useRef(null);
|
|
16
|
+
const restoreFocus = React.useRef(false);
|
|
17
|
+
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
if (!editing) setDraft(workspace ?? '');
|
|
20
|
+
}, [workspace, editing]);
|
|
21
|
+
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
if (editing) inputRef.current?.focus();
|
|
24
|
+
else if (restoreFocus.current) {
|
|
25
|
+
restoreFocus.current = false;
|
|
26
|
+
editButtonRef.current?.focus();
|
|
27
|
+
}
|
|
28
|
+
}, [editing]);
|
|
29
|
+
|
|
30
|
+
const cancel = () => {
|
|
31
|
+
setDraft(workspace ?? '');
|
|
32
|
+
setError(null);
|
|
33
|
+
restoreFocus.current = true;
|
|
34
|
+
setEditing(false);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const submit = async (event) => {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
const value = draft.trim();
|
|
40
|
+
if (!value || saving || disabled) return;
|
|
41
|
+
if (!looksLikeAbsolutePath(value)) {
|
|
42
|
+
setError('工作区必须是绝对路径。');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
setSaving(true);
|
|
46
|
+
setError(null);
|
|
47
|
+
try {
|
|
48
|
+
await onSave?.(value);
|
|
49
|
+
restoreFocus.current = true;
|
|
50
|
+
setEditing(false);
|
|
51
|
+
} catch (cause) {
|
|
52
|
+
setError(cause?.message ?? '工作区修改失败,请重试。');
|
|
53
|
+
} finally {
|
|
54
|
+
setSaving(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return h('div', { className: 'dim-workspace' },
|
|
59
|
+
h('div', { className: 'dim-workspaceHeader' },
|
|
60
|
+
h('span', null, '当前工作区'),
|
|
61
|
+
editing ? null : h('button', {
|
|
62
|
+
type: 'button',
|
|
63
|
+
ref: editButtonRef,
|
|
64
|
+
className: 'dim-workspaceEdit',
|
|
65
|
+
onClick: () => { setEditing(true); setError(null); },
|
|
66
|
+
disabled,
|
|
67
|
+
}, '修改')),
|
|
68
|
+
editing
|
|
69
|
+
? h('form', { className: 'dim-workspaceForm', onSubmit: submit },
|
|
70
|
+
h('input', {
|
|
71
|
+
ref: inputRef,
|
|
72
|
+
value: draft,
|
|
73
|
+
onChange: (event) => setDraft(event.target.value),
|
|
74
|
+
placeholder: '/绝对路径/到/工作区',
|
|
75
|
+
'aria-label': '工作区绝对路径',
|
|
76
|
+
autoCapitalize: 'none',
|
|
77
|
+
autoCorrect: 'off',
|
|
78
|
+
spellCheck: false,
|
|
79
|
+
maxLength: 4_096,
|
|
80
|
+
required: true,
|
|
81
|
+
disabled: saving || disabled,
|
|
82
|
+
}),
|
|
83
|
+
h('div', { className: 'dim-workspaceActions' },
|
|
84
|
+
h('button', {
|
|
85
|
+
type: 'submit', disabled: saving || disabled || !draft.trim(),
|
|
86
|
+
}, saving ? '保存中…' : '保存'),
|
|
87
|
+
h('button', { type: 'button', onClick: cancel, disabled: saving || disabled }, '取消')),
|
|
88
|
+
error ? h('p', { className: 'dim-workspaceError', role: 'alert' }, error) : null)
|
|
89
|
+
: workspace
|
|
90
|
+
? React.createElement('code', {
|
|
91
|
+
className: 'dim-workspacePath',
|
|
92
|
+
title: workspace,
|
|
93
|
+
}, workspace)
|
|
94
|
+
: h('code', { className: 'dim-workspacePath' }, '未设置'),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** Keep every full-snapshot response ordered behind the latest client mutation. */
|
|
4
|
+
export function useWorkspaceSnapshotFence() {
|
|
5
|
+
const state = React.useRef({ version: 0, pendingMutations: 0 });
|
|
6
|
+
return React.useMemo(() => Object.freeze({
|
|
7
|
+
beginStatus() {
|
|
8
|
+
return state.current.pendingMutations === 0 ? state.current.version : null;
|
|
9
|
+
},
|
|
10
|
+
canCommitStatus(version) {
|
|
11
|
+
return version !== null
|
|
12
|
+
&& state.current.pendingMutations === 0
|
|
13
|
+
&& state.current.version === version;
|
|
14
|
+
},
|
|
15
|
+
beginMutation() {
|
|
16
|
+
state.current.pendingMutations += 1;
|
|
17
|
+
state.current.version += 1;
|
|
18
|
+
return state.current.version;
|
|
19
|
+
},
|
|
20
|
+
canCommitMutation(version) {
|
|
21
|
+
return state.current.version === version;
|
|
22
|
+
},
|
|
23
|
+
endMutation() {
|
|
24
|
+
state.current.pendingMutations = Math.max(0, state.current.pendingMutations - 1);
|
|
25
|
+
return state.current.pendingMutations === 0;
|
|
26
|
+
},
|
|
27
|
+
}), []);
|
|
28
|
+
}
|
|
@@ -8,6 +8,12 @@ import { DingtalkController } from '../../../../src/channels/dingtalk/dingtalk-c
|
|
|
8
8
|
import { DingtalkRuntime } from '../../../../src/channels/dingtalk/dingtalk-runtime.mjs';
|
|
9
9
|
import { HarnessClient } from '../../../../src/channels/dingtalk/harness-client.mjs';
|
|
10
10
|
import { DingtalkStateStore } from '../../../../src/channels/dingtalk/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) {
|
|
@@ -26,6 +32,7 @@ function pluginPaths(config) {
|
|
|
26
32
|
root,
|
|
27
33
|
config: resolve(config.configPath ?? join(root, 'config.json')),
|
|
28
34
|
bots: resolve(config.botsDir ?? join(root, 'bots')),
|
|
35
|
+
workspaces: resolve(config.workspacesPath ?? join(root, 'workspaces.json')),
|
|
29
36
|
};
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -45,6 +52,19 @@ 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 canListConfiguredBots = typeof configStore.list === 'function';
|
|
60
|
+
const configuredBots = canListConfiguredBots ? configStore.list() : [];
|
|
61
|
+
if (canListConfiguredBots) {
|
|
62
|
+
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
|
|
63
|
+
}
|
|
64
|
+
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
|
|
65
|
+
const observedConfigStore = typeof configStore.remove === 'function'
|
|
66
|
+
? observeBotWorkspaceRemovals(configStore, { workspaces })
|
|
67
|
+
: configStore;
|
|
48
68
|
const deviceAuth = internals.deviceAuth ?? new DeviceAuth({
|
|
49
69
|
baseUrl: config.registrationBaseUrl,
|
|
50
70
|
});
|
|
@@ -61,23 +81,25 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
61
81
|
};
|
|
62
82
|
const harness = new Harness({
|
|
63
83
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
64
|
-
workspace:
|
|
84
|
+
workspace: defaultWorkspace,
|
|
65
85
|
agentPreset: config.agentPreset ?? 'standard',
|
|
66
86
|
autostart: false,
|
|
67
87
|
dshBin: config.dshBin ?? 'dsh',
|
|
68
88
|
});
|
|
69
|
-
const
|
|
89
|
+
const coreController = new Controller({
|
|
70
90
|
deviceAuth,
|
|
71
91
|
credentials: ctx.credentials,
|
|
72
|
-
configStore,
|
|
92
|
+
configStore: observedConfigStore,
|
|
73
93
|
logger,
|
|
74
94
|
createRuntime: async ({ botId, config: botConfig, clientSecret }) => {
|
|
75
95
|
const state = await stateFor(botId);
|
|
96
|
+
await workspaces.ensure(botId);
|
|
97
|
+
const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
|
|
76
98
|
return new Runtime({
|
|
77
99
|
config: botConfig,
|
|
78
100
|
clientSecret,
|
|
79
|
-
harness,
|
|
80
|
-
state,
|
|
101
|
+
harness: workspaceScope.harness,
|
|
102
|
+
state: workspaceScope.state,
|
|
81
103
|
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
|
82
104
|
maxMessageChars: config.maxMessageChars ?? 4_000,
|
|
83
105
|
connectTimeoutMs: config.connectTimeoutMs ?? 15_000,
|
|
@@ -94,15 +116,16 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
94
116
|
stateStores.delete(botId);
|
|
95
117
|
if (state && typeof state.remove === 'function') {
|
|
96
118
|
await state.remove();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
119
|
+
} else {
|
|
120
|
+
try {
|
|
121
|
+
await unlink(statePath(botId));
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
124
|
+
}
|
|
103
125
|
}
|
|
104
126
|
},
|
|
105
127
|
});
|
|
128
|
+
const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
|
|
106
129
|
const supervisor = createSupervisor({
|
|
107
130
|
controller,
|
|
108
131
|
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 DINGTALK_RPC_CHANNEL = '/dingtalk';
|
|
5
6
|
export const DINGTALK_ENDPOINTS = Object.freeze({
|
|
@@ -10,6 +11,7 @@ export const DINGTALK_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
|
approveSender: 'bot.sender.approve',
|
|
14
16
|
revokeSender: 'bot.sender.revoke',
|
|
15
17
|
});
|
|
@@ -76,6 +78,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
76
78
|
? null
|
|
77
79
|
: 'bot.delete requires a botId and confirm=true.';
|
|
78
80
|
}
|
|
81
|
+
if (endpoint === DINGTALK_ENDPOINTS.setWorkspace) {
|
|
82
|
+
return validWorkspacePayload(payload)
|
|
83
|
+
? null : '请输入工作区绝对路径。';
|
|
84
|
+
}
|
|
79
85
|
if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
|
|
80
86
|
return exactKeys(payload, ['botId', 'requestId', 'confirm'])
|
|
81
87
|
&& validId(payload.botId)
|
|
@@ -207,6 +213,12 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
207
213
|
value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode);
|
|
208
214
|
} else if (endpoint === DINGTALK_ENDPOINTS.deleteBot) {
|
|
209
215
|
value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
|
|
216
|
+
} else if (endpoint === DINGTALK_ENDPOINTS.setWorkspace) {
|
|
217
|
+
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
218
|
+
value = await publicStatus(
|
|
219
|
+
await controller.updateWorkspace(payload.botId, payload.workspace),
|
|
220
|
+
cachedEncode,
|
|
221
|
+
);
|
|
210
222
|
} else if (endpoint === DINGTALK_ENDPOINTS.approveSender) {
|
|
211
223
|
value = await publicStatus(
|
|
212
224
|
await controller.approveSender(payload.botId, payload.requestId),
|
|
@@ -219,8 +231,11 @@ export function createDingtalkRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
219
231
|
);
|
|
220
232
|
}
|
|
221
233
|
return signal?.aborted ? cancelled() : { ok: true, value };
|
|
222
|
-
} catch {
|
|
223
|
-
|
|
234
|
+
} catch (error) {
|
|
235
|
+
const workspaceError = publicWorkspaceError(error);
|
|
236
|
+
return signal?.aborted ? cancelled() : workspaceError
|
|
237
|
+
? { ok: false, error: workspaceError }
|
|
238
|
+
: internalFailure();
|
|
224
239
|
}
|
|
225
240
|
};
|
|
226
241
|
}
|
|
@@ -12,6 +12,12 @@ import {
|
|
|
12
12
|
} from '../../../../src/channels/feishu/plugin-config-store.mjs';
|
|
13
13
|
import { MultiBotDshFeishuController } from '../../../../src/channels/feishu/multi-bot-controller.mjs';
|
|
14
14
|
import { StateStore } from '../../../../src/channels/feishu/state-store.mjs';
|
|
15
|
+
import {
|
|
16
|
+
BotWorkspaceStore,
|
|
17
|
+
createBotWorkspaceScope,
|
|
18
|
+
createWorkspaceAwareController,
|
|
19
|
+
observeBotWorkspaceRemovals,
|
|
20
|
+
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
15
21
|
|
|
16
22
|
function harnessOrigin(webServer, configured) {
|
|
17
23
|
if (configured !== undefined) return new URL(configured);
|
|
@@ -34,6 +40,7 @@ function pluginPaths(config) {
|
|
|
34
40
|
config: resolve(config.configPath ?? join(root, 'config.json')),
|
|
35
41
|
legacyState: resolve(config.statePath ?? join(root, 'state.json')),
|
|
36
42
|
bots: resolve(config.botsDir ?? join(root, 'bots')),
|
|
43
|
+
workspaces: resolve(config.workspacesPath ?? join(root, 'workspaces.json')),
|
|
37
44
|
};
|
|
38
45
|
}
|
|
39
46
|
|
|
@@ -58,6 +65,24 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
58
65
|
: (ctx.logger ?? console);
|
|
59
66
|
const paths = pluginPaths(config);
|
|
60
67
|
const configStore = await new ConfigStore(paths.config).load();
|
|
68
|
+
const defaultWorkspace = resolve(config.workspace ?? process.cwd());
|
|
69
|
+
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
70
|
+
const workspaces = internals.workspaces
|
|
71
|
+
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
72
|
+
const canListConfiguredBots = typeof configStore.list === 'function';
|
|
73
|
+
const listConfiguredBots = () => canListConfiguredBots ? configStore.list() : [];
|
|
74
|
+
const configuredBots = listConfiguredBots();
|
|
75
|
+
if (canListConfiguredBots) {
|
|
76
|
+
await workspaces.reconcile(configuredBots.map((bot) => bot.id));
|
|
77
|
+
}
|
|
78
|
+
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.id)));
|
|
79
|
+
const observedConfigStore = typeof configStore.removeBot === 'function'
|
|
80
|
+
? observeBotWorkspaceRemovals(configStore, {
|
|
81
|
+
workspaces,
|
|
82
|
+
method: 'removeBot',
|
|
83
|
+
botIdFromRemoved: (removed) => removed.id,
|
|
84
|
+
})
|
|
85
|
+
: configStore;
|
|
61
86
|
// State is lazy per bot. A corrupt legacy file can therefore fail only the
|
|
62
87
|
// migrated bot and cannot prevent healthy v2 bots from starting.
|
|
63
88
|
const stateStores = new Map();
|
|
@@ -75,9 +100,14 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
75
100
|
}
|
|
76
101
|
return state;
|
|
77
102
|
};
|
|
103
|
+
const stateForBotId = async (botId) => {
|
|
104
|
+
const botConfig = listConfiguredBots().find((bot) => bot.id === botId);
|
|
105
|
+
if (!botConfig) throw new Error('Unknown Feishu bot');
|
|
106
|
+
return stateFor(botConfig);
|
|
107
|
+
};
|
|
78
108
|
const harness = new Harness({
|
|
79
109
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
80
|
-
workspace:
|
|
110
|
+
workspace: defaultWorkspace,
|
|
81
111
|
agentPreset: config.agentPreset ?? 'standard',
|
|
82
112
|
// This plugin is already hosted by a running DSH process. Starting a
|
|
83
113
|
// second DSH would create a competing server and lifecycle.
|
|
@@ -85,21 +115,24 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
85
115
|
dshBin: config.dshBin ?? 'dsh',
|
|
86
116
|
});
|
|
87
117
|
|
|
88
|
-
const
|
|
118
|
+
const coreController = new Controller({
|
|
89
119
|
registerApp: (options) => lark.registerApp(options),
|
|
90
120
|
verifyApp,
|
|
91
121
|
credentials: ctx.credentials,
|
|
92
|
-
configStore,
|
|
122
|
+
configStore: observedConfigStore,
|
|
93
123
|
createRuntime: async ({ botId, config: botConfig, appSecret }) => {
|
|
94
124
|
const state = await stateFor(botConfig);
|
|
125
|
+
const id = botId ?? botConfig.id ?? botConfig.appId;
|
|
126
|
+
await workspaces.ensure(id);
|
|
127
|
+
const workspaceScope = createBotWorkspaceScope(harness, { botId: id, workspaces, state });
|
|
95
128
|
return new Runtime({
|
|
96
129
|
lark,
|
|
97
130
|
appId: botConfig.appId,
|
|
98
131
|
appSecret,
|
|
99
132
|
domain: botConfig.domain,
|
|
100
133
|
ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId],
|
|
101
|
-
harness,
|
|
102
|
-
state,
|
|
134
|
+
harness: workspaceScope.harness,
|
|
135
|
+
state: workspaceScope.state,
|
|
103
136
|
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
|
104
137
|
logger: {
|
|
105
138
|
error: (...args) => logger.error?.(`[${botId ?? botConfig.id}]`, ...args),
|
|
@@ -118,6 +151,10 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
118
151
|
}
|
|
119
152
|
},
|
|
120
153
|
});
|
|
154
|
+
const controller = createWorkspaceAwareController(coreController, {
|
|
155
|
+
workspaces,
|
|
156
|
+
stateFor: stateForBotId,
|
|
157
|
+
});
|
|
121
158
|
|
|
122
159
|
const supervisor = createSupervisor({
|
|
123
160
|
controller,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import QRCode from 'qrcode';
|
|
2
2
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
3
|
+
import { publicWorkspaceError, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
3
4
|
import {
|
|
4
5
|
FEISHU_ENDPOINTS,
|
|
5
6
|
FEISHU_RPC_CHANNEL,
|
|
@@ -174,6 +175,7 @@ function publicBotEntry(entry) {
|
|
|
174
175
|
bot: publicBot(source.bot),
|
|
175
176
|
health: publicHealth(source, connected),
|
|
176
177
|
};
|
|
178
|
+
if (typeof source.workspace === 'string' && source.workspace) result.workspace = source.workspace;
|
|
177
179
|
const error = publicError(source.error);
|
|
178
180
|
if (error) result.error = error;
|
|
179
181
|
return result;
|
|
@@ -268,6 +270,10 @@ function validPayload(endpoint, payload) {
|
|
|
268
270
|
? null
|
|
269
271
|
: 'Deleting a bot requires a valid botId and confirm=true.';
|
|
270
272
|
}
|
|
273
|
+
if (endpoint === FEISHU_ENDPOINTS.setWorkspace) {
|
|
274
|
+
return validWorkspacePayload(payload)
|
|
275
|
+
? null : '请输入工作区绝对路径。';
|
|
276
|
+
}
|
|
271
277
|
return 'Unknown Feishu endpoint.';
|
|
272
278
|
}
|
|
273
279
|
|
|
@@ -429,14 +435,23 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
|
|
429
435
|
} else if (endpoint === FEISHU_MULTI_ENDPOINTS.disconnectBot) {
|
|
430
436
|
if (typeof controller.disconnectBot !== 'function') throw new Error('Multi-bot disconnect is unavailable');
|
|
431
437
|
value = await toPublicFeishuStatus(await controller.disconnectBot(payload.botId), { encodeQr: cachedEncodeQr });
|
|
438
|
+
} else if (endpoint === FEISHU_ENDPOINTS.setWorkspace) {
|
|
439
|
+
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
440
|
+
value = await toPublicFeishuStatus(
|
|
441
|
+
await controller.updateWorkspace(payload.botId, payload.workspace),
|
|
442
|
+
{ encodeQr: cachedEncodeQr },
|
|
443
|
+
);
|
|
432
444
|
} else {
|
|
433
445
|
if (typeof controller.deleteBot !== 'function') throw new Error('Multi-bot delete is unavailable');
|
|
434
446
|
value = await toPublicFeishuStatus(await controller.deleteBot(payload.botId), { encodeQr: cachedEncodeQr });
|
|
435
447
|
}
|
|
436
448
|
if (signal?.aborted) return cancelled();
|
|
437
449
|
return { ok: true, value };
|
|
438
|
-
} catch {
|
|
439
|
-
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const workspaceError = publicWorkspaceError(error);
|
|
452
|
+
return signal?.aborted ? cancelled() : workspaceError
|
|
453
|
+
? { ok: false, error: { ...workspaceError, details: {} } }
|
|
454
|
+
: internalFailure();
|
|
440
455
|
}
|
|
441
456
|
};
|
|
442
457
|
}
|
|
@@ -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
|
}
|