@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/README.en.md +34 -6
- package/README.md +34 -6
- package/lib/index.js +133 -124
- package/package.json +31 -2
- 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/src/channels/dingtalk/dingtalk-bridge.mjs +81 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +81 -0
- package/src/channels/qq/qq-bridge.mjs +84 -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 +314 -0
- package/src/channels/shared/session-binding-lock.mjs +40 -0
- package/src/channels/shared/text-harness-bridge.mjs +81 -0
- package/src/channels/shared/workspace-session.mjs +21 -9
- package/src/channels/wecom/wecom-bridge.mjs +75 -0
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +81 -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.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.
|
|
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),
|