@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
package/package.json
CHANGED
|
@@ -1,7 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
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
|
-
'
|
|
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, '
|
|
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.
|
|
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),
|