@cjhyy/code-shell-chat 0.8.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/LICENSE +21 -0
- package/README.md +416 -0
- package/dist/adapter-factory.d.ts +30 -0
- package/dist/adapter-factory.js +198 -0
- package/dist/attachments.d.ts +13 -0
- package/dist/attachments.js +74 -0
- package/dist/channel.d.ts +112 -0
- package/dist/channel.js +60 -0
- package/dist/chat-gateway.d.ts +58 -0
- package/dist/chat-gateway.js +232 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +259 -0
- package/dist/codeshell.d.ts +6 -0
- package/dist/codeshell.js +6 -0
- package/dist/config.d.ts +125 -0
- package/dist/config.js +469 -0
- package/dist/delivery-queue.d.ts +93 -0
- package/dist/delivery-queue.js +401 -0
- package/dist/desktop-control-client.d.ts +51 -0
- package/dist/desktop-control-client.js +309 -0
- package/dist/dingtalk.d.ts +19 -0
- package/dist/dingtalk.js +93 -0
- package/dist/discord.d.ts +23 -0
- package/dist/discord.js +134 -0
- package/dist/gateway.d.ts +34 -0
- package/dist/gateway.js +287 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/ingress.d.ts +8 -0
- package/dist/ingress.js +61 -0
- package/dist/instance-lock.d.ts +24 -0
- package/dist/instance-lock.js +100 -0
- package/dist/lark.d.ts +20 -0
- package/dist/lark.js +184 -0
- package/dist/lifecycle.d.ts +7 -0
- package/dist/lifecycle.js +16 -0
- package/dist/line.d.ts +17 -0
- package/dist/line.js +123 -0
- package/dist/matrix.d.ts +21 -0
- package/dist/matrix.js +208 -0
- package/dist/mattermost.d.ts +18 -0
- package/dist/mattermost.js +170 -0
- package/dist/media.d.ts +39 -0
- package/dist/media.js +223 -0
- package/dist/notification-progress.d.ts +46 -0
- package/dist/notification-progress.js +299 -0
- package/dist/notification-relay.d.ts +43 -0
- package/dist/notification-relay.js +285 -0
- package/dist/platform-canary.d.ts +22 -0
- package/dist/platform-canary.js +55 -0
- package/dist/protocol.d.ts +102 -0
- package/dist/protocol.js +1 -0
- package/dist/service-manager.d.ts +47 -0
- package/dist/service-manager.js +222 -0
- package/dist/slack.d.ts +17 -0
- package/dist/slack.js +136 -0
- package/dist/teams.d.ts +26 -0
- package/dist/teams.js +193 -0
- package/dist/telegram.d.ts +33 -0
- package/dist/telegram.js +263 -0
- package/dist/webhook.d.ts +26 -0
- package/dist/webhook.js +91 -0
- package/dist/wechat-login.d.ts +21 -0
- package/dist/wechat-login.js +97 -0
- package/dist/wechat-storage.d.ts +61 -0
- package/dist/wechat-storage.js +251 -0
- package/dist/wechat.d.ts +129 -0
- package/dist/wechat.js +1082 -0
- package/dist/wecom.d.ts +16 -0
- package/dist/wecom.js +149 -0
- package/dist/whatsapp.d.ts +27 -0
- package/dist/whatsapp.js +218 -0
- package/package.json +123 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
export class DeliveryBackpressureError extends Error {
|
|
5
|
+
constructor(limit) {
|
|
6
|
+
super(`Chat Gateway inbox 已满(上限 ${limit})`);
|
|
7
|
+
this.name = "DeliveryBackpressureError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Thrown by the deliver callback when a record can never be routed (e.g. a
|
|
12
|
+
* persisted message references an adapter that no longer exists after a config
|
|
13
|
+
* change). The queue drops such records instead of retrying them forever.
|
|
14
|
+
*/
|
|
15
|
+
export class UnroutableDeliveryError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "UnroutableDeliveryError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Durable, bounded inbox. Text webhook deliveries are acknowledged only after
|
|
23
|
+
* this queue has atomically recorded them; processing happens out of band with
|
|
24
|
+
* per-target ordering and retry. Lazy attachment functions stay in-memory.
|
|
25
|
+
*/
|
|
26
|
+
export class DeliveryQueue {
|
|
27
|
+
config;
|
|
28
|
+
deliver;
|
|
29
|
+
onError;
|
|
30
|
+
pending = [];
|
|
31
|
+
completed = new Map();
|
|
32
|
+
inFlight = new Set();
|
|
33
|
+
targetInFlight = new Map();
|
|
34
|
+
mutation = Promise.resolve();
|
|
35
|
+
retryTimer;
|
|
36
|
+
stopped = true;
|
|
37
|
+
constructor(config, deliver, onError) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
this.deliver = deliver;
|
|
40
|
+
this.onError = onError;
|
|
41
|
+
}
|
|
42
|
+
async start() {
|
|
43
|
+
this.stopped = false;
|
|
44
|
+
if (this.config.path) {
|
|
45
|
+
await this.load();
|
|
46
|
+
// Reclaim spool files no surviving record points at.
|
|
47
|
+
await this.sweepOrphanedSpool();
|
|
48
|
+
}
|
|
49
|
+
this.pump();
|
|
50
|
+
}
|
|
51
|
+
stop() {
|
|
52
|
+
this.stopped = true;
|
|
53
|
+
if (this.retryTimer)
|
|
54
|
+
clearTimeout(this.retryTimer);
|
|
55
|
+
this.retryTimer = undefined;
|
|
56
|
+
}
|
|
57
|
+
status() {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
return {
|
|
60
|
+
pending: this.pending.length,
|
|
61
|
+
inFlight: this.inFlight.size,
|
|
62
|
+
delayed: this.pending.filter((entry) => entry.nextAttemptAt > now).length,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async enqueue(adapterId, message) {
|
|
66
|
+
if (this.stopped)
|
|
67
|
+
throw new Error("Chat Gateway inbox is stopped");
|
|
68
|
+
const dedupeKey = deliveryDedupeKey(message);
|
|
69
|
+
let result = "queued";
|
|
70
|
+
await this.withMutation(async () => {
|
|
71
|
+
this.pruneCompleted();
|
|
72
|
+
if (dedupeKey &&
|
|
73
|
+
(this.completed.has(dedupeKey) ||
|
|
74
|
+
this.pending.some((entry) => entry.dedupeKey === dedupeKey))) {
|
|
75
|
+
result = "duplicate";
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (this.pending.length >= this.config.maxPending) {
|
|
79
|
+
throw new DeliveryBackpressureError(this.config.maxPending);
|
|
80
|
+
}
|
|
81
|
+
const id = randomUUID();
|
|
82
|
+
// Attachments used to make a record non-persistent outright, because
|
|
83
|
+
// `load()` cannot be serialized — so the message lived in memory only while
|
|
84
|
+
// the adapter had already acked it. Spool the bytes instead: the record
|
|
85
|
+
// then persists like any other, carrying file paths in place of closures.
|
|
86
|
+
const spooled = await this.spoolAttachments(id, message);
|
|
87
|
+
const persistent = Boolean(this.config.path && (!message.attachments?.length || spooled !== undefined));
|
|
88
|
+
const safeMessage = persistent ? serializableMessage(message, spooled) : message;
|
|
89
|
+
this.pending.push({
|
|
90
|
+
id,
|
|
91
|
+
...(dedupeKey ? { dedupeKey } : {}),
|
|
92
|
+
adapterId,
|
|
93
|
+
message: safeMessage,
|
|
94
|
+
attempts: 0,
|
|
95
|
+
nextAttemptAt: Date.now(),
|
|
96
|
+
persistent,
|
|
97
|
+
...(spooled ? { spooled } : {}),
|
|
98
|
+
});
|
|
99
|
+
if (persistent)
|
|
100
|
+
await this.persist();
|
|
101
|
+
});
|
|
102
|
+
this.pump();
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
pump() {
|
|
106
|
+
if (this.stopped)
|
|
107
|
+
return;
|
|
108
|
+
if (this.retryTimer)
|
|
109
|
+
clearTimeout(this.retryTimer);
|
|
110
|
+
this.retryTimer = undefined;
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
while (this.inFlight.size < this.config.maxConcurrent) {
|
|
113
|
+
const entry = this.pending.find((candidate) => {
|
|
114
|
+
if (this.inFlight.has(candidate.id) || candidate.nextAttemptAt > now)
|
|
115
|
+
return false;
|
|
116
|
+
return ((this.targetInFlight.get(targetKey(candidate.message)) ?? 0) < this.config.maxPerTarget);
|
|
117
|
+
});
|
|
118
|
+
if (!entry)
|
|
119
|
+
break;
|
|
120
|
+
this.inFlight.add(entry.id);
|
|
121
|
+
const key = targetKey(entry.message);
|
|
122
|
+
this.targetInFlight.set(key, (this.targetInFlight.get(key) ?? 0) + 1);
|
|
123
|
+
void this.process(entry);
|
|
124
|
+
}
|
|
125
|
+
const next = this.pending
|
|
126
|
+
.filter((entry) => !this.inFlight.has(entry.id) && entry.nextAttemptAt > now)
|
|
127
|
+
.reduce((earliest, entry) => (earliest === undefined ? entry.nextAttemptAt : Math.min(earliest, entry.nextAttemptAt)), undefined);
|
|
128
|
+
if (next !== undefined) {
|
|
129
|
+
this.retryTimer = setTimeout(() => this.pump(), Math.max(1, next - Date.now()));
|
|
130
|
+
this.retryTimer.unref?.();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async process(entry) {
|
|
134
|
+
let error;
|
|
135
|
+
try {
|
|
136
|
+
await this.deliver(entry.adapterId, entry.message);
|
|
137
|
+
}
|
|
138
|
+
catch (caught) {
|
|
139
|
+
error = caught;
|
|
140
|
+
this.onError(caught, entry.message);
|
|
141
|
+
}
|
|
142
|
+
await this.withMutation(async () => {
|
|
143
|
+
this.inFlight.delete(entry.id);
|
|
144
|
+
const key = targetKey(entry.message);
|
|
145
|
+
const targetCount = (this.targetInFlight.get(key) ?? 1) - 1;
|
|
146
|
+
if (targetCount <= 0)
|
|
147
|
+
this.targetInFlight.delete(key);
|
|
148
|
+
else
|
|
149
|
+
this.targetInFlight.set(key, targetCount);
|
|
150
|
+
if (error === undefined || error instanceof UnroutableDeliveryError) {
|
|
151
|
+
// Success, or a permanently unroutable record: drop it so a message
|
|
152
|
+
// that can never be delivered does not retry (and re-log) forever.
|
|
153
|
+
const index = this.pending.findIndex((candidate) => candidate.id === entry.id);
|
|
154
|
+
if (index >= 0)
|
|
155
|
+
this.pending.splice(index, 1);
|
|
156
|
+
if (error === undefined && entry.dedupeKey)
|
|
157
|
+
this.completed.set(entry.dedupeKey, Date.now());
|
|
158
|
+
// Terminal outcome — the bytes will never be needed again. Cleaning up
|
|
159
|
+
// here (rather than on a TTL sweep) keeps the spool bounded by what is
|
|
160
|
+
// actually pending.
|
|
161
|
+
await this.discardSpool(entry);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
entry.attempts += 1;
|
|
165
|
+
entry.nextAttemptAt =
|
|
166
|
+
Date.now() +
|
|
167
|
+
Math.min(this.config.retryMaxMs, this.config.retryBaseMs * 2 ** Math.min(entry.attempts - 1, 10));
|
|
168
|
+
}
|
|
169
|
+
this.pruneCompleted();
|
|
170
|
+
if (entry.persistent)
|
|
171
|
+
await this.persist();
|
|
172
|
+
});
|
|
173
|
+
this.pump();
|
|
174
|
+
}
|
|
175
|
+
/** Directory holding spooled attachment bytes, or undefined when not durable. */
|
|
176
|
+
spoolDir() {
|
|
177
|
+
if (!this.config.path)
|
|
178
|
+
return undefined;
|
|
179
|
+
return this.config.attachmentSpoolDir ?? `${this.config.path}.attachments`;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Materialize a message's attachments to disk so the record can be persisted.
|
|
183
|
+
*
|
|
184
|
+
* Returns undefined when there is nothing to spool, or when spooling fails —
|
|
185
|
+
* in which case the caller falls back to the old memory-only behaviour rather
|
|
186
|
+
* than dropping the message.
|
|
187
|
+
*/
|
|
188
|
+
async spoolAttachments(recordId, message) {
|
|
189
|
+
const attachments = message.attachments;
|
|
190
|
+
const dir = this.spoolDir();
|
|
191
|
+
if (!attachments?.length || !dir)
|
|
192
|
+
return undefined;
|
|
193
|
+
const limit = this.config.maxSpooledAttachmentBytes ?? 16 * 1024 * 1024;
|
|
194
|
+
try {
|
|
195
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
196
|
+
const spooled = [];
|
|
197
|
+
for (const [index, attachment] of attachments.entries()) {
|
|
198
|
+
const bytes = await attachment.load();
|
|
199
|
+
// Oversized payloads stay in memory: spooling them would trade a lost
|
|
200
|
+
// message for unbounded disk use.
|
|
201
|
+
if (bytes.byteLength > limit)
|
|
202
|
+
return undefined;
|
|
203
|
+
const file = `${recordId}.${index}`;
|
|
204
|
+
await writeFile(`${dir}/${file}`, bytes, { mode: 0o600 });
|
|
205
|
+
spooled.push({
|
|
206
|
+
id: attachment.id,
|
|
207
|
+
kind: attachment.kind,
|
|
208
|
+
...(attachment.name ? { name: attachment.name } : {}),
|
|
209
|
+
...(attachment.mimeType ? { mimeType: attachment.mimeType } : {}),
|
|
210
|
+
size: bytes.byteLength,
|
|
211
|
+
file,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return spooled;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Spooling is an availability improvement, never a new failure mode.
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** Rebuild attachments whose bytes live in the spool. */
|
|
222
|
+
rehydrateAttachments(spooled) {
|
|
223
|
+
const dir = this.spoolDir();
|
|
224
|
+
if (!spooled?.length || !dir)
|
|
225
|
+
return undefined;
|
|
226
|
+
return spooled.map((entry) => ({
|
|
227
|
+
id: entry.id,
|
|
228
|
+
kind: entry.kind,
|
|
229
|
+
...(entry.name ? { name: entry.name } : {}),
|
|
230
|
+
...(entry.mimeType ? { mimeType: entry.mimeType } : {}),
|
|
231
|
+
size: entry.size,
|
|
232
|
+
load: async () => new Uint8Array(await readFile(`${dir}/${entry.file}`)),
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Delete spool files that no pending record references.
|
|
237
|
+
*
|
|
238
|
+
* `discardSpool` covers the normal path, but files can still be orphaned: a
|
|
239
|
+
* crash between writing the bytes and persisting the record, or the
|
|
240
|
+
* oversized-attachment fallback that spools nothing yet may have written
|
|
241
|
+
* earlier attachments of the same message. Without this the directory only
|
|
242
|
+
* grows, since nothing else ever looks at it.
|
|
243
|
+
*
|
|
244
|
+
* Runs once at load(), when the set of live records is exactly known.
|
|
245
|
+
*/
|
|
246
|
+
async sweepOrphanedSpool() {
|
|
247
|
+
const dir = this.spoolDir();
|
|
248
|
+
if (!dir)
|
|
249
|
+
return;
|
|
250
|
+
const referenced = new Set();
|
|
251
|
+
for (const entry of this.pending) {
|
|
252
|
+
for (const attachment of entry.spooled ?? [])
|
|
253
|
+
referenced.add(attachment.file);
|
|
254
|
+
}
|
|
255
|
+
let files;
|
|
256
|
+
try {
|
|
257
|
+
files = await readdir(dir);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return; // No spool directory yet — nothing to sweep.
|
|
261
|
+
}
|
|
262
|
+
await Promise.all(files
|
|
263
|
+
.filter((file) => !referenced.has(file))
|
|
264
|
+
.map((file) => rm(`${dir}/${file}`, { force: true }).catch(() => undefined)));
|
|
265
|
+
}
|
|
266
|
+
/** Delete a record's spooled files once it will never be delivered again. */
|
|
267
|
+
async discardSpool(entry) {
|
|
268
|
+
const dir = this.spoolDir();
|
|
269
|
+
if (!entry.spooled?.length || !dir)
|
|
270
|
+
return;
|
|
271
|
+
await Promise.all(entry.spooled.map((attachment) => rm(`${dir}/${attachment.file}`, { force: true }).catch(() => undefined)));
|
|
272
|
+
}
|
|
273
|
+
async load() {
|
|
274
|
+
if (!this.config.path)
|
|
275
|
+
return;
|
|
276
|
+
try {
|
|
277
|
+
const parsed = JSON.parse(await readFile(this.config.path, "utf-8"));
|
|
278
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.pending) || !parsed.completed)
|
|
279
|
+
return;
|
|
280
|
+
for (const entry of parsed.pending) {
|
|
281
|
+
if (!validStoredEntry(entry))
|
|
282
|
+
continue;
|
|
283
|
+
// Turn spooled metadata back into real attachments whose load() reads
|
|
284
|
+
// the file. This is the whole point of the spool: after a restart the
|
|
285
|
+
// upstream will not resend, so the bytes have to come from disk.
|
|
286
|
+
const restored = this.rehydrateAttachments(entry.spooled);
|
|
287
|
+
this.pending.push({
|
|
288
|
+
...entry,
|
|
289
|
+
message: restored ? { ...entry.message, attachments: restored } : entry.message,
|
|
290
|
+
persistent: true,
|
|
291
|
+
nextAttemptAt: Date.now(),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
for (const [key, timestamp] of Object.entries(parsed.completed)) {
|
|
295
|
+
if (typeof timestamp === "number")
|
|
296
|
+
this.completed.set(key, timestamp);
|
|
297
|
+
}
|
|
298
|
+
this.pruneCompleted();
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (error.code !== "ENOENT") {
|
|
302
|
+
throw new Error(`无法读取 Chat Gateway inbox:${String(error)}`, { cause: error });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async persist() {
|
|
307
|
+
const path = this.config.path;
|
|
308
|
+
if (!path)
|
|
309
|
+
return;
|
|
310
|
+
const state = {
|
|
311
|
+
version: 1,
|
|
312
|
+
pending: this.pending
|
|
313
|
+
.filter((entry) => entry.persistent)
|
|
314
|
+
.map(({ persistent: _persistent, ...entry }) => {
|
|
315
|
+
// Never write live attachments: JSON drops their `load()`, leaving a
|
|
316
|
+
// record that claims attachments it cannot produce — and whose
|
|
317
|
+
// presence would stop the loader from rehydrating from the spool.
|
|
318
|
+
// `entry.spooled` is the durable representation.
|
|
319
|
+
if (!entry.message.attachments?.length)
|
|
320
|
+
return entry;
|
|
321
|
+
const { attachments: _attachments, ...message } = entry.message;
|
|
322
|
+
return { ...entry, message };
|
|
323
|
+
}),
|
|
324
|
+
completed: Object.fromEntries(this.completed),
|
|
325
|
+
};
|
|
326
|
+
const serialized = `${JSON.stringify(state)}\n`;
|
|
327
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
328
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
329
|
+
try {
|
|
330
|
+
await writeFile(temporary, serialized, { encoding: "utf-8", mode: 0o600 });
|
|
331
|
+
await rename(temporary, path);
|
|
332
|
+
await chmod(path, 0o600).catch(() => undefined);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
pruneCompleted() {
|
|
340
|
+
const cutoff = Date.now() - this.config.completedTtlMs;
|
|
341
|
+
for (const [key, timestamp] of this.completed) {
|
|
342
|
+
if (timestamp < cutoff)
|
|
343
|
+
this.completed.delete(key);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async withMutation(operation) {
|
|
347
|
+
const previous = this.mutation;
|
|
348
|
+
let release;
|
|
349
|
+
this.mutation = new Promise((resolve) => {
|
|
350
|
+
release = resolve;
|
|
351
|
+
});
|
|
352
|
+
await previous;
|
|
353
|
+
try {
|
|
354
|
+
return await operation();
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
release();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function deliveryDedupeKey(message) {
|
|
362
|
+
return message.messageId
|
|
363
|
+
? `${message.channel}\0${message.target}\0${message.senderId}\0${message.messageId}`
|
|
364
|
+
: undefined;
|
|
365
|
+
}
|
|
366
|
+
function targetKey(message) {
|
|
367
|
+
return `${message.channel}\0${message.target}`;
|
|
368
|
+
}
|
|
369
|
+
function serializableMessage(message, spooled) {
|
|
370
|
+
// Drop `attachments` before serializing: each carries a `load()` closure that
|
|
371
|
+
// JSON silently discards, which would leave a persisted message claiming
|
|
372
|
+
// attachments it can no longer produce. Spooled metadata is stored on the
|
|
373
|
+
// RECORD instead, and rehydrated into real load() functions on restart.
|
|
374
|
+
const { attachments: _attachments, ...rest } = message;
|
|
375
|
+
const serialized = JSON.stringify(rest);
|
|
376
|
+
if (serialized === undefined || Buffer.byteLength(serialized, "utf-8") > 1_048_576) {
|
|
377
|
+
throw new Error("Chat Gateway message is not persistable or exceeds 1 MiB");
|
|
378
|
+
}
|
|
379
|
+
const parsed = JSON.parse(serialized);
|
|
380
|
+
// Keep the live attachments for THIS process; the spool only matters after a
|
|
381
|
+
// restart, and re-reading from disk while the originals are in hand is waste.
|
|
382
|
+
if (message.attachments?.length && spooled) {
|
|
383
|
+
return { ...parsed, attachments: message.attachments };
|
|
384
|
+
}
|
|
385
|
+
return parsed;
|
|
386
|
+
}
|
|
387
|
+
function validStoredEntry(entry) {
|
|
388
|
+
if (!entry || typeof entry !== "object")
|
|
389
|
+
return false;
|
|
390
|
+
const value = entry;
|
|
391
|
+
return (typeof value.id === "string" &&
|
|
392
|
+
typeof value.adapterId === "string" &&
|
|
393
|
+
typeof value.attempts === "number" &&
|
|
394
|
+
typeof value.nextAttemptAt === "number" &&
|
|
395
|
+
Boolean(value.message) &&
|
|
396
|
+
typeof value.message?.channel === "string" &&
|
|
397
|
+
typeof value.message?.target === "string" &&
|
|
398
|
+
typeof value.message?.senderId === "string" &&
|
|
399
|
+
typeof value.message?.text === "string" &&
|
|
400
|
+
!value.message?.attachments);
|
|
401
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
import type { DesktopGatewayConfig } from "./config.js";
|
|
3
|
+
import { type DesktopControlDescriptor, type DesktopControlEvent, type DesktopControlEventPage, type MobileRemoteOpenResult, type MobileRemoteStatus, type PetChatRequest, type PetChatResult } from "./protocol.js";
|
|
4
|
+
type SpawnFn = (command: string, args: readonly string[], options: {
|
|
5
|
+
detached: boolean;
|
|
6
|
+
stdio: "ignore";
|
|
7
|
+
}) => ChildProcess;
|
|
8
|
+
export interface DesktopControlClientOptions {
|
|
9
|
+
fetch?: typeof fetch;
|
|
10
|
+
spawn?: SpawnFn;
|
|
11
|
+
readDescriptor?: () => Promise<string>;
|
|
12
|
+
sleep?: (ms: number) => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface DesktopEventWatchOptions {
|
|
15
|
+
checkpointPath?: string;
|
|
16
|
+
onError?: (error: unknown) => void;
|
|
17
|
+
onRecovered?: () => void;
|
|
18
|
+
retryBaseMs?: number;
|
|
19
|
+
retryMaxMs?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface DesktopEventContext {
|
|
22
|
+
streamId: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class DesktopControlUnavailableError extends Error {
|
|
25
|
+
constructor(message?: string);
|
|
26
|
+
}
|
|
27
|
+
export declare class DesktopControlOperationError extends Error {
|
|
28
|
+
constructor(message: string);
|
|
29
|
+
}
|
|
30
|
+
export declare class DesktopControlClient {
|
|
31
|
+
private readonly config;
|
|
32
|
+
private readonly fetchFn;
|
|
33
|
+
private readonly spawnFn;
|
|
34
|
+
private readonly readDescriptorFn;
|
|
35
|
+
private readonly sleepFn;
|
|
36
|
+
constructor(config: DesktopGatewayConfig, opts?: DesktopControlClientOptions);
|
|
37
|
+
open(): Promise<MobileRemoteOpenResult>;
|
|
38
|
+
close(): Promise<void>;
|
|
39
|
+
status(): Promise<MobileRemoteStatus>;
|
|
40
|
+
pairingUrl(): Promise<{
|
|
41
|
+
pairingUrl: string;
|
|
42
|
+
expiresAt: number;
|
|
43
|
+
}>;
|
|
44
|
+
petChat(input: PetChatRequest): Promise<PetChatResult>;
|
|
45
|
+
events(after?: number, waitMs?: number, signal?: AbortSignal): Promise<DesktopControlEventPage>;
|
|
46
|
+
watchEvents(signal: AbortSignal, handle: (event: DesktopControlEvent, context: DesktopEventContext) => Promise<void>, options?: DesktopEventWatchOptions): Promise<void>;
|
|
47
|
+
private ensureDesktopAvailable;
|
|
48
|
+
private request;
|
|
49
|
+
}
|
|
50
|
+
export declare function parseDescriptor(raw: string): DesktopControlDescriptor;
|
|
51
|
+
export {};
|