@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
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.13.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": {
@@ -7,7 +7,7 @@ const EN = Object.freeze({
7
7
  'IM机器人': 'IM bots',
8
8
  'IM机器人设置': 'IM bot settings',
9
9
  'IM 渠道': 'IM channels',
10
- '让聊天机器人轻松接入 DeepSeek Harness': 'Connect IM bots to DeepSeek Harness with ease',
10
+ '让 DeepSeek Harness 触手可及': 'DeepSeek Harness, always within reach',
11
11
  '帮助与反馈 · 前往 GitHub': 'Help & feedback · Open GitHub',
12
12
  '微信': 'WeChat',
13
13
  '飞书': 'Feishu',
@@ -139,7 +139,7 @@ export function IMSettingsTab({
139
139
  width: 48,
140
140
  height: 48,
141
141
  }),
142
- h('p', null, '让聊天机器人轻松接入 DeepSeek Harness')),
142
+ h('p', null, '让 DeepSeek Harness 触手可及')),
143
143
  h('span', { className: 'dim-githubAction' },
144
144
  h('a', {
145
145
  className: 'dim-githubLink',
@@ -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
+ }
@@ -48,6 +48,40 @@ const [client, host, patch, manifestText, lockText, hostSource, clientSource, ex
48
48
  const manifest = JSON.parse(manifestText);
49
49
  const lock = JSON.parse(lockText);
50
50
 
51
+ // DSH runtime packages use module-local Symbol keys, so a second physical copy breaks Host lookup.
52
+ const forbiddenDshDependency = /^@deepseek-ai\/dsh-/;
53
+ const dependencySections = [
54
+ 'dependencies',
55
+ 'devDependencies',
56
+ 'optionalDependencies',
57
+ 'peerDependencies',
58
+ ];
59
+ for (const section of dependencySections) {
60
+ for (const name of Object.keys(manifest[section] ?? {})) {
61
+ if (forbiddenDshDependency.test(name)) {
62
+ throw new Error(
63
+ `${name} must not be declared in ${section}; DSH runtime packages must come from the host`,
64
+ );
65
+ }
66
+ }
67
+ }
68
+ const bundledDependencies = manifest.bundleDependencies ?? manifest.bundledDependencies ?? [];
69
+ if (Array.isArray(bundledDependencies)) {
70
+ for (const name of bundledDependencies) {
71
+ if (forbiddenDshDependency.test(name)) {
72
+ throw new Error(`${name} must not be bundled; DSH runtime packages must come from the host`);
73
+ }
74
+ }
75
+ }
76
+ const forbiddenDshLockPaths = Object.keys(lock.packages ?? {}).filter((path) => (
77
+ /(?:^|\/)node_modules\/@deepseek-ai\/dsh-[^/]+(?:\/|$)/.test(path)
78
+ ));
79
+ if (forbiddenDshLockPaths.length > 0) {
80
+ throw new Error(
81
+ `package lock must not install DSH runtime packages: ${forbiddenDshLockPaths.join(', ')}`,
82
+ );
83
+ }
84
+
51
85
  if (!client.includes('id: "@xmanrui/dsh-im"')) {
52
86
  throw new Error('client bundle does not register the dsh-im loader id');
53
87
  }
@@ -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,11 @@ const HELP_TEXT = [
33
41
  '/workspacelist 列出工作区绝对路径',
34
42
  '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
35
43
  '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
44
+ '/models 按序号列出所有可用模型',
45
+ '/model [序号或完整模型ID] 查看或切换当前会话模型',
46
+ '示例:先发 /models,再发 /model 2',
47
+ '/stop 停止当前任务',
48
+ '/steer 补充指令 纠偏当前任务',
36
49
  '/status 检查连接状态',
37
50
  '/help 显示本帮助',
38
51
  ].join('\n');
@@ -246,6 +259,7 @@ export class DingtalkHarnessBridge {
246
259
  #pendingInteractions = new Map();
247
260
  #interactionKeys = new Map();
248
261
  #interactionTasks = new Set();
262
+ #commandTasks = new Set();
249
263
  #acceptedMessageIds = new Set();
250
264
  #approvals;
251
265
 
@@ -314,6 +328,37 @@ export class DingtalkHarnessBridge {
314
328
  rememberConnectionTestTarget(this.#state, { sessionWebhook });
315
329
  }
316
330
  const pending = this.#pendingInteractions.get(key);
331
+ const promptMessage = dingtalkInboundMessage(message, {
332
+ api: this.#api,
333
+ clientId: this.#clientId,
334
+ clientSecret: this.#clientSecret,
335
+ });
336
+ const commandText = nonEmptyString(promptMessage.content) ?? '';
337
+ const commandRunner = isControlCommand(commandText)
338
+ ? runControlCommand
339
+ : (isModelCommand(commandText) ? runModelCommand : null);
340
+ const addressed = String(message.conversationType) !== '2' || message?.isInAtList === true;
341
+ if (commandRunner && sessionWebhook && addressed) {
342
+ let task;
343
+ task = this.#processFastCommand(
344
+ message,
345
+ messageId,
346
+ key,
347
+ sessionWebhook,
348
+ promptMessage,
349
+ commandRunner,
350
+ ).catch((error) => {
351
+ if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
352
+ this.#status.lastError = '钉钉命令处理失败。';
353
+ this.#logger.error?.('[dsh-dingtalk] failed to process a command', safeErrorDiagnostic(error));
354
+ return this.#send(sessionWebhook, CARD_ERROR_TEXT).catch(() => undefined);
355
+ }).finally(() => {
356
+ this.#acceptedMessageIds.delete(messageId);
357
+ this.#commandTasks.delete(task);
358
+ });
359
+ this.#commandTasks.add(task);
360
+ return task;
361
+ }
317
362
  const approvalReply = this.#approvals.claimReply({
318
363
  key,
319
364
  actor: sender,
@@ -412,9 +457,41 @@ export class DingtalkHarnessBridge {
412
457
  pending.queue ? [pending.queue] : []
413
458
  )),
414
459
  ...this.#interactionTasks,
460
+ ...this.#commandTasks,
415
461
  ]);
416
462
  }
417
463
 
464
+ async #processFastCommand(message, messageId, key, sessionWebhook, prompt, runner) {
465
+ this.#signal?.throwIfAborted();
466
+ if (this.#state.hasSeen(messageId)) return;
467
+ await this.#state.markSeen(messageId);
468
+ increment(this.#status, 'messagesReceived');
469
+ this.#status.lastMessageAt = new Date().toISOString();
470
+ const result = await runner(
471
+ nonEmptyString(prompt.content) ?? '',
472
+ this.#harness,
473
+ this.#state,
474
+ key,
475
+ {
476
+ signal: this.#signal,
477
+ hasImages: hasInboundImages(prompt),
478
+ pendingInteraction: this.#pendingInteractions.has(key)
479
+ || this.#approvals.hasPending(key),
480
+ control: { owner: this, key },
481
+ },
482
+ );
483
+ if (result?.stopped) {
484
+ await Promise.allSettled([
485
+ this.#cancelPendingInteraction(key),
486
+ this.#approvals.closeRoute(key),
487
+ ]);
488
+ }
489
+ for (const reply of result?.messages ?? [result?.message]) {
490
+ if (reply) await this.#send(sessionWebhook, reply);
491
+ }
492
+ this.#status.lastError = null;
493
+ }
494
+
418
495
  async #process(message, messageId, sender, key, { alreadyRecorded = false } = {}) {
419
496
  this.#signal?.throwIfAborted();
420
497
  if (!alreadyRecorded) {
@@ -519,6 +596,7 @@ export class DingtalkHarnessBridge {
519
596
  askOptions: {
520
597
  timeoutMs: this.#replyTimeoutMs,
521
598
  signal: this.#signal,
599
+ control: { owner: this, key },
522
600
  onUpdate: cardStarted
523
601
  ? (update) => cardStream.push(progressText(update))
524
602
  : undefined,
@@ -537,6 +615,10 @@ export class DingtalkHarnessBridge {
537
615
  this.#status.lastReplyAt = new Date().toISOString();
538
616
  this.#status.lastError = null;
539
617
  } catch (error) {
618
+ if (error?.code === 'turn-stopped') {
619
+ if (cardStarted) await cardStream.finish('已停止。').catch(() => undefined);
620
+ return;
621
+ }
540
622
  if (this.#signal?.aborted) return;
541
623
  this.#status.lastError = '钉钉消息处理失败。';
542
624
  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.13.0)',
112
112
  },
113
113
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
114
114
  signal: requestSignal(signal, timeoutMs),