@etazio/agent-sdk 0.1.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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/index.js +1752 -0
- package/index.js.map +6 -0
- package/package.json +51 -0
- package/types/audio.d.ts +86 -0
- package/types/index.d.ts +168 -0
- package/types/shared/agents.d.ts +689 -0
- package/types/shared/avatar.d.ts +137 -0
- package/types/shared/board.d.ts +124 -0
- package/types/shared/calendar.d.ts +64 -0
- package/types/shared/chat.d.ts +161 -0
- package/types/shared/dto.d.ts +235 -0
- package/types/shared/errors.d.ts +15 -0
- package/types/shared/events.d.ts +391 -0
- package/types/shared/generator.d.ts +317 -0
- package/types/shared/images.d.ts +79 -0
- package/types/shared/index.d.ts +17 -0
- package/types/shared/markdown.d.ts +76 -0
- package/types/shared/roles.d.ts +29 -0
- package/types/shared/schemas.d.ts +251 -0
- package/types/shared/sso.d.ts +124 -0
- package/types/shared/tv.d.ts +121 -0
- package/types/shared/view.d.ts +103 -0
- package/types/shared/web.d.ts +297 -0
- package/types/speech.d.ts +39 -0
- package/types/version.d.ts +2 -0
- package/types/voice.d.ts +61 -0
package/index.js
ADDED
|
@@ -0,0 +1,1752 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { io } from "socket.io-client";
|
|
4
|
+
|
|
5
|
+
// ../shared/src/roles.ts
|
|
6
|
+
var ASSIGNABLE_ROLES = ["admin", "member", "guest"];
|
|
7
|
+
var PERMISSIONS = [
|
|
8
|
+
"workspace.delete",
|
|
9
|
+
"workspace.transfer",
|
|
10
|
+
"workspace.update",
|
|
11
|
+
"members.read",
|
|
12
|
+
"members.manage",
|
|
13
|
+
"invitations.manage",
|
|
14
|
+
"office.edit",
|
|
15
|
+
"office.publish",
|
|
16
|
+
"desk.assign",
|
|
17
|
+
"desk.editOwn",
|
|
18
|
+
/** Freien Schreibtisch selbst sichern und eigenen Platz freigeben (SPA-125) – nur wenn der Workspace es erlaubt */
|
|
19
|
+
"desk.claim",
|
|
20
|
+
"avatar.edit",
|
|
21
|
+
"meeting.join",
|
|
22
|
+
"audit.read",
|
|
23
|
+
/** KI-Agenten sehen (SPA-66) */
|
|
24
|
+
"agents.read",
|
|
25
|
+
/** KI-Agenten anlegen, Scopes setzen, Token rotieren, abschalten (SPA-66) */
|
|
26
|
+
"agents.manage"
|
|
27
|
+
];
|
|
28
|
+
var MATRIX = {
|
|
29
|
+
owner: new Set(PERMISSIONS),
|
|
30
|
+
admin: /* @__PURE__ */ new Set([
|
|
31
|
+
"workspace.update",
|
|
32
|
+
"members.read",
|
|
33
|
+
"members.manage",
|
|
34
|
+
"invitations.manage",
|
|
35
|
+
"office.edit",
|
|
36
|
+
"office.publish",
|
|
37
|
+
"desk.assign",
|
|
38
|
+
"desk.editOwn",
|
|
39
|
+
"desk.claim",
|
|
40
|
+
"avatar.edit",
|
|
41
|
+
"meeting.join",
|
|
42
|
+
"audit.read",
|
|
43
|
+
"agents.read",
|
|
44
|
+
"agents.manage"
|
|
45
|
+
]),
|
|
46
|
+
member: /* @__PURE__ */ new Set(["members.read", "desk.editOwn", "desk.claim", "avatar.edit", "meeting.join", "agents.read"]),
|
|
47
|
+
guest: /* @__PURE__ */ new Set(["meeting.join"])
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ../shared/src/chat.ts
|
|
51
|
+
import { z } from "zod";
|
|
52
|
+
var CHAT_GLYPH_LIMITS = {
|
|
53
|
+
/** Eine Reaktion ist genau ein sichtbares Zeichen – Skin-Tone und ZWJ-Sequenz eingeschlossen */
|
|
54
|
+
maxClusters: 1,
|
|
55
|
+
/** Harte Klammer über die Rohlänge: eine Familien-Sequenz mit Skin-Tones ist lang, aber nicht beliebig */
|
|
56
|
+
maxChars: 80
|
|
57
|
+
};
|
|
58
|
+
var segmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter("de", { granularity: "grapheme" }) : null;
|
|
59
|
+
function graphemeCount(s) {
|
|
60
|
+
if (segmenter) {
|
|
61
|
+
let n = 0;
|
|
62
|
+
for (const _ of segmenter.segment(s)) n++;
|
|
63
|
+
return n;
|
|
64
|
+
}
|
|
65
|
+
return [...s.replace(new RegExp("(?:\\u{FE0F}|\\u{200D}\\p{Extended_Pictographic}|\\p{Emoji_Modifier}|[\\u{1F3FB}-\\u{1F3FF}]|[\\u{E0020}-\\u{E007F}])", "gu"), "")].length || (s ? 1 : 0);
|
|
66
|
+
}
|
|
67
|
+
var EMOJI_ONLY = new RegExp("^(?:\\p{Extended_Pictographic}|\\p{Emoji_Component}|\\p{Regional_Indicator}|[\\u{200D}\\u{FE0F}\\u{20E3}#*0-9])+$", "u");
|
|
68
|
+
function isEmojiGlyph(s) {
|
|
69
|
+
if (!s || s.length > CHAT_GLYPH_LIMITS.maxChars) return false;
|
|
70
|
+
if (!EMOJI_ONLY.test(s)) return false;
|
|
71
|
+
if (/^[#*0-9]+$/.test(s)) return false;
|
|
72
|
+
return graphemeCount(s) <= CHAT_GLYPH_LIMITS.maxClusters;
|
|
73
|
+
}
|
|
74
|
+
var glyphSchema = z.string().min(1).max(CHAT_GLYPH_LIMITS.maxChars).refine(isEmojiGlyph, { message: "Nur ein einzelnes Emoji" });
|
|
75
|
+
var CHAT_BLOCK_LIMITS = {
|
|
76
|
+
/** Blöcke je Nachricht */
|
|
77
|
+
maxBlocks: 3,
|
|
78
|
+
maxTitle: 120,
|
|
79
|
+
maxDescription: 600,
|
|
80
|
+
maxFields: 10,
|
|
81
|
+
maxFieldLabel: 60,
|
|
82
|
+
maxFieldValue: 240,
|
|
83
|
+
maxActions: 4,
|
|
84
|
+
maxActionLabel: 40,
|
|
85
|
+
/** Weiter als das darf ein Ablaufzeitpunkt nicht in der Zukunft liegen */
|
|
86
|
+
maxTtlMs: 7 * 24 * 60 * 60 * 1e3
|
|
87
|
+
};
|
|
88
|
+
var CHAT_ACTION_STYLES = ["primary", "secondary", "danger"];
|
|
89
|
+
var blockIdSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/, "Ung\xFCltige Block-ID");
|
|
90
|
+
var actionIdSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}$/, "Ung\xFCltige Aktions-ID");
|
|
91
|
+
var blockFieldSchema = z.object({
|
|
92
|
+
label: z.string().trim().min(1).max(CHAT_BLOCK_LIMITS.maxFieldLabel),
|
|
93
|
+
value: z.string().trim().min(1).max(CHAT_BLOCK_LIMITS.maxFieldValue)
|
|
94
|
+
});
|
|
95
|
+
var blockActionSchema = z.object({
|
|
96
|
+
id: actionIdSchema,
|
|
97
|
+
label: z.string().trim().min(1).max(CHAT_BLOCK_LIMITS.maxActionLabel),
|
|
98
|
+
style: z.enum(CHAT_ACTION_STYLES).default("secondary")
|
|
99
|
+
});
|
|
100
|
+
var confirmBlockInputSchema = z.object({
|
|
101
|
+
type: z.literal("confirm"),
|
|
102
|
+
id: blockIdSchema,
|
|
103
|
+
title: z.string().trim().min(1).max(CHAT_BLOCK_LIMITS.maxTitle),
|
|
104
|
+
description: z.string().trim().max(CHAT_BLOCK_LIMITS.maxDescription).nullish(),
|
|
105
|
+
fields: z.array(blockFieldSchema).max(CHAT_BLOCK_LIMITS.maxFields).default([]),
|
|
106
|
+
actions: z.array(blockActionSchema).min(1).max(CHAT_BLOCK_LIMITS.maxActions),
|
|
107
|
+
expiresAt: z.string().datetime().nullish()
|
|
108
|
+
}).refine((b) => new Set(b.actions.map((a) => a.id)).size === b.actions.length, { message: "Aktions-IDs m\xFCssen eindeutig sein" });
|
|
109
|
+
var chatBlockInputSchema = z.discriminatedUnion("type", [confirmBlockInputSchema]);
|
|
110
|
+
var chatBlocksInputSchema = z.array(chatBlockInputSchema).max(CHAT_BLOCK_LIMITS.maxBlocks).refine((bs) => new Set(bs.map((b) => b.id)).size === bs.length, { message: "Block-IDs m\xFCssen eindeutig sein" });
|
|
111
|
+
var chatActionSchema = z.object({
|
|
112
|
+
blockId: blockIdSchema,
|
|
113
|
+
actionId: actionIdSchema
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ../shared/src/schemas.ts
|
|
117
|
+
import { z as z2 } from "zod";
|
|
118
|
+
var emailSchema = z2.string().trim().toLowerCase().email().max(254);
|
|
119
|
+
var passwordSchema = z2.string().min(10, "Mindestens 10 Zeichen").max(200);
|
|
120
|
+
var displayNameSchema = z2.string().trim().min(1).max(60);
|
|
121
|
+
var registerSchema = z2.object({
|
|
122
|
+
email: emailSchema,
|
|
123
|
+
password: passwordSchema,
|
|
124
|
+
displayName: displayNameSchema
|
|
125
|
+
});
|
|
126
|
+
var loginSchema = z2.object({
|
|
127
|
+
email: emailSchema,
|
|
128
|
+
password: z2.string().min(1).max(200)
|
|
129
|
+
});
|
|
130
|
+
var magicLinkRequestSchema = z2.object({ email: emailSchema });
|
|
131
|
+
var magicLinkConsumeSchema = z2.object({ token: z2.string().min(20).max(200) });
|
|
132
|
+
var slugSchema = z2.string().trim().toLowerCase().regex(/^[a-z0-9](?:[a-z0-9-]{1,38}[a-z0-9])?$/, "Nur Kleinbuchstaben, Ziffern und Bindestriche (3\u201340 Zeichen)");
|
|
133
|
+
var createWorkspaceSchema = z2.object({
|
|
134
|
+
name: z2.string().trim().min(2).max(80),
|
|
135
|
+
slug: slugSchema.optional(),
|
|
136
|
+
/** Start-Office: leer (Default-Office) oder eine Generator-Vorlage (ID aus OFFICE_TEMPLATES) */
|
|
137
|
+
template: z2.string().regex(/^[a-z0-9_]{1,40}$/).optional()
|
|
138
|
+
});
|
|
139
|
+
var updateWorkspaceSchema = z2.object({
|
|
140
|
+
name: z2.string().trim().min(2).max(80).optional(),
|
|
141
|
+
/** Mitglieder dürfen freie Schreibtische selbst sichern (SPA-125) */
|
|
142
|
+
deskSelfClaim: z2.boolean().optional()
|
|
143
|
+
}).refine((v) => v.name !== void 0 || v.deskSelfClaim !== void 0, { message: "Keine \xC4nderung angegeben" });
|
|
144
|
+
var assignableRoleSchema = z2.enum(ASSIGNABLE_ROLES);
|
|
145
|
+
var createInvitationSchema = z2.object({
|
|
146
|
+
email: emailSchema,
|
|
147
|
+
role: assignableRoleSchema.default("member")
|
|
148
|
+
});
|
|
149
|
+
var acceptInvitationSchema = z2.object({ token: z2.string().min(20).max(200) });
|
|
150
|
+
var updateMemberRoleSchema = z2.object({ role: assignableRoleSchema });
|
|
151
|
+
var transferOwnershipSchema = z2.object({ userId: z2.string().min(1) });
|
|
152
|
+
var objectIdSchema = z2.string().regex(/^[a-f0-9]{24}$/, "Ung\xFCltige ID");
|
|
153
|
+
var worldCoord = z2.number().finite().min(-1e3).max(1e3);
|
|
154
|
+
var worldMoveSchema = z2.object({
|
|
155
|
+
seq: z2.number().int().min(0).max(1e12),
|
|
156
|
+
x: worldCoord,
|
|
157
|
+
z: worldCoord,
|
|
158
|
+
rotY: z2.number().finite(),
|
|
159
|
+
anim: z2.enum(["idle", "walk", "run"]),
|
|
160
|
+
t: z2.number().finite().optional()
|
|
161
|
+
});
|
|
162
|
+
var worldJoinSchema = z2.object({
|
|
163
|
+
workspaceId: objectIdSchema,
|
|
164
|
+
participantId: z2.string().regex(/^[a-zA-Z0-9_-]{8,64}$/).optional()
|
|
165
|
+
});
|
|
166
|
+
var worldSitSchema = z2.object({ seatId: z2.string().regex(/^[a-zA-Z0-9_-]{1,64}(:[a-z0-9_]{1,32})?$/) });
|
|
167
|
+
var opIdSchema = z2.string().regex(/^[a-zA-Z0-9_-]{8,64}$/);
|
|
168
|
+
var applyOpEnvelopeSchema = z2.object({
|
|
169
|
+
baseVersion: z2.number().int().min(0),
|
|
170
|
+
opId: opIdSchema,
|
|
171
|
+
op: z2.unknown()
|
|
172
|
+
});
|
|
173
|
+
var publishSchema = z2.object({
|
|
174
|
+
baseVersion: z2.number().int().min(0).optional(),
|
|
175
|
+
note: z2.string().trim().max(200).optional()
|
|
176
|
+
});
|
|
177
|
+
var lockSchema = z2.object({ force: z2.boolean().optional() });
|
|
178
|
+
var deskAssignmentSchema = z2.object({ userId: objectIdSchema.nullable() });
|
|
179
|
+
var deskIdSchema = z2.string().regex(/^[a-zA-Z0-9_-]{1,64}$/);
|
|
180
|
+
var DESK_LIMITS = {
|
|
181
|
+
/** Gegenstände je Schreibtisch (Objektbudget, Roadmap §6) */
|
|
182
|
+
maxItems: 12,
|
|
183
|
+
/** Versatz auf der Ankerebene in Metern (je Achse; `DESK_ANCHOR_MAX_OFFSET` in scene-schema) */
|
|
184
|
+
maxOffset: 0.15,
|
|
185
|
+
/** Obergrenze für die lokale Position frei abgelegter Gegenstände (`anchor: 'surface'`, SPA-95) – die Tischplatte begrenzt enger */
|
|
186
|
+
maxSurfaceOffset: 1.5,
|
|
187
|
+
maxLabel: 40
|
|
188
|
+
};
|
|
189
|
+
var materialOverrideDtoSchema = z2.object({
|
|
190
|
+
color: z2.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
|
|
191
|
+
roughness: z2.number().min(0).max(1).optional(),
|
|
192
|
+
metalness: z2.number().min(0).max(1).optional()
|
|
193
|
+
});
|
|
194
|
+
var overridesSchema = z2.record(z2.string().max(64), materialOverrideDtoSchema);
|
|
195
|
+
var offsetCoord = z2.number().finite().min(-DESK_LIMITS.maxSurfaceOffset).max(DESK_LIMITS.maxSurfaceOffset);
|
|
196
|
+
var deskItemSchema = z2.object({
|
|
197
|
+
id: z2.string().regex(/^[a-zA-Z0-9_-]{1,64}$/),
|
|
198
|
+
asset: z2.string().regex(/^[a-zA-Z0-9_-]{1,64}$/),
|
|
199
|
+
/** Ankername oder `surface` = frei auf der Tischplatte (SPA-95) */
|
|
200
|
+
anchor: z2.string().regex(/^[a-z0-9_]{1,32}$/),
|
|
201
|
+
offset: z2.tuple([offsetCoord, offsetCoord]).default([0, 0]),
|
|
202
|
+
rotationY: z2.number().finite().min(-360).max(360).default(0),
|
|
203
|
+
overrides: overridesSchema.optional()
|
|
204
|
+
});
|
|
205
|
+
var deskCustomizationInputSchema = z2.object({
|
|
206
|
+
label: z2.string().trim().max(DESK_LIMITS.maxLabel).nullable().default(null),
|
|
207
|
+
items: z2.array(deskItemSchema).max(DESK_LIMITS.maxItems).default([]),
|
|
208
|
+
overrides: overridesSchema.default({}),
|
|
209
|
+
chair: overridesSchema.default({})
|
|
210
|
+
});
|
|
211
|
+
var inventoryItemIdSchema = z2.string().regex(/^[a-zA-Z0-9_-]{1,64}$/);
|
|
212
|
+
var zoneIdSchema = z2.string().regex(/^[a-zA-Z0-9_-]{1,64}$/);
|
|
213
|
+
var meetingTokenSchema = z2.object({ target: z2.union([z2.literal("office"), zoneIdSchema]) });
|
|
214
|
+
var zoneAccessInputSchema = z2.object({ userIds: z2.array(objectIdSchema).max(200) });
|
|
215
|
+
var mediaStateSchema = z2.object({ mic: z2.boolean(), cam: z2.boolean(), screen: z2.boolean(), presence: z2.enum(["frei", "fokus", "weg"]).default("frei") });
|
|
216
|
+
var zoneKnockSchema = z2.object({ zoneId: zoneIdSchema });
|
|
217
|
+
var zoneAdmitSchema = z2.object({ zoneId: zoneIdSchema, userId: objectIdSchema, allow: z2.boolean() });
|
|
218
|
+
var CHAT_LIMITS = { maxText: 2e3, pageSize: 50 };
|
|
219
|
+
var convSchema = z2.string().regex(/^(room:(office|[a-zA-Z0-9_-]{1,64})|dm:[a-f0-9]{24}:[a-f0-9]{24})$/);
|
|
220
|
+
var sendMessageSchema = z2.object({
|
|
221
|
+
conv: convSchema,
|
|
222
|
+
text: z2.string().trim().min(1).max(CHAT_LIMITS.maxText),
|
|
223
|
+
parentId: objectIdSchema.optional(),
|
|
224
|
+
/** Thread-Antwort zusätzlich im Kanal zeigen (Slack-Verhalten) – nur zusammen mit `parentId` */
|
|
225
|
+
alsoToChannel: z2.boolean().optional(),
|
|
226
|
+
/** Strukturierte Blöcke; nur Agenten und der Server dürfen sie setzen (Prüfung im Chat-Dienst) */
|
|
227
|
+
blocks: chatBlocksInputSchema.optional()
|
|
228
|
+
}).refine((v) => !v.alsoToChannel || !!v.parentId, { message: "Nur Thread-Antworten k\xF6nnen zus\xE4tzlich in den Kanal" });
|
|
229
|
+
var reactionSchema = z2.object({ glyph: glyphSchema });
|
|
230
|
+
var chatTypingSchema = z2.object({ conv: convSchema, parentId: objectIdSchema.nullish(), typing: z2.boolean() });
|
|
231
|
+
var readSchema = z2.object({ conv: convSchema });
|
|
232
|
+
var messagesQuerySchema = z2.object({
|
|
233
|
+
conv: convSchema,
|
|
234
|
+
before: z2.string().datetime().optional(),
|
|
235
|
+
parentId: objectIdSchema.optional(),
|
|
236
|
+
limit: z2.coerce.number().int().min(1).max(200).default(CHAT_LIMITS.pageSize)
|
|
237
|
+
});
|
|
238
|
+
var OBJECT_LIMITS = {
|
|
239
|
+
/** Objektzustand als JSON, Bytes */
|
|
240
|
+
maxStateBytes: 2048,
|
|
241
|
+
/** Zustandsänderungen je Teilnehmer und Sekunde */
|
|
242
|
+
statePerSecond: 5,
|
|
243
|
+
/** Aktionen je Teilnehmer und Sekunde */
|
|
244
|
+
actionsPerSecond: 3,
|
|
245
|
+
/** Reaktionen (Glyph über dem Kopf) je Sekunde */
|
|
246
|
+
reactPerSecond: 1
|
|
247
|
+
};
|
|
248
|
+
var stateValueSchema = z2.union([z2.string().max(1024), z2.number().finite(), z2.boolean(), z2.null()]);
|
|
249
|
+
var worldObjectIdSchema = z2.string().regex(/^[a-zA-Z0-9_-]{1,64}(:[a-zA-Z0-9_-]{1,64})?$/);
|
|
250
|
+
var objectStateSchema = z2.object({
|
|
251
|
+
placementId: worldObjectIdSchema,
|
|
252
|
+
state: z2.record(z2.string().max(32), stateValueSchema).refine((s) => JSON.stringify(s).length <= OBJECT_LIMITS.maxStateBytes, { message: "Zustand zu gro\xDF" })
|
|
253
|
+
});
|
|
254
|
+
var objectActionSchema = z2.object({
|
|
255
|
+
placementId: deskIdSchema,
|
|
256
|
+
action: z2.string().regex(/^[a-z][a-z0-9_]{0,31}$/),
|
|
257
|
+
data: z2.record(z2.string().max(32), stateValueSchema).optional()
|
|
258
|
+
});
|
|
259
|
+
var reactSchema = z2.object({ glyph: glyphSchema });
|
|
260
|
+
|
|
261
|
+
// ../shared/src/events.ts
|
|
262
|
+
var SOCKET_EVENTS = {
|
|
263
|
+
// Client → Server
|
|
264
|
+
workspaceJoin: "workspace:join",
|
|
265
|
+
workspaceLeave: "workspace:leave",
|
|
266
|
+
worldJoin: "world:join",
|
|
267
|
+
worldLeave: "world:leave",
|
|
268
|
+
worldMove: "world:move",
|
|
269
|
+
worldSit: "world:sit",
|
|
270
|
+
worldStand: "world:stand",
|
|
271
|
+
/** Winken (Phase 4): Server verteilt world:emote */
|
|
272
|
+
worldWave: "world:wave",
|
|
273
|
+
/** Eigener Medienstatus (Phase 5): Server verteilt world:media */
|
|
274
|
+
worldMedia: "world:media",
|
|
275
|
+
/** An einer privaten Zone klopfen (Phase 5) – Hosts in der Zone bekommen zone:knocked */
|
|
276
|
+
zoneKnock: "zone:knock",
|
|
277
|
+
/** Host lässt einen Klopfenden ein oder lehnt ab (Phase 5) */
|
|
278
|
+
zoneAdmit: "zone:admit",
|
|
279
|
+
/** Reaktion über dem Kopf (Phase 7): Server verteilt world:emote mit Glyph */
|
|
280
|
+
worldReact: "world:react",
|
|
281
|
+
// Phase 7: interaktive Objekte
|
|
282
|
+
/** Objektzustand setzen (Client → Server mit Ack) und verteilen (Server → Client), Payload: ObjectStatePayload */
|
|
283
|
+
objectState: "object:state",
|
|
284
|
+
/** Einmalige Aktion an einem Objekt (Client → Server mit Ack) und verteilen (Server → Client), Payload: ObjectActionPayload */
|
|
285
|
+
objectAction: "object:action",
|
|
286
|
+
/** Whiteboard-Operation (Client → Server mit Ack {version}) und verteilen (Server → Client), Payload: BoardOpPayload */
|
|
287
|
+
boardOp: "board:op",
|
|
288
|
+
/** Whiteboard laden (Client → Server mit Ack BoardSyncPayload) */
|
|
289
|
+
boardSync: "board:sync",
|
|
290
|
+
/** Zeigerposition auf einem Whiteboard (flüchtig, ohne Ack, beide Richtungen), Payload: BoardCursorPayload */
|
|
291
|
+
boardCursor: "board:cursor",
|
|
292
|
+
// Phase 7f: Web-Anzeige am Fernseher (Server-Browser)
|
|
293
|
+
/** Web-Anzeige eines TVs abonnieren/abbestellen (Client → Server mit Ack WebStatusPayload), Payload: WebWatchPayload */
|
|
294
|
+
webWatch: "web:watch",
|
|
295
|
+
/** Steuerung übernehmen/abgeben (Client → Server mit Ack WebControlPayload), Payload: { placementId, take } */
|
|
296
|
+
webControl: "web:control",
|
|
297
|
+
/** Eingabe an den Server-Browser (Client → Server, ohne Ack), Payload: { placementId, event } */
|
|
298
|
+
webInput: "web:input",
|
|
299
|
+
/** Navigation im Server-Browser (Client → Server mit Ack), Payload: { placementId, action, url? } */
|
|
300
|
+
webNavigate: "web:navigate",
|
|
301
|
+
/** Sichtfeld des Server-Browsers an die Anzeigegröße des Steuernden angleichen (Client → Server mit Ack), Payload: WebResizePayload */
|
|
302
|
+
webResize: "web:resize",
|
|
303
|
+
// SPA-66: KI-Agenten
|
|
304
|
+
/** Handshake eines Agenten (Agent → Server mit Ack AgentHelloResult), Payload: AgentHelloInput */
|
|
305
|
+
agentHello: "agent:hello",
|
|
306
|
+
/** Laufbefehl mit serverseitiger Wegfindung (Agent → Server mit Ack), Payload: AgentGoToInput */
|
|
307
|
+
agentGoTo: "agent:goTo",
|
|
308
|
+
/** LiveKit-Token für den Raum, in dem der Agent steht (Agent → Server mit Ack MeetingTokenDto) */
|
|
309
|
+
agentMeetingToken: "agent:meetingToken",
|
|
310
|
+
/** Mensch spricht einen Agenten an (Browser → Server mit Ack), Payload: agentInvokeSchema */
|
|
311
|
+
agentInvoke: "agent:invoke",
|
|
312
|
+
// SPA-83: Sprachschleife
|
|
313
|
+
/** Der Agent meldet, was er gerade tut (Agent → Server mit Ack), Payload: agentVoiceStateSchema */
|
|
314
|
+
agentVoiceState: "agent:voice:state",
|
|
315
|
+
/** Mensch beendet das Sprachgespräch (Browser → Server mit Ack), Payload: voiceCloseSchema */
|
|
316
|
+
voiceClose: "voice:close",
|
|
317
|
+
/** Chat-Nachricht senden (Client → Server mit Ack ChatMessageDto), Payload: sendMessageSchema – der Weg für Agenten */
|
|
318
|
+
chatSend: "chat:send",
|
|
319
|
+
/** „Schreibt gerade“ melden (Client → Server, ohne Ack), Payload: chatTypingSchema – Menschen und Agenten (SPA-133) */
|
|
320
|
+
chatTyping: "chat:typing",
|
|
321
|
+
// Server → Client
|
|
322
|
+
presenceJoined: "presence:joined",
|
|
323
|
+
presenceLeft: "presence:left",
|
|
324
|
+
sessionRevoked: "session:revoked",
|
|
325
|
+
worldTick: "world:tick",
|
|
326
|
+
worldParticipantJoined: "world:participantJoined",
|
|
327
|
+
worldParticipantLeft: "world:participantLeft",
|
|
328
|
+
worldPresence: "world:presence",
|
|
329
|
+
/** Dieselbe Person ist woanders beigetreten – diese Verbindung wird abgelöst (SPA-104) */
|
|
330
|
+
worldSuperseded: "world:superseded",
|
|
331
|
+
worldSeat: "world:seat",
|
|
332
|
+
worldCorrect: "world:correct",
|
|
333
|
+
/** Neue Office-Revision veröffentlicht (Payload: OfficePublishedPayload) */
|
|
334
|
+
officePublished: "office:published",
|
|
335
|
+
/** Arbeitsplatzzuweisungen geändert (Payload: { assignments: DeskAssignmentDto[] }) */
|
|
336
|
+
officeAssignments: "office:assignments",
|
|
337
|
+
/** Entwurf geändert (Payload: OfficeDraftChangedPayload) – für parallel geöffnete Editoren */
|
|
338
|
+
officeDraftChanged: "office:draftChanged",
|
|
339
|
+
/** Avatar-Profil eines Teilnehmers geändert (Payload: WorldAvatarPayload) */
|
|
340
|
+
worldAvatar: "world:avatar",
|
|
341
|
+
/** Einmalige Geste eines Teilnehmers (Payload: WorldEmotePayload) */
|
|
342
|
+
worldEmote: "world:emote",
|
|
343
|
+
/** Persönliche Einrichtung eines Schreibtischs geändert (Payload: OfficeDeskPayload) */
|
|
344
|
+
officeDesk: "office:desk",
|
|
345
|
+
/** Inventar eines Mitglieds geändert (Payload: { workspaceId, userId }) – Client lädt sein Inventar neu */
|
|
346
|
+
officeInventory: "office:inventory",
|
|
347
|
+
// Phase 5: Zonen, Medien, Chat
|
|
348
|
+
/** Zone eines Teilnehmers geändert (Payload: WorldZonePayload) */
|
|
349
|
+
worldZone: "world:zone",
|
|
350
|
+
/** Es klopft an einer privaten Zone (Payload: ZoneKnockedPayload) – nur an Hosts in der Zone */
|
|
351
|
+
zoneKnocked: "zone:knocked",
|
|
352
|
+
/** Antwort auf das eigene Klopfen (Payload: ZoneAdmittedPayload) */
|
|
353
|
+
zoneAdmitted: "zone:admitted",
|
|
354
|
+
/** Dauerhafte oder sitzungsweite Freigaben einer Zone geändert (Payload: ZoneAccessPayload) */
|
|
355
|
+
zoneAccess: "zone:access",
|
|
356
|
+
/** Neue Chat-Nachricht (Payload: ChatMessagePayload) */
|
|
357
|
+
chatMessage: "chat:message",
|
|
358
|
+
/** Reaktionen einer Nachricht geändert (Payload: ChatReactionPayload) */
|
|
359
|
+
chatReaction: "chat:reaction",
|
|
360
|
+
/** Jemand schreibt gerade bzw. nicht mehr (Payload: ChatTypingPayload) – verfällt beim Empfänger nach CHAT_TYPING_TTL_MS */
|
|
361
|
+
chatTypingUpdate: "chat:typingUpdate",
|
|
362
|
+
/** Der Nutzer wurde aus dem Workspace entfernt (Payload: { workspaceId }) – Client verlässt das Office */
|
|
363
|
+
workspaceRemoved: "workspace:removed",
|
|
364
|
+
/** Workspace-Einstellungen geändert (Payload: WorkspaceSettingsPayload, SPA-125) */
|
|
365
|
+
workspaceSettings: "workspace:settings",
|
|
366
|
+
/** Fortschritt eines KI-Generator-Jobs (Payload: GeneratorProgressPayload) – an den Workspace-Raum */
|
|
367
|
+
generatorProgress: "generator:progress",
|
|
368
|
+
/** Neues Bild der Web-Anzeige (Payload: WebFramePayload) – nur an Abonnenten */
|
|
369
|
+
webFrame: "web:frame",
|
|
370
|
+
/** Zustand der Web-Anzeige (Payload: WebStatusPayload) – Laden, Fehler, Adresse, Steuerung */
|
|
371
|
+
webStatus: "web:status",
|
|
372
|
+
/** Jemand spricht den Agenten an (Payload: AgentAddressPayload) – nur an den betroffenen Agenten */
|
|
373
|
+
agentAddress: "agent:address",
|
|
374
|
+
/** Der Agent wurde gedrosselt (Payload: AgentThrottledPayload) */
|
|
375
|
+
agentThrottled: "agent:throttled",
|
|
376
|
+
/** Token rotiert, Agent deaktiviert oder gelöscht (Payload: AgentRevokedPayload) – Verbindung endet danach */
|
|
377
|
+
agentRevoked: "agent:revoked",
|
|
378
|
+
/** Sprachgespräch eröffnet oder beendet (Payload: AgentVoicePayload) – nur an den betroffenen Agenten (SPA-83) */
|
|
379
|
+
agentVoice: "agent:voice",
|
|
380
|
+
/** Gesprächszustand für die Oberfläche (Payload: VoiceSessionPayload) – an den Workspace-Raum (SPA-83) */
|
|
381
|
+
voiceSession: "voice:session"
|
|
382
|
+
};
|
|
383
|
+
var CHAT_TYPING_REFRESH_MS = 3e3;
|
|
384
|
+
|
|
385
|
+
// ../shared/src/avatar.ts
|
|
386
|
+
import { z as z3 } from "zod";
|
|
387
|
+
var AVATAR_RIG_VERSION = 2;
|
|
388
|
+
var AVATAR_BODIES = ["slim", "standard", "broad"];
|
|
389
|
+
var AVATAR_HEIGHTS = ["short", "medium", "tall"];
|
|
390
|
+
var AVATAR_HAIRS = ["none", "short", "long", "bun"];
|
|
391
|
+
var AVATAR_TOPS = ["tshirt", "longsleeve", "hoodie"];
|
|
392
|
+
var AVATAR_ACCESSORIES = ["glasses", "headphones", "cap"];
|
|
393
|
+
var hex = z3.string().regex(/^#[0-9a-fA-F]{6}$/, "Farbe als #rrggbb");
|
|
394
|
+
var avatarProfileSchema = z3.object({
|
|
395
|
+
rigVersion: z3.literal(AVATAR_RIG_VERSION).default(AVATAR_RIG_VERSION),
|
|
396
|
+
body: z3.enum(AVATAR_BODIES).default("standard"),
|
|
397
|
+
height: z3.enum(AVATAR_HEIGHTS).default("medium"),
|
|
398
|
+
hair: z3.enum(AVATAR_HAIRS).default("short"),
|
|
399
|
+
top: z3.enum(AVATAR_TOPS).default("tshirt"),
|
|
400
|
+
accessories: z3.array(z3.enum(AVATAR_ACCESSORIES)).max(AVATAR_ACCESSORIES.length).default([]),
|
|
401
|
+
colors: z3.object({
|
|
402
|
+
skin: hex.default("#deae8c"),
|
|
403
|
+
hair: hex.default("#402b1f"),
|
|
404
|
+
shirt: hex.default("#386b9e"),
|
|
405
|
+
pants: hex.default("#333642"),
|
|
406
|
+
shoes: hex.default("#1f1f21"),
|
|
407
|
+
accent: hex.default("#d97340")
|
|
408
|
+
}).prefault({})
|
|
409
|
+
}).transform((p) => ({ ...p, accessories: [...new Set(p.accessories)] })).refine((p) => !(p.accessories.includes("cap") && p.accessories.includes("headphones")), { message: "Kappe und Kopfh\xF6rer passen nicht zusammen" });
|
|
410
|
+
|
|
411
|
+
// ../shared/src/agents.ts
|
|
412
|
+
import { z as z4 } from "zod";
|
|
413
|
+
var AGENT_PROTOCOL = "agent-protocol/v1";
|
|
414
|
+
var AGENT_SCOPES = [
|
|
415
|
+
"office.read",
|
|
416
|
+
"world.presence",
|
|
417
|
+
"world.move",
|
|
418
|
+
"world.seat",
|
|
419
|
+
"world.react",
|
|
420
|
+
"chat.read",
|
|
421
|
+
"chat.write",
|
|
422
|
+
"object.state",
|
|
423
|
+
"object.action",
|
|
424
|
+
"board.write",
|
|
425
|
+
"tv.control",
|
|
426
|
+
"zone.knock",
|
|
427
|
+
"meeting.audio",
|
|
428
|
+
"voice.listen",
|
|
429
|
+
"voice.speak",
|
|
430
|
+
"tools.use"
|
|
431
|
+
];
|
|
432
|
+
var AGENT_DEFAULT_SCOPES = ["office.read", "world.presence", "world.move", "chat.read", "chat.write"];
|
|
433
|
+
function hasScope(scopes, scope) {
|
|
434
|
+
return !!scopes && scopes.includes(scope);
|
|
435
|
+
}
|
|
436
|
+
var AGENT_LIMITS = {
|
|
437
|
+
/** Agenten je Workspace */
|
|
438
|
+
perWorkspace: 10,
|
|
439
|
+
/** Gleichzeitige Verbindungen je Agent */
|
|
440
|
+
connections: 4,
|
|
441
|
+
/** Chat-Nachrichten je Minute */
|
|
442
|
+
chatPerMinute: 30,
|
|
443
|
+
/** Bewegungsbefehle (`agent:goTo`) je Minute */
|
|
444
|
+
goToPerMinute: 20,
|
|
445
|
+
/** Objektzustände je Sekunde */
|
|
446
|
+
statePerSecond: 10,
|
|
447
|
+
/** Objektaktionen je Sekunde */
|
|
448
|
+
actionsPerSecond: 5,
|
|
449
|
+
/** Reaktionen je Sekunde */
|
|
450
|
+
reactPerSecond: 2,
|
|
451
|
+
/** Ereignisse insgesamt je Sekunde (harte Klammer über allem) */
|
|
452
|
+
eventsPerSecond: 40,
|
|
453
|
+
/** So oft darf ein Agent gedrosselt werden, bevor die Verbindung fällt */
|
|
454
|
+
throttleStrikes: 5,
|
|
455
|
+
/** Ohne Lebenszeichen gilt eine Verbindung als hängend (ms) */
|
|
456
|
+
idleMs: 10 * 6e4,
|
|
457
|
+
maxName: 60,
|
|
458
|
+
maxDescription: 400,
|
|
459
|
+
/** Zonen, auf die ein Agent hören kann */
|
|
460
|
+
maxZones: 20,
|
|
461
|
+
// --- Sprache (SPA-83) ---
|
|
462
|
+
/** Gleichzeitige Sprachsitzungen je Agent */
|
|
463
|
+
voiceSessions: 3,
|
|
464
|
+
/** Gleichzeitige Sprachsitzungen je Workspace (über alle Agenten) */
|
|
465
|
+
voiceSessionsPerWorkspace: 6,
|
|
466
|
+
/** Gesprochene Sekunden je Minute und Agent – darüber wird gedrosselt */
|
|
467
|
+
voiceSecondsPerMinute: 45,
|
|
468
|
+
/** Stille im Gespräch, nach der der Server die Sitzung beendet (ms) */
|
|
469
|
+
voiceIdleMs: 45e3,
|
|
470
|
+
/** Harte Obergrenze für eine einzelne Sprachsitzung (ms) */
|
|
471
|
+
voiceMaxSessionMs: 15 * 6e4,
|
|
472
|
+
/** Zustandsmeldungen des Agenten je Sekunde (`agent:voice:state`) */
|
|
473
|
+
voiceStatePerSecond: 10,
|
|
474
|
+
/** Externe Werkzeugaufrufe je Minute und Agent */
|
|
475
|
+
toolCallsPerMinute: 20,
|
|
476
|
+
/** Aufrufe desselben externen Werkzeugs je Minute und Agent */
|
|
477
|
+
toolCallsPerToolPerMinute: 10,
|
|
478
|
+
/** Verbindungen, die ein Workspace verwalten darf */
|
|
479
|
+
connectionsPerWorkspace: 20
|
|
480
|
+
};
|
|
481
|
+
var AGENT_CAPABILITIES = ["chat", "voice", "board", "tv", "move"];
|
|
482
|
+
var AGENT_CONNECTION_KINDS = ["mcp-http", "notion", "google"];
|
|
483
|
+
var AGENT_TOOL_ACCESS = ["read", "read-write"];
|
|
484
|
+
var AGENT_CONNECTION_AUTH = ["none", "token", "oauth"];
|
|
485
|
+
var agentNameSchema = z4.string().trim().min(1).max(AGENT_LIMITS.maxName);
|
|
486
|
+
var agentScopeSchema = z4.enum(AGENT_SCOPES);
|
|
487
|
+
var agentCapabilitySchema = z4.enum(AGENT_CAPABILITIES);
|
|
488
|
+
var zoneRef = z4.string().regex(/^[a-zA-Z0-9_-]{1,64}$/);
|
|
489
|
+
var agentAddressingSchema = z4.object({
|
|
490
|
+
mention: z4.boolean().default(true),
|
|
491
|
+
proximity: z4.boolean().default(true),
|
|
492
|
+
zones: z4.array(zoneRef).max(AGENT_LIMITS.maxZones).default([]),
|
|
493
|
+
desk: z4.boolean().default(false)
|
|
494
|
+
});
|
|
495
|
+
var agentEndpointSchema = z4.string().url().max(500).refine((u) => u.startsWith("https://"), { message: "Nur https" });
|
|
496
|
+
var createAgentSchema = z4.object({
|
|
497
|
+
name: agentNameSchema,
|
|
498
|
+
description: z4.string().trim().max(AGENT_LIMITS.maxDescription).default(""),
|
|
499
|
+
/** Standard ist der Kundenserver: mitgelieferte Agenten legt unser eigener Dienst an, nicht das UI */
|
|
500
|
+
operator: z4.enum(["intern", "extern"]).default("extern"),
|
|
501
|
+
endpointUrl: agentEndpointSchema.nullable().default(null),
|
|
502
|
+
scopes: z4.array(agentScopeSchema).max(AGENT_SCOPES.length).default([...AGENT_DEFAULT_SCOPES]),
|
|
503
|
+
capabilities: z4.array(agentCapabilitySchema).max(AGENT_CAPABILITIES.length).default(["chat"]),
|
|
504
|
+
addressing: agentAddressingSchema.prefault({}),
|
|
505
|
+
avatar: avatarProfileSchema.optional(),
|
|
506
|
+
homeDeskId: z4.string().regex(/^[a-zA-Z0-9_-]{1,64}$/).nullable().default(null)
|
|
507
|
+
});
|
|
508
|
+
var updateAgentSchema = createAgentSchema.partial().extend({
|
|
509
|
+
status: z4.enum(["active", "disabled"]).optional()
|
|
510
|
+
});
|
|
511
|
+
var workspaceAgentSettingsSchema = z4.object({
|
|
512
|
+
/** Kill-Switch für alle Fremdagenten des Workspaces */
|
|
513
|
+
externalAgentsDisabled: z4.boolean()
|
|
514
|
+
});
|
|
515
|
+
var toolNameSchema = z4.string().trim().min(1).max(160).regex(/^[a-zA-Z0-9_.:/-]+$/);
|
|
516
|
+
var headerNameSchema = z4.string().trim().min(1).max(120).regex(/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/, "Ung\xFCltiger Headername");
|
|
517
|
+
var headerValueSchema = z4.string().max(4e3).refine((value) => !/[\u0000-\u001f\u007f]/.test(value), "Steuerzeichen sind nicht erlaubt");
|
|
518
|
+
var connectionHeadersSchema = z4.record(headerNameSchema, headerValueSchema).refine(
|
|
519
|
+
(headers) => Object.keys(headers).length <= 20,
|
|
520
|
+
{ message: "H\xF6chstens 20 Kopfzeilen" }
|
|
521
|
+
);
|
|
522
|
+
var createAgentConnectionSchema = z4.object({
|
|
523
|
+
kind: z4.enum(AGENT_CONNECTION_KINDS).default("mcp-http"),
|
|
524
|
+
name: z4.string().trim().min(1).max(80),
|
|
525
|
+
// Protokoll und private Ziele prüft die API mit DNS-Auflösung und Allowlist; eine reine
|
|
526
|
+
// startsWith-Prüfung würde ausdrücklich erlaubte interne http-MCP-Server blockieren.
|
|
527
|
+
url: z4.string().url().max(1e3),
|
|
528
|
+
auth: z4.enum(AGENT_CONNECTION_AUTH).default("token"),
|
|
529
|
+
/** Bearer-/OAuth-Token; wird nie zurückgegeben und verschlüsselt gespeichert. */
|
|
530
|
+
token: z4.string().max(8e3).nullable().optional(),
|
|
531
|
+
/** Zusätzliche feste Auth-Header; Werte werden nie zurückgegeben. */
|
|
532
|
+
headers: connectionHeadersSchema.optional(),
|
|
533
|
+
status: z4.enum(["active", "disabled"]).default("active")
|
|
534
|
+
});
|
|
535
|
+
var updateAgentConnectionSchema = createAgentConnectionSchema.partial();
|
|
536
|
+
var agentToolGrantSchema = z4.object({
|
|
537
|
+
access: z4.enum(AGENT_TOOL_ACCESS).default("read"),
|
|
538
|
+
tools: z4.array(toolNameSchema).max(200).default([])
|
|
539
|
+
});
|
|
540
|
+
var agentHelloSchema = z4.object({
|
|
541
|
+
protocol: z4.literal(AGENT_PROTOCOL),
|
|
542
|
+
workspaceId: objectIdSchema.optional(),
|
|
543
|
+
capabilities: z4.array(agentCapabilitySchema).max(AGENT_CAPABILITIES.length).default([]),
|
|
544
|
+
client: z4.object({ name: z4.string().trim().max(80).default("unbekannt"), version: z4.string().trim().max(40).default("0") }).prefault({}),
|
|
545
|
+
/** Bei Reconnect: bisherige Teilnehmer-ID, um Position und Sitzplatz zu behalten */
|
|
546
|
+
participantId: z4.string().regex(/^[a-zA-Z0-9_-]{8,64}$/).optional()
|
|
547
|
+
});
|
|
548
|
+
var agentGoToSchema = z4.union([
|
|
549
|
+
z4.object({ x: z4.number().finite().min(-1e3).max(1e3), z: z4.number().finite().min(-1e3).max(1e3) }),
|
|
550
|
+
z4.object({ zoneId: zoneRef }),
|
|
551
|
+
z4.object({ participantId: z4.string().min(8).max(64) })
|
|
552
|
+
]);
|
|
553
|
+
var agentMeetingTokenSchema = z4.object({ target: z4.union([z4.literal("office"), zoneRef]).default("office") });
|
|
554
|
+
var agentInvokeSchema = z4.object({
|
|
555
|
+
agentParticipantId: z4.string().min(8).max(64),
|
|
556
|
+
text: z4.string().trim().max(2e3).optional(),
|
|
557
|
+
/** Gespräch per Sprache eröffnen (SPA-83). Ohne das bleibt es beim Chat wie bisher. */
|
|
558
|
+
voice: z4.boolean().default(false)
|
|
559
|
+
});
|
|
560
|
+
var AGENT_VOICE_STATES = ["idle", "listening", "thinking", "speaking"];
|
|
561
|
+
var agentVoiceStateSchema = z4.object({
|
|
562
|
+
sessionId: z4.string().min(8).max(64),
|
|
563
|
+
state: z4.enum(AGENT_VOICE_STATES),
|
|
564
|
+
/** Was zuletzt verstanden bzw. gesagt wurde – nur für Anzeige und Protokoll, optional */
|
|
565
|
+
text: z4.string().trim().max(2e3).optional()
|
|
566
|
+
});
|
|
567
|
+
var voiceCloseSchema = z4.object({ agentParticipantId: z4.string().min(8).max(64) });
|
|
568
|
+
|
|
569
|
+
// ../shared/src/view.ts
|
|
570
|
+
import { z as z5 } from "zod";
|
|
571
|
+
var VIEW_PRESETS = ["auto", "top", "iso", "third"];
|
|
572
|
+
var CAMERA_FOLLOW = ["follow", "loose", "free"];
|
|
573
|
+
var CAMERA_ROTATE = ["auto", "fixed", "snap90", "free"];
|
|
574
|
+
var WALL_MODES = ["auto", "cutaway", "transparent", "cap", "never"];
|
|
575
|
+
var CEILING_MODES = ["auto", "show", "hide"];
|
|
576
|
+
var CLICK_MOVE = ["double", "single", "off"];
|
|
577
|
+
var KEY_AXES = ["auto", "camera", "world"];
|
|
578
|
+
var RUN_MODES = ["shift", "toggle", "always"];
|
|
579
|
+
var DETAIL_LEVELS = ["auto", "low", "high"];
|
|
580
|
+
var ZOOM_RANGE = {
|
|
581
|
+
auto: [1.2, 40],
|
|
582
|
+
top: [6, 40],
|
|
583
|
+
iso: [4, 20],
|
|
584
|
+
third: [1.2, 12]
|
|
585
|
+
};
|
|
586
|
+
var ZOOM_DEFAULT = { auto: 9, top: 14, iso: 9, third: 5 };
|
|
587
|
+
var zoom = (preset) => z5.number().finite().min(ZOOM_RANGE[preset][0]).max(ZOOM_RANGE[preset][1]).default(ZOOM_DEFAULT[preset]);
|
|
588
|
+
var viewSettingsSchema = z5.object({
|
|
589
|
+
preset: z5.enum(VIEW_PRESETS).default("auto"),
|
|
590
|
+
/** Gemerkter Abstand je Preset */
|
|
591
|
+
zoom: z5.object({ auto: zoom("auto"), top: zoom("top"), iso: zoom("iso"), third: zoom("third") }).prefault({}),
|
|
592
|
+
follow: z5.enum(CAMERA_FOLLOW).default("loose"),
|
|
593
|
+
/** Nachführ-Härte 0…1 (weich … direkt) */
|
|
594
|
+
smoothing: z5.number().min(0).max(1).default(0.5),
|
|
595
|
+
rotate: z5.enum(CAMERA_ROTATE).default("auto"),
|
|
596
|
+
walls: z5.enum(WALL_MODES).default("auto"),
|
|
597
|
+
ceiling: z5.enum(CEILING_MODES).default("auto"),
|
|
598
|
+
clickMove: z5.enum(CLICK_MOVE).default("double"),
|
|
599
|
+
keyAxes: z5.enum(KEY_AXES).default("auto"),
|
|
600
|
+
run: z5.enum(RUN_MODES).default("shift"),
|
|
601
|
+
detail: z5.enum(DETAIL_LEVELS).default("auto"),
|
|
602
|
+
/**
|
|
603
|
+
* Konturen um Objekte und Avatare (SPA-89). Wirkt nur im Office-Stil `toon` – dort entscheidet der
|
|
604
|
+
* Admin über den Stil, der Nutzer über die Konturen, weil sie je Rechner spürbar Leistung kosten
|
|
605
|
+
* (eine zusätzliche Rückseiten-Hülle je Mesh).
|
|
606
|
+
*/
|
|
607
|
+
outlines: z5.boolean().default(true)
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// ../shared/src/board.ts
|
|
611
|
+
import { z as z6 } from "zod";
|
|
612
|
+
var BOARD_LIMITS = {
|
|
613
|
+
/** Punkte je Strich (inkl. Verlängerungen) */
|
|
614
|
+
maxPoints: 500,
|
|
615
|
+
/** Punkte je Operation (Segment beim Live-Zeichnen) */
|
|
616
|
+
maxSegment: 120,
|
|
617
|
+
maxText: 200,
|
|
618
|
+
/** Operationen im Log je Board; darüber verlangt der Server einen Snapshot */
|
|
619
|
+
maxOps: 5e3,
|
|
620
|
+
/** Nach so vielen Operationen seit dem Snapshot lädt der Client ein neues Snapshot-PNG hoch */
|
|
621
|
+
snapshotEvery: 200,
|
|
622
|
+
/** Snapshot-PNG als Base64, Bytes */
|
|
623
|
+
maxSnapshotBytes: 15e5,
|
|
624
|
+
/** Operationen je Teilnehmer und Sekunde */
|
|
625
|
+
opsPerSecond: 30,
|
|
626
|
+
/** Zeigerpositionen je Sekunde */
|
|
627
|
+
cursorHz: 10
|
|
628
|
+
};
|
|
629
|
+
var unit = z6.number().min(-0.05).max(1.05);
|
|
630
|
+
var point = z6.tuple([unit, unit]);
|
|
631
|
+
var boardId = z6.string().regex(/^[a-zA-Z0-9_-]{4,40}$/);
|
|
632
|
+
var color = z6.string().regex(/^#[0-9a-fA-F]{6}$/);
|
|
633
|
+
var boardOpSchema = z6.discriminatedUnion("type", [
|
|
634
|
+
z6.object({ type: z6.literal("stroke"), id: boardId, color, width: z6.number().min(1).max(24), points: z6.array(point).min(1).max(BOARD_LIMITS.maxSegment) }),
|
|
635
|
+
/** Weitere Punkte an einen Strich hängen (Live-Zeichnen in Segmenten) */
|
|
636
|
+
z6.object({ type: z6.literal("extend"), id: boardId, points: z6.array(point).min(1).max(BOARD_LIMITS.maxSegment) }),
|
|
637
|
+
z6.object({ type: z6.literal("note"), id: boardId, x: unit, y: unit, text: z6.string().trim().min(1).max(BOARD_LIMITS.maxText), color }),
|
|
638
|
+
z6.object({ type: z6.literal("move"), id: boardId, x: unit, y: unit }),
|
|
639
|
+
z6.object({ type: z6.literal("erase"), id: boardId }),
|
|
640
|
+
z6.object({ type: z6.literal("clear") })
|
|
641
|
+
]);
|
|
642
|
+
var boardOpEnvelopeSchema = z6.object({
|
|
643
|
+
placementId: z6.string().regex(/^[a-zA-Z0-9_-]{1,64}$/),
|
|
644
|
+
opId: z6.string().regex(/^[a-zA-Z0-9_-]{8,64}$/),
|
|
645
|
+
baseVersion: z6.number().int().min(0),
|
|
646
|
+
op: boardOpSchema
|
|
647
|
+
});
|
|
648
|
+
var boardSyncRequestSchema = z6.object({ placementId: z6.string().regex(/^[a-zA-Z0-9_-]{1,64}$/) });
|
|
649
|
+
var boardCursorSchema = z6.object({
|
|
650
|
+
placementId: z6.string().regex(/^[a-zA-Z0-9_-]{1,64}$/),
|
|
651
|
+
x: unit.nullable(),
|
|
652
|
+
y: unit.nullable()
|
|
653
|
+
});
|
|
654
|
+
var boardSnapshotSchema = z6.object({
|
|
655
|
+
/** Version, bis zu der das PNG alle Operationen enthält */
|
|
656
|
+
version: z6.number().int().min(1),
|
|
657
|
+
/** data:image/png;base64,… */
|
|
658
|
+
png: z6.string().regex(/^data:image\/png;base64,[A-Za-z0-9+/=]+$/).max(BOARD_LIMITS.maxSnapshotBytes)
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// ../shared/src/tv.ts
|
|
662
|
+
import { z as z8 } from "zod";
|
|
663
|
+
|
|
664
|
+
// ../shared/src/images.ts
|
|
665
|
+
import { z as z7 } from "zod";
|
|
666
|
+
var IMAGE_LIMITS = {
|
|
667
|
+
/** Rohbytes je Bild (der Upload kommt als Base64 in JSON – der Body darf ~4/3 davon groß sein) */
|
|
668
|
+
maxBytes: 6 * 1024 * 1024,
|
|
669
|
+
/** Kantenlänge und Gesamtfläche (Schutz gegen Dekomprimierungsbomben) */
|
|
670
|
+
maxDimension: 8e3,
|
|
671
|
+
maxPixels: 4e7,
|
|
672
|
+
/** Bilder je Workspace */
|
|
673
|
+
maxPerWorkspace: 300,
|
|
674
|
+
maxNameLength: 80
|
|
675
|
+
};
|
|
676
|
+
var SLIDESHOW_IMAGE_PREFIX = "img:";
|
|
677
|
+
var imageId = z7.string().regex(/^[0-9a-f]{24}$/);
|
|
678
|
+
var imageName = z7.string().trim().min(1).max(IMAGE_LIMITS.maxNameLength);
|
|
679
|
+
var imageUploadSchema = z7.object({
|
|
680
|
+
name: imageName,
|
|
681
|
+
/** data:image/png;base64,… */
|
|
682
|
+
data: z7.string().regex(/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/).max(Math.ceil(IMAGE_LIMITS.maxBytes * 4 / 3) + 100)
|
|
683
|
+
});
|
|
684
|
+
var imageRenameSchema = z7.object({ name: imageName });
|
|
685
|
+
|
|
686
|
+
// ../shared/src/tv.ts
|
|
687
|
+
var TV_SOURCES = ["auto", "off", "share", "video", "embed", "channel", "web"];
|
|
688
|
+
var TV_CHANNELS = ["clock", "dashboard", "slideshow"];
|
|
689
|
+
var TV_LIMITS = {
|
|
690
|
+
/** URL-Länge in Zeichen */
|
|
691
|
+
maxUrl: 1024,
|
|
692
|
+
maxTitle: 120,
|
|
693
|
+
/** Hosts, die als `embed` erlaubt sind */
|
|
694
|
+
embedHosts: ["youtube.com", "www.youtube.com", "m.youtube.com", "youtu.be", "vimeo.com", "www.vimeo.com", "player.vimeo.com"],
|
|
695
|
+
defaultVolume: 0.8,
|
|
696
|
+
/** Hörweite des Fernsehers in Metern (wie SCREEN_RANGE der Bindung) */
|
|
697
|
+
earshot: 6,
|
|
698
|
+
/** Diashow: Wechsel in Sekunden */
|
|
699
|
+
slideSeconds: 8,
|
|
700
|
+
/** `web`: Länge der Kanalliste (Rotation) in Zeichen */
|
|
701
|
+
maxWeb: 520
|
|
702
|
+
};
|
|
703
|
+
function parseUrl(u) {
|
|
704
|
+
const m = /^(?:([a-z][a-z0-9+.-]*):)?(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?/i.exec(u.trim());
|
|
705
|
+
if (!m) return null;
|
|
706
|
+
const protocol = (m[1] ?? "").toLowerCase();
|
|
707
|
+
const host = (m[2] ?? "").toLowerCase().replace(/^[^@]*@/, "").replace(/:\d+$/, "");
|
|
708
|
+
const path = m[3] ?? "";
|
|
709
|
+
if (!protocol && !u.startsWith("/")) return null;
|
|
710
|
+
if (!protocol && u.startsWith("//")) return null;
|
|
711
|
+
const query = {};
|
|
712
|
+
for (const part of (m[4] ?? "").split("&")) {
|
|
713
|
+
if (!part) continue;
|
|
714
|
+
const [k, v = ""] = part.split("=");
|
|
715
|
+
try {
|
|
716
|
+
query[decodeURIComponent(k)] = decodeURIComponent(v);
|
|
717
|
+
} catch {
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return { protocol, host, path, query };
|
|
721
|
+
}
|
|
722
|
+
var httpsUrl = z8.string().max(TV_LIMITS.maxUrl).refine((u) => {
|
|
723
|
+
if (u.trim().startsWith(SLIDESHOW_IMAGE_PREFIX)) return true;
|
|
724
|
+
const p = parseUrl(u);
|
|
725
|
+
return !!p && (p.protocol === "https" || p.protocol === "http" || p.protocol === "" && u.startsWith("/"));
|
|
726
|
+
}, { message: "Nur https-Adressen oder Bilder aus der Mediathek" });
|
|
727
|
+
var tvStateSchema = z8.object({
|
|
728
|
+
source: z8.enum(TV_SOURCES),
|
|
729
|
+
sharer: z8.string().min(1).max(64).optional(),
|
|
730
|
+
url: httpsUrl.optional(),
|
|
731
|
+
title: z8.string().max(TV_LIMITS.maxTitle).optional(),
|
|
732
|
+
playing: z8.boolean().optional(),
|
|
733
|
+
position: z8.number().min(0).max(1e7).optional(),
|
|
734
|
+
at: z8.number().min(0).optional(),
|
|
735
|
+
by: z8.string().max(64).optional(),
|
|
736
|
+
volume: z8.number().min(0).max(1).optional(),
|
|
737
|
+
channel: z8.enum(TV_CHANNELS).optional(),
|
|
738
|
+
loop: z8.boolean().optional(),
|
|
739
|
+
web: z8.string().max(TV_LIMITS.maxWeb).regex(/^[a-zA-Z0-9_,-]*$/).optional(),
|
|
740
|
+
rotate: z8.number().int().min(10).max(3600).optional()
|
|
741
|
+
}).strict().superRefine((s, ctx) => {
|
|
742
|
+
if (s.source === "share" && !s.sharer) ctx.addIssue({ code: "custom", message: "share braucht sharer", path: ["sharer"] });
|
|
743
|
+
if ((s.source === "video" || s.source === "embed") && !s.url) ctx.addIssue({ code: "custom", message: "video/embed brauchen url", path: ["url"] });
|
|
744
|
+
if (s.source === "embed" && s.url && !embedInfo(s.url)) ctx.addIssue({ code: "custom", message: "embed: nur YouTube oder Vimeo", path: ["url"] });
|
|
745
|
+
if (s.source !== "web" && s.url && parseUrl(s.url)?.protocol === "http") ctx.addIssue({ code: "custom", message: "Nur https-Adressen", path: ["url"] });
|
|
746
|
+
if (s.source === "channel" && !s.channel) ctx.addIssue({ code: "custom", message: "channel braucht channel", path: ["channel"] });
|
|
747
|
+
if (s.source === "web" && !s.web && !s.url) ctx.addIssue({ code: "custom", message: "web braucht einen Kanal oder eine Adresse", path: ["web"] });
|
|
748
|
+
if (s.source !== "web" && (s.web || s.rotate)) ctx.addIssue({ code: "custom", message: "web/rotate nur bei source web", path: ["web"] });
|
|
749
|
+
});
|
|
750
|
+
function embedInfo(url) {
|
|
751
|
+
const u = parseUrl(url);
|
|
752
|
+
if (!u) return null;
|
|
753
|
+
const host = u.host;
|
|
754
|
+
if (!TV_LIMITS.embedHosts.includes(host)) return null;
|
|
755
|
+
if (host.includes("youtu")) {
|
|
756
|
+
let id = null;
|
|
757
|
+
if (host === "youtu.be") id = u.path.slice(1).split("/")[0] ?? null;
|
|
758
|
+
else if (u.path === "/watch") id = u.query.v ?? null;
|
|
759
|
+
else {
|
|
760
|
+
const m2 = u.path.match(/^\/(?:embed|shorts|live|v)\/([\w-]{6,})/);
|
|
761
|
+
id = m2 ? m2[1] : null;
|
|
762
|
+
}
|
|
763
|
+
if (!id || !/^[\w-]{6,20}$/.test(id)) return null;
|
|
764
|
+
return { provider: "youtube", id, poster: `https://i.ytimg.com/vi/${id}/hqdefault.jpg`, embedUrl: `https://www.youtube.com/embed/${id}?enablejsapi=1&playsinline=1&rel=0` };
|
|
765
|
+
}
|
|
766
|
+
const m = u.path.match(/\/(?:video\/)?(\d{5,})/);
|
|
767
|
+
if (!m) return null;
|
|
768
|
+
return { provider: "vimeo", id: m[1], poster: null, embedUrl: `https://player.vimeo.com/video/${m[1]}?api=1&playsinline=1` };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ../shared/src/generator.ts
|
|
772
|
+
import { z as z9 } from "zod";
|
|
773
|
+
var GENERATOR_LIMITS = {
|
|
774
|
+
maxPrompt: 2e3,
|
|
775
|
+
/** Jobs je Tag und Workspace (Default; Server: GENERATOR_DAILY_LIMIT) */
|
|
776
|
+
dailyDefault: 20,
|
|
777
|
+
/** Verlauf: letzte Jobs */
|
|
778
|
+
historySize: 20
|
|
779
|
+
};
|
|
780
|
+
var GENERATOR_FILE_LIMITS = {
|
|
781
|
+
maxFiles: 6,
|
|
782
|
+
maxBytes: 10 * 1024 * 1024,
|
|
783
|
+
/** PDF: so viele Seiten werden höchstens betrachtet */
|
|
784
|
+
maxPdfPages: 20,
|
|
785
|
+
/** Lange Kante nach der Normalisierung */
|
|
786
|
+
maxEdge: 2048,
|
|
787
|
+
/** Uploads je Nutzer und Stunde */
|
|
788
|
+
perHour: 30,
|
|
789
|
+
maxName: 120
|
|
790
|
+
};
|
|
791
|
+
var GENERATOR_FILE_ROLES = ["floorplan", "style"];
|
|
792
|
+
var generatorFileUploadSchema = z9.object({
|
|
793
|
+
name: z9.string().trim().min(1).max(GENERATOR_FILE_LIMITS.maxName),
|
|
794
|
+
data: z9.string().min(16).max(Math.ceil(GENERATOR_FILE_LIMITS.maxBytes * 4 / 3) + 128).regex(/^data:[a-z]+\/[a-z0-9.+-]+;base64,/)
|
|
795
|
+
});
|
|
796
|
+
var generatorFileIdSchema = z9.string().regex(/^[a-f0-9]{24}$/);
|
|
797
|
+
var generatorFileRefSchema = z9.object({
|
|
798
|
+
id: generatorFileIdSchema,
|
|
799
|
+
role: z9.enum(GENERATOR_FILE_ROLES),
|
|
800
|
+
/** PDF: Seite mit dem Grundriss (1-basiert) */
|
|
801
|
+
page: z9.number().int().min(1).max(GENERATOR_FILE_LIMITS.maxPdfPages).optional(),
|
|
802
|
+
/** Grundriss: „Längste Seite in Metern“ (leer = KI schätzt) */
|
|
803
|
+
scaleMeters: z9.number().min(2).max(500).optional()
|
|
804
|
+
});
|
|
805
|
+
var generatorParamsSchema = z9.object({
|
|
806
|
+
seats: z9.number().int().min(0).max(200).optional(),
|
|
807
|
+
meetingRooms: z9.number().int().min(0).max(8).optional(),
|
|
808
|
+
lounge: z9.boolean().optional(),
|
|
809
|
+
fun: z9.boolean().optional(),
|
|
810
|
+
kitchen: z9.boolean().optional(),
|
|
811
|
+
focus: z9.boolean().optional(),
|
|
812
|
+
outdoor: z9.boolean().optional(),
|
|
813
|
+
style: z9.enum(["skandinavisch", "industrial", "bunt", "minimal", "homeoffice"]).optional(),
|
|
814
|
+
sky: z9.enum(["day", "evening", "night"]).optional(),
|
|
815
|
+
size: z9.enum(["kompakt", "normal", "grosszuegig"]).optional()
|
|
816
|
+
});
|
|
817
|
+
var createGenerationJobSchema = z9.object({
|
|
818
|
+
/** Freie Beschreibung; bei `template` optional */
|
|
819
|
+
prompt: z9.string().trim().max(GENERATOR_LIMITS.maxPrompt).default(""),
|
|
820
|
+
params: generatorParamsSchema.default({}),
|
|
821
|
+
/** Feste Vorlage (ID aus OFFICE_TEMPLATES) – dann kein LLM für den Brief */
|
|
822
|
+
template: z9.string().regex(/^[a-z0-9_]{1,40}$/).optional(),
|
|
823
|
+
/** `neu` ersetzt den Entwurf (Rollback über Revisionen); `ergaenzen` folgt in G4 */
|
|
824
|
+
mode: z9.enum(["neu"]).default("neu"),
|
|
825
|
+
/** Brief-Schritt überspringen („Direkt bauen“) */
|
|
826
|
+
skipBrief: z9.boolean().default(false),
|
|
827
|
+
seed: z9.number().int().min(0).max(2147483647).optional(),
|
|
828
|
+
/** Referenzen (G2/G3): höchstens 6, davon höchstens ein Grundriss */
|
|
829
|
+
files: z9.array(generatorFileRefSchema).max(GENERATOR_FILE_LIMITS.maxFiles).default([]).refine((f) => f.filter((x) => x.role === "floorplan").length <= 1, "H\xF6chstens ein Grundriss").refine((f) => new Set(f.map((x) => x.id)).size === f.length, "Jede Datei nur einmal")
|
|
830
|
+
});
|
|
831
|
+
var confirmBriefSchema = z9.object({
|
|
832
|
+
/** Bearbeiteter Brief (OfficeBrief aus @huddle/scene-schema) */
|
|
833
|
+
brief: z9.unknown(),
|
|
834
|
+
answers: z9.record(z9.string().max(32), z9.string().max(200)).optional()
|
|
835
|
+
});
|
|
836
|
+
var applyGenerationSchema = z9.object({ baseVersion: z9.number().int().min(0) });
|
|
837
|
+
var revertGenerationSchema = z9.object({ baseVersion: z9.number().int().min(0) });
|
|
838
|
+
var generationJobIdSchema = z9.string().regex(/^[a-f0-9]{24}$/);
|
|
839
|
+
|
|
840
|
+
// ../shared/src/web.ts
|
|
841
|
+
import { z as z10 } from "zod";
|
|
842
|
+
var WEB_LIMITS = {
|
|
843
|
+
maxUrl: 1024,
|
|
844
|
+
maxName: 60,
|
|
845
|
+
maxCss: 4096,
|
|
846
|
+
/** Kanäle je Workspace */
|
|
847
|
+
maxChannels: 12,
|
|
848
|
+
/** Kanäle gleichzeitig offen je Renderer-Instanz */
|
|
849
|
+
maxContexts: 3,
|
|
850
|
+
/** Bildabstand im Standbildmodus (Sekunden) */
|
|
851
|
+
minRefresh: 2,
|
|
852
|
+
maxRefresh: 300,
|
|
853
|
+
defaultRefresh: 10,
|
|
854
|
+
/** Sichtfenster des Server-Browsers */
|
|
855
|
+
minViewport: 480,
|
|
856
|
+
maxViewport: 2560,
|
|
857
|
+
defaultViewport: { w: 1280, h: 720 },
|
|
858
|
+
/** Zoom der Seite */
|
|
859
|
+
minZoom: 0.25,
|
|
860
|
+
maxZoom: 3,
|
|
861
|
+
/** Steuerung: Dauer der exklusiven Übernahme ohne Eingabe (ms) */
|
|
862
|
+
controlTtlMs: 5 * 6e4,
|
|
863
|
+
/** Eingabeereignisse je Sekunde und Teilnehmer */
|
|
864
|
+
inputPerSecond: 60,
|
|
865
|
+
/** Zeichen, die auf einmal eingefügt werden dürfen (Zwischenablage) */
|
|
866
|
+
maxPaste: 8192,
|
|
867
|
+
/** Ohne Zuschauer: pausieren nach … ms, Seite schließen nach … ms */
|
|
868
|
+
idlePauseMs: 6e4,
|
|
869
|
+
idleCloseMs: 10 * 6e4,
|
|
870
|
+
/** Bildgröße (Bytes, base64) */
|
|
871
|
+
maxFrameBytes: 15e5,
|
|
872
|
+
/** Rotation mehrerer Kanäle: Sekunden je Kanal */
|
|
873
|
+
minRotate: 10,
|
|
874
|
+
maxRotate: 3600,
|
|
875
|
+
/** Bildrate im Bewegtbildmodus */
|
|
876
|
+
maxLiveFps: 10
|
|
877
|
+
};
|
|
878
|
+
var WEB_MODES = ["still", "live"];
|
|
879
|
+
var WEB_BUTTONS = ["left", "right", "middle"];
|
|
880
|
+
var unit2 = z10.number().min(0).max(1);
|
|
881
|
+
var modifiers = z10.number().int().min(0).max(15).optional();
|
|
882
|
+
var webInputEventSchema = z10.discriminatedUnion("kind", [
|
|
883
|
+
z10.object({
|
|
884
|
+
kind: z10.literal("mouse"),
|
|
885
|
+
type: z10.enum(["move", "down", "up", "click"]),
|
|
886
|
+
x: unit2,
|
|
887
|
+
y: unit2,
|
|
888
|
+
button: z10.enum(WEB_BUTTONS).optional(),
|
|
889
|
+
clickCount: z10.number().int().min(1).max(3).optional(),
|
|
890
|
+
modifiers
|
|
891
|
+
}),
|
|
892
|
+
z10.object({ kind: z10.literal("wheel"), x: unit2, y: unit2, dx: z10.number().min(-5e3).max(5e3), dy: z10.number().min(-5e3).max(5e3), modifiers }),
|
|
893
|
+
z10.object({
|
|
894
|
+
kind: z10.literal("key"),
|
|
895
|
+
type: z10.enum(["down", "up", "char"]),
|
|
896
|
+
key: z10.string().min(1).max(32),
|
|
897
|
+
code: z10.string().max(32).optional(),
|
|
898
|
+
text: z10.string().max(8).optional(),
|
|
899
|
+
modifiers
|
|
900
|
+
}),
|
|
901
|
+
/**
|
|
902
|
+
* Text einfügen, ohne eine Taste nachzubauen (CDP `Input.insertText`): layoutunabhängig und damit
|
|
903
|
+
* der einzig verlässliche Weg für Satzzeichen, Umlaute, `@` – und für die Zwischenablage.
|
|
904
|
+
*/
|
|
905
|
+
z10.object({ kind: z10.literal("text"), text: z10.string().min(1).max(WEB_LIMITS.maxPaste) })
|
|
906
|
+
]);
|
|
907
|
+
var webNavigateSchema = z10.object({
|
|
908
|
+
action: z10.enum(["url", "back", "forward", "reload", "home"]),
|
|
909
|
+
url: z10.string().max(WEB_LIMITS.maxUrl).optional()
|
|
910
|
+
});
|
|
911
|
+
var webWatchSchema = z10.object({ placementId: z10.string().max(64), watch: z10.boolean() });
|
|
912
|
+
var webControlSchema = z10.object({ placementId: z10.string().max(64), take: z10.boolean() });
|
|
913
|
+
var webInputSchema = z10.object({ placementId: z10.string().max(64), event: webInputEventSchema });
|
|
914
|
+
var webNavigateRequestSchema = z10.object({ placementId: z10.string().max(64) }).and(webNavigateSchema);
|
|
915
|
+
var webResizeSchema = z10.object({
|
|
916
|
+
placementId: z10.string().max(64),
|
|
917
|
+
w: z10.number().int().min(WEB_LIMITS.minViewport).max(WEB_LIMITS.maxViewport),
|
|
918
|
+
h: z10.number().int().min(WEB_LIMITS.minViewport).max(WEB_LIMITS.maxViewport)
|
|
919
|
+
});
|
|
920
|
+
var viewport = z10.object({
|
|
921
|
+
w: z10.number().int().min(WEB_LIMITS.minViewport).max(WEB_LIMITS.maxViewport),
|
|
922
|
+
h: z10.number().int().min(WEB_LIMITS.minViewport).max(WEB_LIMITS.maxViewport)
|
|
923
|
+
});
|
|
924
|
+
var webChannelInputSchema = z10.object({
|
|
925
|
+
name: z10.string().trim().min(1).max(WEB_LIMITS.maxName),
|
|
926
|
+
url: z10.string().min(1).max(WEB_LIMITS.maxUrl),
|
|
927
|
+
viewport: viewport.optional(),
|
|
928
|
+
mode: z10.enum(WEB_MODES).optional(),
|
|
929
|
+
refreshSeconds: z10.number().int().min(WEB_LIMITS.minRefresh).max(WEB_LIMITS.maxRefresh).optional(),
|
|
930
|
+
zoom: z10.number().min(WEB_LIMITS.minZoom).max(WEB_LIMITS.maxZoom).optional(),
|
|
931
|
+
scroll: z10.object({ x: z10.number().min(0).max(1e5), y: z10.number().min(0).max(1e5) }).optional(),
|
|
932
|
+
css: z10.string().max(WEB_LIMITS.maxCss).optional(),
|
|
933
|
+
/** Feste Kopfzeilen (verschlüsselt abgelegt); leeres Objekt löscht sie */
|
|
934
|
+
headers: z10.record(z10.string().min(1).max(64), z10.string().max(1024)).optional(),
|
|
935
|
+
basicAuth: z10.object({ user: z10.string().max(128), password: z10.string().max(256) }).nullable().optional()
|
|
936
|
+
});
|
|
937
|
+
var webChannelIdSchema = z10.string().regex(/^[a-zA-Z0-9_-]{1,64}$/);
|
|
938
|
+
|
|
939
|
+
// ../shared/src/sso.ts
|
|
940
|
+
import { z as z11 } from "zod";
|
|
941
|
+
var domainSchema = z11.string().trim().toLowerCase().max(253).regex(/^(?=.{1,253}$)(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/, "Keine g\xFCltige Domain");
|
|
942
|
+
var updateWorkspaceAuthSchema = z11.object({
|
|
943
|
+
password: z11.boolean(),
|
|
944
|
+
magicLink: z11.boolean(),
|
|
945
|
+
google: z11.object({ enabled: z11.boolean(), allowedDomains: z11.array(domainSchema).max(20) }).partial(),
|
|
946
|
+
oidcEnabled: z11.boolean(),
|
|
947
|
+
autoJoin: z11.boolean()
|
|
948
|
+
}).partial();
|
|
949
|
+
var scopeSchema = z11.string().trim().regex(/^[\x21\x23-\x5b\x5d-\x7e]{1,100}$/, "Ung\xFCltiger Scope");
|
|
950
|
+
var oidcConnectionSchema = z11.object({
|
|
951
|
+
issuer: z11.string().trim().url().max(500),
|
|
952
|
+
clientId: z11.string().trim().min(1).max(300),
|
|
953
|
+
/** Leer lassen = bisheriges Secret behalten */
|
|
954
|
+
clientSecret: z11.string().max(2e3).optional(),
|
|
955
|
+
scopes: z11.array(scopeSchema).max(20).default(["openid", "email", "profile"]),
|
|
956
|
+
label: z11.string().trim().min(1).max(40).default("Mit SSO anmelden")
|
|
957
|
+
});
|
|
958
|
+
var addDomainSchema = z11.object({ domain: domainSchema });
|
|
959
|
+
var ssoDiscoverSchema = z11.object({ email: z11.string().trim().toLowerCase().email().max(254) });
|
|
960
|
+
|
|
961
|
+
// src/version.ts
|
|
962
|
+
var SDK_VERSION = "0.1.0";
|
|
963
|
+
|
|
964
|
+
// src/audio.ts
|
|
965
|
+
function resample(pcm, from, to) {
|
|
966
|
+
if (from === to) return pcm;
|
|
967
|
+
const ratio = from / to;
|
|
968
|
+
const out = new Int16Array(Math.max(1, Math.floor(pcm.length / ratio)));
|
|
969
|
+
for (let i = 0; i < out.length; i++) {
|
|
970
|
+
const src = i * ratio;
|
|
971
|
+
const a = Math.floor(src);
|
|
972
|
+
const b = Math.min(a + 1, pcm.length - 1);
|
|
973
|
+
const t = src - a;
|
|
974
|
+
out[i] = (pcm[a] ?? 0) * (1 - t) + (pcm[b] ?? 0) * t | 0;
|
|
975
|
+
}
|
|
976
|
+
return out;
|
|
977
|
+
}
|
|
978
|
+
function rms(pcm) {
|
|
979
|
+
if (pcm.length === 0) return 0;
|
|
980
|
+
let sum = 0;
|
|
981
|
+
for (let i = 0; i < pcm.length; i++) {
|
|
982
|
+
const v = pcm[i] / 32768;
|
|
983
|
+
sum += v * v;
|
|
984
|
+
}
|
|
985
|
+
return Math.sqrt(sum / pcm.length);
|
|
986
|
+
}
|
|
987
|
+
var Vad = class {
|
|
988
|
+
sampleRate;
|
|
989
|
+
silenceMs;
|
|
990
|
+
minSpeechMs;
|
|
991
|
+
preRollMs;
|
|
992
|
+
maxSpeechMs;
|
|
993
|
+
baseThreshold;
|
|
994
|
+
/** Gleitender Grundpegel der Stille */
|
|
995
|
+
noise = 5e-3;
|
|
996
|
+
speaking = false;
|
|
997
|
+
speechChunks = [];
|
|
998
|
+
speechSamples = 0;
|
|
999
|
+
silenceSamples = 0;
|
|
1000
|
+
preRoll = [];
|
|
1001
|
+
preRollSamples = 0;
|
|
1002
|
+
constructor(options) {
|
|
1003
|
+
this.sampleRate = options.sampleRate;
|
|
1004
|
+
this.baseThreshold = options.threshold ?? 0.02;
|
|
1005
|
+
this.silenceMs = options.silenceMs ?? 700;
|
|
1006
|
+
this.minSpeechMs = options.minSpeechMs ?? 300;
|
|
1007
|
+
this.preRollMs = options.preRollMs ?? 300;
|
|
1008
|
+
this.maxSpeechMs = options.maxSpeechMs ?? 2e4;
|
|
1009
|
+
}
|
|
1010
|
+
get active() {
|
|
1011
|
+
return this.speaking;
|
|
1012
|
+
}
|
|
1013
|
+
/** Aktuelle Auslöseschwelle – Grundrauschen plus Abstand, nie unter dem Grundwert. */
|
|
1014
|
+
get threshold() {
|
|
1015
|
+
return Math.max(this.baseThreshold, this.noise * 3);
|
|
1016
|
+
}
|
|
1017
|
+
/** Einen Block einspeisen. Liefert die Ereignisse, die dieser Block ausgelöst hat. */
|
|
1018
|
+
push(pcm) {
|
|
1019
|
+
const events = [];
|
|
1020
|
+
const level = rms(pcm);
|
|
1021
|
+
const loud = level > this.threshold;
|
|
1022
|
+
if (!this.speaking) {
|
|
1023
|
+
this.noise = this.noise * 0.95 + level * 0.05;
|
|
1024
|
+
this.preRoll.push(pcm);
|
|
1025
|
+
this.preRollSamples += pcm.length;
|
|
1026
|
+
const keep = this.preRollMs / 1e3 * this.sampleRate;
|
|
1027
|
+
while (this.preRollSamples > keep && this.preRoll.length > 1) {
|
|
1028
|
+
this.preRollSamples -= this.preRoll.shift().length;
|
|
1029
|
+
}
|
|
1030
|
+
if (loud) {
|
|
1031
|
+
this.speaking = true;
|
|
1032
|
+
this.speechChunks = [...this.preRoll];
|
|
1033
|
+
this.speechSamples = this.preRollSamples;
|
|
1034
|
+
this.silenceSamples = 0;
|
|
1035
|
+
this.preRoll = [];
|
|
1036
|
+
this.preRollSamples = 0;
|
|
1037
|
+
events.push({ type: "start" });
|
|
1038
|
+
}
|
|
1039
|
+
return events;
|
|
1040
|
+
}
|
|
1041
|
+
this.speechChunks.push(pcm);
|
|
1042
|
+
this.speechSamples += pcm.length;
|
|
1043
|
+
this.silenceSamples = loud ? 0 : this.silenceSamples + pcm.length;
|
|
1044
|
+
const silenceReached = this.silenceSamples / this.sampleRate * 1e3 >= this.silenceMs;
|
|
1045
|
+
const tooLong = this.speechSamples / this.sampleRate * 1e3 >= this.maxSpeechMs;
|
|
1046
|
+
if (silenceReached || tooLong) events.push(this.finish());
|
|
1047
|
+
return events;
|
|
1048
|
+
}
|
|
1049
|
+
/** Laufende Äußerung sofort abschließen (Gesprächsende, Abbruch). */
|
|
1050
|
+
flush() {
|
|
1051
|
+
if (!this.speaking) return null;
|
|
1052
|
+
return this.finish();
|
|
1053
|
+
}
|
|
1054
|
+
finish() {
|
|
1055
|
+
const pcm = concat(this.speechChunks);
|
|
1056
|
+
const speechMs = (this.speechSamples - this.silenceSamples) / this.sampleRate * 1e3;
|
|
1057
|
+
this.speaking = false;
|
|
1058
|
+
this.speechChunks = [];
|
|
1059
|
+
this.speechSamples = 0;
|
|
1060
|
+
this.silenceSamples = 0;
|
|
1061
|
+
if (speechMs < this.minSpeechMs) return { type: "end", pcm: new Int16Array(0), durationMs: 0 };
|
|
1062
|
+
return { type: "end", pcm, durationMs: pcm.length / this.sampleRate * 1e3 };
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
function concat(chunks) {
|
|
1066
|
+
let n = 0;
|
|
1067
|
+
for (const c of chunks) n += c.length;
|
|
1068
|
+
const out = new Int16Array(n);
|
|
1069
|
+
let at = 0;
|
|
1070
|
+
for (const c of chunks) {
|
|
1071
|
+
out.set(c, at);
|
|
1072
|
+
at += c.length;
|
|
1073
|
+
}
|
|
1074
|
+
return out;
|
|
1075
|
+
}
|
|
1076
|
+
function toWav(pcm, sampleRate) {
|
|
1077
|
+
const bytes = pcm.length * 2;
|
|
1078
|
+
const buf = new ArrayBuffer(44 + bytes);
|
|
1079
|
+
const view = new DataView(buf);
|
|
1080
|
+
const ascii = (at, text) => {
|
|
1081
|
+
for (let i = 0; i < text.length; i++) view.setUint8(at + i, text.charCodeAt(i));
|
|
1082
|
+
};
|
|
1083
|
+
ascii(0, "RIFF");
|
|
1084
|
+
view.setUint32(4, 36 + bytes, true);
|
|
1085
|
+
ascii(8, "WAVE");
|
|
1086
|
+
ascii(12, "fmt ");
|
|
1087
|
+
view.setUint32(16, 16, true);
|
|
1088
|
+
view.setUint16(20, 1, true);
|
|
1089
|
+
view.setUint16(22, 1, true);
|
|
1090
|
+
view.setUint32(24, sampleRate, true);
|
|
1091
|
+
view.setUint32(28, sampleRate * 2, true);
|
|
1092
|
+
view.setUint16(32, 2, true);
|
|
1093
|
+
view.setUint16(34, 16, true);
|
|
1094
|
+
ascii(36, "data");
|
|
1095
|
+
view.setUint32(40, bytes, true);
|
|
1096
|
+
new Int16Array(buf, 44).set(pcm);
|
|
1097
|
+
return new Uint8Array(buf);
|
|
1098
|
+
}
|
|
1099
|
+
var FrameSplitter = class {
|
|
1100
|
+
constructor(frameSamples) {
|
|
1101
|
+
this.frameSamples = frameSamples;
|
|
1102
|
+
}
|
|
1103
|
+
frameSamples;
|
|
1104
|
+
rest = new Int16Array(0);
|
|
1105
|
+
push(pcm) {
|
|
1106
|
+
const merged = concat([this.rest, pcm]);
|
|
1107
|
+
const frames = [];
|
|
1108
|
+
let at = 0;
|
|
1109
|
+
while (at + this.frameSamples <= merged.length) {
|
|
1110
|
+
frames.push(merged.slice(at, at + this.frameSamples));
|
|
1111
|
+
at += this.frameSamples;
|
|
1112
|
+
}
|
|
1113
|
+
this.rest = merged.slice(at);
|
|
1114
|
+
return frames;
|
|
1115
|
+
}
|
|
1116
|
+
/** Rest mit Stille auffüllen und ausgeben (Ende einer Äußerung). */
|
|
1117
|
+
flush() {
|
|
1118
|
+
if (this.rest.length === 0) return null;
|
|
1119
|
+
const frame = new Int16Array(this.frameSamples);
|
|
1120
|
+
frame.set(this.rest);
|
|
1121
|
+
this.rest = new Int16Array(0);
|
|
1122
|
+
return frame;
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
function pcmFromBytes(bytes) {
|
|
1126
|
+
const even = bytes.length - bytes.length % 2;
|
|
1127
|
+
const out = new Int16Array(even / 2);
|
|
1128
|
+
for (let i = 0; i < out.length; i++) out[i] = (bytes[i * 2] | bytes[i * 2 + 1] << 8) << 16 >> 16;
|
|
1129
|
+
return out;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/voice.ts
|
|
1133
|
+
var STT_RATE = 16e3;
|
|
1134
|
+
var FRAME_MS = 10;
|
|
1135
|
+
var RTC_MODULE = "@livekit/rtc-node";
|
|
1136
|
+
var rtcModule = null;
|
|
1137
|
+
async function rtc() {
|
|
1138
|
+
if (rtcModule) return rtcModule;
|
|
1139
|
+
try {
|
|
1140
|
+
rtcModule = await import(RTC_MODULE);
|
|
1141
|
+
return rtcModule;
|
|
1142
|
+
} catch {
|
|
1143
|
+
throw new Error("F\xFCr Sprache fehlt @livekit/rtc-node \u2013 `npm i @livekit/rtc-node` im Agenten-Projekt");
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
var EtazioVoice = class {
|
|
1147
|
+
agent;
|
|
1148
|
+
options;
|
|
1149
|
+
live = /* @__PURE__ */ new Map();
|
|
1150
|
+
started = false;
|
|
1151
|
+
constructor(agent, options) {
|
|
1152
|
+
this.agent = agent;
|
|
1153
|
+
this.options = options;
|
|
1154
|
+
}
|
|
1155
|
+
/** An die Sprachereignisse des Agenten hängen. Ab hier laufen Gespräche von selbst. */
|
|
1156
|
+
start() {
|
|
1157
|
+
if (this.started) return this;
|
|
1158
|
+
this.started = true;
|
|
1159
|
+
this.agent.on("voice", (payload) => {
|
|
1160
|
+
if (payload.kind === "open") void this.open(payload).catch((err) => this.fail(err));
|
|
1161
|
+
else void this.close(payload.sessionId);
|
|
1162
|
+
});
|
|
1163
|
+
this.agent.on("disconnect", () => void this.closeAll());
|
|
1164
|
+
this.agent.on("revoked", () => void this.closeAll());
|
|
1165
|
+
return this;
|
|
1166
|
+
}
|
|
1167
|
+
/** Laufende Gespräche (für Diagnose und geordnetes Herunterfahren). */
|
|
1168
|
+
get sessions() {
|
|
1169
|
+
return [...this.live.values()].map((l) => l.session);
|
|
1170
|
+
}
|
|
1171
|
+
async stop() {
|
|
1172
|
+
await this.closeAll();
|
|
1173
|
+
}
|
|
1174
|
+
// -------------------------------------------------------------------------
|
|
1175
|
+
async open(session) {
|
|
1176
|
+
if (this.live.has(session.sessionId)) return;
|
|
1177
|
+
const mod = await rtc();
|
|
1178
|
+
const token = await this.agent.meetingToken(session.target);
|
|
1179
|
+
const room = new mod.Room();
|
|
1180
|
+
await room.connect(token.url, token.token, { autoSubscribe: false, dynacast: true });
|
|
1181
|
+
const source = new mod.AudioSource(this.options.tts.sampleRate, 1);
|
|
1182
|
+
const track = mod.LocalAudioTrack.createAudioTrack("agent-voice", source);
|
|
1183
|
+
const publication = await room.localParticipant.publishTrack(
|
|
1184
|
+
track,
|
|
1185
|
+
new mod.TrackPublishOptions({ source: mod.TrackSource.SOURCE_MICROPHONE ?? 1 })
|
|
1186
|
+
);
|
|
1187
|
+
const entry = {
|
|
1188
|
+
session,
|
|
1189
|
+
room,
|
|
1190
|
+
source,
|
|
1191
|
+
trackSid: publication?.sid ?? null,
|
|
1192
|
+
vad: new Vad({ sampleRate: STT_RATE, silenceMs: this.options.silenceMs, threshold: this.options.threshold }),
|
|
1193
|
+
turn: null,
|
|
1194
|
+
speaking: false,
|
|
1195
|
+
closed: false,
|
|
1196
|
+
stream: null
|
|
1197
|
+
};
|
|
1198
|
+
this.live.set(session.sessionId, entry);
|
|
1199
|
+
const wanted = session.human.identity;
|
|
1200
|
+
const subscribe = (participant) => {
|
|
1201
|
+
if (participant.identity !== wanted) return;
|
|
1202
|
+
for (const pub of participant.trackPublications.values()) pub.setSubscribed(true);
|
|
1203
|
+
};
|
|
1204
|
+
for (const p of room.remoteParticipants.values()) subscribe(p);
|
|
1205
|
+
room.on(mod.RoomEvent.TrackPublished ?? "trackPublished", ((_pub, participant) => {
|
|
1206
|
+
subscribe(participant);
|
|
1207
|
+
}));
|
|
1208
|
+
room.on(mod.RoomEvent.TrackSubscribed ?? "trackSubscribed", ((audioTrack, _pub, participant) => {
|
|
1209
|
+
if (participant.identity !== wanted) return;
|
|
1210
|
+
void this.listen(entry, audioTrack, mod).catch((err) => this.fail(err));
|
|
1211
|
+
}));
|
|
1212
|
+
await this.agent.voiceState(session.sessionId, "listening").catch(() => void 0);
|
|
1213
|
+
}
|
|
1214
|
+
/** Ton des Menschen lesen, in Äußerungen schneiden und beantworten. */
|
|
1215
|
+
async listen(entry, audioTrack, mod) {
|
|
1216
|
+
const stream = new mod.AudioStream(audioTrack, { sampleRate: STT_RATE, numChannels: 1 });
|
|
1217
|
+
entry.stream = stream;
|
|
1218
|
+
for await (const frame of stream) {
|
|
1219
|
+
if (entry.closed) break;
|
|
1220
|
+
const pcm = frame.sampleRate === STT_RATE ? frame.data : resample(frame.data, frame.sampleRate, STT_RATE);
|
|
1221
|
+
for (const event of entry.vad.push(pcm)) {
|
|
1222
|
+
if (event.type === "start" && entry.speaking) this.bargeIn(entry);
|
|
1223
|
+
if (event.type === "end" && event.pcm.length > 0) void this.turn(entry, event.pcm).catch((err) => this.fail(err));
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
stream.close();
|
|
1227
|
+
}
|
|
1228
|
+
/** Ein Gesprächszug: verstehen → antworten → sprechen. Jeder Schritt kann unterbrochen werden. */
|
|
1229
|
+
async turn(entry, pcm) {
|
|
1230
|
+
const controller = new AbortController();
|
|
1231
|
+
entry.turn = controller;
|
|
1232
|
+
try {
|
|
1233
|
+
await this.state(entry, "thinking");
|
|
1234
|
+
const text = await this.options.stt.transcribe(pcm, STT_RATE, controller.signal);
|
|
1235
|
+
if (!text || controller.signal.aborted || entry.closed) return void await this.state(entry, "listening");
|
|
1236
|
+
this.options.onTranscript?.(text, entry.session);
|
|
1237
|
+
const answer = await this.options.reply({ text, session: entry.session, signal: controller.signal });
|
|
1238
|
+
if (!answer || controller.signal.aborted || entry.closed) return void await this.state(entry, "listening");
|
|
1239
|
+
if (this.options.subtitles !== false) {
|
|
1240
|
+
await this.agent.say(entry.session.conv, answer).catch(() => void 0);
|
|
1241
|
+
}
|
|
1242
|
+
await this.say(entry, answer, controller.signal);
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
if (!controller.signal.aborted) this.fail(err);
|
|
1245
|
+
} finally {
|
|
1246
|
+
if (entry.turn === controller) entry.turn = null;
|
|
1247
|
+
if (!entry.closed && !controller.signal.aborted) await this.state(entry, "listening");
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
/** Text sprechen: Blöcke gehen raus, sobald sie da sind – wer die ganze Datei abwartet, wartet zu lang. */
|
|
1251
|
+
async say(entry, text, signal) {
|
|
1252
|
+
const mod = await rtc();
|
|
1253
|
+
const rate = this.options.tts.sampleRate;
|
|
1254
|
+
const splitter = new FrameSplitter(Math.round(rate * FRAME_MS / 1e3));
|
|
1255
|
+
entry.speaking = true;
|
|
1256
|
+
await this.state(entry, "speaking", text);
|
|
1257
|
+
try {
|
|
1258
|
+
for await (const chunk of this.options.tts.speak(text, signal)) {
|
|
1259
|
+
if (signal.aborted || entry.closed) break;
|
|
1260
|
+
for (const frame of splitter.push(chunk)) {
|
|
1261
|
+
if (signal.aborted || entry.closed) break;
|
|
1262
|
+
await entry.source.captureFrame(new mod.AudioFrame(frame, rate, 1, frame.length));
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
const rest = splitter.flush();
|
|
1266
|
+
if (rest && !signal.aborted && !entry.closed) {
|
|
1267
|
+
await entry.source.captureFrame(new mod.AudioFrame(rest, rate, 1, rest.length));
|
|
1268
|
+
}
|
|
1269
|
+
if (!signal.aborted && !entry.closed) await entry.source.waitForPlayout();
|
|
1270
|
+
} finally {
|
|
1271
|
+
entry.speaking = false;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Barge-in: der Mensch redet dazwischen. Der laufende Zug wird abgebrochen **und** die schon in
|
|
1276
|
+
* LiveKit liegende Warteschlange geleert – ohne das Zweite redet der Agent noch sekundenlang weiter.
|
|
1277
|
+
*/
|
|
1278
|
+
bargeIn(entry) {
|
|
1279
|
+
entry.turn?.abort();
|
|
1280
|
+
entry.turn = null;
|
|
1281
|
+
entry.speaking = false;
|
|
1282
|
+
entry.source.clearQueue();
|
|
1283
|
+
void this.state(entry, "listening");
|
|
1284
|
+
}
|
|
1285
|
+
async state(entry, state, text) {
|
|
1286
|
+
if (entry.closed) return;
|
|
1287
|
+
await this.agent.voiceState(entry.session.sessionId, state, text).catch(() => void 0);
|
|
1288
|
+
}
|
|
1289
|
+
async close(sessionId) {
|
|
1290
|
+
const entry = this.live.get(sessionId);
|
|
1291
|
+
if (!entry) return;
|
|
1292
|
+
this.live.delete(sessionId);
|
|
1293
|
+
entry.closed = true;
|
|
1294
|
+
entry.turn?.abort();
|
|
1295
|
+
entry.vad.flush();
|
|
1296
|
+
entry.stream?.close();
|
|
1297
|
+
entry.source.clearQueue();
|
|
1298
|
+
try {
|
|
1299
|
+
if (entry.trackSid) await entry.room.localParticipant.unpublishTrack(entry.trackSid).catch(() => void 0);
|
|
1300
|
+
await entry.source.close?.();
|
|
1301
|
+
await entry.room.disconnect();
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
this.fail(err);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
async closeAll() {
|
|
1307
|
+
await Promise.all([...this.live.keys()].map((id) => this.close(id)));
|
|
1308
|
+
}
|
|
1309
|
+
fail(err) {
|
|
1310
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1311
|
+
if (this.options.onError) this.options.onError(error);
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
function attachVoice(agent, options) {
|
|
1315
|
+
return new EtazioVoice(agent, options).start();
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// src/speech.ts
|
|
1319
|
+
var OPENAI_BASE = "https://api.openai.com/v1";
|
|
1320
|
+
var OPENAI_TTS_RATE = 24e3;
|
|
1321
|
+
function openAiStt(options) {
|
|
1322
|
+
const base = (options.baseUrl ?? OPENAI_BASE).replace(/\/$/, "");
|
|
1323
|
+
const model = options.sttModel ?? "gpt-4o-mini-transcribe";
|
|
1324
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
1325
|
+
return {
|
|
1326
|
+
async transcribe(pcm, sampleRate, signal) {
|
|
1327
|
+
if (pcm.length === 0) return "";
|
|
1328
|
+
const form = new FormData();
|
|
1329
|
+
form.append("file", new Blob([toWav(pcm, sampleRate)], { type: "audio/wav" }), "speech.wav");
|
|
1330
|
+
form.append("model", model);
|
|
1331
|
+
form.append("response_format", "text");
|
|
1332
|
+
if (options.language) form.append("language", options.language);
|
|
1333
|
+
const res = await fetch(`${base}/audio/transcriptions`, {
|
|
1334
|
+
method: "POST",
|
|
1335
|
+
headers: { authorization: `Bearer ${options.apiKey}` },
|
|
1336
|
+
body: form,
|
|
1337
|
+
signal: signal ?? AbortSignal.timeout(timeoutMs)
|
|
1338
|
+
});
|
|
1339
|
+
if (!res.ok) throw new Error(`Erkennung fehlgeschlagen: HTTP ${res.status}`);
|
|
1340
|
+
return (await res.text()).trim();
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function openAiTts(options) {
|
|
1345
|
+
const base = (options.baseUrl ?? OPENAI_BASE).replace(/\/$/, "");
|
|
1346
|
+
const model = options.ttsModel ?? "gpt-4o-mini-tts";
|
|
1347
|
+
const voice = options.voice ?? "alloy";
|
|
1348
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
1349
|
+
return {
|
|
1350
|
+
sampleRate: OPENAI_TTS_RATE,
|
|
1351
|
+
async *speak(text, signal) {
|
|
1352
|
+
const res = await fetch(`${base}/audio/speech`, {
|
|
1353
|
+
method: "POST",
|
|
1354
|
+
headers: { authorization: `Bearer ${options.apiKey}`, "content-type": "application/json" },
|
|
1355
|
+
body: JSON.stringify({ model, voice, input: text, response_format: "pcm" }),
|
|
1356
|
+
signal: signal ?? AbortSignal.timeout(timeoutMs)
|
|
1357
|
+
});
|
|
1358
|
+
if (!res.ok || !res.body) throw new Error(`Stimme fehlgeschlagen: HTTP ${res.status}`);
|
|
1359
|
+
let carry = null;
|
|
1360
|
+
for await (const chunk of res.body) {
|
|
1361
|
+
let bytes = chunk;
|
|
1362
|
+
if (carry !== null) {
|
|
1363
|
+
const merged = new Uint8Array(bytes.length + 1);
|
|
1364
|
+
merged[0] = carry;
|
|
1365
|
+
merged.set(bytes, 1);
|
|
1366
|
+
bytes = merged;
|
|
1367
|
+
carry = null;
|
|
1368
|
+
}
|
|
1369
|
+
if (bytes.length % 2 === 1) {
|
|
1370
|
+
carry = bytes[bytes.length - 1];
|
|
1371
|
+
bytes = bytes.subarray(0, bytes.length - 1);
|
|
1372
|
+
}
|
|
1373
|
+
if (bytes.length > 0) yield pcmFromBytes(bytes);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// src/index.ts
|
|
1380
|
+
var EtazioAgentError = class extends Error {
|
|
1381
|
+
code;
|
|
1382
|
+
constructor(code, message) {
|
|
1383
|
+
super(message);
|
|
1384
|
+
this.name = "EtazioAgentError";
|
|
1385
|
+
this.code = code;
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
var DEFAULT_ACK_TIMEOUT_MS = 15e3;
|
|
1389
|
+
var EtazioAgent = class {
|
|
1390
|
+
options;
|
|
1391
|
+
bus = new EventEmitter();
|
|
1392
|
+
socket = null;
|
|
1393
|
+
helloResult = null;
|
|
1394
|
+
/** Über Reconnects hinweg festgehalten: damit Position und Sitzplatz erhalten bleiben */
|
|
1395
|
+
lastParticipantId = null;
|
|
1396
|
+
/** Wer sitzt gerade wo – nur so lässt sich beim Aufstehen der richtige Platz wieder freigeben */
|
|
1397
|
+
seatOf = /* @__PURE__ */ new Map();
|
|
1398
|
+
closed = false;
|
|
1399
|
+
constructor(options) {
|
|
1400
|
+
this.options = options;
|
|
1401
|
+
this.bus.setMaxListeners(50);
|
|
1402
|
+
}
|
|
1403
|
+
/** Ergebnis des letzten Handshakes (Scopes, Welt-Digest, Teilnehmer-ID). */
|
|
1404
|
+
get hello() {
|
|
1405
|
+
return this.helloResult;
|
|
1406
|
+
}
|
|
1407
|
+
get scopes() {
|
|
1408
|
+
return this.helloResult?.scopes ?? [];
|
|
1409
|
+
}
|
|
1410
|
+
/** Weltbeschreibung des letzten Handshakes – Räume, Objekte, Sitzplätze, wer da ist. */
|
|
1411
|
+
get world() {
|
|
1412
|
+
return this.helloResult?.world ?? null;
|
|
1413
|
+
}
|
|
1414
|
+
/** Teilnehmer-ID des Agenten in der Welt (null, solange kein Handshake lief). */
|
|
1415
|
+
get participantId() {
|
|
1416
|
+
return this.lastParticipantId;
|
|
1417
|
+
}
|
|
1418
|
+
get connected() {
|
|
1419
|
+
return !!this.socket?.connected && !!this.helloResult;
|
|
1420
|
+
}
|
|
1421
|
+
/** Darf der Agent das? Spart den Server-Roundtrip für Dinge, die ohnehin abgelehnt würden. */
|
|
1422
|
+
can(scope) {
|
|
1423
|
+
return hasScope(this.scopes, scope);
|
|
1424
|
+
}
|
|
1425
|
+
on(event, listener) {
|
|
1426
|
+
this.bus.on(event, listener);
|
|
1427
|
+
return this;
|
|
1428
|
+
}
|
|
1429
|
+
off(event, listener) {
|
|
1430
|
+
this.bus.off(event, listener);
|
|
1431
|
+
return this;
|
|
1432
|
+
}
|
|
1433
|
+
once(event, listener) {
|
|
1434
|
+
this.bus.once(event, listener);
|
|
1435
|
+
return this;
|
|
1436
|
+
}
|
|
1437
|
+
// -------------------------------------------------------------------------
|
|
1438
|
+
// Verbindung
|
|
1439
|
+
/** Verbindet und führt den Handshake aus; das Ergebnis ist danach auch als `agent.hello` verfügbar. */
|
|
1440
|
+
async connect() {
|
|
1441
|
+
if (this.socket) throw new EtazioAgentError("conflict", "Bereits verbunden");
|
|
1442
|
+
this.closed = false;
|
|
1443
|
+
const socket = io(this.options.url, {
|
|
1444
|
+
auth: { token: this.options.token },
|
|
1445
|
+
transports: ["websocket", "polling"],
|
|
1446
|
+
reconnection: this.options.autoReconnect !== false,
|
|
1447
|
+
// Backoff: schnell beim Schluckauf, geduldig beim Ausfall – und gestreut, damit nicht alle
|
|
1448
|
+
// Agenten eines Workspaces gleichzeitig wieder anklopfen.
|
|
1449
|
+
reconnectionDelay: 500,
|
|
1450
|
+
reconnectionDelayMax: 1e4,
|
|
1451
|
+
randomizationFactor: 0.5
|
|
1452
|
+
});
|
|
1453
|
+
this.socket = socket;
|
|
1454
|
+
this.wire(socket);
|
|
1455
|
+
return new Promise((resolve, reject) => {
|
|
1456
|
+
const timer = setTimeout(() => {
|
|
1457
|
+
cleanup();
|
|
1458
|
+
void this.close();
|
|
1459
|
+
reject(new EtazioAgentError("timeout", "Keine Antwort auf agent:hello"));
|
|
1460
|
+
}, this.options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS);
|
|
1461
|
+
const onReady = (result) => {
|
|
1462
|
+
cleanup();
|
|
1463
|
+
resolve(result);
|
|
1464
|
+
};
|
|
1465
|
+
const onError = (err) => {
|
|
1466
|
+
cleanup();
|
|
1467
|
+
reject(err);
|
|
1468
|
+
};
|
|
1469
|
+
const cleanup = () => {
|
|
1470
|
+
clearTimeout(timer);
|
|
1471
|
+
this.bus.off("ready", onReady);
|
|
1472
|
+
this.bus.off("error", onError);
|
|
1473
|
+
};
|
|
1474
|
+
this.bus.once("ready", onReady);
|
|
1475
|
+
this.bus.once("error", onError);
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Handshake erneut ausführen. Nötig nur, wenn sich das Office selbst geändert hat
|
|
1480
|
+
* (`office:published`) – wer kommt, geht oder sich setzt, wird ohnehin laufend nachgepflegt.
|
|
1481
|
+
*
|
|
1482
|
+
* Achtung: Der Server behandelt das wie ein Verlassen und erneutes Betreten der Welt; der Agent
|
|
1483
|
+
* steht danach wieder am Startpunkt. Ist er der einzige Teilnehmer, wird die Welt dabei entladen
|
|
1484
|
+
* und alle weiteren Befehle scheitern mit `not_found` (SPA-78).
|
|
1485
|
+
*/
|
|
1486
|
+
async refresh() {
|
|
1487
|
+
return this.sayHello();
|
|
1488
|
+
}
|
|
1489
|
+
/** Verbindung bewusst beenden (kein Reconnect mehr). */
|
|
1490
|
+
close() {
|
|
1491
|
+
this.closed = true;
|
|
1492
|
+
this.socket?.disconnect();
|
|
1493
|
+
this.socket = null;
|
|
1494
|
+
this.helloResult = null;
|
|
1495
|
+
}
|
|
1496
|
+
wire(socket) {
|
|
1497
|
+
socket.on("connect", () => {
|
|
1498
|
+
this.sayHello().catch((err) => this.fireError(err));
|
|
1499
|
+
});
|
|
1500
|
+
socket.on("connect_error", (err) => {
|
|
1501
|
+
const code = err.message === "unauthenticated" || err.message === "rate_limited" ? err.message : "unavailable";
|
|
1502
|
+
this.fire("error", new EtazioAgentError(code, err.message));
|
|
1503
|
+
});
|
|
1504
|
+
socket.on("disconnect", (reason) => {
|
|
1505
|
+
this.helloResult = null;
|
|
1506
|
+
this.fire("disconnect", reason);
|
|
1507
|
+
});
|
|
1508
|
+
socket.on(SOCKET_EVENTS.agentAddress, (p) => this.fire("address", p));
|
|
1509
|
+
socket.on(SOCKET_EVENTS.agentVoice, (p) => this.fire("voice", p));
|
|
1510
|
+
socket.on(SOCKET_EVENTS.chatMessage, (p) => this.fire("chat", p));
|
|
1511
|
+
socket.on(SOCKET_EVENTS.worldParticipantJoined, (p) => {
|
|
1512
|
+
this.trackJoined(p);
|
|
1513
|
+
this.fire("participantJoined", p);
|
|
1514
|
+
});
|
|
1515
|
+
socket.on(SOCKET_EVENTS.worldParticipantLeft, (p) => {
|
|
1516
|
+
this.trackLeft(p.participantId);
|
|
1517
|
+
this.fire("participantLeft", p);
|
|
1518
|
+
});
|
|
1519
|
+
socket.on(SOCKET_EVENTS.worldZone, (p) => {
|
|
1520
|
+
this.trackZone(p);
|
|
1521
|
+
this.fire("zone", p);
|
|
1522
|
+
});
|
|
1523
|
+
socket.on(SOCKET_EVENTS.worldSeat, (p) => {
|
|
1524
|
+
this.trackSeat(p);
|
|
1525
|
+
this.fire("seat", p);
|
|
1526
|
+
});
|
|
1527
|
+
socket.on(SOCKET_EVENTS.worldTick, (p) => this.trackTick(p));
|
|
1528
|
+
socket.on(SOCKET_EVENTS.worldEmote, (p) => this.fire("emote", p));
|
|
1529
|
+
socket.on(SOCKET_EVENTS.objectState, (p) => this.fire("objectState", p));
|
|
1530
|
+
socket.on(SOCKET_EVENTS.objectAction, (p) => this.fire("objectAction", p));
|
|
1531
|
+
socket.on(SOCKET_EVENTS.agentThrottled, (p) => this.fire("throttled", p));
|
|
1532
|
+
socket.on(SOCKET_EVENTS.agentRevoked, (p) => {
|
|
1533
|
+
this.closed = true;
|
|
1534
|
+
socket.io.opts.reconnection = false;
|
|
1535
|
+
this.fire("revoked", p);
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
async sayHello() {
|
|
1539
|
+
const input = {
|
|
1540
|
+
protocol: AGENT_PROTOCOL,
|
|
1541
|
+
capabilities: this.options.capabilities ?? [],
|
|
1542
|
+
client: { name: this.options.client?.name ?? "etazio-agent-sdk", version: this.options.client?.version ?? SDK_VERSION },
|
|
1543
|
+
...this.options.workspaceId ? { workspaceId: this.options.workspaceId } : {},
|
|
1544
|
+
...this.lastParticipantId ? { participantId: this.lastParticipantId } : {}
|
|
1545
|
+
};
|
|
1546
|
+
const result = await this.call(SOCKET_EVENTS.agentHello, input);
|
|
1547
|
+
this.helloResult = result;
|
|
1548
|
+
this.seatOf.clear();
|
|
1549
|
+
this.lastParticipantId = result.participantId;
|
|
1550
|
+
this.fire("ready", result);
|
|
1551
|
+
return result;
|
|
1552
|
+
}
|
|
1553
|
+
// -------------------------------------------------------------------------
|
|
1554
|
+
// Helfer (jeweils ein Server-Ereignis mit Ack)
|
|
1555
|
+
/** Chat-Nachricht in eine Konversation (`room:office`, `room:<zoneId>`, `dm:<a>:<b>`). */
|
|
1556
|
+
async say(conv, text, parentId) {
|
|
1557
|
+
const data = await this.call(SOCKET_EVENTS.chatSend, { conv, text, ...parentId ? { parentId } : {} });
|
|
1558
|
+
return data.message;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* „Schreibt gerade“ im Chat anzeigen (SPA-133). Die Anzeige verfällt beim Empfänger nach ~6 s –
|
|
1562
|
+
* für längere Arbeit `typingWhile()` nutzen, das selbst auffrischt. Mit `say()` endet sie ohnehin.
|
|
1563
|
+
*/
|
|
1564
|
+
typing(conv, on = true, parentId) {
|
|
1565
|
+
this.socket?.emit(SOCKET_EVENTS.chatTyping, { conv, parentId: parentId ?? null, typing: on });
|
|
1566
|
+
}
|
|
1567
|
+
/** `typing()` halten, solange `work` läuft – danach wieder aus. */
|
|
1568
|
+
async typingWhile(conv, work, parentId) {
|
|
1569
|
+
this.typing(conv, true, parentId);
|
|
1570
|
+
const timer = setInterval(() => this.typing(conv, true, parentId), CHAT_TYPING_REFRESH_MS);
|
|
1571
|
+
try {
|
|
1572
|
+
return await work();
|
|
1573
|
+
} finally {
|
|
1574
|
+
clearInterval(timer);
|
|
1575
|
+
this.typing(conv, false, parentId);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Fragen, die man anklickt statt abtippt (SPA-117): eine Bestätigungs-Karte in den Chat stellen.
|
|
1580
|
+
* `text` bleibt Pflicht und trägt dieselbe Frage in Worten – für Suche, Vorschau und alle, die
|
|
1581
|
+
* die Karte nicht sehen können. Die Antwort kommt als `agent:address` mit `kind: 'card.decision'`
|
|
1582
|
+
* zurück, nicht als Rückgabewert: entschieden wird, wann jemand mag.
|
|
1583
|
+
*/
|
|
1584
|
+
async ask(conv, text, block, parentId) {
|
|
1585
|
+
const data = await this.call(SOCKET_EVENTS.chatSend, {
|
|
1586
|
+
conv,
|
|
1587
|
+
text,
|
|
1588
|
+
blocks: [{ type: "confirm", ...block }],
|
|
1589
|
+
...parentId ? { parentId } : {}
|
|
1590
|
+
});
|
|
1591
|
+
return data.message;
|
|
1592
|
+
}
|
|
1593
|
+
/** Hingehen mit serverseitiger Wegfindung – kein Teleport, der Agent läuft wie ein Mensch. */
|
|
1594
|
+
async goTo(target) {
|
|
1595
|
+
return this.call(SOCKET_EVENTS.agentGoTo, target);
|
|
1596
|
+
}
|
|
1597
|
+
async sit(seatId) {
|
|
1598
|
+
return this.call(SOCKET_EVENTS.worldSit, { seatId });
|
|
1599
|
+
}
|
|
1600
|
+
async stand() {
|
|
1601
|
+
return this.call(SOCKET_EVENTS.worldStand, {});
|
|
1602
|
+
}
|
|
1603
|
+
/** Objektzustand setzen (Lampe, Fernseher, …). Der Zustand ist flach und ≤ 2 KB. */
|
|
1604
|
+
async setState(placementId, state) {
|
|
1605
|
+
return this.call(SOCKET_EVENTS.objectState, { placementId, state });
|
|
1606
|
+
}
|
|
1607
|
+
/** Einmalige Objektaktion auslösen (Kicker, Automat, …). */
|
|
1608
|
+
async action(placementId, action, data) {
|
|
1609
|
+
return this.call(SOCKET_EVENTS.objectAction, { placementId, action, ...data ? { data } : {} });
|
|
1610
|
+
}
|
|
1611
|
+
/** Bequemer Weg zum Fernseher: der TV-Kanal ist nur ein besonderer Objektzustand. */
|
|
1612
|
+
async setTv(placementId, state) {
|
|
1613
|
+
const flat = {};
|
|
1614
|
+
for (const [key, value] of Object.entries(state)) {
|
|
1615
|
+
if (value !== void 0) flat[key] = value;
|
|
1616
|
+
}
|
|
1617
|
+
return this.setState(placementId, flat);
|
|
1618
|
+
}
|
|
1619
|
+
/** Reaktion über dem Kopf (Glyph). */
|
|
1620
|
+
async react(glyph) {
|
|
1621
|
+
await this.call(SOCKET_EVENTS.worldReact, { glyph });
|
|
1622
|
+
}
|
|
1623
|
+
async wave() {
|
|
1624
|
+
await this.call(SOCKET_EVENTS.worldWave, {});
|
|
1625
|
+
}
|
|
1626
|
+
/** LiveKit-Token für den Raum, in dem der Agent laut Welt gerade steht (Scope `meeting.audio`). */
|
|
1627
|
+
async meetingToken(target = "office") {
|
|
1628
|
+
return this.call(SOCKET_EVENTS.agentMeetingToken, { target });
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Sprechzustand melden (Scope `voice.speak`, SPA-83). Das ist zweierlei zugleich: die Anzeige am
|
|
1632
|
+
* Avatar für alle im Raum und das Lebenszeichen der Sitzung – bleibt es aus, beendet der Server
|
|
1633
|
+
* das Gespräch wegen Stille.
|
|
1634
|
+
*/
|
|
1635
|
+
async voiceState(sessionId, state, text) {
|
|
1636
|
+
return this.call(SOCKET_EVENTS.agentVoiceState, { sessionId, state, ...text ? { text } : {} });
|
|
1637
|
+
}
|
|
1638
|
+
/** Whiteboard-Operation (Scope `board.write`); `baseVersion` kommt aus `board:sync`. */
|
|
1639
|
+
async boardOp(envelope) {
|
|
1640
|
+
return this.call(SOCKET_EVENTS.boardOp, envelope);
|
|
1641
|
+
}
|
|
1642
|
+
// -------------------------------------------------------------------------
|
|
1643
|
+
// Innereien
|
|
1644
|
+
/** Ein Ereignis mit Ack: jede Ablehnung des Servers wird hier zu einem Fehler mit Code. */
|
|
1645
|
+
call(event, payload) {
|
|
1646
|
+
const socket = this.socket;
|
|
1647
|
+
if (!socket || !socket.connected) return Promise.reject(new EtazioAgentError("unavailable", "Nicht verbunden"));
|
|
1648
|
+
return new Promise((resolve, reject) => {
|
|
1649
|
+
let settled = false;
|
|
1650
|
+
const timer = setTimeout(() => {
|
|
1651
|
+
if (settled) return;
|
|
1652
|
+
settled = true;
|
|
1653
|
+
reject(new EtazioAgentError("timeout", `Keine Antwort auf ${event}`));
|
|
1654
|
+
}, this.options.ackTimeoutMs ?? DEFAULT_ACK_TIMEOUT_MS);
|
|
1655
|
+
socket.emit(event, payload, (ack) => {
|
|
1656
|
+
if (settled) return;
|
|
1657
|
+
settled = true;
|
|
1658
|
+
clearTimeout(timer);
|
|
1659
|
+
if (!ack?.ok) return reject(new EtazioAgentError(ack?.error?.code ?? "internal", ack?.error?.message ?? `${event} fehlgeschlagen`));
|
|
1660
|
+
resolve(ack.data);
|
|
1661
|
+
});
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
// Der Welt-Digest aus dem Handshake ist eine Momentaufnahme. Die beweglichen Teile werden aus den
|
|
1665
|
+
// laufenden Ereignissen nachgepflegt – sonst müsste ein Agent für jede Frage „wer ist da?“ einen
|
|
1666
|
+
// neuen Handshake schicken, und das Office bekäme jedes Mal ein Kommen und Gehen zu sehen.
|
|
1667
|
+
trackJoined(p) {
|
|
1668
|
+
const world = this.helloResult?.world;
|
|
1669
|
+
if (!world || world.participants.some((x) => x.id === p.id)) return;
|
|
1670
|
+
world.participants.push({ id: p.id, name: p.displayName, kind: p.kind ?? "user", zoneId: p.zoneId, x: p.x, z: p.z });
|
|
1671
|
+
if (p.seatId) {
|
|
1672
|
+
this.seatOf.set(p.id, p.seatId);
|
|
1673
|
+
this.markSeat(p.seatId, true);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
trackLeft(participantId) {
|
|
1677
|
+
const world = this.helloResult?.world;
|
|
1678
|
+
if (!world) return;
|
|
1679
|
+
world.participants = world.participants.filter((x) => x.id !== participantId);
|
|
1680
|
+
const seatId = this.seatOf.get(participantId);
|
|
1681
|
+
if (seatId) {
|
|
1682
|
+
this.seatOf.delete(participantId);
|
|
1683
|
+
this.markSeat(seatId, false);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
trackZone(payload) {
|
|
1687
|
+
const p = this.helloResult?.world.participants.find((x) => x.id === payload.participantId);
|
|
1688
|
+
if (p) p.zoneId = payload.zoneId;
|
|
1689
|
+
}
|
|
1690
|
+
trackSeat(payload) {
|
|
1691
|
+
const previous = this.seatOf.get(payload.participantId);
|
|
1692
|
+
if (previous && previous !== payload.seatId) this.markSeat(previous, false);
|
|
1693
|
+
if (payload.seatId) {
|
|
1694
|
+
this.seatOf.set(payload.participantId, payload.seatId);
|
|
1695
|
+
this.markSeat(payload.seatId, true);
|
|
1696
|
+
} else {
|
|
1697
|
+
this.seatOf.delete(payload.participantId);
|
|
1698
|
+
}
|
|
1699
|
+
const p = this.helloResult?.world.participants.find((x) => x.id === payload.participantId);
|
|
1700
|
+
if (p) {
|
|
1701
|
+
p.x = payload.x;
|
|
1702
|
+
p.z = payload.z;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
markSeat(seatId, taken) {
|
|
1706
|
+
const seat = this.helloResult?.world.seats.find((s) => s.id === seatId);
|
|
1707
|
+
if (seat) seat.taken = taken;
|
|
1708
|
+
}
|
|
1709
|
+
trackTick(payload) {
|
|
1710
|
+
const world = this.helloResult?.world;
|
|
1711
|
+
if (!world) return;
|
|
1712
|
+
for (const [id, x, z12] of payload.u) {
|
|
1713
|
+
const p = world.participants.find((entry) => entry.id === id);
|
|
1714
|
+
if (p) {
|
|
1715
|
+
p.x = x;
|
|
1716
|
+
p.z = z12;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
fire(event, ...args) {
|
|
1721
|
+
if (event === "error" && this.bus.listenerCount("error") === 0) return;
|
|
1722
|
+
this.bus.emit(event, ...args);
|
|
1723
|
+
}
|
|
1724
|
+
fireError(err) {
|
|
1725
|
+
if (this.closed) return;
|
|
1726
|
+
this.fire("error", err instanceof EtazioAgentError ? err : new EtazioAgentError("internal", err instanceof Error ? err.message : "Fehler"));
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
async function connect(options) {
|
|
1730
|
+
const agent = new EtazioAgent(options);
|
|
1731
|
+
await agent.connect();
|
|
1732
|
+
return agent;
|
|
1733
|
+
}
|
|
1734
|
+
export {
|
|
1735
|
+
AGENT_PROTOCOL,
|
|
1736
|
+
EtazioAgent,
|
|
1737
|
+
EtazioAgentError,
|
|
1738
|
+
EtazioVoice,
|
|
1739
|
+
FrameSplitter,
|
|
1740
|
+
SDK_VERSION,
|
|
1741
|
+
Vad,
|
|
1742
|
+
attachVoice,
|
|
1743
|
+
concat,
|
|
1744
|
+
connect,
|
|
1745
|
+
openAiStt,
|
|
1746
|
+
openAiTts,
|
|
1747
|
+
pcmFromBytes,
|
|
1748
|
+
resample,
|
|
1749
|
+
rms,
|
|
1750
|
+
toWav
|
|
1751
|
+
};
|
|
1752
|
+
//# sourceMappingURL=index.js.map
|