@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.
package/package.json CHANGED
@@ -1,7 +1,36 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "0.11.0",
4
- "description": "通过扫码、App Manifest或机器人凭据把IM机器人接入DeepSeek Harness(支持飞书、微信、钉钉、企业微信、QQ、Slack、Telegram、Discord和WhatsApp)。",
3
+ "version": "0.12.0",
4
+ "description": "通过扫码或机器人凭据把IM机器人接入DeepSeek Harness(支持飞书、微信、钉钉、企业微信、QQ、Slack、Telegram、Discord和WhatsApp)。 Connect IM bots to DeepSeek Harness via QR code or credentials (9 channels).",
5
+ "keywords": [
6
+ "deepseek-harness",
7
+ "dsh",
8
+ "dsh-plugin",
9
+ "ai-agent",
10
+ "im",
11
+ "instant-messaging",
12
+ "chatbot",
13
+ "feishu",
14
+ "lark",
15
+ "wechat",
16
+ "wecom",
17
+ "dingtalk",
18
+ "qq",
19
+ "slack",
20
+ "telegram",
21
+ "discord",
22
+ "whatsapp"
23
+ ],
24
+ "author": {
25
+ "name": "xmanrui",
26
+ "url": "https://github.com/xmanrui"
27
+ },
28
+ "contributors": [
29
+ {
30
+ "name": "C3H3-AI",
31
+ "url": "https://github.com/C3H3-AI"
32
+ }
33
+ ],
5
34
  "license": "MIT",
6
35
  "type": "module",
7
36
  "repository": {
@@ -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);
@@ -81,6 +82,10 @@ export async function createProductionController(ctx, config = {}, internals = {
81
82
  return state;
82
83
  };
83
84
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
85
+ const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
86
+ controlExecutor: internals.controlExecutor,
87
+ sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
88
+ });
84
89
  const harness = new Harness({
85
90
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
86
91
  workspace: defaultWorkspace,
@@ -88,6 +93,8 @@ export async function createProductionController(ctx, config = {}, internals = {
88
93
  autostart: false,
89
94
  dshBin: config.dshBin ?? 'dsh',
90
95
  ...(commandExecutor ? { commandExecutor } : {}),
96
+ ...(controlExecutor ? { controlExecutor } : {}),
97
+ ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
91
98
  });
92
99
  const coreController = new Controller({
93
100
  deviceAuth,
@@ -4,6 +4,7 @@ import { unlink } from 'node:fs/promises';
4
4
  import * as Lark from '@larksuiteoapi/node-sdk';
5
5
  import { createConnectionSupervisor } from './connection-supervisor.mjs';
6
6
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
7
+ import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
7
8
  import { verifyFeishuApp } from '../../../../src/channels/feishu/feishu-app.mjs';
8
9
  import { FeishuRuntime } from '../../../../src/channels/feishu/feishu-runtime.mjs';
9
10
  import { HarnessClient } from '../../../../src/channels/feishu/harness-client.mjs';
@@ -107,6 +108,10 @@ export async function createProductionController(ctx, config = {}, internals = {
107
108
  return stateFor(botConfig);
108
109
  };
109
110
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
111
+ const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
112
+ controlExecutor: internals.controlExecutor,
113
+ sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
114
+ });
110
115
  const harness = new Harness({
111
116
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
112
117
  workspace: defaultWorkspace,
@@ -116,6 +121,8 @@ export async function createProductionController(ctx, config = {}, internals = {
116
121
  autostart: false,
117
122
  dshBin: config.dshBin ?? 'dsh',
118
123
  ...(commandExecutor ? { commandExecutor } : {}),
124
+ ...(controlExecutor ? { controlExecutor } : {}),
125
+ ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
119
126
  });
120
127
 
121
128
  const coreController = new Controller({
@@ -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);
@@ -72,6 +73,10 @@ export async function createProductionController(ctx, config = {}, internals = {
72
73
  return state;
73
74
  };
74
75
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
76
+ const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
77
+ controlExecutor: internals.controlExecutor,
78
+ sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
79
+ });
75
80
  const harness = new Harness({
76
81
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
77
82
  workspace: defaultWorkspace,
@@ -79,6 +84,8 @@ export async function createProductionController(ctx, config = {}, internals = {
79
84
  autostart: false,
80
85
  dshBin: config.dshBin ?? 'dsh',
81
86
  ...(commandExecutor ? { commandExecutor } : {}),
87
+ ...(controlExecutor ? { controlExecutor } : {}),
88
+ ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
82
89
  });
83
90
  const coreController = new Controller({
84
91
  qrAuth,
@@ -4,6 +4,7 @@ import { join, resolve } from 'node:path';
4
4
 
5
5
  import { createTokenConnectionSupervisor } from './connection-supervisor.mjs';
6
6
  import { createHarnessCommandExecutor } from '../../harness-command-executor.mjs';
7
+ import { createHarnessSessionExecutors } from '../../harness-session-coordinator.mjs';
7
8
  import {
8
9
  BotWorkspaceStore,
9
10
  createBotWorkspaceScope,
@@ -66,6 +67,10 @@ export async function createTokenProductionController(ctx, config, internals, de
66
67
  return state;
67
68
  };
68
69
  const commandExecutor = createHarnessCommandExecutor(ctx, internals.commandExecutor);
70
+ const { controlExecutor, sessionMaintenanceExecutor } = createHarnessSessionExecutors(ctx, {
71
+ controlExecutor: internals.controlExecutor,
72
+ sessionMaintenanceExecutor: internals.sessionMaintenanceExecutor,
73
+ });
69
74
  const harness = new ResolvedHarness({
70
75
  baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
71
76
  workspace: defaultWorkspace,
@@ -73,6 +78,8 @@ export async function createTokenProductionController(ctx, config, internals, de
73
78
  autostart: false,
74
79
  dshBin: config.dshBin ?? 'dsh',
75
80
  ...(commandExecutor ? { commandExecutor } : {}),
81
+ ...(controlExecutor ? { controlExecutor } : {}),
82
+ ...(sessionMaintenanceExecutor ? { sessionMaintenanceExecutor } : {}),
76
83
  });
77
84
  const coreController = new ResolvedController({
78
85
  credentials: ctx.credentials,
@@ -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,6 +52,10 @@ 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,
@@ -58,6 +63,8 @@ export async function createProductionController(ctx, config = {}, internals = {
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,
@@ -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,6 +76,10 @@ 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,
@@ -82,6 +87,8 @@ export async function createProductionController(ctx, config = {}, internals = {
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,
@@ -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,6 +76,10 @@ 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,
@@ -82,6 +87,8 @@ export async function createProductionController(ctx, config = {}, internals = {
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,
@@ -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,6 +79,10 @@ 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,
@@ -85,6 +90,8 @@ export async function createProductionController(ctx, config = {}, internals = {
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,
@@ -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 {
@@ -33,6 +41,10 @@ const HELP_TEXT = [
33
41
  '/workspacelist 列出工作区绝对路径',
34
42
  '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
35
43
  '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
44
+ '/models 列出所有可用模型',
45
+ '/model [模型ID] 查看或切换当前会话模型',
46
+ '/stop 停止当前任务',
47
+ '/steer 补充指令 纠偏当前任务',
36
48
  '/status 检查连接状态',
37
49
  '/help 显示本帮助',
38
50
  ].join('\n');
@@ -246,6 +258,7 @@ export class DingtalkHarnessBridge {
246
258
  #pendingInteractions = new Map();
247
259
  #interactionKeys = new Map();
248
260
  #interactionTasks = new Set();
261
+ #commandTasks = new Set();
249
262
  #acceptedMessageIds = new Set();
250
263
  #approvals;
251
264
 
@@ -314,6 +327,37 @@ export class DingtalkHarnessBridge {
314
327
  rememberConnectionTestTarget(this.#state, { sessionWebhook });
315
328
  }
316
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
+ }
317
361
  const approvalReply = this.#approvals.claimReply({
318
362
  key,
319
363
  actor: sender,
@@ -412,9 +456,41 @@ export class DingtalkHarnessBridge {
412
456
  pending.queue ? [pending.queue] : []
413
457
  )),
414
458
  ...this.#interactionTasks,
459
+ ...this.#commandTasks,
415
460
  ]);
416
461
  }
417
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
+
418
494
  async #process(message, messageId, sender, key, { alreadyRecorded = false } = {}) {
419
495
  this.#signal?.throwIfAborted();
420
496
  if (!alreadyRecorded) {
@@ -519,6 +595,7 @@ export class DingtalkHarnessBridge {
519
595
  askOptions: {
520
596
  timeoutMs: this.#replyTimeoutMs,
521
597
  signal: this.#signal,
598
+ control: { owner: this, key },
522
599
  onUpdate: cardStarted
523
600
  ? (update) => cardStream.push(progressText(update))
524
601
  : undefined,
@@ -537,6 +614,10 @@ export class DingtalkHarnessBridge {
537
614
  this.#status.lastReplyAt = new Date().toISOString();
538
615
  this.#status.lastError = null;
539
616
  } catch (error) {
617
+ if (error?.code === 'turn-stopped') {
618
+ if (cardStarted) await cardStream.finish('已停止。').catch(() => undefined);
619
+ return;
620
+ }
540
621
  if (this.#signal?.aborted) return;
541
622
  this.#status.lastError = '钉钉消息处理失败。';
542
623
  this.#logger.error?.(
@@ -108,7 +108,7 @@ export class DiscordApi {
108
108
  headers: {
109
109
  authorization: `Bot ${this.#token}`,
110
110
  'content-type': 'application/json',
111
- 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.11.0)',
111
+ 'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.12.0)',
112
112
  },
113
113
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
114
114
  signal: requestSignal(signal, timeoutMs),
@@ -18,7 +18,15 @@ import {
18
18
  } from '../shared/harness-question.mjs';
19
19
  import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
20
20
  import { runCompactCommand } from '../shared/compact-command.mjs';
21
+ import {
22
+ isControlCommand,
23
+ runControlCommand,
24
+ } from '../shared/control-command.mjs';
21
25
  import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
26
+ import {
27
+ isModelCommand,
28
+ runModelCommand,
29
+ } from '../shared/model-command.mjs';
22
30
  import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
23
31
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
24
32
 
@@ -35,6 +43,10 @@ const HELP_TEXT = [
35
43
  '/workspacelist 列出工作区绝对路径',
36
44
  '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
37
45
  '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
46
+ '/models 列出所有可用模型',
47
+ '/model [模型ID] 查看或切换当前会话模型',
48
+ '/stop 停止当前任务',
49
+ '/steer 补充指令 纠偏当前任务',
38
50
  '/status 检查连接状态',
39
51
  '/help 显示本帮助',
40
52
  ].join('\n');
@@ -77,6 +89,7 @@ export class FeishuHarnessBridge {
77
89
  #resolvedQuestionReplies = new Map();
78
90
  #acceptedMessageIds = new Set();
79
91
  #interactionTasks = new Set();
92
+ #commandTasks = new Set();
80
93
  #approvals;
81
94
  #status;
82
95
  #allowedSenderOpenIds;
@@ -141,6 +154,37 @@ export class FeishuHarnessBridge {
141
154
 
142
155
  this.#acceptedMessageIds.add(messageId);
143
156
  const processingReaction = this.#addReaction(messageId, 'OnIt');
157
+ const commandMessage = extractInboundMessage(event, this.#client);
158
+ const commandText = nonEmptyString(commandMessage.content) ?? '';
159
+ const commandRunner = isControlCommand(commandText)
160
+ ? runControlCommand
161
+ : (isModelCommand(commandText) ? runModelCommand : null);
162
+ const addressed = event?.message?.chat_type === 'p2p'
163
+ || (Array.isArray(event?.message?.mentions) && event.message.mentions.length > 0);
164
+ if (commandRunner && addressed) {
165
+ const processing = this.#processFastCommand(
166
+ event,
167
+ messageId,
168
+ key,
169
+ commandMessage,
170
+ commandRunner,
171
+ );
172
+ let current;
173
+ current = processing
174
+ .then(() => this.#finishReaction(messageId, processingReaction, 'DONE'))
175
+ .catch((error) => this.#handleMessageFailure(
176
+ event,
177
+ messageId,
178
+ processingReaction,
179
+ error,
180
+ ))
181
+ .finally(() => {
182
+ this.#acceptedMessageIds.delete(messageId);
183
+ this.#commandTasks.delete(current);
184
+ });
185
+ this.#commandTasks.add(current);
186
+ return current;
187
+ }
144
188
  if (this.#isResolvedQuestionReply(event, key)) {
145
189
  const current = Promise.resolve()
146
190
  .then(() => this.#discardResolvedInteractionReply(event, messageId))
@@ -276,6 +320,10 @@ export class FeishuHarnessBridge {
276
320
  }
277
321
 
278
322
  async #handleMessageFailure(event, messageId, processingReaction, error) {
323
+ if (error?.code === 'turn-stopped') {
324
+ await this.#removeProcessingReaction(messageId, processingReaction);
325
+ return;
326
+ }
279
327
  if (this.#signal?.aborted) {
280
328
  await this.#removeProcessingReaction(messageId, processingReaction);
281
329
  return;
@@ -297,9 +345,41 @@ export class FeishuHarnessBridge {
297
345
  pending.queue ? [pending.queue] : []
298
346
  )),
299
347
  ...this.#interactionTasks,
348
+ ...this.#commandTasks,
300
349
  ]);
301
350
  }
302
351
 
352
+ async #processFastCommand(event, messageId, key, message, runner) {
353
+ this.#signal?.throwIfAborted();
354
+ if (this.#state.hasSeen(messageId)) return;
355
+ await this.#state.markSeen(messageId);
356
+ this.#status.lastMessageAt = new Date().toISOString();
357
+ this.#status.messagesReceived += 1;
358
+ const result = await runner(
359
+ nonEmptyString(message.content) ?? '',
360
+ this.#harness,
361
+ this.#state,
362
+ key,
363
+ {
364
+ signal: this.#signal,
365
+ hasImages: hasInboundImages(message),
366
+ pendingInteraction: this.#pendingInteractions.has(key)
367
+ || this.#approvals.hasPending(key),
368
+ control: { owner: this, key },
369
+ },
370
+ );
371
+ if (result?.stopped) {
372
+ await Promise.allSettled([
373
+ this.#cancelPendingInteraction(key),
374
+ this.#approvals.closeRoute(key),
375
+ ]);
376
+ }
377
+ for (const reply of result?.messages ?? [result?.message]) {
378
+ if (reply) await this.#send(event.message.chat_id, reply);
379
+ }
380
+ this.#status.lastError = null;
381
+ }
382
+
303
383
  async #handle(event, key, { alreadyRecorded = false } = {}) {
304
384
  this.#signal?.throwIfAborted();
305
385
  const messageId = event.message.message_id;
@@ -372,6 +452,7 @@ export class FeishuHarnessBridge {
372
452
  return {
373
453
  timeoutMs: this.#replyTimeoutMs,
374
454
  signal: this.#signal,
455
+ control: { owner: this, key },
375
456
  onInteraction: (interaction) => this.#handleInteraction(interaction, {
376
457
  key,
377
458
  actor: senderOpenId(event),