@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.
Files changed (32) hide show
  1. package/README.en.md +71 -37
  2. package/README.md +73 -39
  3. package/lib/client.js +2 -2
  4. package/lib/index.js +134 -124
  5. package/package.json +31 -2
  6. package/plugin-src/client/i18n.js +1 -1
  7. package/plugin-src/client/index.js +1 -1
  8. package/plugin-src/host/channels/dingtalk/production.mjs +7 -0
  9. package/plugin-src/host/channels/feishu/production.mjs +7 -0
  10. package/plugin-src/host/channels/qq/production.mjs +7 -0
  11. package/plugin-src/host/channels/shared/production.mjs +7 -0
  12. package/plugin-src/host/channels/slack/production.mjs +7 -0
  13. package/plugin-src/host/channels/wecom/production.mjs +7 -0
  14. package/plugin-src/host/channels/weixin/production.mjs +7 -0
  15. package/plugin-src/host/channels/whatsapp/production.mjs +7 -0
  16. package/plugin-src/host/harness-session-coordinator.mjs +134 -0
  17. package/scripts/verify-package.mjs +34 -0
  18. package/src/channels/dingtalk/dingtalk-bridge.mjs +82 -0
  19. package/src/channels/discord/discord-api.mjs +1 -1
  20. package/src/channels/feishu/bridge.mjs +82 -0
  21. package/src/channels/qq/qq-bridge.mjs +85 -0
  22. package/src/channels/shared/bot-workspace-store.mjs +45 -1
  23. package/src/channels/shared/control-command.mjs +84 -0
  24. package/src/channels/shared/harness-approval.mjs +4 -0
  25. package/src/channels/shared/harness-client.mjs +336 -29
  26. package/src/channels/shared/model-command.mjs +352 -0
  27. package/src/channels/shared/session-binding-lock.mjs +40 -0
  28. package/src/channels/shared/text-harness-bridge.mjs +82 -0
  29. package/src/channels/shared/workspace-session.mjs +21 -9
  30. package/src/channels/wecom/wecom-bridge.mjs +76 -0
  31. package/src/channels/weixin/weixin-api.mjs +1 -1
  32. 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');
@@ -184,7 +184,7 @@ function authenticatedHeaders(token) {
184
184
  function baseInfo() {
185
185
  return {
186
186
  channel_version: WEIXIN_PROTOCOL_VERSION,
187
- bot_agent: 'DeepSeekHarness/0.11.0',
187
+ bot_agent: 'DeepSeekHarness/0.13.0',
188
188
  };
189
189
  }
190
190
 
@@ -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);