@xmanrui/dsh-im 0.10.0 → 0.12.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.en.md +36 -6
- package/README.md +36 -6
- package/lib/client.js +377 -27
- package/lib/index.js +145 -135
- package/package.json +31 -2
- package/plugin-src/client/channels/dingtalk/api.js +19 -0
- package/plugin-src/client/channels/dingtalk/index.js +67 -10
- package/plugin-src/client/channels/feishu/index.js +35 -5
- package/plugin-src/client/channels/qq/api.js +21 -0
- package/plugin-src/client/channels/qq/index.js +39 -1
- package/plugin-src/client/channels/shared/token-channel.js +29 -3
- package/plugin-src/client/channels/wecom/api.js +11 -0
- package/plugin-src/client/channels/wecom/index.js +71 -1
- package/plugin-src/client/channels/weixin/api.js +11 -0
- package/plugin-src/client/channels/weixin/index.js +40 -4
- package/plugin-src/client/channels/whatsapp/index.js +41 -2
- package/plugin-src/client/i18n.js +13 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +8 -1
- package/plugin-src/host/channels/dingtalk/rpc.mjs +29 -3
- package/plugin-src/host/channels/feishu/production.mjs +8 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +34 -3
- package/plugin-src/host/channels/qq/production.mjs +8 -1
- package/plugin-src/host/channels/qq/rpc.mjs +35 -2
- package/plugin-src/host/channels/shared/production.mjs +8 -1
- package/plugin-src/host/channels/shared/rpc.mjs +25 -1
- package/plugin-src/host/channels/slack/production.mjs +8 -1
- package/plugin-src/host/channels/slack/rpc.mjs +28 -2
- package/plugin-src/host/channels/wecom/production.mjs +8 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +31 -2
- package/plugin-src/host/channels/weixin/production.mjs +8 -1
- package/plugin-src/host/channels/weixin/rpc.mjs +29 -3
- package/plugin-src/host/channels/whatsapp/production.mjs +8 -1
- package/plugin-src/host/channels/whatsapp/rpc.mjs +31 -3
- package/plugin-src/host/harness-session-coordinator.mjs +134 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +85 -0
- package/src/channels/dingtalk/dingtalk-controller.mjs +18 -0
- package/src/channels/dingtalk/dingtalk-runtime.mjs +21 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/discord/discord-runtime.mjs +10 -0
- package/src/channels/feishu/bridge.mjs +87 -0
- package/src/channels/feishu/feishu-runtime.mjs +45 -0
- package/src/channels/feishu/message-utils.mjs +94 -7
- package/src/channels/feishu/multi-bot-controller.mjs +21 -0
- package/src/channels/qq/qq-bridge.mjs +91 -0
- package/src/channels/qq/qq-controller.mjs +18 -0
- package/src/channels/qq/qq-runtime.mjs +26 -0
- package/src/channels/shared/bot-workspace-store.mjs +47 -1
- package/src/channels/shared/connection-test.mjs +54 -0
- package/src/channels/shared/control-command.mjs +84 -0
- package/src/channels/shared/harness-approval.mjs +4 -0
- package/src/channels/shared/harness-client.mjs +342 -35
- package/src/channels/shared/model-command.mjs +314 -0
- package/src/channels/shared/session-binding-lock.mjs +40 -0
- package/src/channels/shared/text-harness-bridge.mjs +100 -0
- package/src/channels/shared/token-bot-controller.mjs +21 -0
- package/src/channels/shared/workspace-session.mjs +21 -9
- package/src/channels/slack/slack-controller.mjs +19 -0
- package/src/channels/slack/slack-runtime.mjs +10 -0
- package/src/channels/telegram/telegram-runtime.mjs +10 -0
- package/src/channels/wecom/wecom-bridge.mjs +79 -0
- package/src/channels/wecom/wecom-controller.mjs +18 -0
- package/src/channels/wecom/wecom-runtime.mjs +18 -0
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +85 -0
- package/src/channels/weixin/weixin-controller.mjs +18 -0
- package/src/channels/weixin/weixin-runtime.mjs +25 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +20 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +16 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
2
|
+
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
|
2
3
|
import {
|
|
3
4
|
publicWorkspaceError,
|
|
4
5
|
SET_WORKSPACE_ENDPOINT,
|
|
@@ -44,7 +45,8 @@ function payloadFailure(endpoint, payload) {
|
|
|
44
45
|
? null : 'bot.bind-credentials requires a Bot Token.';
|
|
45
46
|
}
|
|
46
47
|
if (endpoint === TOKEN_BOT_ENDPOINTS.reconnectBot) {
|
|
47
|
-
return exactKeys(payload, ['botId']) && validId(payload.botId)
|
|
48
|
+
return exactKeys(payload, ['botId', 'sendTest']) && validId(payload.botId)
|
|
49
|
+
&& (payload.sendTest === undefined || typeof payload.sendTest === 'boolean')
|
|
48
50
|
? null : 'bot.reconnect requires a botId.';
|
|
49
51
|
}
|
|
50
52
|
if (endpoint === TOKEN_BOT_ENDPOINTS.deleteBot) {
|
|
@@ -105,6 +107,28 @@ export function createTokenBotRpcHandler(controller, { channel }) {
|
|
|
105
107
|
value = await controller.bindCredentials(payload);
|
|
106
108
|
} else if (endpoint === TOKEN_BOT_ENDPOINTS.reconnectBot) {
|
|
107
109
|
value = await controller.reconnectBot(payload.botId);
|
|
110
|
+
if (signal?.aborted) {
|
|
111
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
|
112
|
+
}
|
|
113
|
+
if (payload.sendTest === true) {
|
|
114
|
+
let testError = null;
|
|
115
|
+
try {
|
|
116
|
+
if (value?.bots?.find((bot) => bot?.botId === payload.botId)?.connected !== true) {
|
|
117
|
+
const unavailable = new Error('Bot is not connected');
|
|
118
|
+
unavailable.code = 'test-target-unavailable';
|
|
119
|
+
throw unavailable;
|
|
120
|
+
}
|
|
121
|
+
if (typeof controller.sendConnectionTest !== 'function') {
|
|
122
|
+
const unavailable = new Error('Connection test is unavailable');
|
|
123
|
+
unavailable.code = 'test-target-unavailable';
|
|
124
|
+
throw unavailable;
|
|
125
|
+
}
|
|
126
|
+
await controller.sendConnectionTest(payload.botId);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
testError = error;
|
|
129
|
+
}
|
|
130
|
+
value = { ...value, testMessage: publicConnectionTestResult(testError) };
|
|
131
|
+
}
|
|
108
132
|
} else if (endpoint === TOKEN_BOT_ENDPOINTS.setWorkspace) {
|
|
109
133
|
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
110
134
|
value = await controller.updateWorkspace(payload.botId, payload.workspace);
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
|
|
16
16
|
import { harnessOrigin, pluginPaths } from '../shared/production.mjs';
|
|
17
17
|
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
|
|
18
|
+
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
|
|
18
19
|
|
|
19
20
|
export async function createProductionController(ctx, config = {}, internals = {}) {
|
|
20
21
|
if (!ctx?.credentials) throw new TypeError('dsh-im slack requires ctx.credentials');
|
|
@@ -51,13 +52,19 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
51
52
|
return state;
|
|
52
53
|
};
|
|
53
54
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
55
|
+
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
|
|
56
|
+
controlExecutor: internals.controlExecutor,
|
|
57
|
+
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
58
|
+
});
|
|
54
59
|
const harness = new ResolvedHarness({
|
|
55
60
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
56
61
|
workspace: defaultWorkspace,
|
|
57
|
-
agentPreset: config.agentPreset
|
|
62
|
+
...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
|
|
58
63
|
autostart: false,
|
|
59
64
|
dshBin: config.dshBin ?? 'dsh',
|
|
60
65
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
66
|
+
...(controlExecutor ? { controlExecutor } : {}),
|
|
67
|
+
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
61
68
|
});
|
|
62
69
|
const coreController = new ResolvedController({
|
|
63
70
|
credentials: ctx.credentials,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
2
|
+
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
|
2
3
|
import {
|
|
3
4
|
publicWorkspaceError,
|
|
4
5
|
SET_WORKSPACE_ENDPOINT,
|
|
@@ -53,7 +54,8 @@ function payloadFailure(endpoint, payload) {
|
|
|
53
54
|
? null : 'bot.bind-credentials requires xoxb Bot Token and xapp App Token.';
|
|
54
55
|
}
|
|
55
56
|
if (endpoint === SLACK_ENDPOINTS.reconnectBot) {
|
|
56
|
-
return exactKeys(payload, ['botId']) && validId(payload.botId)
|
|
57
|
+
return exactKeys(payload, ['botId', 'sendTest']) && validId(payload.botId)
|
|
58
|
+
&& (payload.sendTest === undefined || typeof payload.sendTest === 'boolean')
|
|
57
59
|
? null : 'bot.reconnect requires a botId.';
|
|
58
60
|
}
|
|
59
61
|
if (endpoint === SLACK_ENDPOINTS.deleteBot) {
|
|
@@ -114,7 +116,31 @@ export function createSlackRpcHandler(controller) {
|
|
|
114
116
|
let value;
|
|
115
117
|
if (endpoint === SLACK_ENDPOINTS.status) value = await controller.status();
|
|
116
118
|
else if (endpoint === SLACK_ENDPOINTS.bindCredentials) value = await controller.bindCredentials(payload);
|
|
117
|
-
else if (endpoint === SLACK_ENDPOINTS.reconnectBot)
|
|
119
|
+
else if (endpoint === SLACK_ENDPOINTS.reconnectBot) {
|
|
120
|
+
value = await controller.reconnectBot(payload.botId);
|
|
121
|
+
if (signal?.aborted) {
|
|
122
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
|
123
|
+
}
|
|
124
|
+
if (payload.sendTest === true) {
|
|
125
|
+
let testError = null;
|
|
126
|
+
try {
|
|
127
|
+
if (value?.bots?.find((bot) => bot?.botId === payload.botId)?.connected !== true) {
|
|
128
|
+
const unavailable = new Error('Bot is not connected');
|
|
129
|
+
unavailable.code = 'test-target-unavailable';
|
|
130
|
+
throw unavailable;
|
|
131
|
+
}
|
|
132
|
+
if (typeof controller.sendConnectionTest !== 'function') {
|
|
133
|
+
const unavailable = new Error('Connection test is unavailable');
|
|
134
|
+
unavailable.code = 'test-target-unavailable';
|
|
135
|
+
throw unavailable;
|
|
136
|
+
}
|
|
137
|
+
await controller.sendConnectionTest(payload.botId);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
testError = error;
|
|
140
|
+
}
|
|
141
|
+
value = { ...value, testMessage: publicConnectionTestResult(testError) };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
118
144
|
else if (endpoint === SLACK_ENDPOINTS.setWorkspace) {
|
|
119
145
|
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
120
146
|
value = await controller.updateWorkspace(payload.botId, payload.workspace);
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
17
17
|
import { createConnectionSupervisor } from './connection-supervisor.mjs';
|
|
18
18
|
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
|
|
19
|
+
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
|
|
19
20
|
|
|
20
21
|
function harnessOrigin(webServer, configured) {
|
|
21
22
|
if (configured !== undefined) return new URL(configured);
|
|
@@ -75,13 +76,19 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
75
76
|
return state;
|
|
76
77
|
};
|
|
77
78
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
79
|
+
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
|
|
80
|
+
controlExecutor: internals.controlExecutor,
|
|
81
|
+
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
82
|
+
});
|
|
78
83
|
const harness = new Harness({
|
|
79
84
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
80
85
|
workspace: defaultWorkspace,
|
|
81
|
-
agentPreset: config.agentPreset
|
|
86
|
+
...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
|
|
82
87
|
autostart: false,
|
|
83
88
|
dshBin: config.dshBin ?? 'dsh',
|
|
84
89
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
90
|
+
...(controlExecutor ? { controlExecutor } : {}),
|
|
91
|
+
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
85
92
|
});
|
|
86
93
|
const coreController = new Controller({
|
|
87
94
|
qrAuth,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import QRCode from 'qrcode';
|
|
2
2
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
3
3
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
4
|
+
import {
|
|
5
|
+
connectionTestTargetUnavailable,
|
|
6
|
+
publicConnectionTestResult,
|
|
7
|
+
} from '../../../../src/channels/shared/connection-test.mjs';
|
|
4
8
|
|
|
5
9
|
export const WECOM_RPC_CHANNEL = '/wecom';
|
|
6
10
|
export const WECOM_ENDPOINTS = Object.freeze({
|
|
@@ -53,7 +57,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
53
57
|
? null : 'bot.bind-credentials requires Bot ID and Secret.';
|
|
54
58
|
}
|
|
55
59
|
if (endpoint === WECOM_ENDPOINTS.reconnectBot) {
|
|
56
|
-
return exactKeys(payload, ['botId'
|
|
60
|
+
return exactKeys(payload, ['botId', 'sendTest'])
|
|
61
|
+
&& validId(payload.botId)
|
|
62
|
+
&& (payload.sendTest === undefined || payload.sendTest === true)
|
|
63
|
+
? null : 'bot.reconnect requires a botId and optional sendTest=true.';
|
|
57
64
|
}
|
|
58
65
|
if (endpoint === WECOM_ENDPOINTS.deleteBot) {
|
|
59
66
|
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
|
|
@@ -130,7 +137,29 @@ export function createWecomRpcHandler(controller, { encodeQr = qrDataUrl } = {})
|
|
|
130
137
|
} else if (endpoint === WECOM_ENDPOINTS.bindCredentials) {
|
|
131
138
|
value = await publicStatus(await controller.bindCredentials(payload), cachedEncode);
|
|
132
139
|
} else if (endpoint === WECOM_ENDPOINTS.reconnectBot) {
|
|
133
|
-
|
|
140
|
+
const snapshot = await controller.reconnectBot(payload.botId);
|
|
141
|
+
if (signal?.aborted) {
|
|
142
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
|
143
|
+
}
|
|
144
|
+
let testMessage;
|
|
145
|
+
if (payload.sendTest === true) {
|
|
146
|
+
const connected = snapshot?.bots?.some(
|
|
147
|
+
(bot) => bot?.botId === payload.botId && bot?.connected === true,
|
|
148
|
+
);
|
|
149
|
+
if (!connected || typeof controller.sendConnectionTest !== 'function') {
|
|
150
|
+
testMessage = publicConnectionTestResult(
|
|
151
|
+
connectionTestTargetUnavailable('企业微信机器人'),
|
|
152
|
+
);
|
|
153
|
+
} else {
|
|
154
|
+
try {
|
|
155
|
+
await controller.sendConnectionTest(payload.botId);
|
|
156
|
+
testMessage = publicConnectionTestResult();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
testMessage = publicConnectionTestResult(error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
value = await publicStatus({ ...snapshot, ...(testMessage ? { testMessage } : {}) }, cachedEncode);
|
|
134
163
|
} else if (endpoint === WECOM_ENDPOINTS.setWorkspace) {
|
|
135
164
|
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
136
165
|
value = await publicStatus(
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
17
17
|
import { createConnectionSupervisor } from './connection-supervisor.mjs';
|
|
18
18
|
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
|
|
19
|
+
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
|
|
19
20
|
|
|
20
21
|
function harnessOrigin(webServer, configured) {
|
|
21
22
|
if (configured !== undefined) return new URL(configured);
|
|
@@ -75,13 +76,19 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
75
76
|
return state;
|
|
76
77
|
};
|
|
77
78
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
79
|
+
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
|
|
80
|
+
controlExecutor: internals.controlExecutor,
|
|
81
|
+
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
82
|
+
});
|
|
78
83
|
const harness = new Harness({
|
|
79
84
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
80
85
|
workspace: defaultWorkspace,
|
|
81
|
-
agentPreset: config.agentPreset
|
|
86
|
+
...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
|
|
82
87
|
autostart: false,
|
|
83
88
|
dshBin: config.dshBin ?? 'dsh',
|
|
84
89
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
90
|
+
...(controlExecutor ? { controlExecutor } : {}),
|
|
91
|
+
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
85
92
|
});
|
|
86
93
|
const coreController = new Controller({
|
|
87
94
|
api,
|
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
SET_WORKSPACE_ENDPOINT,
|
|
6
6
|
validWorkspacePayload,
|
|
7
7
|
} from '../shared/workspace-rpc.mjs';
|
|
8
|
+
import {
|
|
9
|
+
connectionTestTargetUnavailable,
|
|
10
|
+
publicConnectionTestResult,
|
|
11
|
+
} from '../../../../src/channels/shared/connection-test.mjs';
|
|
8
12
|
|
|
9
13
|
export const WEIXIN_RPC_CHANNEL = '/weixin';
|
|
10
14
|
export const WEIXIN_ENDPOINTS = Object.freeze({
|
|
@@ -55,9 +59,11 @@ function payloadFailure(endpoint, payload) {
|
|
|
55
59
|
: 'provision.verify requires an attemptId and a 4-to-8-digit code.';
|
|
56
60
|
}
|
|
57
61
|
if (endpoint === WEIXIN_ENDPOINTS.reconnectBot) {
|
|
58
|
-
return exactKeys(payload, ['botId'])
|
|
62
|
+
return exactKeys(payload, ['botId', 'sendTest'])
|
|
63
|
+
&& validId(payload.botId)
|
|
64
|
+
&& (payload.sendTest === undefined || payload.sendTest === true)
|
|
59
65
|
? null
|
|
60
|
-
: 'bot.reconnect requires a botId.';
|
|
66
|
+
: 'bot.reconnect requires a botId and optional sendTest=true.';
|
|
61
67
|
}
|
|
62
68
|
if (endpoint === WEIXIN_ENDPOINTS.deleteBot) {
|
|
63
69
|
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId) && payload.confirm === true
|
|
@@ -165,7 +171,27 @@ export function createWeixinRpcHandler(controller, { encodeQr = qrDataUrl } = {}
|
|
|
165
171
|
value = await controller.cancelProvisioning(payload.attemptId);
|
|
166
172
|
if (!value) return badRequest('The provisioning attempt no longer exists.');
|
|
167
173
|
} else if (endpoint === WEIXIN_ENDPOINTS.reconnectBot) {
|
|
168
|
-
|
|
174
|
+
const snapshot = await controller.reconnectBot(payload.botId);
|
|
175
|
+
if (signal?.aborted) return cancelled();
|
|
176
|
+
let testMessage;
|
|
177
|
+
if (payload.sendTest === true) {
|
|
178
|
+
const connected = snapshot?.bots?.some(
|
|
179
|
+
(bot) => bot?.botId === payload.botId && bot?.connected === true,
|
|
180
|
+
);
|
|
181
|
+
if (!connected || typeof controller.sendConnectionTest !== 'function') {
|
|
182
|
+
testMessage = publicConnectionTestResult(
|
|
183
|
+
connectionTestTargetUnavailable('微信机器人'),
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
try {
|
|
187
|
+
await controller.sendConnectionTest(payload.botId);
|
|
188
|
+
testMessage = publicConnectionTestResult();
|
|
189
|
+
} catch (error) {
|
|
190
|
+
testMessage = publicConnectionTestResult(error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
value = await publicStatus({ ...snapshot, ...(testMessage ? { testMessage } : {}) }, cachedEncode);
|
|
169
195
|
} else if (endpoint === WEIXIN_ENDPOINTS.setWorkspace) {
|
|
170
196
|
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
171
197
|
value = await publicStatus(
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
17
17
|
import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
|
|
18
18
|
import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
|
|
19
|
+
import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
|
|
19
20
|
|
|
20
21
|
const AUTH_DIRECTORY_PATTERN = /^[a-f0-9-]{36}$/;
|
|
21
22
|
|
|
@@ -78,13 +79,19 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
78
79
|
return state;
|
|
79
80
|
};
|
|
80
81
|
const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
|
|
82
|
+
const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
|
|
83
|
+
controlExecutor: internals.controlExecutor,
|
|
84
|
+
sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
|
|
85
|
+
});
|
|
81
86
|
const harness = new Harness({
|
|
82
87
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
83
88
|
workspace: defaultWorkspace,
|
|
84
|
-
agentPreset: config.agentPreset
|
|
89
|
+
...(config.agentPreset == null ? {} : { agentPreset: config.agentPreset }),
|
|
85
90
|
autostart: false,
|
|
86
91
|
dshBin: config.dshBin ?? 'dsh',
|
|
87
92
|
...(commandExecutor ? { commandExecutor } : {}),
|
|
93
|
+
...(controlExecutor ? { controlExecutor } : {}),
|
|
94
|
+
...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
|
|
88
95
|
});
|
|
89
96
|
const coreController = new Controller({
|
|
90
97
|
configStore: observedConfigStore,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import QRCode from 'qrcode';
|
|
2
2
|
|
|
3
|
+
import { publicConnectionTestResult } from '../../../../src/channels/shared/connection-test.mjs';
|
|
3
4
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
4
5
|
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
5
6
|
|
|
@@ -39,8 +40,9 @@ function payloadFailure(endpoint, payload) {
|
|
|
39
40
|
? null : `${endpoint} requires an attemptId.`;
|
|
40
41
|
}
|
|
41
42
|
if (endpoint === WHATSAPP_ENDPOINTS.reconnectBot) {
|
|
42
|
-
return exactKeys(payload, ['botId']) && validId(payload.botId)
|
|
43
|
-
|
|
43
|
+
return exactKeys(payload, ['botId', 'sendTest']) && validId(payload.botId)
|
|
44
|
+
&& (payload.sendTest === undefined || typeof payload.sendTest === 'boolean')
|
|
45
|
+
? null : 'bot.reconnect requires a botId and optional sendTest flag.';
|
|
44
46
|
}
|
|
45
47
|
if (endpoint === WHATSAPP_ENDPOINTS.deleteBot) {
|
|
46
48
|
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId)
|
|
@@ -119,7 +121,33 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
119
121
|
} else if (endpoint === WHATSAPP_ENDPOINTS.cancelProvisioning) {
|
|
120
122
|
value = sanitizePublic(await controller.cancelProvisioning(payload.attemptId));
|
|
121
123
|
} else if (endpoint === WHATSAPP_ENDPOINTS.reconnectBot) {
|
|
122
|
-
|
|
124
|
+
const checked = await controller.reconnectBot(payload.botId);
|
|
125
|
+
if (signal?.aborted) {
|
|
126
|
+
return { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } };
|
|
127
|
+
}
|
|
128
|
+
value = await publicStatus(checked, cachedEncode);
|
|
129
|
+
if (payload.sendTest === true) {
|
|
130
|
+
let testError = null;
|
|
131
|
+
const connected = checked?.bots?.some(
|
|
132
|
+
(bot) => bot?.botId === payload.botId && bot.connected === true,
|
|
133
|
+
) === true;
|
|
134
|
+
if (!connected) {
|
|
135
|
+
testError = new Error('WhatsApp bot is not connected');
|
|
136
|
+
testError.code = 'test-target-unavailable';
|
|
137
|
+
} else {
|
|
138
|
+
try {
|
|
139
|
+
if (typeof controller.sendConnectionTest !== 'function') {
|
|
140
|
+
const unavailable = new Error('Connection test is unavailable');
|
|
141
|
+
unavailable.code = 'test-target-unavailable';
|
|
142
|
+
throw unavailable;
|
|
143
|
+
}
|
|
144
|
+
await controller.sendConnectionTest(payload.botId);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
testError = error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
value = { ...value, testMessage: publicConnectionTestResult(testError) };
|
|
150
|
+
}
|
|
123
151
|
} else if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
|
|
124
152
|
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
125
153
|
value = await publicStatus(
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
function agentsFromContext(ctx) {
|
|
4
|
+
if (typeof ctx?.get !== 'function') return undefined;
|
|
5
|
+
let agents;
|
|
6
|
+
try {
|
|
7
|
+
agents = ctx.get('agents');
|
|
8
|
+
} catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
if (agents === undefined || agents === null) return undefined;
|
|
12
|
+
if (typeof agents.get !== 'function') {
|
|
13
|
+
throw new TypeError('dsh-im requires a callable AgentRegistry when ctx.get("agents") is present');
|
|
14
|
+
}
|
|
15
|
+
return agents;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function currentOwnedTurn(agent, expectedTurn, promptRpcId) {
|
|
19
|
+
if (agent?.status !== 'running') return false;
|
|
20
|
+
const events = agent?.session?.events;
|
|
21
|
+
if (!Array.isArray(events)) return false;
|
|
22
|
+
|
|
23
|
+
let openTurn = null;
|
|
24
|
+
let owned = false;
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
if (event?.type === 'turn/start') {
|
|
27
|
+
openTurn = event.data?.turn ?? null;
|
|
28
|
+
owned = false;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (event?.type === 'turn/end' && event.data?.turn === openTurn) {
|
|
32
|
+
openTurn = null;
|
|
33
|
+
owned = false;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (openTurn === expectedTurn
|
|
37
|
+
&& event?.type === 'user/message'
|
|
38
|
+
&& event.data?.source?.rpcId === promptRpcId) {
|
|
39
|
+
owned = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return openTurn === expectedTurn && owned;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function deepFreeze(value) {
|
|
46
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
47
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
48
|
+
return Object.freeze(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function steeringMessage(text) {
|
|
52
|
+
return deepFreeze({
|
|
53
|
+
id: randomUUID(),
|
|
54
|
+
role: 'user',
|
|
55
|
+
content: [{ type: 'text', text }],
|
|
56
|
+
source: { kind: 'user' },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function agentBusyError(cause) {
|
|
61
|
+
const error = new Error('Session is busy with an active turn or maintenance task', { cause });
|
|
62
|
+
error.code = 'agent-busy';
|
|
63
|
+
return error;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createControlExecutor(agents) {
|
|
67
|
+
return ({ sessionId, expectedTurn, promptRpcId, action, text }) => {
|
|
68
|
+
const agent = agents.get(sessionId);
|
|
69
|
+
// An unattached Session cannot be coordinated in-process. Let the client
|
|
70
|
+
// retain its legacy HTTP path for deployments where attachment is lazy.
|
|
71
|
+
if (!agent) return undefined;
|
|
72
|
+
if (!currentOwnedTurn(agent, expectedTurn, promptRpcId)) return false;
|
|
73
|
+
|
|
74
|
+
if (action === 'stop') {
|
|
75
|
+
agent.cancel({ kind: 'user' }, { keepInbox: true });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (action === 'steer') {
|
|
79
|
+
if (typeof text !== 'string' || !text.trim()) return false;
|
|
80
|
+
// inject() never wakes an idle driver. Because validation and injection
|
|
81
|
+
// share one JS tick, this context can only target this live turn's next step.
|
|
82
|
+
agent.inject(steeringMessage(text));
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
throw new TypeError(`Unsupported Harness control action: ${String(action)}`);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function createSessionMaintenanceExecutor(agents) {
|
|
90
|
+
return ({ sessionId, operation }) => {
|
|
91
|
+
if (typeof operation !== 'function') throw new TypeError('maintenance operation is required');
|
|
92
|
+
const agent = agents.get(sessionId);
|
|
93
|
+
if (!agent) return operation();
|
|
94
|
+
try {
|
|
95
|
+
// runMaintenance claims the true-idle phase synchronously. A prompt or a
|
|
96
|
+
// second maintenance operation therefore cannot interleave with the RPC.
|
|
97
|
+
return agent.runMaintenance((signal) => {
|
|
98
|
+
if (agents.get(sessionId) !== agent) {
|
|
99
|
+
const error = new Error('Session agent changed before maintenance started');
|
|
100
|
+
error.code = 'agent-unavailable';
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
return operation(signal);
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw agentBusyError(error);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build optional same-process Session executors without adding a hard Cordis
|
|
113
|
+
* service injection. Fixtures and deployments without AgentRegistry preserve
|
|
114
|
+
* the existing HTTP behavior.
|
|
115
|
+
*/
|
|
116
|
+
export function createHarnessSessionExecutors(ctx, provided = {}) {
|
|
117
|
+
const { controlExecutor, sessionMaintenanceExecutor } = provided;
|
|
118
|
+
if (controlExecutor !== undefined && typeof controlExecutor !== 'function') {
|
|
119
|
+
throw new TypeError('controlExecutor must be a function');
|
|
120
|
+
}
|
|
121
|
+
if (sessionMaintenanceExecutor !== undefined
|
|
122
|
+
&& typeof sessionMaintenanceExecutor !== 'function') {
|
|
123
|
+
throw new TypeError('sessionMaintenanceExecutor must be a function');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const agents = controlExecutor && sessionMaintenanceExecutor
|
|
127
|
+
? undefined
|
|
128
|
+
: agentsFromContext(ctx);
|
|
129
|
+
return {
|
|
130
|
+
controlExecutor: controlExecutor ?? (agents ? createControlExecutor(agents) : undefined),
|
|
131
|
+
sessionMaintenanceExecutor: sessionMaintenanceExecutor
|
|
132
|
+
?? (agents ? createSessionMaintenanceExecutor(agents) : undefined),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -10,6 +10,14 @@ import {
|
|
|
10
10
|
} from '../shared/harness-question.mjs';
|
|
11
11
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
12
12
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
13
|
+
import {
|
|
14
|
+
isControlCommand,
|
|
15
|
+
runControlCommand,
|
|
16
|
+
} from '../shared/control-command.mjs';
|
|
17
|
+
import {
|
|
18
|
+
isModelCommand,
|
|
19
|
+
runModelCommand,
|
|
20
|
+
} from '../shared/model-command.mjs';
|
|
13
21
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
14
22
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
15
23
|
import {
|
|
@@ -17,6 +25,7 @@ import {
|
|
|
17
25
|
imagePromptUserMessage,
|
|
18
26
|
promptContentForMessage,
|
|
19
27
|
} from '../shared/image-prompt.mjs';
|
|
28
|
+
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
|
20
29
|
|
|
21
30
|
const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
|
22
31
|
const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
|
|
@@ -32,6 +41,10 @@ const HELP_TEXT = [
|
|
|
32
41
|
'/workspacelist 列出工作区绝对路径',
|
|
33
42
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
34
43
|
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
44
|
+
'/models 列出所有可用模型',
|
|
45
|
+
'/model [模型ID] 查看或切换当前会话模型',
|
|
46
|
+
'/stop 停止当前任务',
|
|
47
|
+
'/steer 补充指令 纠偏当前任务',
|
|
35
48
|
'/status 检查连接状态',
|
|
36
49
|
'/help 显示本帮助',
|
|
37
50
|
].join('\n');
|
|
@@ -245,6 +258,7 @@ export class DingtalkHarnessBridge {
|
|
|
245
258
|
#pendingInteractions = new Map();
|
|
246
259
|
#interactionKeys = new Map();
|
|
247
260
|
#interactionTasks = new Set();
|
|
261
|
+
#commandTasks = new Set();
|
|
248
262
|
#acceptedMessageIds = new Set();
|
|
249
263
|
#approvals;
|
|
250
264
|
|
|
@@ -309,7 +323,41 @@ export class DingtalkHarnessBridge {
|
|
|
309
323
|
} catch {
|
|
310
324
|
// An unsafe reply route must never be able to submit an approval.
|
|
311
325
|
}
|
|
326
|
+
if (sessionWebhook && String(message.conversationType) !== '2') {
|
|
327
|
+
rememberConnectionTestTarget(this.#state, { sessionWebhook });
|
|
328
|
+
}
|
|
312
329
|
const pending = this.#pendingInteractions.get(key);
|
|
330
|
+
const promptMessage = dingtalkInboundMessage(message, {
|
|
331
|
+
api: this.#api,
|
|
332
|
+
clientId: this.#clientId,
|
|
333
|
+
clientSecret: this.#clientSecret,
|
|
334
|
+
});
|
|
335
|
+
const commandText = nonEmptyString(promptMessage.content) ?? '';
|
|
336
|
+
const commandRunner = isControlCommand(commandText)
|
|
337
|
+
? runControlCommand
|
|
338
|
+
: (isModelCommand(commandText) ? runModelCommand : null);
|
|
339
|
+
const addressed = String(message.conversationType) !== '2' || message?.isInAtList === true;
|
|
340
|
+
if (commandRunner && sessionWebhook && addressed) {
|
|
341
|
+
let task;
|
|
342
|
+
task = this.#processFastCommand(
|
|
343
|
+
message,
|
|
344
|
+
messageId,
|
|
345
|
+
key,
|
|
346
|
+
sessionWebhook,
|
|
347
|
+
promptMessage,
|
|
348
|
+
commandRunner,
|
|
349
|
+
).catch((error) => {
|
|
350
|
+
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
351
|
+
this.#status.lastError = '钉钉命令处理失败。';
|
|
352
|
+
this.#logger.error?.('[dsh-dingtalk] failed to process a command', safeErrorDiagnostic(error));
|
|
353
|
+
return this.#send(sessionWebhook, CARD_ERROR_TEXT).catch(() => undefined);
|
|
354
|
+
}).finally(() => {
|
|
355
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
356
|
+
this.#commandTasks.delete(task);
|
|
357
|
+
});
|
|
358
|
+
this.#commandTasks.add(task);
|
|
359
|
+
return task;
|
|
360
|
+
}
|
|
313
361
|
const approvalReply = this.#approvals.claimReply({
|
|
314
362
|
key,
|
|
315
363
|
actor: sender,
|
|
@@ -408,9 +456,41 @@ export class DingtalkHarnessBridge {
|
|
|
408
456
|
pending.queue ? [pending.queue] : []
|
|
409
457
|
)),
|
|
410
458
|
...this.#interactionTasks,
|
|
459
|
+
...this.#commandTasks,
|
|
411
460
|
]);
|
|
412
461
|
}
|
|
413
462
|
|
|
463
|
+
async #processFastCommand(message, messageId, key, sessionWebhook, prompt, runner) {
|
|
464
|
+
this.#signal?.throwIfAborted();
|
|
465
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
466
|
+
await this.#state.markSeen(messageId);
|
|
467
|
+
increment(this.#status, 'messagesReceived');
|
|
468
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
469
|
+
const result = await runner(
|
|
470
|
+
nonEmptyString(prompt.content) ?? '',
|
|
471
|
+
this.#harness,
|
|
472
|
+
this.#state,
|
|
473
|
+
key,
|
|
474
|
+
{
|
|
475
|
+
signal: this.#signal,
|
|
476
|
+
hasImages: hasInboundImages(prompt),
|
|
477
|
+
pendingInteraction: this.#pendingInteractions.has(key)
|
|
478
|
+
|| this.#approvals.hasPending(key),
|
|
479
|
+
control: { owner: this, key },
|
|
480
|
+
},
|
|
481
|
+
);
|
|
482
|
+
if (result?.stopped) {
|
|
483
|
+
await Promise.allSettled([
|
|
484
|
+
this.#cancelPendingInteraction(key),
|
|
485
|
+
this.#approvals.closeRoute(key),
|
|
486
|
+
]);
|
|
487
|
+
}
|
|
488
|
+
for (const reply of result?.messages ?? [result?.message]) {
|
|
489
|
+
if (reply) await this.#send(sessionWebhook, reply);
|
|
490
|
+
}
|
|
491
|
+
this.#status.lastError = null;
|
|
492
|
+
}
|
|
493
|
+
|
|
414
494
|
async #process(message, messageId, sender, key, { alreadyRecorded = false } = {}) {
|
|
415
495
|
this.#signal?.throwIfAborted();
|
|
416
496
|
if (!alreadyRecorded) {
|
|
@@ -515,6 +595,7 @@ export class DingtalkHarnessBridge {
|
|
|
515
595
|
askOptions: {
|
|
516
596
|
timeoutMs: this.#replyTimeoutMs,
|
|
517
597
|
signal: this.#signal,
|
|
598
|
+
control: { owner: this, key },
|
|
518
599
|
onUpdate: cardStarted
|
|
519
600
|
? (update) => cardStream.push(progressText(update))
|
|
520
601
|
: undefined,
|
|
@@ -533,6 +614,10 @@ export class DingtalkHarnessBridge {
|
|
|
533
614
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
534
615
|
this.#status.lastError = null;
|
|
535
616
|
} catch (error) {
|
|
617
|
+
if (error?.code === 'turn-stopped') {
|
|
618
|
+
if (cardStarted) await cardStream.finish('已停止。').catch(() => undefined);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
536
621
|
if (this.#signal?.aborted) return;
|
|
537
622
|
this.#status.lastError = '钉钉消息处理失败。';
|
|
538
623
|
this.#logger.error?.(
|