@xmanrui/dsh-im 0.7.0 → 0.7.2
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/lib/index.js +119 -121
- package/package.json +1 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +423 -8
- package/src/channels/dingtalk/harness-client.mjs +16 -366
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/discord/discord-runtime.mjs +11 -1
- package/src/channels/discord/harness-client.mjs +10 -2
- package/src/channels/feishu/bridge.mjs +571 -50
- package/src/channels/feishu/feishu-runtime.mjs +41 -1
- package/src/channels/feishu/harness-client.mjs +16 -335
- package/src/channels/qq/harness-client.mjs +10 -2
- package/src/channels/qq/qq-bridge.mjs +428 -28
- package/src/channels/qq/qq-runtime.mjs +14 -3
- package/src/channels/shared/harness-approval.mjs +472 -0
- package/src/channels/shared/harness-client.mjs +858 -0
- package/src/channels/shared/harness-question.mjs +85 -0
- package/src/channels/shared/text-harness-bridge.mjs +486 -24
- package/src/channels/slack/harness-client.mjs +10 -2
- package/src/channels/slack/slack-runtime.mjs +11 -1
- package/src/channels/telegram/harness-client.mjs +10 -2
- package/src/channels/telegram/telegram-runtime.mjs +15 -4
- package/src/channels/wecom/harness-client.mjs +10 -2
- package/src/channels/wecom/wecom-bridge.mjs +434 -14
- package/src/channels/wecom/wecom-runtime.mjs +6 -0
- package/src/channels/weixin/harness-client.mjs +16 -326
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +451 -22
- package/src/channels/weixin/weixin-runtime.mjs +56 -7
- package/src/channels/whatsapp/harness-client.mjs +10 -2
- package/src/channels/whatsapp/whatsapp-runtime.mjs +1 -0
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { FeishuHarnessBridge } from './bridge.mjs';
|
|
2
2
|
import { VerifiedFeishuChannel } from './feishu-channel.mjs';
|
|
3
3
|
|
|
4
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
5
|
+
|
|
6
|
+
function httpInstanceWithTimeout(httpInstance, timeoutMs) {
|
|
7
|
+
if (!httpInstance || typeof httpInstance.request !== 'function') return undefined;
|
|
8
|
+
const optionsWithTimeout = (options) => ({
|
|
9
|
+
...(options ?? {}),
|
|
10
|
+
timeout: options?.timeout ?? timeoutMs,
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
request: (options) => httpInstance.request(optionsWithTimeout(options)),
|
|
14
|
+
get: (url, options) => httpInstance.get(url, optionsWithTimeout(options)),
|
|
15
|
+
delete: (url, options) => httpInstance.delete(url, optionsWithTimeout(options)),
|
|
16
|
+
head: (url, options) => httpInstance.head(url, optionsWithTimeout(options)),
|
|
17
|
+
options: (url, options) => httpInstance.options(url, optionsWithTimeout(options)),
|
|
18
|
+
post: (url, data, options) => httpInstance.post(url, data, optionsWithTimeout(options)),
|
|
19
|
+
put: (url, data, options) => httpInstance.put(url, data, optionsWithTimeout(options)),
|
|
20
|
+
patch: (url, data, options) => httpInstance.patch(url, data, optionsWithTimeout(options)),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
export function createBridgeStatus({ allowedSenderCount = 1 } = {}) {
|
|
5
25
|
return {
|
|
6
26
|
startedAt: null,
|
|
@@ -43,11 +63,13 @@ export class FeishuRuntime {
|
|
|
43
63
|
#state;
|
|
44
64
|
#replyTimeoutMs;
|
|
45
65
|
#connectTimeoutMs;
|
|
66
|
+
#requestTimeoutMs;
|
|
46
67
|
#logger;
|
|
47
68
|
#client = null;
|
|
48
69
|
#bridge = null;
|
|
49
70
|
#wsClient = null;
|
|
50
71
|
#starting = null;
|
|
72
|
+
#abortController = null;
|
|
51
73
|
#status;
|
|
52
74
|
|
|
53
75
|
constructor({
|
|
@@ -61,6 +83,7 @@ export class FeishuRuntime {
|
|
|
61
83
|
state,
|
|
62
84
|
replyTimeoutMs = 600000,
|
|
63
85
|
connectTimeoutMs = 15000,
|
|
86
|
+
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
64
87
|
logger = console,
|
|
65
88
|
}) {
|
|
66
89
|
if (!lark) throw new Error('FeishuRuntime requires the Feishu SDK');
|
|
@@ -70,6 +93,9 @@ export class FeishuRuntime {
|
|
|
70
93
|
if (normalizedOwners.length === 0) throw new Error('FeishuRuntime requires at least one owner open_id');
|
|
71
94
|
if (!harness) throw new Error('FeishuRuntime requires a Harness client');
|
|
72
95
|
if (!state) throw new Error('FeishuRuntime requires a state store');
|
|
96
|
+
if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0) {
|
|
97
|
+
throw new TypeError('FeishuRuntime requestTimeoutMs must be a positive number');
|
|
98
|
+
}
|
|
73
99
|
|
|
74
100
|
this.#lark = lark;
|
|
75
101
|
this.#appId = appId;
|
|
@@ -80,6 +106,7 @@ export class FeishuRuntime {
|
|
|
80
106
|
this.#state = state;
|
|
81
107
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
82
108
|
this.#connectTimeoutMs = connectTimeoutMs;
|
|
109
|
+
this.#requestTimeoutMs = requestTimeoutMs;
|
|
83
110
|
this.#logger = logger;
|
|
84
111
|
this.#status = createBridgeStatus({ allowedSenderCount: normalizedOwners.length });
|
|
85
112
|
}
|
|
@@ -99,12 +126,15 @@ export class FeishuRuntime {
|
|
|
99
126
|
}
|
|
100
127
|
|
|
101
128
|
async #start() {
|
|
129
|
+
const abortController = new AbortController();
|
|
130
|
+
this.#abortController = abortController;
|
|
131
|
+
const { signal } = abortController;
|
|
102
132
|
this.#status.startedAt = new Date().toISOString();
|
|
103
133
|
this.#status.feishuLongConnectionState = 'connecting';
|
|
104
134
|
this.#status.lastError = null;
|
|
105
135
|
|
|
106
136
|
try {
|
|
107
|
-
await this.#harness.ensureRunning();
|
|
137
|
+
await this.#harness.ensureRunning({ signal });
|
|
108
138
|
this.#status.harnessReachable = true;
|
|
109
139
|
|
|
110
140
|
const sdkDomain = this.#domain === 'lark'
|
|
@@ -115,6 +145,11 @@ export class FeishuRuntime {
|
|
|
115
145
|
appSecret: this.#appSecret,
|
|
116
146
|
domain: sdkDomain,
|
|
117
147
|
};
|
|
148
|
+
const httpInstance = httpInstanceWithTimeout(
|
|
149
|
+
this.#lark.defaultHttpInstance,
|
|
150
|
+
this.#requestTimeoutMs,
|
|
151
|
+
);
|
|
152
|
+
if (httpInstance) larkConfig.httpInstance = httpInstance;
|
|
118
153
|
this.#client = new this.#lark.Client(larkConfig);
|
|
119
154
|
const channel = new VerifiedFeishuChannel({
|
|
120
155
|
client: this.#client,
|
|
@@ -128,6 +163,8 @@ export class FeishuRuntime {
|
|
|
128
163
|
status: this.#status,
|
|
129
164
|
allowedSenderOpenIds: new Set(this.#ownerOpenIds),
|
|
130
165
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
166
|
+
signal,
|
|
167
|
+
logger: this.#logger,
|
|
131
168
|
});
|
|
132
169
|
|
|
133
170
|
const dispatcher = new this.#lark.EventDispatcher({}).register({
|
|
@@ -205,6 +242,9 @@ export class FeishuRuntime {
|
|
|
205
242
|
|
|
206
243
|
async stop({ preserveError = false } = {}) {
|
|
207
244
|
const error = preserveError ? this.#status.lastError : null;
|
|
245
|
+
const abortController = this.#abortController;
|
|
246
|
+
this.#abortController = null;
|
|
247
|
+
abortController?.abort(new DOMException('Feishu runtime stopped', 'AbortError'));
|
|
208
248
|
this.#status.ready = false;
|
|
209
249
|
if (this.#wsClient) {
|
|
210
250
|
this.#wsClient.close({ force: true });
|
|
@@ -1,338 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (!Array.isArray(workspaceList?.items)
|
|
18
|
-
|| !Array.isArray(workspaceList?.archivedSessionIds)) {
|
|
19
|
-
throw new Error('Harness returned an invalid response for workspace.list');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const workspace = workspaceList.items.find((item) => item?.path === workspacePath);
|
|
23
|
-
if (!workspace) return null;
|
|
24
|
-
if (!Array.isArray(workspace.sessionIds)
|
|
25
|
-
|| workspace.sessionIds.some((sessionId) => typeof sessionId !== 'string')) {
|
|
26
|
-
throw new Error('Harness returned invalid session IDs for workspace.list');
|
|
27
|
-
}
|
|
28
|
-
return workspace;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function workspaceSessions(workspace, archivedSessionIds, sessionList) {
|
|
32
|
-
if (!Array.isArray(sessionList?.items)) {
|
|
33
|
-
throw new Error('Harness returned an invalid response for session.list');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const archived = new Set(archivedSessionIds);
|
|
37
|
-
const summaries = new Map(sessionList.items.flatMap((item) => (
|
|
38
|
-
typeof item?.sessionId === 'string' ? [[item.sessionId, item]] : []
|
|
39
|
-
)));
|
|
40
|
-
return {
|
|
41
|
-
workspace: workspace.path,
|
|
42
|
-
sessions: workspace.sessionIds.map((sessionId) => {
|
|
43
|
-
const summary = summaries.get(sessionId);
|
|
44
|
-
const title = summary?.projections?.values?.title;
|
|
45
|
-
return {
|
|
46
|
-
sessionId,
|
|
47
|
-
title: typeof title === 'string' ? title : null,
|
|
48
|
-
archived: archived.has(sessionId),
|
|
49
|
-
blank: summary?.blank === true,
|
|
50
|
-
origin: summary?.origin === 'subagent' ? 'subagent' : null,
|
|
51
|
-
summaryAvailable: summary !== undefined,
|
|
52
|
-
};
|
|
53
|
-
}),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function messageText(event) {
|
|
58
|
-
return (event?.data?.message?.content ?? [])
|
|
59
|
-
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
|
60
|
-
.map((part) => part.text)
|
|
61
|
-
.join('\n')
|
|
62
|
-
.trim();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export class HarnessReplyTracker {
|
|
66
|
-
#promptRpcId;
|
|
67
|
-
#lastSeq;
|
|
68
|
-
#openTurn = null;
|
|
69
|
-
#targetTurn = null;
|
|
70
|
-
#stepText = new Map();
|
|
71
|
-
#latestText = '';
|
|
72
|
-
#finished = false;
|
|
73
|
-
#reason = null;
|
|
74
|
-
|
|
75
|
-
constructor({ promptRpcId, afterSeq = -1 }) {
|
|
76
|
-
this.#promptRpcId = promptRpcId;
|
|
77
|
-
this.#lastSeq = afterSeq;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
get finished() {
|
|
81
|
-
return this.#finished;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
get answer() {
|
|
85
|
-
return this.#latestText.trim();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
get reason() {
|
|
89
|
-
return this.#reason;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
consume(entries) {
|
|
93
|
-
let update = null;
|
|
94
|
-
const ordered = [...entries]
|
|
95
|
-
.map((entry) => entry?.event ?? entry)
|
|
96
|
-
.filter(Boolean)
|
|
97
|
-
.sort((left, right) => (left.seq ?? -1) - (right.seq ?? -1));
|
|
98
|
-
|
|
99
|
-
for (const event of ordered) {
|
|
100
|
-
const seq = event.seq ?? -1;
|
|
101
|
-
if (seq <= this.#lastSeq) continue;
|
|
102
|
-
this.#lastSeq = seq;
|
|
103
|
-
|
|
104
|
-
if (event.type === 'turn/start') {
|
|
105
|
-
this.#openTurn = event.data?.turn ?? null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (event.type === 'user/message'
|
|
109
|
-
&& event.data?.source?.rpcId === this.#promptRpcId) {
|
|
110
|
-
this.#targetTurn = this.#openTurn;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (this.#targetTurn === null) continue;
|
|
115
|
-
|
|
116
|
-
if (event.type === 'turn/end') {
|
|
117
|
-
if (event.data?.turn !== this.#targetTurn) continue;
|
|
118
|
-
this.#finished = true;
|
|
119
|
-
this.#reason = event.data?.reason ?? null;
|
|
120
|
-
this.#openTurn = null;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (event.data?.turn !== this.#targetTurn) continue;
|
|
125
|
-
|
|
126
|
-
if (event.type === 'assistant/chunk' && event.data?.chunk?.type === 'text-delta') {
|
|
127
|
-
const step = event.data?.step ?? 0;
|
|
128
|
-
const index = event.data.chunk.index ?? 0;
|
|
129
|
-
const key = `${step}:${index}`;
|
|
130
|
-
this.#stepText.set(key, (this.#stepText.get(key) ?? '') + event.data.chunk.text);
|
|
131
|
-
const stepPrefix = `${step}:`;
|
|
132
|
-
const text = [...this.#stepText.entries()]
|
|
133
|
-
.filter(([partKey]) => partKey.startsWith(stepPrefix))
|
|
134
|
-
.sort(([left], [right]) => Number(left.split(':')[1]) - Number(right.split(':')[1]))
|
|
135
|
-
.map(([, part]) => part)
|
|
136
|
-
.join('\n')
|
|
137
|
-
.trim();
|
|
138
|
-
if (text && text !== this.#latestText) {
|
|
139
|
-
this.#latestText = text;
|
|
140
|
-
update = { type: 'text', text };
|
|
141
|
-
}
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (event.type === 'assistant/message') {
|
|
146
|
-
const text = messageText(event);
|
|
147
|
-
if (text && text !== this.#latestText) {
|
|
148
|
-
this.#latestText = text;
|
|
149
|
-
update = { type: 'text', text };
|
|
150
|
-
}
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (event.type === 'tool/call') {
|
|
155
|
-
update = { type: 'tool', name: event.data?.name ?? '工具' };
|
|
156
|
-
} else if (event.type === 'tool/result') {
|
|
157
|
-
update = { type: 'status', text: '正在整理结果…' };
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return update;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export class HarnessRpcError extends Error {
|
|
166
|
-
constructor(method, error) {
|
|
167
|
-
super(`${method}: ${error?.message ?? 'unknown Harness RPC error'}`);
|
|
168
|
-
this.name = 'HarnessRpcError';
|
|
169
|
-
this.method = method;
|
|
170
|
-
this.code = error?.code ?? 'internal';
|
|
171
|
-
this.details = error?.details ?? {};
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export class HarnessClient {
|
|
176
|
-
#baseUrl;
|
|
177
|
-
#workspace;
|
|
178
|
-
#agentPreset;
|
|
179
|
-
#autostart;
|
|
180
|
-
#dshBin;
|
|
181
|
-
#managedProcess = null;
|
|
182
|
-
|
|
183
|
-
constructor({ baseUrl, workspace, agentPreset, autostart, dshBin }) {
|
|
184
|
-
this.#baseUrl = new URL(baseUrl);
|
|
185
|
-
this.#workspace = workspace;
|
|
186
|
-
this.#agentPreset = agentPreset;
|
|
187
|
-
this.#autostart = autostart;
|
|
188
|
-
this.#dshBin = dshBin;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async rpc(method, payload = {}, timeoutMs = 30000, options = {}) {
|
|
192
|
-
const rpcId = options.rpcId ?? `feishu-${randomUUID()}`;
|
|
193
|
-
const response = await fetch(new URL(`/api/${method}`, this.#baseUrl), {
|
|
194
|
-
method: 'POST',
|
|
195
|
-
headers: { 'content-type': 'application/json' },
|
|
196
|
-
body: JSON.stringify({ type: 'client-request', rpcId, method, payload }),
|
|
197
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
1
|
+
import {
|
|
2
|
+
HarnessClient as SharedHarnessClient,
|
|
3
|
+
} from '../shared/harness-client.mjs';
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
HarnessInteractionError,
|
|
7
|
+
HarnessReplyTracker,
|
|
8
|
+
HarnessRpcError,
|
|
9
|
+
} from '../shared/harness-client.mjs';
|
|
10
|
+
|
|
11
|
+
export class HarnessClient extends SharedHarnessClient {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super({
|
|
14
|
+
...options,
|
|
15
|
+
rpcIdPrefix: 'feishu',
|
|
16
|
+
logPrefix: 'dsh-feishu',
|
|
198
17
|
});
|
|
199
|
-
if (!response.ok) throw new Error(`Harness transport ${method} failed: HTTP ${response.status}`);
|
|
200
|
-
const body = await response.json();
|
|
201
|
-
if (body?.type !== 'server-response' || body?.rpcId !== rpcId) {
|
|
202
|
-
throw new Error(`Harness returned an invalid response for ${method}`);
|
|
203
|
-
}
|
|
204
|
-
if (!body.result?.ok) throw new HarnessRpcError(method, body.result?.error);
|
|
205
|
-
return body.result.value;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async health() {
|
|
209
|
-
await this.rpc('host.describe', {}, 5000);
|
|
210
|
-
return true;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async ensureRunning() {
|
|
214
|
-
try {
|
|
215
|
-
return await this.health();
|
|
216
|
-
} catch (firstError) {
|
|
217
|
-
if (!this.#autostart) throw firstError;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (!this.#managedProcess || this.#managedProcess.exitCode !== null) {
|
|
221
|
-
const port = this.#baseUrl.port || (this.#baseUrl.protocol === 'https:' ? '443' : '80');
|
|
222
|
-
this.#managedProcess = spawn(this.#dshBin, [
|
|
223
|
-
'web',
|
|
224
|
-
'--host',
|
|
225
|
-
this.#baseUrl.hostname,
|
|
226
|
-
'--port',
|
|
227
|
-
port,
|
|
228
|
-
], {
|
|
229
|
-
cwd: this.#workspace,
|
|
230
|
-
env: process.env,
|
|
231
|
-
stdio: ['ignore', 'inherit', 'inherit'],
|
|
232
|
-
});
|
|
233
|
-
this.#managedProcess.on('error', (error) => {
|
|
234
|
-
console.error('[bridge] failed to start Harness:', error.message);
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const deadline = Date.now() + 60000;
|
|
239
|
-
let lastError;
|
|
240
|
-
while (Date.now() < deadline) {
|
|
241
|
-
await sleep(1000);
|
|
242
|
-
try {
|
|
243
|
-
return await this.health();
|
|
244
|
-
} catch (error) {
|
|
245
|
-
lastError = error;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async listWorkspaces(options = {}) {
|
|
252
|
-
await this.ensureRunning();
|
|
253
|
-
return workspacePaths(await this.rpc('workspace.list', {}, 30000, options));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async listWorkspaceSessions(workspacePath, options = {}) {
|
|
257
|
-
await this.ensureRunning();
|
|
258
|
-
const workspaceList = await this.rpc('workspace.list', {}, 30000, options);
|
|
259
|
-
const workspace = workspaceFromList(workspacePath, workspaceList);
|
|
260
|
-
if (!workspace) return { workspace: workspacePath, sessions: [] };
|
|
261
|
-
const sessionList = await this.rpc('session.list', {}, 30000, options);
|
|
262
|
-
return workspaceSessions(workspace, workspaceList.archivedSessionIds, sessionList);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async adoptWorkspaceSession(value, options = {}) {
|
|
266
|
-
return adoptRegisteredWorkspaceSession(this, value, options, 30000);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async workspaceId(options = {}) {
|
|
270
|
-
const workspace = options.workspace ?? this.#workspace;
|
|
271
|
-
const { items } = await this.rpc('workspace.list', {});
|
|
272
|
-
const existing = items.find((item) => item.path === workspace);
|
|
273
|
-
if (existing) return existing.workspaceId;
|
|
274
|
-
const created = await this.rpc('workspace.create', { path: workspace });
|
|
275
|
-
return created.workspace.workspaceId;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
async createSession(options = {}) {
|
|
279
|
-
await this.ensureRunning();
|
|
280
|
-
const workspaceId = await this.workspaceId(options);
|
|
281
|
-
const created = await this.rpc('session.create', {
|
|
282
|
-
workspaceId,
|
|
283
|
-
agentPreset: this.#agentPreset,
|
|
284
|
-
});
|
|
285
|
-
return created.sessionId;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
async sessionExists(sessionId) {
|
|
289
|
-
try {
|
|
290
|
-
await this.rpc('session.history', { sessionId, maxMessages: 1 });
|
|
291
|
-
return true;
|
|
292
|
-
} catch (error) {
|
|
293
|
-
if (error instanceof HarnessRpcError && error.code === 'session-not-found') return false;
|
|
294
|
-
throw error;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async ask(sessionId, text, options = {}) {
|
|
299
|
-
if (typeof options === 'number') options = { timeoutMs: options };
|
|
300
|
-
const timeoutMs = options.timeoutMs ?? 600000;
|
|
301
|
-
const onUpdate = typeof options.onUpdate === 'function' ? options.onUpdate : null;
|
|
302
|
-
await this.ensureRunning();
|
|
303
|
-
const before = await this.rpc('session.history', { sessionId, maxMessages: 1 });
|
|
304
|
-
const baselineSeq = Math.max(-1, ...(before.events ?? []).map(({ event }) => event.seq ?? -1));
|
|
305
|
-
const promptRpcId = `feishu-${randomUUID()}`;
|
|
306
|
-
const tracker = new HarnessReplyTracker({ promptRpcId, afterSeq: baselineSeq });
|
|
307
|
-
|
|
308
|
-
await this.rpc('session.prompt', {
|
|
309
|
-
sessionId,
|
|
310
|
-
mode: 'queue',
|
|
311
|
-
content: [{ type: 'text', text }],
|
|
312
|
-
clientTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
313
|
-
}, 30000, { rpcId: promptRpcId });
|
|
314
|
-
|
|
315
|
-
const deadline = Date.now() + timeoutMs;
|
|
316
|
-
while (Date.now() < deadline) {
|
|
317
|
-
await sleep(300);
|
|
318
|
-
const history = await this.rpc('session.history', { sessionId, maxMessages: 50 });
|
|
319
|
-
const update = tracker.consume(history.events ?? []);
|
|
320
|
-
if (update && onUpdate) {
|
|
321
|
-
try {
|
|
322
|
-
await onUpdate(update);
|
|
323
|
-
} catch (error) {
|
|
324
|
-
console.warn('[bridge] ignored a progress update failure:', error.message);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
if (!tracker.finished) continue;
|
|
328
|
-
if (tracker.answer) return tracker.answer;
|
|
329
|
-
|
|
330
|
-
throw new Error(`Harness turn ended without a text reply${tracker.reason ? ` (${JSON.stringify(tracker.reason)})` : ''}`);
|
|
331
|
-
}
|
|
332
|
-
throw new Error(`Harness reply timed out after ${Math.round(timeoutMs / 1000)} seconds`);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
stopManagedProcess() {
|
|
336
|
-
if (this.#managedProcess?.exitCode === null) this.#managedProcess.kill('SIGTERM');
|
|
337
18
|
}
|
|
338
19
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
import { HarnessClient } from '../
|
|
1
|
+
import { HarnessClient } from '../shared/harness-client.mjs';
|
|
2
2
|
|
|
3
|
-
export class QqHarnessClient extends HarnessClient {
|
|
3
|
+
export class QqHarnessClient extends HarnessClient {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
super({
|
|
6
|
+
...options,
|
|
7
|
+
rpcIdPrefix: 'qq',
|
|
8
|
+
logPrefix: 'dsh-qq',
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|