@xmanrui/dsh-im 0.11.0 → 0.13.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 +71 -37
- package/README.md +73 -39
- package/lib/client.js +2 -2
- package/lib/index.js +134 -124
- package/package.json +31 -2
- package/plugin-src/client/i18n.js +1 -1
- package/plugin-src/client/index.js +1 -1
- package/plugin-src/host/channels/dingtalk/production.mjs +7 -0
- package/plugin-src/host/channels/feishu/production.mjs +7 -0
- package/plugin-src/host/channels/qq/production.mjs +7 -0
- package/plugin-src/host/channels/shared/production.mjs +7 -0
- package/plugin-src/host/channels/slack/production.mjs +7 -0
- package/plugin-src/host/channels/wecom/production.mjs +7 -0
- package/plugin-src/host/channels/weixin/production.mjs +7 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +7 -0
- package/plugin-src/host/harness-session-coordinator.mjs +134 -0
- package/scripts/verify-package.mjs +34 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +82 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +82 -0
- package/src/channels/qq/qq-bridge.mjs +85 -0
- package/src/channels/shared/bot-workspace-store.mjs +45 -1
- 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 +336 -29
- package/src/channels/shared/model-command.mjs +352 -0
- package/src/channels/shared/session-binding-lock.mjs +40 -0
- package/src/channels/shared/text-harness-bridge.mjs +82 -0
- package/src/channels/shared/workspace-session.mjs +21 -9
- package/src/channels/wecom/wecom-bridge.mjs +76 -0
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +82 -0
|
@@ -6,6 +6,14 @@ import {
|
|
|
6
6
|
} from '../shared/harness-question.mjs';
|
|
7
7
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
8
8
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
9
|
+
import {
|
|
10
|
+
isControlCommand,
|
|
11
|
+
runControlCommand,
|
|
12
|
+
} from '../shared/control-command.mjs';
|
|
13
|
+
import {
|
|
14
|
+
isModelCommand,
|
|
15
|
+
runModelCommand,
|
|
16
|
+
} from '../shared/model-command.mjs';
|
|
9
17
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
10
18
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
11
19
|
import {
|
|
@@ -26,6 +34,11 @@ const HELP_TEXT = [
|
|
|
26
34
|
'/workspacelist 列出工作区绝对路径',
|
|
27
35
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
28
36
|
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
37
|
+
'/models 按序号列出所有可用模型',
|
|
38
|
+
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
|
39
|
+
'示例:先发 /models,再发 /model 2',
|
|
40
|
+
'/stop 停止当前任务',
|
|
41
|
+
'/steer 补充指令 纠偏当前任务',
|
|
29
42
|
'/status 检查连接状态',
|
|
30
43
|
'/help 显示本帮助',
|
|
31
44
|
].join('\n');
|
|
@@ -223,6 +236,7 @@ export class WecomHarnessBridge {
|
|
|
223
236
|
#interactionKeys = new Map();
|
|
224
237
|
#acceptedMessageIds = new Set();
|
|
225
238
|
#approvalTasks = new Set();
|
|
239
|
+
#commandTasks = new Set();
|
|
226
240
|
#approvals;
|
|
227
241
|
#prefetchedImageCount = 0;
|
|
228
242
|
|
|
@@ -274,6 +288,33 @@ export class WecomHarnessBridge {
|
|
|
274
288
|
rememberConnectionTestTarget(this.#state, { chatId });
|
|
275
289
|
}
|
|
276
290
|
const pending = this.#pendingInteractions.get(key);
|
|
291
|
+
const commandMessage = wecomInboundMessage(frame, this.#client);
|
|
292
|
+
const commandText = nonEmptyString(commandMessage.content) ?? '';
|
|
293
|
+
const commandRunner = isControlCommand(commandText)
|
|
294
|
+
? runControlCommand
|
|
295
|
+
: (isModelCommand(commandText) ? runModelCommand : null);
|
|
296
|
+
if (commandRunner) {
|
|
297
|
+
let task;
|
|
298
|
+
task = this.#processFastCommand(
|
|
299
|
+
frame,
|
|
300
|
+
messageId,
|
|
301
|
+
chatId,
|
|
302
|
+
key,
|
|
303
|
+
commandMessage,
|
|
304
|
+
commandRunner,
|
|
305
|
+
).catch((error) => {
|
|
306
|
+
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
307
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
308
|
+
this.#logger.error?.('[dsh-im:wecom] failed to process a command');
|
|
309
|
+
return this.#sendImmediate(frame, chatId, '消息处理失败,请稍后重试。')
|
|
310
|
+
.catch(() => undefined);
|
|
311
|
+
}).finally(() => {
|
|
312
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
313
|
+
this.#commandTasks.delete(task);
|
|
314
|
+
});
|
|
315
|
+
this.#commandTasks.add(task);
|
|
316
|
+
return task;
|
|
317
|
+
}
|
|
277
318
|
const approval = this.#approvals.claimReply({
|
|
278
319
|
key,
|
|
279
320
|
actor: senderId,
|
|
@@ -377,9 +418,35 @@ export class WecomHarnessBridge {
|
|
|
377
418
|
pending.queue ? [pending.queue] : []
|
|
378
419
|
)),
|
|
379
420
|
...this.#approvalTasks,
|
|
421
|
+
...this.#commandTasks,
|
|
380
422
|
]);
|
|
381
423
|
}
|
|
382
424
|
|
|
425
|
+
async #processFastCommand(frame, messageId, chatId, key, message, runner) {
|
|
426
|
+
this.#signal?.throwIfAborted();
|
|
427
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
428
|
+
await this.#state.markSeen(messageId);
|
|
429
|
+
this.#status.messagesReceived += 1;
|
|
430
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
431
|
+
const result = await runner(message.content, this.#harness, this.#state, key, {
|
|
432
|
+
signal: this.#signal,
|
|
433
|
+
hasImages: hasInboundImages(message),
|
|
434
|
+
pendingInteraction: this.#pendingInteractions.has(key)
|
|
435
|
+
|| this.#approvals.hasPending(key),
|
|
436
|
+
control: { owner: this, key },
|
|
437
|
+
});
|
|
438
|
+
if (result?.stopped) {
|
|
439
|
+
await Promise.allSettled([
|
|
440
|
+
this.#cancelPendingInteraction(key),
|
|
441
|
+
this.#approvals.closeRoute(key),
|
|
442
|
+
]);
|
|
443
|
+
}
|
|
444
|
+
for (const reply of result?.messages ?? [result?.message]) {
|
|
445
|
+
if (reply) await this.#sendImmediate(frame, chatId, reply);
|
|
446
|
+
}
|
|
447
|
+
this.#status.lastError = null;
|
|
448
|
+
}
|
|
449
|
+
|
|
383
450
|
async #sendActive(chatId, text) {
|
|
384
451
|
for (const chunk of splitUtf8(text)) {
|
|
385
452
|
this.#signal?.throwIfAborted();
|
|
@@ -490,6 +557,7 @@ export class WecomHarnessBridge {
|
|
|
490
557
|
askOptions: {
|
|
491
558
|
timeoutMs: this.#replyTimeoutMs,
|
|
492
559
|
signal: this.#signal,
|
|
560
|
+
control: { owner: this, key },
|
|
493
561
|
onUpdate: streamStarted && typeof this.#client.replyStreamNonBlocking === 'function'
|
|
494
562
|
? async (update) => {
|
|
495
563
|
const progress = splitUtf8(progressText(update))[0];
|
|
@@ -525,6 +593,14 @@ export class WecomHarnessBridge {
|
|
|
525
593
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
526
594
|
this.#status.lastError = null;
|
|
527
595
|
} catch (error) {
|
|
596
|
+
if (error?.code === 'turn-stopped') {
|
|
597
|
+
if (streamStarted && streamId) {
|
|
598
|
+
await this.#client.replyStream(frame, streamId, '已停止。', true)
|
|
599
|
+
.catch(() => undefined);
|
|
600
|
+
}
|
|
601
|
+
await this.#state.markSeen(messageId);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
528
604
|
if (this.#signal?.aborted) return;
|
|
529
605
|
this.#status.lastError = error?.message ?? String(error);
|
|
530
606
|
this.#logger.error?.('[dsh-im:wecom] failed to process an inbound message');
|
|
@@ -11,6 +11,14 @@ import {
|
|
|
11
11
|
} from '../shared/harness-question.mjs';
|
|
12
12
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
13
13
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
14
|
+
import {
|
|
15
|
+
isControlCommand,
|
|
16
|
+
runControlCommand,
|
|
17
|
+
} from '../shared/control-command.mjs';
|
|
18
|
+
import {
|
|
19
|
+
isModelCommand,
|
|
20
|
+
runModelCommand,
|
|
21
|
+
} from '../shared/model-command.mjs';
|
|
14
22
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
15
23
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
16
24
|
import {
|
|
@@ -32,6 +40,11 @@ const HELP_TEXT = [
|
|
|
32
40
|
'/workspacelist 列出工作区绝对路径',
|
|
33
41
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
34
42
|
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
43
|
+
'/models 按序号列出所有可用模型',
|
|
44
|
+
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
|
45
|
+
'示例:先发 /models,再发 /model 2',
|
|
46
|
+
'/stop 停止当前任务',
|
|
47
|
+
'/steer 补充指令 纠偏当前任务',
|
|
35
48
|
'/status 检查连接状态',
|
|
36
49
|
'/help 显示本帮助',
|
|
37
50
|
].join('\n');
|
|
@@ -85,6 +98,7 @@ export class WeixinHarnessBridge {
|
|
|
85
98
|
#interactionKeys = new Map();
|
|
86
99
|
#acceptedMessageIds = new Set();
|
|
87
100
|
#approvalTasks = new Set();
|
|
101
|
+
#commandTasks = new Set();
|
|
88
102
|
#approvals;
|
|
89
103
|
|
|
90
104
|
constructor({
|
|
@@ -136,6 +150,34 @@ export class WeixinHarnessBridge {
|
|
|
136
150
|
const contextToken = nonEmptyString(message?.context_token) ?? undefined;
|
|
137
151
|
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
|
138
152
|
const pending = this.#pendingInteractions.get(key);
|
|
153
|
+
const commandText = nonEmptyString(extractWeixinText(message)) ?? '';
|
|
154
|
+
const commandRunner = isControlCommand(commandText)
|
|
155
|
+
? runControlCommand
|
|
156
|
+
: (isModelCommand(commandText) ? runModelCommand : null);
|
|
157
|
+
if (commandRunner && sender === this.#ownerUserId) {
|
|
158
|
+
let task;
|
|
159
|
+
task = this.#processFastCommand(
|
|
160
|
+
message,
|
|
161
|
+
messageId,
|
|
162
|
+
key,
|
|
163
|
+
sender,
|
|
164
|
+
contextToken,
|
|
165
|
+
runId,
|
|
166
|
+
commandText,
|
|
167
|
+
commandRunner,
|
|
168
|
+
).catch((error) => {
|
|
169
|
+
if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
|
|
170
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
171
|
+
this.#logger.error?.('[dsh-weixin] failed to process a command:', error);
|
|
172
|
+
return this.#send(sender, '消息处理失败,请稍后重试。', contextToken, runId)
|
|
173
|
+
.catch(() => undefined);
|
|
174
|
+
}).finally(() => {
|
|
175
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
176
|
+
this.#commandTasks.delete(task);
|
|
177
|
+
});
|
|
178
|
+
this.#commandTasks.add(task);
|
|
179
|
+
return task;
|
|
180
|
+
}
|
|
139
181
|
const approval = this.#approvals.claimReply({
|
|
140
182
|
key,
|
|
141
183
|
actor: sender,
|
|
@@ -211,9 +253,44 @@ export class WeixinHarnessBridge {
|
|
|
211
253
|
pending.queue ? [pending.queue] : []
|
|
212
254
|
)),
|
|
213
255
|
...this.#approvalTasks,
|
|
256
|
+
...this.#commandTasks,
|
|
214
257
|
]);
|
|
215
258
|
}
|
|
216
259
|
|
|
260
|
+
async #processFastCommand(
|
|
261
|
+
message,
|
|
262
|
+
messageId,
|
|
263
|
+
key,
|
|
264
|
+
sender,
|
|
265
|
+
contextToken,
|
|
266
|
+
runId,
|
|
267
|
+
text,
|
|
268
|
+
runner,
|
|
269
|
+
) {
|
|
270
|
+
this.#signal?.throwIfAborted();
|
|
271
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
272
|
+
await this.#state.markSeen(messageId);
|
|
273
|
+
this.#status.messagesReceived += 1;
|
|
274
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
275
|
+
const result = await runner(text, this.#harness, this.#state, key, {
|
|
276
|
+
signal: this.#signal,
|
|
277
|
+
hasImages: hasWeixinImageItems(message),
|
|
278
|
+
pendingInteraction: this.#pendingInteractions.has(key)
|
|
279
|
+
|| this.#approvals.hasPending(key),
|
|
280
|
+
control: { owner: this, key },
|
|
281
|
+
});
|
|
282
|
+
if (result?.stopped) {
|
|
283
|
+
await Promise.allSettled([
|
|
284
|
+
this.#cancelPendingInteraction(key),
|
|
285
|
+
this.#approvals.closeRoute(key),
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
288
|
+
for (const reply of result?.messages ?? [result?.message]) {
|
|
289
|
+
if (reply) await this.#send(sender, reply, contextToken, runId);
|
|
290
|
+
}
|
|
291
|
+
this.#status.lastError = null;
|
|
292
|
+
}
|
|
293
|
+
|
|
217
294
|
async #process(message, key, { alreadyRecorded = false } = {}) {
|
|
218
295
|
this.#signal?.throwIfAborted();
|
|
219
296
|
const messageId = weixinMessageId(message);
|
|
@@ -303,6 +380,7 @@ export class WeixinHarnessBridge {
|
|
|
303
380
|
askOptions: {
|
|
304
381
|
timeoutMs: this.#replyTimeoutMs,
|
|
305
382
|
signal: this.#signal,
|
|
383
|
+
control: { owner: this, key },
|
|
306
384
|
onInteraction: (interaction) => this.#handleInteraction(interaction, {
|
|
307
385
|
key,
|
|
308
386
|
actor: sender,
|
|
@@ -324,6 +402,10 @@ export class WeixinHarnessBridge {
|
|
|
324
402
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
325
403
|
this.#status.lastError = null;
|
|
326
404
|
} catch (error) {
|
|
405
|
+
if (error?.code === 'turn-stopped') {
|
|
406
|
+
await this.#state.markSeen(messageId);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
327
409
|
if (this.#signal?.aborted) return;
|
|
328
410
|
this.#status.lastError = error?.message ?? String(error);
|
|
329
411
|
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
|