@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
|
@@ -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
|
}
|
|
@@ -8,6 +8,12 @@ import { WhatsappStateStore } from '../../../../src/channels/whatsapp/state-stor
|
|
|
8
8
|
import { WhatsappController } from '../../../../src/channels/whatsapp/whatsapp-controller.mjs';
|
|
9
9
|
import { WhatsappRuntime } from '../../../../src/channels/whatsapp/whatsapp-runtime.mjs';
|
|
10
10
|
import { createWhatsappWebSession } from '../../../../src/channels/whatsapp/whatsapp-web-session.mjs';
|
|
11
|
+
import {
|
|
12
|
+
BotWorkspaceStore,
|
|
13
|
+
createBotWorkspaceScope,
|
|
14
|
+
createWorkspaceAwareController,
|
|
15
|
+
observeBotWorkspaceRemovals,
|
|
16
|
+
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
11
17
|
import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
|
|
12
18
|
|
|
13
19
|
const AUTH_DIRECTORY_PATTERN = /^[a-f0-9-]{36}$/;
|
|
@@ -32,6 +38,7 @@ function pluginPaths(config) {
|
|
|
32
38
|
return {
|
|
33
39
|
config: resolve(config.configPath ?? join(root, 'config.json')),
|
|
34
40
|
bots: resolve(config.botsDir ?? join(root, 'bots')),
|
|
41
|
+
workspaces: resolve(config.workspacesPath ?? join(root, 'workspaces.json')),
|
|
35
42
|
authPath,
|
|
36
43
|
};
|
|
37
44
|
}
|
|
@@ -49,6 +56,16 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
49
56
|
const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
|
|
50
57
|
const paths = pluginPaths(config);
|
|
51
58
|
const configStore = await new ConfigStore(paths.config).load();
|
|
59
|
+
const defaultWorkspace = resolve(config.workspace ?? process.cwd());
|
|
60
|
+
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
61
|
+
const workspaces = internals.workspaces
|
|
62
|
+
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
63
|
+
const configuredBots = configStore.list();
|
|
64
|
+
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
|
|
65
|
+
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
|
|
66
|
+
const observedConfigStore = typeof configStore.remove === 'function'
|
|
67
|
+
? observeBotWorkspaceRemovals(configStore, { workspaces })
|
|
68
|
+
: configStore;
|
|
52
69
|
const stateStores = new Map();
|
|
53
70
|
const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
|
|
54
71
|
const stateFor = async (botId) => {
|
|
@@ -61,31 +78,36 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
61
78
|
};
|
|
62
79
|
const harness = new Harness({
|
|
63
80
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
64
|
-
workspace:
|
|
81
|
+
workspace: defaultWorkspace,
|
|
65
82
|
agentPreset: config.agentPreset ?? 'standard',
|
|
66
83
|
autostart: false,
|
|
67
84
|
dshBin: config.dshBin ?? 'dsh',
|
|
68
85
|
});
|
|
69
|
-
const
|
|
70
|
-
configStore,
|
|
86
|
+
const coreController = new Controller({
|
|
87
|
+
configStore: observedConfigStore,
|
|
71
88
|
authPath: paths.authPath,
|
|
72
89
|
createSession,
|
|
73
90
|
logger,
|
|
74
|
-
createRuntime: async ({ botId, config: botConfig, authDir }) =>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
harness,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
createRuntime: async ({ botId, config: botConfig, authDir }) => {
|
|
92
|
+
const state = await stateFor(botId);
|
|
93
|
+
await workspaces.ensure(botId);
|
|
94
|
+
const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
|
|
95
|
+
return new Runtime({
|
|
96
|
+
config: botConfig,
|
|
97
|
+
authDir,
|
|
98
|
+
harness: workspaceScope.harness,
|
|
99
|
+
state: workspaceScope.state,
|
|
100
|
+
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
|
101
|
+
connectTimeoutMs: config.connectTimeoutMs ?? 30_000,
|
|
102
|
+
createSession,
|
|
103
|
+
logger: {
|
|
104
|
+
error: (...args) => logger.error?.(`[${botId}]`, ...args),
|
|
105
|
+
warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
|
|
106
|
+
info: (...args) => logger.info?.(`[${botId}]`, ...args),
|
|
107
|
+
debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
},
|
|
89
111
|
deleteAuth: (authDirectory) => rm(paths.authPath(authDirectory), {
|
|
90
112
|
recursive: true,
|
|
91
113
|
force: true,
|
|
@@ -93,14 +115,18 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
93
115
|
deleteState: async ({ botId }) => {
|
|
94
116
|
const state = stateStores.get(botId);
|
|
95
117
|
stateStores.delete(botId);
|
|
96
|
-
if (state && typeof state.remove === 'function')
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
118
|
+
if (state && typeof state.remove === 'function') {
|
|
119
|
+
await state.remove();
|
|
120
|
+
} else {
|
|
121
|
+
try {
|
|
122
|
+
await unlink(statePath(botId));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
125
|
+
}
|
|
101
126
|
}
|
|
102
127
|
},
|
|
103
128
|
});
|
|
129
|
+
const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
|
|
104
130
|
const supervisor = createSupervisor({
|
|
105
131
|
channel: 'whatsapp',
|
|
106
132
|
controller,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import QRCode from 'qrcode';
|
|
2
2
|
|
|
3
3
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
4
|
+
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
4
5
|
|
|
5
6
|
export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
|
|
6
7
|
export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|
@@ -10,6 +11,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|
|
10
11
|
cancelProvisioning: 'provision.cancel',
|
|
11
12
|
reconnectBot: 'bot.reconnect',
|
|
12
13
|
deleteBot: 'bot.delete',
|
|
14
|
+
setWorkspace: SET_WORKSPACE_ENDPOINT,
|
|
13
15
|
});
|
|
14
16
|
export const WHATSAPP_RPC_ENDPOINTS = Object.freeze(Object.values(WHATSAPP_ENDPOINTS));
|
|
15
17
|
|
|
@@ -44,6 +46,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
44
46
|
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId)
|
|
45
47
|
&& payload.confirm === true ? null : 'bot.delete requires a botId and confirm=true.';
|
|
46
48
|
}
|
|
49
|
+
if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
|
|
50
|
+
return validWorkspacePayload(payload)
|
|
51
|
+
? null : '请输入工作区绝对路径。';
|
|
52
|
+
}
|
|
47
53
|
return 'Unknown WhatsApp endpoint.';
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -114,16 +120,24 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
114
120
|
value = sanitizePublic(await controller.cancelProvisioning(payload.attemptId));
|
|
115
121
|
} else if (endpoint === WHATSAPP_ENDPOINTS.reconnectBot) {
|
|
116
122
|
value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode);
|
|
123
|
+
} else if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
|
|
124
|
+
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
125
|
+
value = await publicStatus(
|
|
126
|
+
await controller.updateWorkspace(payload.botId, payload.workspace),
|
|
127
|
+
cachedEncode,
|
|
128
|
+
);
|
|
117
129
|
} else {
|
|
118
130
|
value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
|
|
119
131
|
}
|
|
120
132
|
return signal?.aborted
|
|
121
133
|
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
122
134
|
: { ok: true, value };
|
|
123
|
-
} catch {
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const workspaceError = publicWorkspaceError(error);
|
|
124
137
|
return signal?.aborted
|
|
125
138
|
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
126
|
-
: { ok: false, error:
|
|
139
|
+
: { ok: false, error: workspaceError
|
|
140
|
+
?? { code: 'whatsapp-operation-failed', message: 'WhatsApp 操作失败,请稍后重试。' } };
|
|
127
141
|
}
|
|
128
142
|
};
|
|
129
143
|
}
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
splitDingtalkText,
|
|
4
4
|
} from './dingtalk-api.mjs';
|
|
5
5
|
import { createDingTalkCardStream } from './dingtalk-card-stream.mjs';
|
|
6
|
+
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
7
|
+
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
6
8
|
|
|
7
9
|
const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
|
8
10
|
const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
|
|
@@ -12,6 +14,7 @@ const HELP_TEXT = [
|
|
|
12
14
|
'',
|
|
13
15
|
'直接发送文字即可继续当前会话。',
|
|
14
16
|
'/new 开启一个全新会话',
|
|
17
|
+
'/workspace 工作区绝对路径 切换工作区',
|
|
15
18
|
'/status 检查连接状态',
|
|
16
19
|
'/help 显示本帮助',
|
|
17
20
|
].join('\n');
|
|
@@ -213,12 +216,12 @@ export class DingtalkHarnessBridge {
|
|
|
213
216
|
await this.#send(sessionWebhook, '已开启新会话。请发送你的问题。');
|
|
214
217
|
return;
|
|
215
218
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
await this.#state.setSession(key, sessionId);
|
|
219
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
220
|
+
if (workspaceCommand) {
|
|
221
|
+
await this.#send(sessionWebhook, workspaceCommand.message);
|
|
222
|
+
return;
|
|
221
223
|
}
|
|
224
|
+
|
|
222
225
|
if (typeof this.#api.createAiCard === 'function'
|
|
223
226
|
&& typeof this.#api.updateAiCard === 'function'
|
|
224
227
|
&& typeof this.#api.finishAiCard === 'function') {
|
|
@@ -232,12 +235,20 @@ export class DingtalkHarnessBridge {
|
|
|
232
235
|
});
|
|
233
236
|
cardStarted = await cardStream.start(CARD_INITIAL_TEXT);
|
|
234
237
|
}
|
|
235
|
-
const answer = await
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
const { answer } = await askInWorkspaceSession({
|
|
239
|
+
harness: this.#harness,
|
|
240
|
+
state: this.#state,
|
|
241
|
+
key,
|
|
242
|
+
text,
|
|
243
|
+
createOptions: { signal: this.#signal },
|
|
244
|
+
existsOptions: { signal: this.#signal },
|
|
245
|
+
askOptions: {
|
|
246
|
+
timeoutMs: this.#replyTimeoutMs,
|
|
247
|
+
signal: this.#signal,
|
|
248
|
+
onUpdate: cardStarted
|
|
249
|
+
? (update) => cardStream.push(progressText(update))
|
|
250
|
+
: undefined,
|
|
251
|
+
},
|
|
241
252
|
});
|
|
242
253
|
const streamed = cardStarted && await cardStream.finish(answer);
|
|
243
254
|
if (!streamed) await this.#send(sessionWebhook, answer);
|
|
@@ -610,7 +610,12 @@ export class DingtalkController {
|
|
|
610
610
|
await this.#stopRuntime(identity.botId);
|
|
611
611
|
if (previousConfig) await this.#configStore.save(previousConfig).catch(() => undefined);
|
|
612
612
|
else if (this.#configStore.get(identity.botId)) {
|
|
613
|
-
await this.#configStore.remove(identity.botId).catch(() =>
|
|
613
|
+
const removed = await this.#configStore.remove(identity.botId).catch(() => null);
|
|
614
|
+
if (removed) {
|
|
615
|
+
await this.#deleteState({ botId: identity.botId, config }).catch((cleanupError) => {
|
|
616
|
+
this.#logger.warn?.('[dsh-dingtalk] failed to clean up cancelled bot state:', cleanupError);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
614
619
|
}
|
|
615
620
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
616
621
|
if (previousConfig && cleanString(previousSecret?.value)) {
|
|
@@ -217,10 +217,11 @@ export class HarnessClient {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
async workspaceId(options = {}) {
|
|
220
|
-
const {
|
|
221
|
-
const
|
|
220
|
+
const { workspace = this.#workspace, ...rpcOptions } = options;
|
|
221
|
+
const { items } = await this.rpc('workspace.list', {}, 30_000, rpcOptions);
|
|
222
|
+
const existing = items.find((item) => item.path === workspace);
|
|
222
223
|
if (existing) return existing.workspaceId;
|
|
223
|
-
const created = await this.rpc('workspace.create', { path:
|
|
224
|
+
const created = await this.rpc('workspace.create', { path: workspace }, 30_000, rpcOptions);
|
|
224
225
|
return created.workspace.workspaceId;
|
|
225
226
|
}
|
|
226
227
|
|
|
@@ -117,6 +117,11 @@ export class DingtalkStateStore {
|
|
|
117
117
|
await this.#persist();
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
async clearSessions() {
|
|
121
|
+
this.#state.sessions = {};
|
|
122
|
+
await this.#persist();
|
|
123
|
+
}
|
|
124
|
+
|
|
120
125
|
hasSeen(messageId) {
|
|
121
126
|
const id = nonEmptyString(messageId);
|
|
122
127
|
return Boolean(id && this.#state.seenMessageIds.includes(id));
|
|
@@ -108,7 +108,7 @@ export class DiscordApi {
|
|
|
108
108
|
headers: {
|
|
109
109
|
authorization: `Bot ${this.#token}`,
|
|
110
110
|
'content-type': 'application/json',
|
|
111
|
-
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.
|
|
111
|
+
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.5.0)',
|
|
112
112
|
},
|
|
113
113
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
114
114
|
signal: requestSignal(signal, timeoutMs),
|
|
@@ -5,12 +5,15 @@ import {
|
|
|
5
5
|
isBotSender,
|
|
6
6
|
splitText,
|
|
7
7
|
} from './message-utils.mjs';
|
|
8
|
+
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
9
|
+
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
8
10
|
|
|
9
11
|
const HELP_TEXT = [
|
|
10
12
|
'北汇星河 AIOS 已连接 DeepSeek Harness。',
|
|
11
13
|
'',
|
|
12
14
|
'直接发送问题即可继续当前会话。',
|
|
13
15
|
'/new 开启一个全新会话',
|
|
16
|
+
'/workspace 工作区绝对路径 切换工作区',
|
|
14
17
|
'/status 检查连接状态',
|
|
15
18
|
'/help 显示本帮助',
|
|
16
19
|
].join('\n');
|
|
@@ -106,25 +109,30 @@ export class FeishuHarnessBridge {
|
|
|
106
109
|
await this.#send(event.message.chat_id, '飞书机器人与 DeepSeek Harness 连接正常。');
|
|
107
110
|
return;
|
|
108
111
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
await this.#state.setSession(key, sessionId);
|
|
112
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
113
|
+
if (workspaceCommand) {
|
|
114
|
+
await this.#send(event.message.chat_id, workspaceCommand.message);
|
|
115
|
+
return;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
console.info(`[bridge] processing ${event.message.chat_type} message ${messageId}
|
|
117
|
-
await this.#answerWithStream(event,
|
|
118
|
+
console.info(`[bridge] processing ${event.message.chat_type} message ${messageId}`);
|
|
119
|
+
await this.#answerWithStream(event, key, text);
|
|
118
120
|
this.#status.messagesReplied += 1;
|
|
119
121
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
120
122
|
this.#status.lastError = null;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
async #answerWithStream(event,
|
|
125
|
+
async #answerWithStream(event, key, text) {
|
|
124
126
|
const chatId = event.message.chat_id;
|
|
125
127
|
const messageId = event.message.message_id;
|
|
126
128
|
if (!this.#channel?.stream) {
|
|
127
|
-
const answer = await
|
|
129
|
+
const { answer } = await askInWorkspaceSession({
|
|
130
|
+
harness: this.#harness,
|
|
131
|
+
state: this.#state,
|
|
132
|
+
key,
|
|
133
|
+
text,
|
|
134
|
+
askOptions: { timeoutMs: this.#replyTimeoutMs },
|
|
135
|
+
});
|
|
128
136
|
for (const chunk of splitText(answer)) await this.#send(chatId, chunk);
|
|
129
137
|
this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1;
|
|
130
138
|
return;
|
|
@@ -136,13 +144,19 @@ export class FeishuHarnessBridge {
|
|
|
136
144
|
await this.#channel.stream(chatId, {
|
|
137
145
|
markdown: async (controller) => {
|
|
138
146
|
promptStarted = true;
|
|
139
|
-
completedAnswer = await
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
({ answer: completedAnswer } = await askInWorkspaceSession({
|
|
148
|
+
harness: this.#harness,
|
|
149
|
+
state: this.#state,
|
|
150
|
+
key,
|
|
151
|
+
text,
|
|
152
|
+
askOptions: {
|
|
153
|
+
timeoutMs: this.#replyTimeoutMs,
|
|
154
|
+
onUpdate: async (update) => {
|
|
155
|
+
await controller.setContent(this.#progressText(update));
|
|
156
|
+
this.#status.streamUpdates = (this.#status.streamUpdates ?? 0) + 1;
|
|
157
|
+
},
|
|
144
158
|
},
|
|
145
|
-
});
|
|
159
|
+
}));
|
|
146
160
|
await controller.setContent(completedAnswer);
|
|
147
161
|
},
|
|
148
162
|
}, { replyTo: messageId });
|
|
@@ -158,7 +172,13 @@ export class FeishuHarnessBridge {
|
|
|
158
172
|
if (promptStarted) throw error;
|
|
159
173
|
|
|
160
174
|
console.warn('[bridge] native Feishu stream unavailable; using text fallback:', error.message);
|
|
161
|
-
const answer = await
|
|
175
|
+
const { answer } = await askInWorkspaceSession({
|
|
176
|
+
harness: this.#harness,
|
|
177
|
+
state: this.#state,
|
|
178
|
+
key,
|
|
179
|
+
text,
|
|
180
|
+
askOptions: { timeoutMs: this.#replyTimeoutMs },
|
|
181
|
+
});
|
|
162
182
|
for (const chunk of splitText(answer)) await this.#send(chatId, chunk);
|
|
163
183
|
this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1;
|
|
164
184
|
}
|
|
@@ -197,17 +197,18 @@ export class HarnessClient {
|
|
|
197
197
|
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
async workspaceId() {
|
|
200
|
+
async workspaceId(options = {}) {
|
|
201
|
+
const workspace = options.workspace ?? this.#workspace;
|
|
201
202
|
const { items } = await this.rpc('workspace.list', {});
|
|
202
|
-
const existing = items.find((item) => item.path ===
|
|
203
|
+
const existing = items.find((item) => item.path === workspace);
|
|
203
204
|
if (existing) return existing.workspaceId;
|
|
204
|
-
const created = await this.rpc('workspace.create', { path:
|
|
205
|
+
const created = await this.rpc('workspace.create', { path: workspace });
|
|
205
206
|
return created.workspace.workspaceId;
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
async createSession() {
|
|
209
|
+
async createSession(options = {}) {
|
|
209
210
|
await this.ensureRunning();
|
|
210
|
-
const workspaceId = await this.workspaceId();
|
|
211
|
+
const workspaceId = await this.workspaceId(options);
|
|
211
212
|
const created = await this.rpc('session.create', {
|
|
212
213
|
workspaceId,
|
|
213
214
|
agentPreset: this.#agentPreset,
|