@qwen-code/channel-dws 0.22.2-preview.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/dist/dws-channel.d.ts +168 -0
- package/dist/dws-channel.d.ts.map +1 -0
- package/dist/dws-channel.js +1622 -0
- package/dist/dws-channel.js.map +1 -0
- package/dist/dws-client.d.ts +111 -0
- package/dist/dws-client.d.ts.map +1 -0
- package/dist/dws-client.js +858 -0
- package/dist/dws-client.js.map +1 -0
- package/dist/dws-environment.d.ts +7 -0
- package/dist/dws-environment.d.ts.map +1 -0
- package/dist/dws-environment.js +46 -0
- package/dist/dws-environment.js.map +1 -0
- package/dist/dws-event-stream.d.ts +17 -0
- package/dist/dws-event-stream.d.ts.map +1 -0
- package/dist/dws-event-stream.js +181 -0
- package/dist/dws-event-stream.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Qwen Team
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { dwsProcessEnvironment } from './dws-environment.js';
|
|
8
|
+
import { startDwsEventProcess, } from './dws-event-stream.js';
|
|
9
|
+
const DWS_PROCESS_TIMEOUT_MS = 45_000;
|
|
10
|
+
const DWS_PROCESS_FORCE_KILL_DELAY_MS = 5_000;
|
|
11
|
+
const MINIMUM_DWS_VERSION = [1, 0, 57];
|
|
12
|
+
const DWS_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
13
|
+
const MAX_MESSAGE_PAGES = 100;
|
|
14
|
+
const MAX_TODO_PAGES = 50;
|
|
15
|
+
const TODO_PAGE_SIZE = 20;
|
|
16
|
+
export class DwsCommandError extends Error {
|
|
17
|
+
outcome;
|
|
18
|
+
constructor(message, outcome) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.outcome = outcome;
|
|
21
|
+
this.name = 'DwsCommandError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The errno family `uv_spawn` reports when it fails BEFORE the child ever
|
|
26
|
+
* runs. Nothing was sent, so the caller can retry the whole task without
|
|
27
|
+
* risking a duplicate delivery. The resource errnos matter as much as the
|
|
28
|
+
* path ones: under fd or memory exhaustion the daemon never starts `dws`, and
|
|
29
|
+
* classifying that as `unknown` makes the reply paths in dws-channel.ts drop a
|
|
30
|
+
* user's answer permanently on one log line.
|
|
31
|
+
*
|
|
32
|
+
* Everything else the callback can report happened with a child already
|
|
33
|
+
* running — a non-zero exit (`code` is a number), a timeout kill (`code` is
|
|
34
|
+
* null), an abort (`ABORT_ERR`), a `maxBuffer` overrun
|
|
35
|
+
* (`ERR_CHILD_PROCESS_STDIO_MAXBUFFER`) — so whether the command sent
|
|
36
|
+
* anything is genuinely unknown and a retry could duplicate it.
|
|
37
|
+
*/
|
|
38
|
+
const DWS_NOT_SENT_ERROR_CODES = new Set([
|
|
39
|
+
'E2BIG',
|
|
40
|
+
'EACCES',
|
|
41
|
+
'EAGAIN',
|
|
42
|
+
'EBUSY',
|
|
43
|
+
'EFAULT',
|
|
44
|
+
'EIO',
|
|
45
|
+
'EISDIR',
|
|
46
|
+
'ELOOP',
|
|
47
|
+
'EMFILE',
|
|
48
|
+
'ENAMETOOLONG',
|
|
49
|
+
'ENFILE',
|
|
50
|
+
'ENOENT',
|
|
51
|
+
'ENOEXEC',
|
|
52
|
+
'ENOMEM',
|
|
53
|
+
'ENOSYS',
|
|
54
|
+
'ENOTDIR',
|
|
55
|
+
'EPERM',
|
|
56
|
+
'ETXTBSY',
|
|
57
|
+
]);
|
|
58
|
+
/**
|
|
59
|
+
* Classify an `execFile` callback error by its `code`. Exported so the
|
|
60
|
+
* spawn-failure table can be driven directly: the resource errnos need fd or
|
|
61
|
+
* memory exhaustion to reproduce through a real spawn, which no unit test can
|
|
62
|
+
* stage safely.
|
|
63
|
+
*/
|
|
64
|
+
export function classifyDwsCommandFailure(code) {
|
|
65
|
+
return typeof code === 'string' && DWS_NOT_SENT_ERROR_CODES.has(code)
|
|
66
|
+
? 'not_sent'
|
|
67
|
+
: 'unknown';
|
|
68
|
+
}
|
|
69
|
+
function runDwsProcess(executable, args, signal) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const child = execFile(executable, args, {
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
env: dwsProcessEnvironment(),
|
|
74
|
+
maxBuffer: DWS_MAX_OUTPUT_BYTES,
|
|
75
|
+
timeout: DWS_PROCESS_TIMEOUT_MS,
|
|
76
|
+
windowsHide: true,
|
|
77
|
+
signal,
|
|
78
|
+
}, (error, stdout, stderr) => {
|
|
79
|
+
clearTimeout(forceKillTimer);
|
|
80
|
+
if (error) {
|
|
81
|
+
const code = error
|
|
82
|
+
.code;
|
|
83
|
+
const outcome = classifyDwsCommandFailure(code);
|
|
84
|
+
reject(new DwsCommandError(`DWS command failed${code === undefined ? '' : ` (${String(code)})`}.`, outcome));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
resolve({ stdout: String(stdout), stderr: String(stderr) });
|
|
88
|
+
});
|
|
89
|
+
const forceKillTimer = setTimeout(() => {
|
|
90
|
+
if (child.exitCode === null)
|
|
91
|
+
child.kill('SIGKILL');
|
|
92
|
+
}, DWS_PROCESS_TIMEOUT_MS + DWS_PROCESS_FORCE_KILL_DELAY_MS);
|
|
93
|
+
forceKillTimer.unref?.();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function isRecord(value) {
|
|
97
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
98
|
+
}
|
|
99
|
+
function firstString(value, keys) {
|
|
100
|
+
for (const key of keys) {
|
|
101
|
+
const candidate = value[key];
|
|
102
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
103
|
+
return candidate;
|
|
104
|
+
}
|
|
105
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate)) {
|
|
106
|
+
return String(candidate);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
function nestedRecord(value, keys) {
|
|
112
|
+
for (const key of keys) {
|
|
113
|
+
const candidate = value[key];
|
|
114
|
+
if (isRecord(candidate))
|
|
115
|
+
return candidate;
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
function findScalar(value, keys) {
|
|
120
|
+
const pending = [value];
|
|
121
|
+
while (pending.length > 0) {
|
|
122
|
+
const current = pending.pop();
|
|
123
|
+
if (Array.isArray(current)) {
|
|
124
|
+
for (let index = current.length - 1; index >= 0; index--) {
|
|
125
|
+
pending.push(current[index]);
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!isRecord(current))
|
|
130
|
+
continue;
|
|
131
|
+
for (const [key, candidate] of Object.entries(current)) {
|
|
132
|
+
if (keys.has(key) &&
|
|
133
|
+
(typeof candidate === 'string' ||
|
|
134
|
+
typeof candidate === 'number' ||
|
|
135
|
+
typeof candidate === 'boolean')) {
|
|
136
|
+
return candidate;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const values = Object.values(current);
|
|
140
|
+
for (let index = values.length - 1; index >= 0; index--) {
|
|
141
|
+
pending.push(values[index]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
function findExactOpenDingTalkId(value, userId) {
|
|
147
|
+
const matches = new Set();
|
|
148
|
+
const pending = [value];
|
|
149
|
+
while (pending.length > 0) {
|
|
150
|
+
const current = pending.pop();
|
|
151
|
+
if (Array.isArray(current)) {
|
|
152
|
+
for (let index = current.length - 1; index >= 0; index--) {
|
|
153
|
+
pending.push(current[index]);
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!isRecord(current))
|
|
158
|
+
continue;
|
|
159
|
+
if (firstString(current, ['userId', 'user_id']) === userId) {
|
|
160
|
+
const openDingTalkId = firstString(current, [
|
|
161
|
+
'openDingTalkId',
|
|
162
|
+
'open_dingtalk_id',
|
|
163
|
+
]);
|
|
164
|
+
if (openDingTalkId)
|
|
165
|
+
matches.add(openDingTalkId);
|
|
166
|
+
}
|
|
167
|
+
const values = Object.values(current);
|
|
168
|
+
for (let index = values.length - 1; index >= 0; index--) {
|
|
169
|
+
pending.push(values[index]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return matches.size === 1 ? [...matches][0] : undefined;
|
|
173
|
+
}
|
|
174
|
+
function collectProfiles(value, profiles = []) {
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
for (const item of value) {
|
|
177
|
+
collectProfiles(item, profiles);
|
|
178
|
+
}
|
|
179
|
+
return profiles;
|
|
180
|
+
}
|
|
181
|
+
if (!isRecord(value))
|
|
182
|
+
return profiles;
|
|
183
|
+
const explicit = firstString(value, ['profile']);
|
|
184
|
+
const corpId = firstString(value, ['corpId', 'corp_id']);
|
|
185
|
+
const profile = explicit ?? corpId;
|
|
186
|
+
if (profile) {
|
|
187
|
+
profiles.push({
|
|
188
|
+
profile,
|
|
189
|
+
current: value['isCurrent'] === true || value['is_current'] === true,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
for (const candidate of Object.values(value)) {
|
|
193
|
+
collectProfiles(candidate, profiles);
|
|
194
|
+
}
|
|
195
|
+
return profiles;
|
|
196
|
+
}
|
|
197
|
+
function resolveProfile(value, selected) {
|
|
198
|
+
const profiles = collectProfiles(value);
|
|
199
|
+
const candidates = selected
|
|
200
|
+
? profiles.filter((item) => item.profile === selected)
|
|
201
|
+
: profiles.filter((item) => item.current);
|
|
202
|
+
const unique = [
|
|
203
|
+
...new Map(candidates.map((item) => [item.profile, item])).values(),
|
|
204
|
+
];
|
|
205
|
+
return unique.length === 1 ? unique[0] : undefined;
|
|
206
|
+
}
|
|
207
|
+
function parseJson(text, description) {
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(text);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
throw new Error(`DWS returned invalid JSON for ${description}.`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function parseOutput(stdout) {
|
|
216
|
+
const trimmed = stdout.trim();
|
|
217
|
+
if (!trimmed) {
|
|
218
|
+
throw new DwsCommandError('DWS returned an empty response.', 'unknown');
|
|
219
|
+
}
|
|
220
|
+
let parsed;
|
|
221
|
+
try {
|
|
222
|
+
parsed = parseJson(trimmed, 'a command response');
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
throw new DwsCommandError(error instanceof Error ? error.message : 'DWS returned invalid JSON.', 'unknown');
|
|
226
|
+
}
|
|
227
|
+
if (isRecord(parsed) && parsed['success'] === false) {
|
|
228
|
+
throw new Error('DWS request failed.');
|
|
229
|
+
}
|
|
230
|
+
return parsed;
|
|
231
|
+
}
|
|
232
|
+
function parseVersion(value) {
|
|
233
|
+
const version = findScalar(value, new Set(['version']));
|
|
234
|
+
if (typeof version !== 'string')
|
|
235
|
+
return undefined;
|
|
236
|
+
const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)/u);
|
|
237
|
+
return match ? match.slice(1).map(Number) : undefined;
|
|
238
|
+
}
|
|
239
|
+
function versionAtLeast(actual, minimum) {
|
|
240
|
+
for (let index = 0; index < minimum.length; index++) {
|
|
241
|
+
const difference = (actual[index] ?? 0) - (minimum[index] ?? 0);
|
|
242
|
+
if (difference !== 0)
|
|
243
|
+
return difference > 0;
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
function findConversationList(value) {
|
|
248
|
+
if (!isRecord(value))
|
|
249
|
+
return undefined;
|
|
250
|
+
const conversations = value['conversationMessagesList'];
|
|
251
|
+
if (Array.isArray(conversations))
|
|
252
|
+
return conversations;
|
|
253
|
+
for (const key of ['result', 'data', 'content']) {
|
|
254
|
+
const found = findConversationList(value[key]);
|
|
255
|
+
if (found)
|
|
256
|
+
return found;
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
function formatDwsDateTime(timestamp) {
|
|
261
|
+
const date = new Date(timestamp);
|
|
262
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
263
|
+
return [
|
|
264
|
+
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`,
|
|
265
|
+
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`,
|
|
266
|
+
].join(' ');
|
|
267
|
+
}
|
|
268
|
+
function findMarkdown(value) {
|
|
269
|
+
if (typeof value === 'string')
|
|
270
|
+
return value;
|
|
271
|
+
if (!isRecord(value))
|
|
272
|
+
return undefined;
|
|
273
|
+
const direct = firstString(value, ['markdown']);
|
|
274
|
+
if (direct !== undefined)
|
|
275
|
+
return direct;
|
|
276
|
+
for (const key of ['result', 'data', 'content']) {
|
|
277
|
+
const found = findMarkdown(value[key]);
|
|
278
|
+
if (found !== undefined)
|
|
279
|
+
return found;
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
function findTodoCards(value) {
|
|
284
|
+
if (Array.isArray(value))
|
|
285
|
+
return value;
|
|
286
|
+
if (!isRecord(value))
|
|
287
|
+
return undefined;
|
|
288
|
+
if (Array.isArray(value['todoCards']))
|
|
289
|
+
return value['todoCards'];
|
|
290
|
+
for (const key of ['result', 'data', 'content']) {
|
|
291
|
+
const found = findTodoCards(value[key]);
|
|
292
|
+
if (found !== undefined)
|
|
293
|
+
return found;
|
|
294
|
+
}
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
function findTodoDetail(value) {
|
|
298
|
+
if (!isRecord(value))
|
|
299
|
+
return undefined;
|
|
300
|
+
if (isRecord(value['todoDetailModel']))
|
|
301
|
+
return value['todoDetailModel'];
|
|
302
|
+
if (firstString(value, ['taskId', 'task_id']))
|
|
303
|
+
return value;
|
|
304
|
+
for (const key of ['result', 'data', 'content']) {
|
|
305
|
+
const found = findTodoDetail(value[key]);
|
|
306
|
+
if (found)
|
|
307
|
+
return found;
|
|
308
|
+
}
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
function parseTodoTask(value, fallbackTaskId) {
|
|
312
|
+
if (!isRecord(value))
|
|
313
|
+
return undefined;
|
|
314
|
+
const creator = nestedRecord(value, [
|
|
315
|
+
'creator',
|
|
316
|
+
'creatorInfo',
|
|
317
|
+
'creatorUser',
|
|
318
|
+
]);
|
|
319
|
+
const taskId = firstString(value, ['taskId', 'task_id', 'id']) ?? fallbackTaskId;
|
|
320
|
+
if (!taskId)
|
|
321
|
+
return undefined;
|
|
322
|
+
return {
|
|
323
|
+
taskId,
|
|
324
|
+
title: firstString(value, ['subject', 'title', 'name']) ?? taskId,
|
|
325
|
+
creatorId: firstString(value, [
|
|
326
|
+
'creatorId',
|
|
327
|
+
'creator_id',
|
|
328
|
+
'creator',
|
|
329
|
+
'creatorUserId',
|
|
330
|
+
'creatorUid',
|
|
331
|
+
'creatorStaffId',
|
|
332
|
+
]) ??
|
|
333
|
+
(creator
|
|
334
|
+
? firstString(creator, [
|
|
335
|
+
'userId',
|
|
336
|
+
'uid',
|
|
337
|
+
'staffId',
|
|
338
|
+
'openDingTalkId',
|
|
339
|
+
'id',
|
|
340
|
+
])
|
|
341
|
+
: undefined),
|
|
342
|
+
creatorName: firstString(value, ['creatorName', 'creator_name']) ??
|
|
343
|
+
(creator
|
|
344
|
+
? firstString(creator, ['name', 'nick', 'displayName'])
|
|
345
|
+
: undefined),
|
|
346
|
+
data: value,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function unwrapEvent(value) {
|
|
350
|
+
if (!isRecord(value))
|
|
351
|
+
return undefined;
|
|
352
|
+
const data = value['data'];
|
|
353
|
+
if (typeof data === 'string') {
|
|
354
|
+
const parsed = parseJson(data, 'an event payload');
|
|
355
|
+
if (isRecord(parsed))
|
|
356
|
+
return parsed;
|
|
357
|
+
}
|
|
358
|
+
if (isRecord(data))
|
|
359
|
+
return data;
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
function messageContent(value, structured = false) {
|
|
363
|
+
if (typeof value !== 'string')
|
|
364
|
+
return '';
|
|
365
|
+
if (!structured)
|
|
366
|
+
return value;
|
|
367
|
+
const trimmed = value.trim();
|
|
368
|
+
if (!trimmed.startsWith('{'))
|
|
369
|
+
return value;
|
|
370
|
+
try {
|
|
371
|
+
const parsed = JSON.parse(trimmed);
|
|
372
|
+
if (!isRecord(parsed))
|
|
373
|
+
return value;
|
|
374
|
+
return firstString(parsed, ['content', 'text']) ?? value;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function quotedMessageText(record, key) {
|
|
381
|
+
const quoted = record?.[key];
|
|
382
|
+
if (!isRecord(quoted))
|
|
383
|
+
return undefined;
|
|
384
|
+
const content = messageContent(quoted['content'], true).trim();
|
|
385
|
+
return content || undefined;
|
|
386
|
+
}
|
|
387
|
+
function eventTime(...records) {
|
|
388
|
+
for (const record of records) {
|
|
389
|
+
if (!record)
|
|
390
|
+
continue;
|
|
391
|
+
const value = record['event_time'] ??
|
|
392
|
+
record['eventTime'] ??
|
|
393
|
+
record['timestamp'] ??
|
|
394
|
+
record['create_time'] ??
|
|
395
|
+
record['createTime'];
|
|
396
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
397
|
+
return value < 1_000_000_000_000 ? value * 1_000 : value;
|
|
398
|
+
}
|
|
399
|
+
if (typeof value === 'string' && value.trim()) {
|
|
400
|
+
const numeric = Number(value);
|
|
401
|
+
if (Number.isFinite(numeric)) {
|
|
402
|
+
return numeric < 1_000_000_000_000 ? numeric * 1_000 : numeric;
|
|
403
|
+
}
|
|
404
|
+
const parsed = Date.parse(value);
|
|
405
|
+
if (Number.isFinite(parsed))
|
|
406
|
+
return parsed;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
export function parseDwsImEvent(line) {
|
|
412
|
+
const outer = parseJson(line, 'an event');
|
|
413
|
+
const outerRecord = isRecord(outer) ? outer : undefined;
|
|
414
|
+
const event = unwrapEvent(outer);
|
|
415
|
+
if (!event)
|
|
416
|
+
throw new Error('DWS event payload is not an object.');
|
|
417
|
+
const payload = nestedRecord(event, ['payload']);
|
|
418
|
+
const body = nestedRecord(event, ['body']) ??
|
|
419
|
+
(payload ? nestedRecord(payload, ['body']) : undefined);
|
|
420
|
+
const type = firstString(event, ['type', 'event_type', 'eventType']);
|
|
421
|
+
if (type !== 'user_im_message_receive_at' &&
|
|
422
|
+
type !== 'user_im_message_receive_o2o' &&
|
|
423
|
+
type !== 'user_im_message_receive_o2o_all' &&
|
|
424
|
+
type !== 'user_im_message_receive_group' &&
|
|
425
|
+
type !== 'user_im_message_receive_group_all') {
|
|
426
|
+
throw new Error(`Unsupported DWS event type: ${type ?? 'unknown'}.`);
|
|
427
|
+
}
|
|
428
|
+
const messageId = firstString(event, ['message_id', 'messageId', 'openMessageId']) ??
|
|
429
|
+
(body ? firstString(body, ['openMessageId', 'messageId']) : undefined);
|
|
430
|
+
const conversationId = firstString(event, [
|
|
431
|
+
'conversation_id',
|
|
432
|
+
'conversationId',
|
|
433
|
+
'openConversationId',
|
|
434
|
+
]) ??
|
|
435
|
+
(body
|
|
436
|
+
? firstString(body, ['openConversationId', 'conversationId'])
|
|
437
|
+
: undefined);
|
|
438
|
+
const senderId = firstString(event, [
|
|
439
|
+
'sender_open_dingtalk_id',
|
|
440
|
+
'senderOpenDingTalkId',
|
|
441
|
+
'sender_id',
|
|
442
|
+
'senderId',
|
|
443
|
+
]) ??
|
|
444
|
+
(body
|
|
445
|
+
? firstString(body, ['senderOpenDingTalkId', 'senderId'])
|
|
446
|
+
: undefined);
|
|
447
|
+
if (!messageId || !conversationId || !senderId) {
|
|
448
|
+
throw new Error('DWS message event is missing message, conversation, or sender identity.');
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
type,
|
|
452
|
+
eventId: firstString(event, ['event_id', 'eventId', 'id']) ?? messageId,
|
|
453
|
+
messageId,
|
|
454
|
+
conversationId,
|
|
455
|
+
content: typeof event['content'] === 'string'
|
|
456
|
+
? messageContent(event['content'])
|
|
457
|
+
: messageContent(body?.['content'], true),
|
|
458
|
+
senderId,
|
|
459
|
+
senderName: firstString(event, ['sender', 'sender_name', 'senderName']) ??
|
|
460
|
+
(body ? firstString(body, ['sender', 'senderName']) : undefined) ??
|
|
461
|
+
senderId,
|
|
462
|
+
referencedText: quotedMessageText(event, 'quoted_message') ??
|
|
463
|
+
quotedMessageText(event, 'quotedMessage') ??
|
|
464
|
+
quotedMessageText(body, 'quoted_message') ??
|
|
465
|
+
quotedMessageText(body, 'quotedMessage'),
|
|
466
|
+
eventTime: eventTime(event, body, outerRecord),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function eventKey(source) {
|
|
470
|
+
switch (source.kind) {
|
|
471
|
+
case 'at':
|
|
472
|
+
return 'user_im_message_receive_at';
|
|
473
|
+
case 'direct':
|
|
474
|
+
return 'user_im_message_receive_o2o_all';
|
|
475
|
+
case 'group-all':
|
|
476
|
+
return 'user_im_message_receive_group_all';
|
|
477
|
+
case 'group':
|
|
478
|
+
return 'user_im_message_receive_group';
|
|
479
|
+
default:
|
|
480
|
+
throw new Error('Unsupported DWS IM source.');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
export class DwsClient {
|
|
484
|
+
executable;
|
|
485
|
+
profile;
|
|
486
|
+
profileResolved = false;
|
|
487
|
+
runner;
|
|
488
|
+
eventStarter;
|
|
489
|
+
constructor(options, runner = runDwsProcess, eventStarter = startDwsEventProcess) {
|
|
490
|
+
this.executable = options.executable;
|
|
491
|
+
this.profile = options.profile?.trim() || undefined;
|
|
492
|
+
if (this.profile?.includes(',')) {
|
|
493
|
+
throw new Error('DWS channel profile must select exactly one login profile.');
|
|
494
|
+
}
|
|
495
|
+
this.runner = runner;
|
|
496
|
+
this.eventStarter = eventStarter;
|
|
497
|
+
}
|
|
498
|
+
async assertCompatible(signal) {
|
|
499
|
+
const response = await this.run(['version'], signal, false);
|
|
500
|
+
const version = parseVersion(response);
|
|
501
|
+
if (!version || !versionAtLeast(version, MINIMUM_DWS_VERSION)) {
|
|
502
|
+
throw new Error('DWS channel requires dws 1.0.57 or newer on the daemon PATH.');
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async assertAuthenticated(signal) {
|
|
506
|
+
if (!this.profileResolved) {
|
|
507
|
+
const selected = this.profile;
|
|
508
|
+
const profiles = await this.run(['profile', 'list'], signal, false);
|
|
509
|
+
const resolved = resolveProfile(profiles, selected);
|
|
510
|
+
if (!resolved) {
|
|
511
|
+
throw new Error(selected
|
|
512
|
+
? 'DWS profile must exactly match one entry from `dws profile list`.'
|
|
513
|
+
: 'DWS has no active profile. Run `dws auth login` or configure an exact login profile.');
|
|
514
|
+
}
|
|
515
|
+
this.profile = resolved.profile;
|
|
516
|
+
this.profileResolved = true;
|
|
517
|
+
}
|
|
518
|
+
const response = await this.run(['auth', 'status'], signal);
|
|
519
|
+
const authenticated = findScalar(response, new Set(['authenticated']));
|
|
520
|
+
if (authenticated !== true) {
|
|
521
|
+
throw new Error('DWS is not authenticated. Run `dws auth login` for the selected profile.');
|
|
522
|
+
}
|
|
523
|
+
const resolvedSelfSenderId = findScalar(response, new Set([
|
|
524
|
+
'openDingTalkId',
|
|
525
|
+
'open_dingtalk_id',
|
|
526
|
+
'senderOpenDingTalkId',
|
|
527
|
+
'sender_open_dingtalk_id',
|
|
528
|
+
]));
|
|
529
|
+
let selfSenderId = typeof resolvedSelfSenderId === 'string' && resolvedSelfSenderId.trim()
|
|
530
|
+
? resolvedSelfSenderId
|
|
531
|
+
: undefined;
|
|
532
|
+
if (!selfSenderId) {
|
|
533
|
+
const userId = findScalar(response, new Set(['userId', 'user_id']));
|
|
534
|
+
const userName = findScalar(response, new Set(['userName', 'user_name']));
|
|
535
|
+
if (typeof userId === 'string' &&
|
|
536
|
+
userId.trim() &&
|
|
537
|
+
typeof userName === 'string' &&
|
|
538
|
+
userName.trim()) {
|
|
539
|
+
const exactUserId = userId.trim();
|
|
540
|
+
const query = userName.trim();
|
|
541
|
+
let contacts;
|
|
542
|
+
try {
|
|
543
|
+
contacts = await this.run(['contact', 'user', 'search', '--query', query], signal);
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
signal?.throwIfAborted();
|
|
547
|
+
}
|
|
548
|
+
selfSenderId = findExactOpenDingTalkId(contacts, exactUserId);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
profile: this.profile,
|
|
553
|
+
selfSenderIds: selfSenderId ? [selfSenderId] : undefined,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
async subscribeToIm(source, onMessage, onError) {
|
|
557
|
+
const args = [
|
|
558
|
+
...this.profileArgs(),
|
|
559
|
+
'event',
|
|
560
|
+
'consume',
|
|
561
|
+
eventKey(source),
|
|
562
|
+
'--format',
|
|
563
|
+
'compact',
|
|
564
|
+
];
|
|
565
|
+
if (source.kind === 'group') {
|
|
566
|
+
args.push('--group', source.conversationId);
|
|
567
|
+
}
|
|
568
|
+
return this.eventStarter(this.executable, args, async (line) => onMessage(parseDwsImEvent(line)), onError);
|
|
569
|
+
}
|
|
570
|
+
async sendImMessage(target, content, idempotencyKey) {
|
|
571
|
+
const targetArgs = target.kind === 'group'
|
|
572
|
+
? ['--group', target.conversationId]
|
|
573
|
+
: ['--open-dingtalk-id', target.openDingTalkId];
|
|
574
|
+
await this.run([
|
|
575
|
+
'chat',
|
|
576
|
+
'message',
|
|
577
|
+
'send',
|
|
578
|
+
...targetArgs,
|
|
579
|
+
'--text',
|
|
580
|
+
content,
|
|
581
|
+
'--uuid',
|
|
582
|
+
idempotencyKey,
|
|
583
|
+
]);
|
|
584
|
+
}
|
|
585
|
+
async replyToImMessage(conversationId, messageId, senderId, content, idempotencyKey) {
|
|
586
|
+
await this.run([
|
|
587
|
+
'chat',
|
|
588
|
+
'message',
|
|
589
|
+
'reply',
|
|
590
|
+
'--conversation-id',
|
|
591
|
+
conversationId,
|
|
592
|
+
'--ref-msg-id',
|
|
593
|
+
messageId,
|
|
594
|
+
'--ref-sender',
|
|
595
|
+
senderId,
|
|
596
|
+
'--text',
|
|
597
|
+
content,
|
|
598
|
+
'--uuid',
|
|
599
|
+
idempotencyKey,
|
|
600
|
+
]);
|
|
601
|
+
}
|
|
602
|
+
async addImReaction(conversationId, messageId, reactionName) {
|
|
603
|
+
await this.run([
|
|
604
|
+
'chat',
|
|
605
|
+
'message',
|
|
606
|
+
'add-emoji',
|
|
607
|
+
'--conversation-id',
|
|
608
|
+
conversationId,
|
|
609
|
+
'--msg-id',
|
|
610
|
+
messageId,
|
|
611
|
+
'--emoji',
|
|
612
|
+
reactionName,
|
|
613
|
+
]);
|
|
614
|
+
}
|
|
615
|
+
async removeImReaction(conversationId, messageId, reactionName) {
|
|
616
|
+
await this.run([
|
|
617
|
+
'chat',
|
|
618
|
+
'message',
|
|
619
|
+
'remove-emoji',
|
|
620
|
+
'--conversation-id',
|
|
621
|
+
conversationId,
|
|
622
|
+
'--msg-id',
|
|
623
|
+
messageId,
|
|
624
|
+
'--emoji',
|
|
625
|
+
reactionName,
|
|
626
|
+
]);
|
|
627
|
+
}
|
|
628
|
+
async listDirectMessages(startTime, endTime, signal, initialCursor = '0') {
|
|
629
|
+
const messages = [];
|
|
630
|
+
const seenCursors = new Set([initialCursor]);
|
|
631
|
+
let cursor = initialCursor;
|
|
632
|
+
for (let page = 0; page < MAX_MESSAGE_PAGES; page++) {
|
|
633
|
+
signal?.throwIfAborted();
|
|
634
|
+
const response = await this.run([
|
|
635
|
+
'chat',
|
|
636
|
+
'message',
|
|
637
|
+
'list-all',
|
|
638
|
+
'--start',
|
|
639
|
+
formatDwsDateTime(startTime),
|
|
640
|
+
'--end',
|
|
641
|
+
formatDwsDateTime(endTime),
|
|
642
|
+
'--limit',
|
|
643
|
+
'50',
|
|
644
|
+
'--cursor',
|
|
645
|
+
cursor,
|
|
646
|
+
], signal);
|
|
647
|
+
const conversations = findConversationList(response);
|
|
648
|
+
if (!conversations) {
|
|
649
|
+
throw new Error('DWS message-history response did not contain a conversation list.');
|
|
650
|
+
}
|
|
651
|
+
for (const conversation of conversations) {
|
|
652
|
+
if (!isRecord(conversation) || conversation['singleChat'] !== true) {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const entries = conversation['messages'];
|
|
656
|
+
if (!Array.isArray(entries))
|
|
657
|
+
continue;
|
|
658
|
+
for (const entry of entries) {
|
|
659
|
+
if (!isRecord(entry))
|
|
660
|
+
continue;
|
|
661
|
+
const messageId = firstString(entry, ['openMessageId', 'messageId']);
|
|
662
|
+
const conversationId = firstString(entry, [
|
|
663
|
+
'openConversationId',
|
|
664
|
+
'conversationId',
|
|
665
|
+
]);
|
|
666
|
+
const senderId = firstString(entry, [
|
|
667
|
+
'senderOpenDingTalkId',
|
|
668
|
+
'senderId',
|
|
669
|
+
]);
|
|
670
|
+
if (!messageId || !conversationId || !senderId)
|
|
671
|
+
continue;
|
|
672
|
+
messages.push({
|
|
673
|
+
type: 'user_im_message_receive_o2o_all',
|
|
674
|
+
eventId: messageId,
|
|
675
|
+
messageId,
|
|
676
|
+
conversationId,
|
|
677
|
+
content: messageContent(entry['content']),
|
|
678
|
+
senderId,
|
|
679
|
+
senderName: firstString(entry, ['sender', 'senderName']) ?? senderId,
|
|
680
|
+
eventTime: eventTime(entry),
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (findScalar(response, new Set(['hasMore'])) !== true) {
|
|
685
|
+
return { messages };
|
|
686
|
+
}
|
|
687
|
+
const next = findScalar(response, new Set(['nextCursor']));
|
|
688
|
+
if (typeof next !== 'string' || !next || seenCursors.has(next)) {
|
|
689
|
+
throw new Error('DWS returned an invalid message pagination cursor.');
|
|
690
|
+
}
|
|
691
|
+
seenCursors.add(next);
|
|
692
|
+
if (page === MAX_MESSAGE_PAGES - 1) {
|
|
693
|
+
return { messages, nextCursor: next };
|
|
694
|
+
}
|
|
695
|
+
cursor = next;
|
|
696
|
+
}
|
|
697
|
+
return { messages };
|
|
698
|
+
}
|
|
699
|
+
async listMentionedMessages(startTime, endTime, signal, cursor = '0') {
|
|
700
|
+
const response = await this.run([
|
|
701
|
+
'chat',
|
|
702
|
+
'message',
|
|
703
|
+
'list-mentions',
|
|
704
|
+
'--start',
|
|
705
|
+
formatDwsDateTime(startTime),
|
|
706
|
+
'--end',
|
|
707
|
+
formatDwsDateTime(endTime),
|
|
708
|
+
'--limit',
|
|
709
|
+
'50',
|
|
710
|
+
'--cursor',
|
|
711
|
+
cursor,
|
|
712
|
+
], signal);
|
|
713
|
+
const conversations = findConversationList(response);
|
|
714
|
+
if (!conversations) {
|
|
715
|
+
if (findScalar(response, new Set(['success'])) === true &&
|
|
716
|
+
findScalar(response, new Set(['hasMore'])) !== true) {
|
|
717
|
+
return { messages: [] };
|
|
718
|
+
}
|
|
719
|
+
throw new Error('DWS mention-history response did not contain a conversation list.');
|
|
720
|
+
}
|
|
721
|
+
const messages = [];
|
|
722
|
+
for (const conversation of conversations) {
|
|
723
|
+
if (!isRecord(conversation) || conversation['singleChat'] !== false) {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
const entries = conversation['messages'];
|
|
727
|
+
if (!Array.isArray(entries))
|
|
728
|
+
continue;
|
|
729
|
+
for (const entry of entries) {
|
|
730
|
+
if (!isRecord(entry))
|
|
731
|
+
continue;
|
|
732
|
+
const messageId = firstString(entry, ['openMessageId', 'messageId']);
|
|
733
|
+
const conversationId = firstString(entry, [
|
|
734
|
+
'openConversationId',
|
|
735
|
+
'conversationId',
|
|
736
|
+
]);
|
|
737
|
+
const senderId = firstString(entry, [
|
|
738
|
+
'senderOpenDingTalkId',
|
|
739
|
+
'senderId',
|
|
740
|
+
]);
|
|
741
|
+
if (!messageId || !conversationId || !senderId)
|
|
742
|
+
continue;
|
|
743
|
+
messages.push({
|
|
744
|
+
type: 'user_im_message_receive_at',
|
|
745
|
+
eventId: messageId,
|
|
746
|
+
messageId,
|
|
747
|
+
conversationId,
|
|
748
|
+
content: messageContent(entry['content']),
|
|
749
|
+
senderId,
|
|
750
|
+
senderName: firstString(entry, ['sender', 'senderName']) ?? senderId,
|
|
751
|
+
referencedText: quotedMessageText(entry, 'quotedMessage'),
|
|
752
|
+
eventTime: eventTime(entry),
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const next = findScalar(response, new Set(['nextCursor']));
|
|
757
|
+
if (findScalar(response, new Set(['hasMore'])) !== true) {
|
|
758
|
+
return { messages };
|
|
759
|
+
}
|
|
760
|
+
if (typeof next !== 'string' || !next || next === cursor) {
|
|
761
|
+
throw new Error('DWS mention-history response did not contain a valid next cursor.');
|
|
762
|
+
}
|
|
763
|
+
return { messages, nextCursor: next };
|
|
764
|
+
}
|
|
765
|
+
async readDocument(documentId, signal) {
|
|
766
|
+
const response = await this.run(['doc', 'read', '--node', documentId], signal);
|
|
767
|
+
const markdown = findMarkdown(response);
|
|
768
|
+
if (markdown === undefined) {
|
|
769
|
+
throw new Error('DWS document response did not contain Markdown content.');
|
|
770
|
+
}
|
|
771
|
+
return markdown;
|
|
772
|
+
}
|
|
773
|
+
async replyToComment(documentId, commentKey, content) {
|
|
774
|
+
await this.run([
|
|
775
|
+
'doc',
|
|
776
|
+
'comment',
|
|
777
|
+
'reply',
|
|
778
|
+
'--node',
|
|
779
|
+
documentId,
|
|
780
|
+
'--comment-key',
|
|
781
|
+
commentKey,
|
|
782
|
+
'--content',
|
|
783
|
+
content,
|
|
784
|
+
]);
|
|
785
|
+
}
|
|
786
|
+
async listTodoTasks(signal) {
|
|
787
|
+
const tasks = new Map();
|
|
788
|
+
for (let page = 1; page <= MAX_TODO_PAGES; page++) {
|
|
789
|
+
signal?.throwIfAborted();
|
|
790
|
+
const response = await this.run([
|
|
791
|
+
'todo',
|
|
792
|
+
'task',
|
|
793
|
+
'list',
|
|
794
|
+
'--page',
|
|
795
|
+
String(page),
|
|
796
|
+
'--size',
|
|
797
|
+
String(TODO_PAGE_SIZE),
|
|
798
|
+
'--status',
|
|
799
|
+
'false',
|
|
800
|
+
'--role-types',
|
|
801
|
+
'executor',
|
|
802
|
+
], signal);
|
|
803
|
+
const cards = findTodoCards(response);
|
|
804
|
+
if (!cards) {
|
|
805
|
+
throw new Error('DWS todo response did not contain a todoCards list.');
|
|
806
|
+
}
|
|
807
|
+
for (const card of cards) {
|
|
808
|
+
const task = parseTodoTask(card);
|
|
809
|
+
if (!task) {
|
|
810
|
+
throw new Error('DWS todo response contained a task without taskId.');
|
|
811
|
+
}
|
|
812
|
+
tasks.set(task.taskId, task);
|
|
813
|
+
}
|
|
814
|
+
if (findScalar(response, new Set(['hasMore'])) !== true) {
|
|
815
|
+
return [...tasks.values()];
|
|
816
|
+
}
|
|
817
|
+
if (cards.length === 0) {
|
|
818
|
+
throw new Error('DWS todo pagination returned an empty page with hasMore.');
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
throw new Error(`DWS todo pagination exceeded ${MAX_TODO_PAGES} pages.`);
|
|
822
|
+
}
|
|
823
|
+
async getTodoTask(taskId, signal) {
|
|
824
|
+
const response = await this.run(['todo', 'task', 'get', '--task-id', taskId], signal);
|
|
825
|
+
const task = parseTodoTask(findTodoDetail(response), taskId);
|
|
826
|
+
if (!task) {
|
|
827
|
+
throw new Error('DWS todo detail response did not contain task data.');
|
|
828
|
+
}
|
|
829
|
+
return task;
|
|
830
|
+
}
|
|
831
|
+
async addTodoComment(taskId, content) {
|
|
832
|
+
await this.run([
|
|
833
|
+
'todo',
|
|
834
|
+
'comment',
|
|
835
|
+
'add',
|
|
836
|
+
'--task-id',
|
|
837
|
+
taskId,
|
|
838
|
+
'--content',
|
|
839
|
+
content,
|
|
840
|
+
]);
|
|
841
|
+
}
|
|
842
|
+
profileArgs() {
|
|
843
|
+
return this.profile ? ['--profile', this.profile] : [];
|
|
844
|
+
}
|
|
845
|
+
async run(command, signal, scoped = true) {
|
|
846
|
+
const args = [
|
|
847
|
+
...(scoped ? this.profileArgs() : []),
|
|
848
|
+
...command,
|
|
849
|
+
'--format',
|
|
850
|
+
'json',
|
|
851
|
+
];
|
|
852
|
+
const result = signal
|
|
853
|
+
? await this.runner(this.executable, args, signal)
|
|
854
|
+
: await this.runner(this.executable, args);
|
|
855
|
+
return parseOutput(result.stdout);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
//# sourceMappingURL=dws-client.js.map
|