@zooid/transport-matrix 0.7.4 → 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 +271 -9
- 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 +242 -0
- package/src/transport.ts +227 -4
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,6 +1012,9 @@ 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();
|
|
@@ -843,6 +1040,29 @@ function createMatrixTransport(opts) {
|
|
|
843
1040
|
buffers.set(event.sessionId, current + prefix + block.text);
|
|
844
1041
|
if (event.messageId !== void 0)
|
|
845
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
|
+
}
|
|
846
1066
|
} else {
|
|
847
1067
|
console.warn(`[matrix:${name}] dropped chunk block type=${block.type}`, block);
|
|
848
1068
|
}
|
|
@@ -985,6 +1205,18 @@ function createMatrixTransport(opts) {
|
|
|
985
1205
|
continue;
|
|
986
1206
|
}
|
|
987
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
|
+
}
|
|
988
1220
|
const promotedRoot = inboundThreadRoot2(evt) ?? evt.event_id;
|
|
989
1221
|
const inboundRel = inboundThreadRoot2(evt);
|
|
990
1222
|
if (evt.type === "m.room.message" && inboundRel && !threadStates.has(inboundRel) && evt.room_id) {
|
|
@@ -1101,10 +1333,30 @@ function createMatrixTransport(opts) {
|
|
|
1101
1333
|
try {
|
|
1102
1334
|
const rawBody = evt.content?.body ?? "";
|
|
1103
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");
|
|
1104
1356
|
await agents.prompt(agent.name, {
|
|
1105
1357
|
threadId: sessionKey,
|
|
1106
1358
|
channelId: evt.room_id,
|
|
1107
|
-
content: [{ type: "text", text:
|
|
1359
|
+
content: [...blocks, { type: "text", text: fullPromptText }]
|
|
1108
1360
|
});
|
|
1109
1361
|
const drainStart = Date.now();
|
|
1110
1362
|
let drained = buffers.get(sessionId) ?? "";
|
|
@@ -1249,17 +1501,27 @@ async function startWorkforcePublisher(opts) {
|
|
|
1249
1501
|
}
|
|
1250
1502
|
export {
|
|
1251
1503
|
BotPool,
|
|
1504
|
+
INLINE_IMAGE_MIMES,
|
|
1505
|
+
MAX_DOWNLOAD_BYTES,
|
|
1506
|
+
MAX_INLINE_IMAGE_BYTES,
|
|
1507
|
+
MAX_MEDIA_PER_TURN,
|
|
1508
|
+
MEDIA_MSGTYPES,
|
|
1252
1509
|
MatrixClient,
|
|
1253
1510
|
MatrixContextProvider,
|
|
1511
|
+
MediaClient,
|
|
1512
|
+
PendingMediaStore,
|
|
1254
1513
|
buildWorkforceRoster,
|
|
1255
1514
|
createMatrixTransport,
|
|
1256
1515
|
ensureDefaultChannel,
|
|
1257
1516
|
ensureWorkforceSpace,
|
|
1258
1517
|
extractMentions,
|
|
1518
|
+
isMediaMsgtype,
|
|
1519
|
+
parseMxcUri,
|
|
1259
1520
|
publishWorkforce,
|
|
1260
1521
|
renderRegistration,
|
|
1261
1522
|
route,
|
|
1262
1523
|
serverNameFromMxid,
|
|
1263
|
-
startWorkforcePublisher
|
|
1524
|
+
startWorkforcePublisher,
|
|
1525
|
+
writeAttachment
|
|
1264
1526
|
};
|
|
1265
1527
|
//# sourceMappingURL=index.js.map
|