@xmanrui/dsh-im 0.11.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.
@@ -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,10 @@ const HELP_TEXT = [
32
40
  '/workspacelist 列出工作区绝对路径',
33
41
  '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
34
42
  '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
43
+ '/models 列出所有可用模型',
44
+ '/model [模型ID] 查看或切换当前会话模型',
45
+ '/stop 停止当前任务',
46
+ '/steer 补充指令 纠偏当前任务',
35
47
  '/status 检查连接状态',
36
48
  '/help 显示本帮助',
37
49
  ].join('\n');
@@ -85,6 +97,7 @@ export class WeixinHarnessBridge {
85
97
  #interactionKeys = new Map();
86
98
  #acceptedMessageIds = new Set();
87
99
  #approvalTasks = new Set();
100
+ #commandTasks = new Set();
88
101
  #approvals;
89
102
 
90
103
  constructor({
@@ -136,6 +149,34 @@ export class WeixinHarnessBridge {
136
149
  const contextToken = nonEmptyString(message?.context_token) ?? undefined;
137
150
  const runId = nonEmptyString(message?.run_id) ?? undefined;
138
151
  const pending = this.#pendingInteractions.get(key);
152
+ const commandText = nonEmptyString(extractWeixinText(message)) ?? '';
153
+ const commandRunner = isControlCommand(commandText)
154
+ ? runControlCommand
155
+ : (isModelCommand(commandText) ? runModelCommand : null);
156
+ if (commandRunner && sender === this.#ownerUserId) {
157
+ let task;
158
+ task = this.#processFastCommand(
159
+ message,
160
+ messageId,
161
+ key,
162
+ sender,
163
+ contextToken,
164
+ runId,
165
+ commandText,
166
+ commandRunner,
167
+ ).catch((error) => {
168
+ if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
169
+ this.#status.lastError = error?.message ?? String(error);
170
+ this.#logger.error?.('[dsh-weixin] failed to process a command:', error);
171
+ return this.#send(sender, '消息处理失败,请稍后重试。', contextToken, runId)
172
+ .catch(() => undefined);
173
+ }).finally(() => {
174
+ this.#acceptedMessageIds.delete(messageId);
175
+ this.#commandTasks.delete(task);
176
+ });
177
+ this.#commandTasks.add(task);
178
+ return task;
179
+ }
139
180
  const approval = this.#approvals.claimReply({
140
181
  key,
141
182
  actor: sender,
@@ -211,9 +252,44 @@ export class WeixinHarnessBridge {
211
252
  pending.queue ? [pending.queue] : []
212
253
  )),
213
254
  ...this.#approvalTasks,
255
+ ...this.#commandTasks,
214
256
  ]);
215
257
  }
216
258
 
259
+ async #processFastCommand(
260
+ message,
261
+ messageId,
262
+ key,
263
+ sender,
264
+ contextToken,
265
+ runId,
266
+ text,
267
+ runner,
268
+ ) {
269
+ this.#signal?.throwIfAborted();
270
+ if (this.#state.hasSeen(messageId)) return;
271
+ await this.#state.markSeen(messageId);
272
+ this.#status.messagesReceived += 1;
273
+ this.#status.lastMessageAt = new Date().toISOString();
274
+ const result = await runner(text, this.#harness, this.#state, key, {
275
+ signal: this.#signal,
276
+ hasImages: hasWeixinImageItems(message),
277
+ pendingInteraction: this.#pendingInteractions.has(key)
278
+ || this.#approvals.hasPending(key),
279
+ control: { owner: this, key },
280
+ });
281
+ if (result?.stopped) {
282
+ await Promise.allSettled([
283
+ this.#cancelPendingInteraction(key),
284
+ this.#approvals.closeRoute(key),
285
+ ]);
286
+ }
287
+ for (const reply of result?.messages ?? [result?.message]) {
288
+ if (reply) await this.#send(sender, reply, contextToken, runId);
289
+ }
290
+ this.#status.lastError = null;
291
+ }
292
+
217
293
  async #process(message, key, { alreadyRecorded = false } = {}) {
218
294
  this.#signal?.throwIfAborted();
219
295
  const messageId = weixinMessageId(message);
@@ -303,6 +379,7 @@ export class WeixinHarnessBridge {
303
379
  askOptions: {
304
380
  timeoutMs: this.#replyTimeoutMs,
305
381
  signal: this.#signal,
382
+ control: { owner: this, key },
306
383
  onInteraction: (interaction) => this.#handleInteraction(interaction, {
307
384
  key,
308
385
  actor: sender,
@@ -324,6 +401,10 @@ export class WeixinHarnessBridge {
324
401
  this.#status.lastReplyAt = new Date().toISOString();
325
402
  this.#status.lastError = null;
326
403
  } catch (error) {
404
+ if (error?.code === 'turn-stopped') {
405
+ await this.#state.markSeen(messageId);
406
+ return;
407
+ }
327
408
  if (this.#signal?.aborted) return;
328
409
  this.#status.lastError = error?.message ?? String(error);
329
410
  this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);