@zooid/transport-matrix 0.7.3 → 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/dist/index.d.ts +109 -1
- package/dist/index.js +280 -10
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/attachments.test.ts +58 -0
- package/src/attachments.ts +30 -0
- package/src/context-provider.test.ts +33 -0
- package/src/context-provider.ts +21 -3
- package/src/index.ts +8 -2
- package/src/media-client.test.ts +102 -0
- package/src/media-client.ts +69 -0
- package/src/pending-media.test.ts +51 -0
- package/src/pending-media.ts +37 -0
- package/src/router.test.ts +22 -1
- package/src/router.ts +11 -0
- package/src/space-provisioner.test.ts +26 -1
- package/src/space-provisioner.ts +15 -4
- package/src/transport.test.ts +336 -0
- package/src/transport.ts +252 -7
package/dist/index.d.ts
CHANGED
|
@@ -221,7 +221,13 @@ interface AgentBinding {
|
|
|
221
221
|
*/
|
|
222
222
|
rooms: RoomBinding[];
|
|
223
223
|
trigger: 'mention' | 'any';
|
|
224
|
+
/** Host path of the agent's workspace (resolved agent.workdir). Media files land here. */
|
|
225
|
+
workspaceDir?: string;
|
|
226
|
+
/** Path prefix as the agent sees it: '/workspace' for containers, = workspaceDir for local. */
|
|
227
|
+
agentWorkspacePath?: string;
|
|
224
228
|
}
|
|
229
|
+
declare const MEDIA_MSGTYPES: Set<string>;
|
|
230
|
+
declare function isMediaMsgtype(t: string | undefined): boolean;
|
|
225
231
|
interface ThreadState {
|
|
226
232
|
/** Agent names that have posted in this thread, in order. */
|
|
227
233
|
participants: string[];
|
|
@@ -266,6 +272,36 @@ declare class BotPool {
|
|
|
266
272
|
findByName(name: string): AgentBinding | undefined;
|
|
267
273
|
}
|
|
268
274
|
|
|
275
|
+
interface WriteAttachmentInput {
|
|
276
|
+
workspaceDir: string;
|
|
277
|
+
agentWorkspacePath: string;
|
|
278
|
+
eventId: string;
|
|
279
|
+
filename: string;
|
|
280
|
+
data: Uint8Array;
|
|
281
|
+
}
|
|
282
|
+
declare function writeAttachment(input: WriteAttachmentInput): {
|
|
283
|
+
hostPath: string;
|
|
284
|
+
agentPath: string;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
interface MediaClientLike {
|
|
288
|
+
download(input: {
|
|
289
|
+
mxcUri: string;
|
|
290
|
+
asUserId: string;
|
|
291
|
+
maxBytes?: number;
|
|
292
|
+
}): Promise<{
|
|
293
|
+
data: Uint8Array;
|
|
294
|
+
contentType: string;
|
|
295
|
+
}>;
|
|
296
|
+
upload(input: {
|
|
297
|
+
data: Uint8Array;
|
|
298
|
+
contentType: string;
|
|
299
|
+
filename?: string;
|
|
300
|
+
asUserId: string;
|
|
301
|
+
}): Promise<{
|
|
302
|
+
content_uri: string;
|
|
303
|
+
}>;
|
|
304
|
+
}
|
|
269
305
|
interface CreateMatrixTransportOptions {
|
|
270
306
|
agents: AcpRegistry;
|
|
271
307
|
approvals: ApprovalCorrelator;
|
|
@@ -280,6 +316,10 @@ interface CreateMatrixTransportOptions {
|
|
|
280
316
|
drainQuietMs?: number;
|
|
281
317
|
/** Hard cap on the post-turn drain. Defaults to `DRAIN_MAX_MS`. */
|
|
282
318
|
drainMaxMs?: number;
|
|
319
|
+
/** Injected media client for downloading/uploading Matrix media. */
|
|
320
|
+
media?: MediaClientLike;
|
|
321
|
+
/** Injected attachment writer (defaults to the real writeAttachment). */
|
|
322
|
+
writeAttachmentFn?: typeof writeAttachment;
|
|
283
323
|
}
|
|
284
324
|
declare function createMatrixTransport(opts: CreateMatrixTransportOptions): {
|
|
285
325
|
app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
|
@@ -304,6 +344,15 @@ interface EnsureSpaceOpts {
|
|
|
304
344
|
* if the alias already resolves we return the existing room untouched.
|
|
305
345
|
*/
|
|
306
346
|
admins?: string[];
|
|
347
|
+
/**
|
|
348
|
+
* Join rule pinned on the space at creation. Defaults to `invite`: a
|
|
349
|
+
* workspace is joined by invitation, not self-service, so it can't be walked
|
|
350
|
+
* into (which would otherwise satisfy every restricted child room's `allow`).
|
|
351
|
+
* `zooid dev` passes `public` so a self-service-registered local account can
|
|
352
|
+
* join `#<space>` straight from the web client without an invite — acceptable
|
|
353
|
+
* because the dev homeserver is local-only and never deployed.
|
|
354
|
+
*/
|
|
355
|
+
joinRule?: 'invite' | 'public';
|
|
307
356
|
}
|
|
308
357
|
declare function ensureWorkforceSpace(opts: EnsureSpaceOpts): Promise<string>;
|
|
309
358
|
interface EnsureDefaultChannelOpts {
|
|
@@ -359,4 +408,63 @@ interface StartOpts {
|
|
|
359
408
|
}
|
|
360
409
|
declare function startWorkforcePublisher(opts: StartOpts): Promise<PublisherHandle>;
|
|
361
410
|
|
|
362
|
-
|
|
411
|
+
/** Limits are routing policy, not enforcement — see ZOD057 (enforcement lives
|
|
412
|
+
* in Tuwunel config + the Zoon composer). */
|
|
413
|
+
declare const MAX_INLINE_IMAGE_BYTES = 524288;
|
|
414
|
+
declare const INLINE_IMAGE_MIMES: string[];
|
|
415
|
+
declare const MAX_DOWNLOAD_BYTES = 33554432;
|
|
416
|
+
interface MediaClientOptions {
|
|
417
|
+
homeserver: string;
|
|
418
|
+
asToken: string;
|
|
419
|
+
fetch?: typeof globalThis.fetch;
|
|
420
|
+
}
|
|
421
|
+
declare function parseMxcUri(uri: string): {
|
|
422
|
+
serverName: string;
|
|
423
|
+
mediaId: string;
|
|
424
|
+
} | null;
|
|
425
|
+
declare class MediaClient {
|
|
426
|
+
private readonly homeserver;
|
|
427
|
+
private readonly asToken;
|
|
428
|
+
private readonly fetch;
|
|
429
|
+
constructor(opts: MediaClientOptions);
|
|
430
|
+
download(input: {
|
|
431
|
+
mxcUri: string;
|
|
432
|
+
asUserId: string;
|
|
433
|
+
maxBytes?: number;
|
|
434
|
+
}): Promise<{
|
|
435
|
+
data: Uint8Array;
|
|
436
|
+
contentType: string;
|
|
437
|
+
}>;
|
|
438
|
+
upload(input: {
|
|
439
|
+
data: Uint8Array;
|
|
440
|
+
contentType: string;
|
|
441
|
+
filename?: string;
|
|
442
|
+
asUserId: string;
|
|
443
|
+
}): Promise<{
|
|
444
|
+
content_uri: string;
|
|
445
|
+
}>;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
declare const MAX_MEDIA_PER_TURN = 8;
|
|
449
|
+
interface PendingMediaItem {
|
|
450
|
+
eventId: string;
|
|
451
|
+
sender: string;
|
|
452
|
+
msgtype: string;
|
|
453
|
+
body: string;
|
|
454
|
+
filename?: string;
|
|
455
|
+
url: string;
|
|
456
|
+
info?: {
|
|
457
|
+
mimetype?: string;
|
|
458
|
+
size?: number;
|
|
459
|
+
w?: number;
|
|
460
|
+
h?: number;
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
declare class PendingMediaStore {
|
|
464
|
+
private readonly queues;
|
|
465
|
+
private key;
|
|
466
|
+
add(roomId: string, threadKey: string | undefined, item: PendingMediaItem): void;
|
|
467
|
+
drain(roomId: string, threadKey: string | undefined, sender: string): PendingMediaItem[];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export { type AgentBinding, BotPool, type CreateMatrixTransportOptions, type EnsureDefaultChannelOpts, type EnsureSpaceOpts, INLINE_IMAGE_MIMES, MAX_DOWNLOAD_BYTES, MAX_INLINE_IMAGE_BYTES, MAX_MEDIA_PER_TURN, MEDIA_MSGTYPES, MatrixClient, type MatrixClientOptions, MatrixContextProvider, type MatrixContextProviderOpts, type MatrixTransportConfig, type MaybeMessage, MediaClient, type MediaClientLike, type MediaClientOptions, type PendingMediaItem, PendingMediaStore, type PublishOpts, type PublisherHandle, type RouteMatch, type SendCustomEventInput, type SendMessageInput, type StartOpts as StartWorkforcePublisherOpts, type WorkforceRoster, type WriteAttachmentInput, buildWorkforceRoster, createMatrixTransport, ensureDefaultChannel, ensureWorkforceSpace, extractMentions, isMediaMsgtype, parseMxcUri, publishWorkforce, renderRegistration, route, serverNameFromMxid, startWorkforcePublisher, writeAttachment };
|
package/dist/index.js
CHANGED
|
@@ -359,14 +359,29 @@ var MatrixContextProvider = class {
|
|
|
359
359
|
}
|
|
360
360
|
toMessage(ev) {
|
|
361
361
|
if (ev.type !== "m.room.message") return null;
|
|
362
|
-
|
|
362
|
+
const msgtype = ev.content?.msgtype;
|
|
363
|
+
const body = ev.content?.body;
|
|
363
364
|
const agent = this.opts.agentBots.get(ev.sender);
|
|
364
|
-
const relatesTo = ev.content["m.relates_to"];
|
|
365
|
+
const relatesTo = ev.content?.["m.relates_to"];
|
|
365
366
|
const threadId = relatesTo?.rel_type === "m.thread" && relatesTo.event_id ? relatesTo.event_id : void 0;
|
|
367
|
+
if (msgtype === "m.image" || msgtype === "m.file" || msgtype === "m.video" || msgtype === "m.audio") {
|
|
368
|
+
const kind = msgtype.slice(2);
|
|
369
|
+
const name = typeof body === "string" && body ? body : "untitled";
|
|
370
|
+
return {
|
|
371
|
+
id: ev.event_id,
|
|
372
|
+
sender: ev.sender,
|
|
373
|
+
text: `[${kind}: ${name}]`,
|
|
374
|
+
timestamp: new Date(ev.origin_server_ts).toISOString(),
|
|
375
|
+
is_agent: agent !== void 0,
|
|
376
|
+
...agent !== void 0 ? { agent_name: agent } : {},
|
|
377
|
+
...threadId !== void 0 ? { thread_id: threadId } : {}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (msgtype !== "m.text" || typeof body !== "string") return null;
|
|
366
381
|
return {
|
|
367
382
|
id: ev.event_id,
|
|
368
383
|
sender: ev.sender,
|
|
369
|
-
text:
|
|
384
|
+
text: body,
|
|
370
385
|
timestamp: new Date(ev.origin_server_ts).toISOString(),
|
|
371
386
|
is_agent: agent !== void 0,
|
|
372
387
|
...agent !== void 0 ? { agent_name: agent } : {},
|
|
@@ -440,6 +455,10 @@ function stripMention(body, userId) {
|
|
|
440
455
|
}
|
|
441
456
|
|
|
442
457
|
// src/router.ts
|
|
458
|
+
var MEDIA_MSGTYPES = /* @__PURE__ */ new Set(["m.image", "m.file", "m.video", "m.audio"]);
|
|
459
|
+
function isMediaMsgtype(t) {
|
|
460
|
+
return t !== void 0 && MEDIA_MSGTYPES.has(t);
|
|
461
|
+
}
|
|
443
462
|
function inboundThreadRoot(event) {
|
|
444
463
|
const r = event.content?.["m.relates_to"];
|
|
445
464
|
return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
|
|
@@ -447,6 +466,7 @@ function inboundThreadRoot(event) {
|
|
|
447
466
|
function route(event, agents, threadStates) {
|
|
448
467
|
if (event.type !== "m.room.message") return [];
|
|
449
468
|
if (!event.content?.msgtype) return [];
|
|
469
|
+
if (isMediaMsgtype(event.content.msgtype)) return [];
|
|
450
470
|
const mentions = new Set(extractMentions(event));
|
|
451
471
|
const matches = [];
|
|
452
472
|
const threadRoot = inboundThreadRoot(event);
|
|
@@ -485,10 +505,12 @@ async function ensureWorkforceSpace(opts) {
|
|
|
485
505
|
name: display,
|
|
486
506
|
preset: opts.preset,
|
|
487
507
|
creation_content: { type: "m.space" },
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
initial_state: [
|
|
508
|
+
// Pin the join rule regardless of preset. Defaults to invite so the space
|
|
509
|
+
// can't be walked into (which would otherwise satisfy every restricted
|
|
510
|
+
// child room's allow); `zooid dev` opts into `public` for local-only use.
|
|
511
|
+
initial_state: [
|
|
512
|
+
{ type: "m.room.join_rules", state_key: "", content: { join_rule: opts.joinRule ?? "invite" } }
|
|
513
|
+
]
|
|
492
514
|
};
|
|
493
515
|
if (opts.admins && opts.admins.length > 0) {
|
|
494
516
|
body.invite = opts.admins;
|
|
@@ -804,8 +826,180 @@ function toMatrixHtml(markdown) {
|
|
|
804
826
|
});
|
|
805
827
|
}
|
|
806
828
|
|
|
829
|
+
// src/pending-media.ts
|
|
830
|
+
var MAX_MEDIA_PER_TURN = 8;
|
|
831
|
+
var PendingMediaStore = class {
|
|
832
|
+
queues = /* @__PURE__ */ new Map();
|
|
833
|
+
key(roomId, threadKey) {
|
|
834
|
+
return `${roomId} ${threadKey ?? "main"}`;
|
|
835
|
+
}
|
|
836
|
+
add(roomId, threadKey, item) {
|
|
837
|
+
const k = this.key(roomId, threadKey);
|
|
838
|
+
const q = this.queues.get(k) ?? [];
|
|
839
|
+
q.push(item);
|
|
840
|
+
while (q.length > MAX_MEDIA_PER_TURN) q.shift();
|
|
841
|
+
this.queues.set(k, q);
|
|
842
|
+
}
|
|
843
|
+
drain(roomId, threadKey, sender) {
|
|
844
|
+
const k = this.key(roomId, threadKey);
|
|
845
|
+
const q = this.queues.get(k) ?? [];
|
|
846
|
+
const mine = q.filter((i) => i.sender === sender);
|
|
847
|
+
const rest = q.filter((i) => i.sender !== sender);
|
|
848
|
+
if (rest.length) this.queues.set(k, rest);
|
|
849
|
+
else this.queues.delete(k);
|
|
850
|
+
return mine;
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// src/media-client.ts
|
|
855
|
+
var MAX_INLINE_IMAGE_BYTES = 524288;
|
|
856
|
+
var INLINE_IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
857
|
+
var MAX_DOWNLOAD_BYTES = 33554432;
|
|
858
|
+
function parseMxcUri(uri) {
|
|
859
|
+
const m = /^mxc:\/\/([^/]+)\/(.+)$/.exec(uri);
|
|
860
|
+
return m ? { serverName: m[1], mediaId: m[2] } : null;
|
|
861
|
+
}
|
|
862
|
+
var MediaClient = class {
|
|
863
|
+
homeserver;
|
|
864
|
+
asToken;
|
|
865
|
+
fetch;
|
|
866
|
+
constructor(opts) {
|
|
867
|
+
this.homeserver = opts.homeserver.replace(/\/$/, "");
|
|
868
|
+
this.asToken = opts.asToken;
|
|
869
|
+
this.fetch = opts.fetch ?? globalThis.fetch;
|
|
870
|
+
}
|
|
871
|
+
async download(input) {
|
|
872
|
+
const parsed = parseMxcUri(input.mxcUri);
|
|
873
|
+
if (!parsed) throw new Error(`not an mxc uri: ${input.mxcUri}`);
|
|
874
|
+
const url = `${this.homeserver}/_matrix/client/v1/media/download/${encodeURIComponent(parsed.serverName)}/${encodeURIComponent(parsed.mediaId)}?user_id=${encodeURIComponent(input.asUserId)}`;
|
|
875
|
+
const r = await this.fetch(url, {
|
|
876
|
+
headers: { Authorization: `Bearer ${this.asToken}` }
|
|
877
|
+
});
|
|
878
|
+
if (!r.ok) throw new Error(`media download failed: ${r.status}`);
|
|
879
|
+
const buf = new Uint8Array(await r.arrayBuffer());
|
|
880
|
+
const max = input.maxBytes ?? MAX_DOWNLOAD_BYTES;
|
|
881
|
+
if (buf.byteLength > max) {
|
|
882
|
+
throw new Error(`media too large: ${buf.byteLength} > ${max}`);
|
|
883
|
+
}
|
|
884
|
+
return { data: buf, contentType: r.headers.get("content-type") ?? "application/octet-stream" };
|
|
885
|
+
}
|
|
886
|
+
async upload(input) {
|
|
887
|
+
const params = new URLSearchParams();
|
|
888
|
+
if (input.filename) params.set("filename", input.filename);
|
|
889
|
+
params.set("user_id", input.asUserId);
|
|
890
|
+
const r = await this.fetch(`${this.homeserver}/_matrix/media/v3/upload?${params}`, {
|
|
891
|
+
method: "POST",
|
|
892
|
+
headers: { Authorization: `Bearer ${this.asToken}`, "Content-Type": input.contentType },
|
|
893
|
+
body: input.data
|
|
894
|
+
});
|
|
895
|
+
if (!r.ok) throw new Error(`media upload failed: ${r.status}`);
|
|
896
|
+
return await r.json();
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
// src/attachments.ts
|
|
901
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
902
|
+
import { join } from "path";
|
|
903
|
+
import { posix } from "path";
|
|
904
|
+
function sanitize(s, fallback) {
|
|
905
|
+
const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "").replace(/^\.+/, "");
|
|
906
|
+
return cleaned || fallback;
|
|
907
|
+
}
|
|
908
|
+
function writeAttachment(input) {
|
|
909
|
+
const dir = sanitize(input.eventId, "event");
|
|
910
|
+
const name = sanitize(input.filename, "file");
|
|
911
|
+
const hostDir = join(input.workspaceDir, ".zooid", "attachments", dir);
|
|
912
|
+
mkdirSync(hostDir, { recursive: true });
|
|
913
|
+
const hostPath = join(hostDir, name);
|
|
914
|
+
writeFileSync(hostPath, input.data);
|
|
915
|
+
const agentPath = posix.join(input.agentWorkspacePath, ".zooid", "attachments", dir, name);
|
|
916
|
+
return { hostPath, agentPath };
|
|
917
|
+
}
|
|
918
|
+
|
|
807
919
|
// src/transport.ts
|
|
808
920
|
var STARTUP_GRACE_MS = 5e3;
|
|
921
|
+
async function buildMediaBlocks(items, opts) {
|
|
922
|
+
const blocks = [];
|
|
923
|
+
const pathLines = [];
|
|
924
|
+
if (!opts.media || items.length === 0) return { blocks, pathLines };
|
|
925
|
+
for (const item of items) {
|
|
926
|
+
try {
|
|
927
|
+
const isInlineCandidate = item.msgtype === "m.image" && INLINE_IMAGE_MIMES.includes(item.info?.mimetype ?? "") && (item.info?.size === void 0 || item.info.size <= MAX_INLINE_IMAGE_BYTES);
|
|
928
|
+
if (isInlineCandidate) {
|
|
929
|
+
const { data, contentType } = await opts.media.download({
|
|
930
|
+
mxcUri: item.url,
|
|
931
|
+
asUserId: opts.agent.userId
|
|
932
|
+
});
|
|
933
|
+
if (data.byteLength <= MAX_INLINE_IMAGE_BYTES) {
|
|
934
|
+
blocks.push({
|
|
935
|
+
type: "image",
|
|
936
|
+
data: Buffer.from(data).toString("base64"),
|
|
937
|
+
mimeType: contentType
|
|
938
|
+
});
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (opts.agent.workspaceDir) {
|
|
942
|
+
const paths = opts.writeAttachmentFn({
|
|
943
|
+
workspaceDir: opts.agent.workspaceDir,
|
|
944
|
+
agentWorkspacePath: opts.agent.agentWorkspacePath ?? opts.agent.workspaceDir,
|
|
945
|
+
eventId: item.eventId,
|
|
946
|
+
filename: item.filename ?? item.body,
|
|
947
|
+
data
|
|
948
|
+
});
|
|
949
|
+
blocks.push({
|
|
950
|
+
type: "resource_link",
|
|
951
|
+
uri: `file://${paths.agentPath}`,
|
|
952
|
+
name: item.filename ?? item.body
|
|
953
|
+
});
|
|
954
|
+
pathLines.push(`Attached file: ${paths.agentPath}`);
|
|
955
|
+
}
|
|
956
|
+
} else {
|
|
957
|
+
if (!opts.agent.workspaceDir) continue;
|
|
958
|
+
const { data } = await opts.media.download({
|
|
959
|
+
mxcUri: item.url,
|
|
960
|
+
asUserId: opts.agent.userId
|
|
961
|
+
});
|
|
962
|
+
const paths = opts.writeAttachmentFn({
|
|
963
|
+
workspaceDir: opts.agent.workspaceDir,
|
|
964
|
+
agentWorkspacePath: opts.agent.agentWorkspacePath ?? opts.agent.workspaceDir,
|
|
965
|
+
eventId: item.eventId,
|
|
966
|
+
filename: item.filename ?? item.body,
|
|
967
|
+
data
|
|
968
|
+
});
|
|
969
|
+
blocks.push({
|
|
970
|
+
type: "resource_link",
|
|
971
|
+
uri: `file://${paths.agentPath}`,
|
|
972
|
+
name: item.filename ?? item.body,
|
|
973
|
+
mimeType: item.info?.mimetype,
|
|
974
|
+
size: item.info?.size
|
|
975
|
+
});
|
|
976
|
+
pathLines.push(`Attached file: ${paths.agentPath}`);
|
|
977
|
+
}
|
|
978
|
+
} catch (err) {
|
|
979
|
+
opts.onError(item, err);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return { blocks, pathLines };
|
|
983
|
+
}
|
|
984
|
+
async function sendMediaError(ctx, _err, message, client) {
|
|
985
|
+
await client.sendCustomEvent({
|
|
986
|
+
roomId: ctx.roomId,
|
|
987
|
+
asUserId: ctx.agent.userId,
|
|
988
|
+
eventType: "eco.zoon.error",
|
|
989
|
+
content: toErrorBody(
|
|
990
|
+
{
|
|
991
|
+
kind: "error",
|
|
992
|
+
agentId: ctx.agent.name,
|
|
993
|
+
sessionId: null,
|
|
994
|
+
turnId: null,
|
|
995
|
+
code: "media_failed",
|
|
996
|
+
message: message.slice(0, 250),
|
|
997
|
+
transient: false
|
|
998
|
+
},
|
|
999
|
+
ctx.threadRoot
|
|
1000
|
+
)
|
|
1001
|
+
}).catch((e) => console.warn(`[matrix:${ctx.agent.name}] eco.zoon.error send failed:`, e));
|
|
1002
|
+
}
|
|
809
1003
|
var SEEN_EVENT_CAP = 5e3;
|
|
810
1004
|
var DRAIN_QUIET_MS = 300;
|
|
811
1005
|
var DRAIN_MAX_MS = 3e4;
|
|
@@ -818,9 +1012,13 @@ function createMatrixTransport(opts) {
|
|
|
818
1012
|
const { agents, approvals, client, bindings, hsToken, adminUserId } = opts;
|
|
819
1013
|
const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
|
|
820
1014
|
const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
|
|
1015
|
+
const mediaClient = opts.media;
|
|
1016
|
+
const writeAttachmentFn = opts.writeAttachmentFn ?? writeAttachment;
|
|
1017
|
+
const pendingMedia = new PendingMediaStore();
|
|
821
1018
|
const pool = new BotPool(client, bindings);
|
|
822
1019
|
const sessions = /* @__PURE__ */ new Map();
|
|
823
1020
|
const buffers = /* @__PURE__ */ new Map();
|
|
1021
|
+
const bufferMessageIds = /* @__PURE__ */ new Map();
|
|
824
1022
|
const sendQueue = /* @__PURE__ */ new Map();
|
|
825
1023
|
const threadStates = /* @__PURE__ */ new Map();
|
|
826
1024
|
const cutoffTs = Date.now() - STARTUP_GRACE_MS;
|
|
@@ -835,8 +1033,36 @@ function createMatrixTransport(opts) {
|
|
|
835
1033
|
const block = event.content;
|
|
836
1034
|
if (block.type === "text" && typeof block.text === "string") {
|
|
837
1035
|
const current = buffers.get(event.sessionId) ?? "";
|
|
838
|
-
const
|
|
1036
|
+
const prevMessageId = bufferMessageIds.get(event.sessionId);
|
|
1037
|
+
const messageChanged = event.messageId !== void 0 && prevMessageId !== void 0 && event.messageId !== prevMessageId;
|
|
1038
|
+
const needsBreak = current.length > 0 && (block.text === "" || messageChanged);
|
|
1039
|
+
const prefix = needsBreak ? "\n\n" : "";
|
|
839
1040
|
buffers.set(event.sessionId, current + prefix + block.text);
|
|
1041
|
+
if (event.messageId !== void 0)
|
|
1042
|
+
bufferMessageIds.set(event.sessionId, event.messageId);
|
|
1043
|
+
} else if (block.type === "image" && typeof block.data === "string" && typeof block.mimeType === "string" && mediaClient) {
|
|
1044
|
+
const ctx2 = sessions.get(event.sessionId);
|
|
1045
|
+
if (ctx2) {
|
|
1046
|
+
const bytes = Buffer.from(block.data, "base64");
|
|
1047
|
+
const ext = (block.mimeType.split("/")[1] ?? "png").replace(/[^a-z0-9]/gi, "");
|
|
1048
|
+
const filename = `image.${ext}`;
|
|
1049
|
+
void mediaClient.upload({ data: bytes, contentType: block.mimeType, filename, asUserId: ctx2.agent.userId }).then(
|
|
1050
|
+
({ content_uri }) => client.sendMessage({
|
|
1051
|
+
roomId: ctx2.roomId,
|
|
1052
|
+
asUserId: ctx2.agent.userId,
|
|
1053
|
+
threadRoot: ctx2.threadRoot,
|
|
1054
|
+
content: {
|
|
1055
|
+
msgtype: "m.image",
|
|
1056
|
+
body: filename,
|
|
1057
|
+
url: content_uri,
|
|
1058
|
+
info: { mimetype: block.mimeType, size: bytes.length }
|
|
1059
|
+
}
|
|
1060
|
+
})
|
|
1061
|
+
).catch((err) => {
|
|
1062
|
+
console.warn(`[matrix:${name}] outbound image upload failed:`, err);
|
|
1063
|
+
void sendMediaError(ctx2, err, "agent image upload failed", client);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
840
1066
|
} else {
|
|
841
1067
|
console.warn(`[matrix:${name}] dropped chunk block type=${block.type}`, block);
|
|
842
1068
|
}
|
|
@@ -979,6 +1205,18 @@ function createMatrixTransport(opts) {
|
|
|
979
1205
|
continue;
|
|
980
1206
|
}
|
|
981
1207
|
logInbound(evt);
|
|
1208
|
+
if (evt.type === "m.room.message" && isMediaMsgtype(evt.content?.msgtype) && evt.room_id && evt.event_id && evt.sender && evt.content?.url && !bindings.some((b) => b.userId === evt.sender)) {
|
|
1209
|
+
pendingMedia.add(evt.room_id, inboundThreadRoot2(evt), {
|
|
1210
|
+
eventId: evt.event_id,
|
|
1211
|
+
sender: evt.sender,
|
|
1212
|
+
msgtype: evt.content.msgtype,
|
|
1213
|
+
body: evt.content.body ?? "",
|
|
1214
|
+
filename: evt.content.filename,
|
|
1215
|
+
url: evt.content.url,
|
|
1216
|
+
info: evt.content.info
|
|
1217
|
+
});
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
982
1220
|
const promotedRoot = inboundThreadRoot2(evt) ?? evt.event_id;
|
|
983
1221
|
const inboundRel = inboundThreadRoot2(evt);
|
|
984
1222
|
if (evt.type === "m.room.message" && inboundRel && !threadStates.has(inboundRel) && evt.room_id) {
|
|
@@ -1079,6 +1317,7 @@ function createMatrixTransport(opts) {
|
|
|
1079
1317
|
const sessionId = await agents.ensureSession(agent.name, sessionKey, evt.room_id);
|
|
1080
1318
|
sessions.set(sessionId, { agent, roomId: evt.room_id, threadRoot });
|
|
1081
1319
|
buffers.set(sessionId, "");
|
|
1320
|
+
bufferMessageIds.delete(sessionId);
|
|
1082
1321
|
const roomId = evt.room_id;
|
|
1083
1322
|
const TYPING_TTL_MS = 3e4;
|
|
1084
1323
|
const TYPING_REFRESH_MS = 25e3;
|
|
@@ -1094,10 +1333,30 @@ function createMatrixTransport(opts) {
|
|
|
1094
1333
|
try {
|
|
1095
1334
|
const rawBody = evt.content?.body ?? "";
|
|
1096
1335
|
const promptText = stripMention(rawBody, agent.userId);
|
|
1336
|
+
const pendingItems = pendingMedia.drain(
|
|
1337
|
+
evt.room_id,
|
|
1338
|
+
inboundThreadRoot2(evt),
|
|
1339
|
+
evt.sender ?? ""
|
|
1340
|
+
);
|
|
1341
|
+
const { blocks, pathLines } = await buildMediaBlocks(pendingItems, {
|
|
1342
|
+
agent,
|
|
1343
|
+
media: mediaClient,
|
|
1344
|
+
writeAttachmentFn,
|
|
1345
|
+
onError: (item, err) => {
|
|
1346
|
+
console.warn(`[matrix:${agent.name}] media_failed for ${item.body}:`, err);
|
|
1347
|
+
void sendMediaError(
|
|
1348
|
+
{ agent, roomId: evt.room_id, threadRoot },
|
|
1349
|
+
err,
|
|
1350
|
+
`Could not process attachment: ${item.body}`,
|
|
1351
|
+
client
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
const fullPromptText = [promptText, ...pathLines].filter(Boolean).join("\n");
|
|
1097
1356
|
await agents.prompt(agent.name, {
|
|
1098
1357
|
threadId: sessionKey,
|
|
1099
1358
|
channelId: evt.room_id,
|
|
1100
|
-
content: [{ type: "text", text:
|
|
1359
|
+
content: [...blocks, { type: "text", text: fullPromptText }]
|
|
1101
1360
|
});
|
|
1102
1361
|
const drainStart = Date.now();
|
|
1103
1362
|
let drained = buffers.get(sessionId) ?? "";
|
|
@@ -1139,6 +1398,7 @@ function createMatrixTransport(opts) {
|
|
|
1139
1398
|
await safeTyping(false);
|
|
1140
1399
|
await safePresence("online");
|
|
1141
1400
|
buffers.delete(sessionId);
|
|
1401
|
+
bufferMessageIds.delete(sessionId);
|
|
1142
1402
|
}
|
|
1143
1403
|
}
|
|
1144
1404
|
return {
|
|
@@ -1241,17 +1501,27 @@ async function startWorkforcePublisher(opts) {
|
|
|
1241
1501
|
}
|
|
1242
1502
|
export {
|
|
1243
1503
|
BotPool,
|
|
1504
|
+
INLINE_IMAGE_MIMES,
|
|
1505
|
+
MAX_DOWNLOAD_BYTES,
|
|
1506
|
+
MAX_INLINE_IMAGE_BYTES,
|
|
1507
|
+
MAX_MEDIA_PER_TURN,
|
|
1508
|
+
MEDIA_MSGTYPES,
|
|
1244
1509
|
MatrixClient,
|
|
1245
1510
|
MatrixContextProvider,
|
|
1511
|
+
MediaClient,
|
|
1512
|
+
PendingMediaStore,
|
|
1246
1513
|
buildWorkforceRoster,
|
|
1247
1514
|
createMatrixTransport,
|
|
1248
1515
|
ensureDefaultChannel,
|
|
1249
1516
|
ensureWorkforceSpace,
|
|
1250
1517
|
extractMentions,
|
|
1518
|
+
isMediaMsgtype,
|
|
1519
|
+
parseMxcUri,
|
|
1251
1520
|
publishWorkforce,
|
|
1252
1521
|
renderRegistration,
|
|
1253
1522
|
route,
|
|
1254
1523
|
serverNameFromMxid,
|
|
1255
|
-
startWorkforcePublisher
|
|
1524
|
+
startWorkforcePublisher,
|
|
1525
|
+
writeAttachment
|
|
1256
1526
|
};
|
|
1257
1527
|
//# sourceMappingURL=index.js.map
|