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