@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,3 +1,6 @@
|
|
|
1
|
+
import { runWorkspaceCommand } from './workspace-command.mjs';
|
|
2
|
+
import { askInWorkspaceSession } from './workspace-session.mjs';
|
|
3
|
+
|
|
1
4
|
function cleanText(value) {
|
|
2
5
|
return typeof value === 'string' ? value.trim() : '';
|
|
3
6
|
}
|
|
@@ -97,6 +100,7 @@ export class TextHarnessBridge {
|
|
|
97
100
|
'',
|
|
98
101
|
'直接发送文字即可继续当前会话。',
|
|
99
102
|
'/new 开启一个全新会话',
|
|
103
|
+
'/workspace 工作区绝对路径 切换工作区',
|
|
100
104
|
'/status 检查连接状态',
|
|
101
105
|
'/help 显示本帮助',
|
|
102
106
|
].join('\n'));
|
|
@@ -109,6 +113,12 @@ export class TextHarnessBridge {
|
|
|
109
113
|
await this.#state.markSeen(messageId);
|
|
110
114
|
return;
|
|
111
115
|
}
|
|
116
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
117
|
+
if (workspaceCommand) {
|
|
118
|
+
await this.#bot.sendText(target, workspaceCommand.message);
|
|
119
|
+
await this.#state.markSeen(messageId);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
112
122
|
const conversationKey = `${message.kind}:${message.conversationId}`;
|
|
113
123
|
if (command === '/new') {
|
|
114
124
|
await this.#state.clearSession(conversationKey);
|
|
@@ -117,12 +127,6 @@ export class TextHarnessBridge {
|
|
|
117
127
|
return;
|
|
118
128
|
}
|
|
119
129
|
|
|
120
|
-
let sessionId = this.#state.sessionFor(conversationKey);
|
|
121
|
-
if (!sessionId || !(await this.#harness.sessionExists(sessionId))) {
|
|
122
|
-
sessionId = await this.#harness.createSession();
|
|
123
|
-
await this.#state.setSession(conversationKey, sessionId);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
130
|
await this.#bot.sendTyping?.(target).catch((error) => {
|
|
127
131
|
this.#logger.warn?.(`[dsh-im:${this.#descriptor.key}] typing indicator failed:`, error);
|
|
128
132
|
});
|
|
@@ -138,13 +142,19 @@ export class TextHarnessBridge {
|
|
|
138
142
|
);
|
|
139
143
|
}
|
|
140
144
|
}
|
|
141
|
-
const answer = await
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
const { answer } = await askInWorkspaceSession({
|
|
146
|
+
harness: this.#harness,
|
|
147
|
+
state: this.#state,
|
|
148
|
+
key: conversationKey,
|
|
149
|
+
text,
|
|
150
|
+
askOptions: {
|
|
151
|
+
timeoutMs: this.#replyTimeoutMs,
|
|
152
|
+
onUpdate: stream ? async (update) => {
|
|
153
|
+
const progress = update.type === 'text' ? update.text
|
|
154
|
+
: update.type === 'tool' ? `正在使用${update.name}…` : update.text;
|
|
155
|
+
if (progress) await stream.update(progress);
|
|
156
|
+
} : undefined,
|
|
157
|
+
},
|
|
148
158
|
});
|
|
149
159
|
if (stream) {
|
|
150
160
|
try {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const WORKSPACE_COMMAND = /^\/workspace(?:\s+([\s\S]+))?$/i;
|
|
2
|
+
|
|
3
|
+
export async function runWorkspaceCommand(text, harness) {
|
|
4
|
+
if (typeof text !== 'string') return null;
|
|
5
|
+
const match = WORKSPACE_COMMAND.exec(text.trim());
|
|
6
|
+
if (!match) return null;
|
|
7
|
+
const workspace = match[1]?.trim();
|
|
8
|
+
if (!workspace) {
|
|
9
|
+
return { handled: true, message: '用法:/workspace 工作区绝对路径' };
|
|
10
|
+
}
|
|
11
|
+
if (typeof harness?.switchWorkspace !== 'function') {
|
|
12
|
+
return { handled: true, message: '当前机器人暂不支持切换工作区。' };
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const current = await harness.switchWorkspace(workspace);
|
|
16
|
+
return { handled: true, message: `工作区已切换为:${current}` };
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (['workspace-not-absolute', 'workspace-not-found', 'workspace-not-directory'].includes(error?.code)) {
|
|
19
|
+
return { handled: true, message: `${error.message}\n用法:/workspace 工作区绝对路径` };
|
|
20
|
+
}
|
|
21
|
+
if (error?.code === 'workspace-bot-not-found') {
|
|
22
|
+
return { handled: true, message: '机器人正在移除或已重新接入,无法切换原会话的工作区。' };
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const WORKSPACE_SESSION_STALE = 'workspace-session-stale';
|
|
2
|
+
|
|
3
|
+
async function sessionExists(harness, sessionId, options) {
|
|
4
|
+
return options === undefined
|
|
5
|
+
? harness.sessionExists(sessionId)
|
|
6
|
+
: harness.sessionExists(sessionId, options);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function createSession(harness, options) {
|
|
10
|
+
return options === undefined
|
|
11
|
+
? harness.createSession()
|
|
12
|
+
: harness.createSession(options);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve, persist, and ask through a session that belongs to the bot's
|
|
17
|
+
* current workspace. A concurrent workspace switch invalidates the scoped
|
|
18
|
+
* session and retries before any prompt is sent to the stale session.
|
|
19
|
+
*/
|
|
20
|
+
export async function askInWorkspaceSession({
|
|
21
|
+
harness,
|
|
22
|
+
state,
|
|
23
|
+
key,
|
|
24
|
+
text,
|
|
25
|
+
createOptions,
|
|
26
|
+
existsOptions,
|
|
27
|
+
askOptions,
|
|
28
|
+
}) {
|
|
29
|
+
while (true) {
|
|
30
|
+
let sessionId = state.sessionFor(key);
|
|
31
|
+
if (!sessionId || !(await sessionExists(harness, sessionId, existsOptions))) {
|
|
32
|
+
sessionId = await createSession(harness, createOptions);
|
|
33
|
+
if (await state.setSession(key, sessionId) === false) continue;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return {
|
|
37
|
+
sessionId,
|
|
38
|
+
answer: await harness.ask(sessionId, text, askOptions),
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error?.code !== WORKSPACE_SESSION_STALE) throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { generateReqId } from '@wecom/aibot-node-sdk';
|
|
2
|
+
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
3
|
+
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
2
4
|
|
|
3
5
|
const HELP_TEXT = [
|
|
4
6
|
'企业微信机器人已连接 DeepSeek Harness。',
|
|
5
7
|
'',
|
|
6
8
|
'直接发送文字即可继续当前会话。',
|
|
7
9
|
'/new 开启一个全新会话',
|
|
10
|
+
'/workspace 工作区绝对路径 切换工作区',
|
|
8
11
|
'/status 检查连接状态',
|
|
9
12
|
'/help 显示本帮助',
|
|
10
13
|
].join('\n');
|
|
@@ -182,11 +185,11 @@ export class WecomHarnessBridge {
|
|
|
182
185
|
await this.#state.markSeen(messageId);
|
|
183
186
|
return;
|
|
184
187
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
189
|
+
if (workspaceCommand) {
|
|
190
|
+
await this.#sendImmediate(frame, chatId, workspaceCommand.message);
|
|
191
|
+
await this.#state.markSeen(messageId);
|
|
192
|
+
return;
|
|
190
193
|
}
|
|
191
194
|
|
|
192
195
|
streamId = this.#generateReqId('stream');
|
|
@@ -197,14 +200,20 @@ export class WecomHarnessBridge {
|
|
|
197
200
|
this.#logger.warn?.('[dsh-im:wecom] unable to start a stream; using an active reply:', error);
|
|
198
201
|
}
|
|
199
202
|
|
|
200
|
-
const answer = await
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
:
|
|
203
|
+
const { answer } = await askInWorkspaceSession({
|
|
204
|
+
harness: this.#harness,
|
|
205
|
+
state: this.#state,
|
|
206
|
+
key,
|
|
207
|
+
text,
|
|
208
|
+
askOptions: {
|
|
209
|
+
timeoutMs: this.#replyTimeoutMs,
|
|
210
|
+
onUpdate: streamStarted && typeof this.#client.replyStreamNonBlocking === 'function'
|
|
211
|
+
? async (update) => {
|
|
212
|
+
const progress = splitUtf8(progressText(update))[0];
|
|
213
|
+
if (progress) await this.#client.replyStreamNonBlocking(frame, streamId, progress, false);
|
|
214
|
+
}
|
|
215
|
+
: undefined,
|
|
216
|
+
},
|
|
208
217
|
});
|
|
209
218
|
|
|
210
219
|
const chunks = splitUtf8(answer || '任务已完成,但没有生成可显示的文本。');
|
|
@@ -400,7 +400,14 @@ export class WecomController {
|
|
|
400
400
|
if (record.controller.signal.aborted) {
|
|
401
401
|
await this.#stopRuntime(identity.botId);
|
|
402
402
|
if (previousConfig) await this.#configStore.save(previousConfig).catch(() => undefined);
|
|
403
|
-
else
|
|
403
|
+
else {
|
|
404
|
+
const removed = await this.#configStore.remove(identity.botId).catch(() => null);
|
|
405
|
+
if (removed) {
|
|
406
|
+
await this.#deleteState({ botId: identity.botId, config }).catch((cleanupError) => {
|
|
407
|
+
this.#logger.warn?.('[dsh-im:wecom] cancelled bot state cleanup failed:', cleanupError);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
404
411
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
405
412
|
throw error;
|
|
406
413
|
}
|
|
@@ -187,17 +187,18 @@ export class HarnessClient {
|
|
|
187
187
|
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
async workspaceId() {
|
|
190
|
+
async workspaceId(options = {}) {
|
|
191
|
+
const workspace = options.workspace ?? this.#workspace;
|
|
191
192
|
const { items } = await this.rpc('workspace.list', {});
|
|
192
|
-
const existing = items.find((item) => item.path ===
|
|
193
|
+
const existing = items.find((item) => item.path === workspace);
|
|
193
194
|
if (existing) return existing.workspaceId;
|
|
194
|
-
const created = await this.rpc('workspace.create', { path:
|
|
195
|
+
const created = await this.rpc('workspace.create', { path: workspace });
|
|
195
196
|
return created.workspace.workspaceId;
|
|
196
197
|
}
|
|
197
198
|
|
|
198
|
-
async createSession() {
|
|
199
|
+
async createSession(options = {}) {
|
|
199
200
|
await this.ensureRunning();
|
|
200
|
-
const workspaceId = await this.workspaceId();
|
|
201
|
+
const workspaceId = await this.workspaceId(options);
|
|
201
202
|
const created = await this.rpc('session.create', {
|
|
202
203
|
workspaceId,
|
|
203
204
|
agentPreset: this.#agentPreset,
|
|
@@ -3,12 +3,15 @@ import {
|
|
|
3
3
|
splitWeixinText,
|
|
4
4
|
weixinMessageId,
|
|
5
5
|
} from './weixin-api.mjs';
|
|
6
|
+
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
7
|
+
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
6
8
|
|
|
7
9
|
const HELP_TEXT = [
|
|
8
10
|
'微信已连接 DeepSeek Harness。',
|
|
9
11
|
'',
|
|
10
12
|
'直接发送文字或带文字识别结果的语音即可继续当前会话。',
|
|
11
13
|
'/new 开启一个全新会话',
|
|
14
|
+
'/workspace 工作区绝对路径 切换工作区',
|
|
12
15
|
'/status 检查连接状态',
|
|
13
16
|
'/help 显示本帮助',
|
|
14
17
|
].join('\n');
|
|
@@ -133,14 +136,21 @@ export class WeixinHarnessBridge {
|
|
|
133
136
|
await this.#state.markSeen(messageId);
|
|
134
137
|
return;
|
|
135
138
|
}
|
|
139
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
140
|
+
if (workspaceCommand) {
|
|
141
|
+
await this.#send(sender, workspaceCommand.message, contextToken, runId);
|
|
142
|
+
await this.#state.markSeen(messageId);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
136
145
|
|
|
137
146
|
const key = conversationKey(sender);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
const { answer } = await askInWorkspaceSession({
|
|
148
|
+
harness: this.#harness,
|
|
149
|
+
state: this.#state,
|
|
150
|
+
key,
|
|
151
|
+
text,
|
|
152
|
+
askOptions: { timeoutMs: this.#replyTimeoutMs },
|
|
153
|
+
});
|
|
144
154
|
await this.#send(sender, answer, contextToken, runId);
|
|
145
155
|
await this.#state.markSeen(messageId);
|
|
146
156
|
this.#status.messagesReplied += 1;
|
|
@@ -468,7 +468,12 @@ export class WeixinController {
|
|
|
468
468
|
await this.#stopRuntime(identity.botId);
|
|
469
469
|
if (previousConfig) await this.#configStore.save(previousConfig).catch(() => undefined);
|
|
470
470
|
else if (this.#configStore.get(identity.botId)) {
|
|
471
|
-
await this.#configStore.remove(identity.botId).catch(() =>
|
|
471
|
+
const removed = await this.#configStore.remove(identity.botId).catch(() => null);
|
|
472
|
+
if (removed) {
|
|
473
|
+
await this.#deleteState({ botId: identity.botId, config }).catch((cleanupError) => {
|
|
474
|
+
this.#logger.warn?.('[dsh-weixin] failed to clean up cancelled bot state:', cleanupError);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
472
477
|
}
|
|
473
478
|
await this.#restoreCredential(identity.tokenRef, previousToken);
|
|
474
479
|
if (previousConfig && previousToken?.value) {
|
|
@@ -311,7 +311,14 @@ export class WhatsappController {
|
|
|
311
311
|
if (record.controller.signal.aborted || this.#closed || error?.name === 'AbortError') {
|
|
312
312
|
await this.#stopRuntime(botId);
|
|
313
313
|
if (previous) await this.#configStore.save(previous).catch(() => undefined);
|
|
314
|
-
else
|
|
314
|
+
else {
|
|
315
|
+
const removed = await this.#configStore.remove(botId).catch(() => null);
|
|
316
|
+
if (removed) {
|
|
317
|
+
await this.#deleteState({ botId, config }).catch((cleanupError) => {
|
|
318
|
+
this.#logger.warn?.('[dsh-im:whatsapp] cancelled bot state cleanup failed:', cleanupError);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
315
322
|
await this.#deleteAuth(record.authDirectory).catch(() => undefined);
|
|
316
323
|
if (previous) await this.#startRuntime(previous).catch(() => undefined);
|
|
317
324
|
record.state = 'cancelled';
|