@fraqjs/fraq 0.10.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,201 +1,4 @@
1
1
  import mitt from "mitt";
2
- //#region src/protocol/client.ts
3
- var MilkyClientBase = class {
4
- baseUrl;
5
- wsBaseUrl;
6
- accessToken;
7
- baseHeaders;
8
- constructor(baseUrl, options) {
9
- const normalizedBaseUrl = baseUrl.toString();
10
- this.baseUrl = normalizedBaseUrl.endsWith("/") ? normalizedBaseUrl.slice(0, -1) : normalizedBaseUrl;
11
- this.wsBaseUrl = this.baseUrl.replace(/^http/, "ws");
12
- this.accessToken = options?.accessToken;
13
- this.baseHeaders = { "Content-Type": "application/json" };
14
- if (options?.accessToken) this.baseHeaders.Authorization = `Bearer ${options.accessToken}`;
15
- }
16
- async callApi(endpoint, params) {
17
- const response = await fetch(`${this.baseUrl}/api/${endpoint}`, {
18
- method: "POST",
19
- body: JSON.stringify(params ?? {}),
20
- headers: this.baseHeaders
21
- });
22
- if (!response.ok) throw new Error(`API call ${endpoint} failed with HTTP status ${response.status}`);
23
- const json = await response.json();
24
- if (json.status === "failed") throw new Error(`API call ${endpoint} failed with retcode ${json.retcode}: ${json.message}`);
25
- return json.data;
26
- }
27
- async startEvents(onEvent) {
28
- const ws = new WebSocket(`${this.wsBaseUrl}/event${this.accessToken ? `?access_token=${this.accessToken}` : ""}`);
29
- let closeSubscription = () => {};
30
- const closed = new Promise((resolve, reject) => {
31
- closeSubscription = (error) => {
32
- if (error) reject(error);
33
- else resolve();
34
- };
35
- });
36
- ws.addEventListener("message", async (event) => {
37
- try {
38
- if (typeof event.data !== "string") throw new Error(`Expected text frame, got ${typeof event.data}`);
39
- await onEvent(JSON.parse(event.data));
40
- } catch (error) {
41
- closeSubscription(error);
42
- ws.close();
43
- }
44
- });
45
- await new Promise((resolve, reject) => {
46
- ws.addEventListener("open", () => resolve(), { once: true });
47
- ws.addEventListener("error", (event) => reject(/* @__PURE__ */ new Error(`WebSocket error: ${event}`)));
48
- });
49
- ws.addEventListener("error", (event) => closeSubscription(/* @__PURE__ */ new Error(`WebSocket error: ${event}`)));
50
- ws.addEventListener("close", () => closeSubscription(), { once: true });
51
- return {
52
- closed,
53
- stop() {
54
- ws.close();
55
- }
56
- };
57
- }
58
- };
59
- function createMilkyClient(...params) {
60
- const base = new MilkyClientBase(...params);
61
- return new Proxy(base, { get(target, endpoint) {
62
- if (typeof endpoint === "string" && endpoint.includes("_")) return (params) => target.callApi(endpoint, params);
63
- else return target[endpoint];
64
- } });
65
- }
66
- //#endregion
67
- //#region src/protocol/segment.ts
68
- function msg(strings, ...values) {
69
- let buffer = "";
70
- const segments = [];
71
- for (let i = 0; i < strings.length; i++) {
72
- buffer += strings[i];
73
- if (i < values.length) {
74
- const value = values[i];
75
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") buffer += value.toString();
76
- else {
77
- if (buffer) {
78
- segments.push({
79
- type: "text",
80
- data: { text: buffer }
81
- });
82
- buffer = "";
83
- }
84
- segments.push(value);
85
- }
86
- }
87
- }
88
- if (buffer) segments.push({
89
- type: "text",
90
- data: { text: buffer }
91
- });
92
- return trimBoundaryTextSegments(segments);
93
- }
94
- function isTextSegment(segment) {
95
- return segment.type === "text";
96
- }
97
- function withText(segment, text) {
98
- return {
99
- ...segment,
100
- data: {
101
- ...segment.data,
102
- text
103
- }
104
- };
105
- }
106
- function trimBoundaryTextSegments(segments) {
107
- const result = [...segments];
108
- const first = result[0];
109
- if (first && isTextSegment(first)) {
110
- const text = first.data.text.trimStart();
111
- if (text) result[0] = withText(first, text);
112
- else result.shift();
113
- }
114
- const lastIndex = result.length - 1;
115
- const last = result[lastIndex];
116
- if (last && isTextSegment(last)) {
117
- const text = last.data.text.trimEnd();
118
- if (text) result[lastIndex] = withText(last, text);
119
- else result.pop();
120
- }
121
- return result;
122
- }
123
- let seg;
124
- (function(_seg) {
125
- function mention(userId) {
126
- return {
127
- type: "mention",
128
- data: { user_id: userId }
129
- };
130
- }
131
- _seg.mention = mention;
132
- function mentionAll() {
133
- return {
134
- type: "mention_all",
135
- data: {}
136
- };
137
- }
138
- _seg.mentionAll = mentionAll;
139
- function face(faceId, options) {
140
- return {
141
- type: "face",
142
- data: {
143
- face_id: faceId.toString(),
144
- is_large: options?.isLarge ?? false
145
- }
146
- };
147
- }
148
- _seg.face = face;
149
- function reply(messageSeq) {
150
- return {
151
- type: "reply",
152
- data: { message_seq: messageSeq }
153
- };
154
- }
155
- _seg.reply = reply;
156
- function image(uri, options) {
157
- return {
158
- type: "image",
159
- data: {
160
- uri,
161
- sub_type: options?.subType,
162
- summary: options?.summary
163
- }
164
- };
165
- }
166
- _seg.image = image;
167
- function record(uri) {
168
- return {
169
- type: "record",
170
- data: { uri }
171
- };
172
- }
173
- _seg.record = record;
174
- function video(uri, options) {
175
- return {
176
- type: "video",
177
- data: {
178
- uri,
179
- thumb_uri: options?.thumbUri
180
- }
181
- };
182
- }
183
- _seg.video = video;
184
- function forward(messages, options) {
185
- return {
186
- type: "forward",
187
- data: {
188
- messages,
189
- title: options?.title,
190
- preview: options?.preview,
191
- summary: options?.summary,
192
- prompt: options?.prompt
193
- }
194
- };
195
- }
196
- _seg.forward = forward;
197
- })(seg || (seg = {}));
198
- //#endregion
199
2
  //#region src/routing/command.ts
200
3
  var CommandBuilder = class {
201
4
  name;
@@ -205,6 +8,7 @@ var CommandBuilder = class {
205
8
  description;
206
9
  aliases;
207
10
  hidden;
11
+ routeMeta;
208
12
  constructor(name, sink = (command) => command) {
209
13
  this.name = name;
210
14
  this.sink = sink;
@@ -226,6 +30,14 @@ var CommandBuilder = class {
226
30
  this.hidden = true;
227
31
  return this;
228
32
  }
33
+ meta(meta) {
34
+ this.routeMeta = mergeRouteMeta(this.routeMeta, meta);
35
+ return this;
36
+ }
37
+ tag(...tags) {
38
+ this.routeMeta = mergeRouteMeta(this.routeMeta, { tags });
39
+ return this;
40
+ }
229
41
  execute(executor) {
230
42
  this.executor = executor;
231
43
  const command = {
@@ -236,9 +48,31 @@ var CommandBuilder = class {
236
48
  if (this.description !== void 0) command.description = this.description;
237
49
  if (this.aliases !== void 0) command.aliases = this.aliases;
238
50
  if (this.hidden !== void 0) command.hidden = this.hidden;
51
+ if (this.routeMeta !== void 0) command.meta = this.routeMeta;
239
52
  return this.sink(command);
240
53
  }
241
54
  };
55
+ function mergeRouteMeta(left, right) {
56
+ if (left === void 0 && right === void 0) return;
57
+ const merged = {
58
+ ...left ?? {},
59
+ ...right ?? {}
60
+ };
61
+ const tags = uniqueTags(left?.tags, right?.tags);
62
+ if (tags.length > 0) merged.tags = tags;
63
+ else delete merged.tags;
64
+ return Object.keys(merged).length > 0 ? merged : void 0;
65
+ }
66
+ function uniqueTags(...tagLists) {
67
+ const tags = [];
68
+ const seen = /* @__PURE__ */ new Set();
69
+ for (const tagList of tagLists) for (const tag of tagList ?? []) {
70
+ if (seen.has(tag)) continue;
71
+ seen.add(tag);
72
+ tags.push(tag);
73
+ }
74
+ return tags;
75
+ }
242
76
  //#endregion
243
77
  //#region src/routing/tokenizer.ts
244
78
  const WHITESPACE_CHARS = new Set([
@@ -343,6 +177,33 @@ var Tokenizer = class {
343
177
  this.subOffset = void 0;
344
178
  return segments;
345
179
  }
180
+ consumeMention(userId) {
181
+ const token = this.peek();
182
+ if (typeof token === "object" && token !== null && token.type === "mention" && token.data.user_id === userId) {
183
+ this.next();
184
+ return true;
185
+ }
186
+ return false;
187
+ }
188
+ consumeTextPrefix(prefix) {
189
+ if (prefix.length === 0) return false;
190
+ const position = this.findNextPosition();
191
+ if (position === void 0) return false;
192
+ const segment = this.segments[position.offset];
193
+ if (segment.type !== "text") return false;
194
+ const prefixStart = this.findTextTokenStart(segment.data.text, position.subOffset ?? 0);
195
+ if (!segment.data.text.startsWith(prefix, prefixStart)) return false;
196
+ const prefixEnd = prefixStart + prefix.length;
197
+ const nextSubOffset = this.findTextTokenStart(segment.data.text, prefixEnd);
198
+ if (nextSubOffset < segment.data.text.length) {
199
+ this.offset = position.offset;
200
+ this.subOffset = nextSubOffset;
201
+ } else {
202
+ this.offset = position.offset + 1;
203
+ this.subOffset = void 0;
204
+ }
205
+ return true;
206
+ }
346
207
  findNextPosition() {
347
208
  let offset = this.offset;
348
209
  let subOffset = this.subOffset;
@@ -374,19 +235,35 @@ var Tokenizer = class {
374
235
  };
375
236
  //#endregion
376
237
  //#region src/routing/router.ts
238
+ const DEFAULT_ACTIVATIONS = [{ type: "direct" }];
239
+ const defaultRouteActivationResolver = () => DEFAULT_ACTIVATIONS;
377
240
  var Router = class Router {
378
241
  entries = [];
379
242
  groups = /* @__PURE__ */ new Map();
243
+ activationResolver = defaultRouteActivationResolver;
244
+ scopeMeta;
245
+ setActivationResolver(resolver) {
246
+ this.activationResolver = resolver;
247
+ return this;
248
+ }
249
+ withMeta(meta) {
250
+ const router = new Router();
251
+ router.entries = this.entries;
252
+ router.groups = this.groups;
253
+ router.scopeMeta = mergeRouteMeta(this.scopeMeta, meta);
254
+ return router;
255
+ }
380
256
  command(command) {
381
257
  if (typeof command === "string") return new CommandBuilder(command, (builtCommand) => {
382
258
  this.command(builtCommand);
383
259
  return builtCommand;
384
260
  });
385
- this.validatePattern(command.pattern);
386
- this.resolveAliasConflicts(command);
261
+ const scopedCommand = this.applyScopeMeta(command);
262
+ this.validatePattern(scopedCommand.pattern);
263
+ this.resolveAliasConflicts(scopedCommand);
387
264
  this.entries.push({
388
265
  type: "command",
389
- command
266
+ command: scopedCommand
390
267
  });
391
268
  return this;
392
269
  }
@@ -395,10 +272,11 @@ var Router = class Router {
395
272
  this.rawPattern(builtCommand);
396
273
  return builtCommand;
397
274
  });
398
- this.validatePattern(rawPattern.pattern, { rawPattern: true });
275
+ const scopedRawPattern = this.applyScopeMeta(rawPattern);
276
+ this.validatePattern(scopedRawPattern.pattern, { rawPattern: true });
399
277
  this.entries.push({
400
278
  type: "rawPattern",
401
- rawPattern
279
+ rawPattern: scopedRawPattern
402
280
  });
403
281
  return this;
404
282
  }
@@ -413,7 +291,7 @@ var Router = class Router {
413
291
  router
414
292
  });
415
293
  }
416
- return router;
294
+ return this.scopeMeta ? router.withMeta(this.scopeMeta) : router;
417
295
  }
418
296
  filter(predicate) {
419
297
  const router = new Router();
@@ -422,7 +300,7 @@ var Router = class Router {
422
300
  predicate,
423
301
  router
424
302
  });
425
- return router;
303
+ return this.scopeMeta ? router.withMeta(this.scopeMeta) : router;
426
304
  }
427
305
  routes() {
428
306
  return this.entries;
@@ -432,11 +310,19 @@ var Router = class Router {
432
310
  return [];
433
311
  }
434
312
  branches(session) {
435
- return this.branchesFrom(session, []);
313
+ return [...this.branchesFrom(session, [], { includeHidden: false })];
436
314
  }
437
315
  match(session, message) {
438
- const tokenizer = new Tokenizer(message.segments);
439
- return this.matchFrom(session, tokenizer, []);
316
+ for (const branch of this.branchesFrom(session, [], { includeHidden: true })) {
317
+ const descriptor = this.describeBranch(branch);
318
+ const activations = this.activationResolver(descriptor, session) ?? DEFAULT_ACTIVATIONS;
319
+ for (const activation of activations) {
320
+ const tokenizer = new Tokenizer(message.segments);
321
+ if (!this.consumeActivation(tokenizer, activation, session)) continue;
322
+ const match = this.matchBranch(branch, tokenizer, activation);
323
+ if (match !== void 0) return match;
324
+ }
325
+ }
440
326
  }
441
327
  async dispatch(session, message) {
442
328
  const match = this.match(session, message);
@@ -451,26 +337,51 @@ var Router = class Router {
451
337
  }
452
338
  return true;
453
339
  }
454
- matchFrom(session, tokenizer, path) {
455
- const initialState = tokenizer.getState();
456
- for (const entry of this.entries) {
457
- tokenizer.setState(initialState);
458
- const match = this.matchEntry(entry, session, tokenizer, path);
459
- if (match !== void 0) return match;
340
+ describeBranch(branch) {
341
+ switch (branch.type) {
342
+ case "command": {
343
+ const descriptor = {
344
+ type: "command",
345
+ path: [...branch.path],
346
+ name: branch.command.name
347
+ };
348
+ if (branch.command.aliases !== void 0) descriptor.aliases = [...branch.command.aliases];
349
+ if (branch.command.meta !== void 0) descriptor.meta = branch.command.meta;
350
+ return descriptor;
351
+ }
352
+ case "rawPattern": {
353
+ const descriptor = {
354
+ type: "rawPattern",
355
+ path: [...branch.path]
356
+ };
357
+ if (branch.rawPattern.meta !== void 0) descriptor.meta = branch.rawPattern.meta;
358
+ return descriptor;
359
+ }
460
360
  }
461
- tokenizer.setState(initialState);
462
361
  }
463
- matchEntry(entry, session, tokenizer, path) {
464
- switch (entry.type) {
465
- case "command": return this.matchCommand(entry.command, tokenizer, path);
466
- case "group": return this.matchGroup(entry.name, entry.router, session, tokenizer, path);
467
- case "filter":
468
- if (entry.predicate(session) !== true) return;
469
- return entry.router.matchFrom(session, tokenizer, path);
470
- case "rawPattern": return this.matchRawPattern(entry.rawPattern, tokenizer, path);
362
+ consumeActivation(tokenizer, activation, session) {
363
+ switch (activation.type) {
364
+ case "direct": return true;
365
+ case "mention": return tokenizer.consumeMention(session.selfId);
366
+ case "prefix": return tokenizer.consumeTextPrefix(activation.prefix);
367
+ }
368
+ }
369
+ matchBranch(branch, tokenizer, activation) {
370
+ if (!this.matchPath(branch.path, tokenizer)) return;
371
+ switch (branch.type) {
372
+ case "command": return this.matchCommand(branch.command, tokenizer, branch.path, activation);
373
+ case "rawPattern": return this.matchRawPattern(branch.rawPattern, tokenizer, branch.path, activation);
374
+ }
375
+ }
376
+ matchPath(path, tokenizer) {
377
+ for (const name of path) {
378
+ const token = tokenizer.peek();
379
+ if (typeof token !== "string" || token !== name) return false;
380
+ tokenizer.next();
471
381
  }
382
+ return true;
472
383
  }
473
- matchCommand(command, tokenizer, path) {
384
+ matchCommand(command, tokenizer, path, activation) {
474
385
  const token = tokenizer.peek();
475
386
  if (typeof token !== "string" || token !== command.name && !command.aliases?.includes(token)) return;
476
387
  tokenizer.next();
@@ -480,50 +391,55 @@ var Router = class Router {
480
391
  type: "command",
481
392
  path: [...path],
482
393
  command,
483
- params
394
+ params,
395
+ activation
484
396
  };
485
397
  }
486
- matchGroup(name, router, session, tokenizer, path) {
487
- const token = tokenizer.peek();
488
- if (typeof token !== "string" || token !== name) return;
489
- tokenizer.next();
490
- return router.matchFrom(session, tokenizer, [...path, name]);
491
- }
492
- matchRawPattern(rawPattern, tokenizer, path) {
398
+ matchRawPattern(rawPattern, tokenizer, path, activation) {
493
399
  const params = this.capturePattern(rawPattern.pattern, tokenizer);
494
400
  if (params === void 0 || tokenizer.hasNext()) return;
495
401
  return {
496
402
  type: "rawPattern",
497
403
  path: [...path],
498
404
  rawPattern,
499
- params
405
+ params,
406
+ activation
500
407
  };
501
408
  }
502
- branchesFrom(session, path) {
503
- const branches = [];
409
+ *branchesFrom(session, path, options) {
504
410
  for (const entry of this.entries) switch (entry.type) {
505
411
  case "command":
506
- if (!entry.command.hidden) branches.push({
412
+ if (options.includeHidden || !entry.command.hidden) yield {
507
413
  type: "command",
508
414
  path: [...path],
509
- command: entry.command
510
- });
415
+ command: entry.command,
416
+ meta: entry.command.meta
417
+ };
511
418
  break;
512
419
  case "group":
513
- branches.push(...entry.router.branchesFrom(session, [...path, entry.name]));
420
+ yield* entry.router.branchesFrom(session, [...path, entry.name], options);
514
421
  break;
515
422
  case "filter":
516
- if (entry.predicate(session) === true) branches.push(...entry.router.branchesFrom(session, path));
423
+ if (entry.predicate(session) === true) yield* entry.router.branchesFrom(session, path, options);
517
424
  break;
518
425
  case "rawPattern":
519
- branches.push({
426
+ yield {
520
427
  type: "rawPattern",
521
428
  path: [...path],
522
- rawPattern: entry.rawPattern
523
- });
429
+ rawPattern: entry.rawPattern,
430
+ meta: entry.rawPattern.meta
431
+ };
524
432
  break;
525
433
  }
526
- return branches;
434
+ }
435
+ applyScopeMeta(route) {
436
+ if (this.scopeMeta === void 0) return route;
437
+ const meta = mergeRouteMeta(route.meta, this.scopeMeta);
438
+ if (meta === void 0) return route;
439
+ return {
440
+ ...route,
441
+ meta
442
+ };
527
443
  }
528
444
  resolveAliasConflicts(command) {
529
445
  for (const entry of this.entries) {
@@ -570,6 +486,283 @@ var Router = class Router {
570
486
  }
571
487
  };
572
488
  //#endregion
489
+ //#region src/core/activation.ts
490
+ function createContextRouteActivationResolver(routing, fallback = defaultRouteActivationResolver) {
491
+ if (!routing) return fallback;
492
+ if (routing.activation && routing.activationResolver) throw new Error("Context routing cannot specify both activation and activationResolver.");
493
+ if (routing.activationResolver) return routing.activationResolver;
494
+ if (routing.activation) return compileContextRouteActivationConfig(routing.activation, fallback);
495
+ return fallback;
496
+ }
497
+ function compileContextRouteActivationConfig(config, fallback) {
498
+ return (route, session) => {
499
+ for (const rule of contextRouteActivationRules(config.rules)) {
500
+ if (!matchesContextRouteActivationRule(route, session, rule)) continue;
501
+ const activation = resolveContextRouteActivation(rule.activation, session.raw.message_scene);
502
+ if (activation !== void 0) return activation;
503
+ }
504
+ if (config.default !== void 0) {
505
+ const activation = resolveContextRouteActivation(config.default, session.raw.message_scene);
506
+ if (activation !== void 0) return activation;
507
+ }
508
+ return fallback(route, session);
509
+ };
510
+ }
511
+ function contextRouteActivationRules(rules) {
512
+ if (rules === void 0) return [];
513
+ return isContextRouteActivationRuleArray(rules) ? rules : [rules];
514
+ }
515
+ function isContextRouteActivationRuleArray(rules) {
516
+ return Array.isArray(rules);
517
+ }
518
+ function resolveContextRouteActivation(activation, scene) {
519
+ if (isRouteActivationArray(activation)) return activation;
520
+ return activation[scene] ?? activation.default;
521
+ }
522
+ function isRouteActivationArray(activation) {
523
+ return Array.isArray(activation);
524
+ }
525
+ function matchesContextRouteActivationRule(route, session, rule) {
526
+ const match = rule.match;
527
+ if (!match) return true;
528
+ if (!matchesStringSelector(match.type, route.type)) return false;
529
+ if (!matchesStringSelector(match.plugin, routeMetaString(route, "plugin"))) return false;
530
+ if (!matchesStringSelector(match.context, routeMetaString(route, "context"))) return false;
531
+ if (!matchesTagSelector(match.tag, route.meta?.tags)) return false;
532
+ if (match.command !== void 0 && (route.type !== "command" || !matchesStringSelector(match.command, route.name))) return false;
533
+ if (match.path !== void 0 && !sameStringArray(match.path, route.path)) return false;
534
+ if (match.route !== void 0 && !sameStringArray(match.route, routeSegments(route))) return false;
535
+ if (match.predicate && match.predicate(route, session) !== true) return false;
536
+ return true;
537
+ }
538
+ function routeMetaString(route, key) {
539
+ const value = route.meta?.[key];
540
+ return typeof value === "string" ? value : void 0;
541
+ }
542
+ function matchesStringSelector(selector, value) {
543
+ if (selector === void 0) return true;
544
+ if (value === void 0) return false;
545
+ return typeof selector === "string" ? selector === value : selector.includes(value);
546
+ }
547
+ function matchesTagSelector(selector, tags) {
548
+ if (selector === void 0) return true;
549
+ if (!tags) return false;
550
+ return typeof selector === "string" ? tags.includes(selector) : selector.some((tag) => tags.includes(tag));
551
+ }
552
+ function routeSegments(route) {
553
+ if (route.type === "command") return [...route.path, route.name];
554
+ return route.path;
555
+ }
556
+ function sameStringArray(left, right) {
557
+ return left.length === right.length && left.every((value, index) => value === right[index]);
558
+ }
559
+ //#endregion
560
+ //#region src/protocol/client.ts
561
+ var MilkyClientBase = class {
562
+ baseUrl;
563
+ wsBaseUrl;
564
+ accessToken;
565
+ baseHeaders;
566
+ constructor(baseUrl, options) {
567
+ const normalizedBaseUrl = baseUrl.toString();
568
+ this.baseUrl = normalizedBaseUrl.endsWith("/") ? normalizedBaseUrl.slice(0, -1) : normalizedBaseUrl;
569
+ this.wsBaseUrl = this.baseUrl.replace(/^http/, "ws");
570
+ this.accessToken = options?.accessToken;
571
+ this.baseHeaders = { "Content-Type": "application/json" };
572
+ if (options?.accessToken) this.baseHeaders.Authorization = `Bearer ${options.accessToken}`;
573
+ }
574
+ async callApi(endpoint, params) {
575
+ const response = await fetch(`${this.baseUrl}/api/${endpoint}`, {
576
+ method: "POST",
577
+ body: JSON.stringify(params ?? {}),
578
+ headers: this.baseHeaders
579
+ });
580
+ if (!response.ok) throw new Error(`API call ${endpoint} failed with HTTP status ${response.status}`);
581
+ const json = await response.json();
582
+ if (json.status === "failed") throw new Error(`API call ${endpoint} failed with retcode ${json.retcode}: ${json.message}`);
583
+ return json.data;
584
+ }
585
+ };
586
+ function createMilkyClient(...params) {
587
+ const base = new MilkyClientBase(...params);
588
+ return new Proxy(base, { get(target, endpoint) {
589
+ if (typeof endpoint === "string" && endpoint.includes("_")) return (params) => target.callApi(endpoint, params);
590
+ else return target[endpoint];
591
+ } });
592
+ }
593
+ //#endregion
594
+ //#region src/protocol/event.ts
595
+ function createMilkyWebSocketEventSource(baseUrl, options) {
596
+ const normalizedBaseUrl = baseUrl.toString();
597
+ const wsBaseUrl = normalizedBaseUrl.endsWith("/") ? normalizedBaseUrl.slice(0, -1).replace(/^http/, "ws") : normalizedBaseUrl.replace(/^http/, "ws");
598
+ return {
599
+ name: "websocket",
600
+ async start(onEvent) {
601
+ const ws = new WebSocket(`${wsBaseUrl}/event${options?.accessToken ? `?access_token=${options.accessToken}` : ""}`);
602
+ let closeSubscription = () => {};
603
+ const closed = new Promise((resolve, reject) => {
604
+ closeSubscription = (error) => {
605
+ if (error) reject(error);
606
+ else resolve();
607
+ };
608
+ });
609
+ ws.addEventListener("message", async (event) => {
610
+ try {
611
+ if (typeof event.data !== "string") throw new Error(`Expected text frame, got ${typeof event.data}`);
612
+ await onEvent(JSON.parse(event.data));
613
+ } catch (error) {
614
+ closeSubscription(error);
615
+ ws.close();
616
+ }
617
+ });
618
+ await new Promise((resolve, reject) => {
619
+ ws.addEventListener("open", () => resolve(), { once: true });
620
+ ws.addEventListener("error", (event) => reject(/* @__PURE__ */ new Error(`WebSocket error: ${event}`)));
621
+ });
622
+ ws.addEventListener("error", (event) => closeSubscription(/* @__PURE__ */ new Error(`WebSocket error: ${event}`)));
623
+ ws.addEventListener("close", () => closeSubscription(), { once: true });
624
+ return {
625
+ closed,
626
+ stop() {
627
+ ws.close();
628
+ }
629
+ };
630
+ }
631
+ };
632
+ }
633
+ //#endregion
634
+ //#region src/protocol/segment.ts
635
+ function msg(strings, ...values) {
636
+ let buffer = "";
637
+ const segments = [];
638
+ for (let i = 0; i < strings.length; i++) {
639
+ buffer += strings[i];
640
+ if (i < values.length) {
641
+ const value = values[i];
642
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") buffer += value.toString();
643
+ else {
644
+ if (buffer) {
645
+ segments.push({
646
+ type: "text",
647
+ data: { text: buffer }
648
+ });
649
+ buffer = "";
650
+ }
651
+ segments.push(value);
652
+ }
653
+ }
654
+ }
655
+ if (buffer) segments.push({
656
+ type: "text",
657
+ data: { text: buffer }
658
+ });
659
+ return trimBoundaryTextSegments(segments);
660
+ }
661
+ function isTextSegment(segment) {
662
+ return segment.type === "text";
663
+ }
664
+ function withText(segment, text) {
665
+ return {
666
+ ...segment,
667
+ data: {
668
+ ...segment.data,
669
+ text
670
+ }
671
+ };
672
+ }
673
+ function trimBoundaryTextSegments(segments) {
674
+ const result = [...segments];
675
+ const first = result[0];
676
+ if (first && isTextSegment(first)) {
677
+ const text = first.data.text.trimStart();
678
+ if (text) result[0] = withText(first, text);
679
+ else result.shift();
680
+ }
681
+ const lastIndex = result.length - 1;
682
+ const last = result[lastIndex];
683
+ if (last && isTextSegment(last)) {
684
+ const text = last.data.text.trimEnd();
685
+ if (text) result[lastIndex] = withText(last, text);
686
+ else result.pop();
687
+ }
688
+ return result;
689
+ }
690
+ let seg;
691
+ (function(_seg) {
692
+ function mention(userId) {
693
+ return {
694
+ type: "mention",
695
+ data: { user_id: userId }
696
+ };
697
+ }
698
+ _seg.mention = mention;
699
+ function mentionAll() {
700
+ return {
701
+ type: "mention_all",
702
+ data: {}
703
+ };
704
+ }
705
+ _seg.mentionAll = mentionAll;
706
+ function face(faceId, options) {
707
+ return {
708
+ type: "face",
709
+ data: {
710
+ face_id: faceId.toString(),
711
+ is_large: options?.isLarge ?? false
712
+ }
713
+ };
714
+ }
715
+ _seg.face = face;
716
+ function reply(messageSeq) {
717
+ return {
718
+ type: "reply",
719
+ data: { message_seq: messageSeq }
720
+ };
721
+ }
722
+ _seg.reply = reply;
723
+ function image(uri, options) {
724
+ return {
725
+ type: "image",
726
+ data: {
727
+ uri,
728
+ sub_type: options?.subType,
729
+ summary: options?.summary
730
+ }
731
+ };
732
+ }
733
+ _seg.image = image;
734
+ function record(uri) {
735
+ return {
736
+ type: "record",
737
+ data: { uri }
738
+ };
739
+ }
740
+ _seg.record = record;
741
+ function video(uri, options) {
742
+ return {
743
+ type: "video",
744
+ data: {
745
+ uri,
746
+ thumb_uri: options?.thumbUri
747
+ }
748
+ };
749
+ }
750
+ _seg.video = video;
751
+ function forward(messages, options) {
752
+ return {
753
+ type: "forward",
754
+ data: {
755
+ messages,
756
+ title: options?.title,
757
+ preview: options?.preview,
758
+ summary: options?.summary,
759
+ prompt: options?.prompt
760
+ }
761
+ };
762
+ }
763
+ _seg.forward = forward;
764
+ })(seg || (seg = {}));
765
+ //#endregion
573
766
  //#region src/core/logging.ts
574
767
  var Logger = class {
575
768
  logHandler;
@@ -622,12 +815,14 @@ var Context = class Context {
622
815
  router = new Router();
623
816
  logger;
624
817
  name;
818
+ routeActivationResolver;
625
819
  parent;
626
820
  filter;
627
821
  eventBus = mitt();
628
822
  plugins = [];
629
823
  services = /* @__PURE__ */ new Map();
630
824
  subContexts = /* @__PURE__ */ new Map();
825
+ eventSourceRuntimes = /* @__PURE__ */ new Map();
631
826
  parentEventForwarder;
632
827
  timers = /* @__PURE__ */ new Set();
633
828
  initialReconnectDelayMs;
@@ -636,10 +831,6 @@ var Context = class Context {
636
831
  state = "idle";
637
832
  startPromise;
638
833
  stopPromise;
639
- eventSubscription;
640
- eventStreamTask;
641
- reconnectTimer;
642
- resolveReconnectTimer;
643
834
  constructor(client, options, name, parent, filter) {
644
835
  this.client = client;
645
836
  this.initialReconnectDelayMs = options?.reconnect?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
@@ -647,6 +838,8 @@ var Context = class Context {
647
838
  this.logHandler = options?.logHandler ?? parent?.logHandler;
648
839
  this.name = name ?? "root";
649
840
  this.logger = new Logger((message) => this.logHandler?.(message), `context:${this.name}`);
841
+ this.routeActivationResolver = createContextRouteActivationResolver(options?.routing, parent?.routeActivationResolver);
842
+ this.router.setActivationResolver(this.routeActivationResolver);
650
843
  this.parent = parent;
651
844
  this.filter = filter;
652
845
  if (parent) {
@@ -656,10 +849,10 @@ var Context = class Context {
656
849
  };
657
850
  parent.eventBus.on("*", this.parentEventForwarder);
658
851
  }
659
- this.eventBus.on("message_receive", async ({ data: message }) => {
852
+ this.eventBus.on("message_receive", async ({ self_id, data: message }) => {
660
853
  try {
661
854
  if (this.state === "stopping" || this.state === "stopped") return;
662
- await this.router.dispatch(this.createSession(message), message);
855
+ await this.router.dispatch(this.createSession(self_id, message), message);
663
856
  } catch (error) {
664
857
  this.logger.error(`Error routing command (scene=${message.message_scene} peer=${message.peer_id} sender=${message.sender_id} seq=${message.message_seq})`, error);
665
858
  }
@@ -685,6 +878,11 @@ var Context = class Context {
685
878
  args
686
879
  });
687
880
  }
881
+ installEventSource(eventSource) {
882
+ if (this.eventSourceRuntimes.has(eventSource)) return;
883
+ this.eventSourceRuntimes.set(eventSource, {});
884
+ if (this.state === "started") this.startEventSource(eventSource);
885
+ }
688
886
  provide(service, instance) {
689
887
  if (this.services.has(service)) throw new Error(`Service ${service.name} has already been provided in this context.`);
690
888
  if (implementsESNextDisposable(instance) && !isDisposable(instance)) throw new Error(`
@@ -735,8 +933,9 @@ and implement the dispose method to clean up resources when the context stops.
735
933
  this.timers.add(interval);
736
934
  return interval;
737
935
  }
738
- createSession(message) {
936
+ createSession(selfId, message) {
739
937
  return {
938
+ selfId,
740
939
  raw: message,
741
940
  reply: async (textOrSegments, options) => {
742
941
  const actualSegments = [];
@@ -819,8 +1018,10 @@ and implement the dispose method to clean up resources when the context stops.
819
1018
  for (const context of startingContexts) if (context.state === "starting") context.state = "idle";
820
1019
  throw error;
821
1020
  }
822
- for (const { context } of appliedContextPlugins) context.state = "started";
823
- if (!this.parent) this.eventStreamTask = this.runEventStream();
1021
+ for (const { context } of appliedContextPlugins) {
1022
+ context.state = "started";
1023
+ for (const eventSource of context.eventSourceRuntimes.keys()) context.startEventSource(eventSource);
1024
+ }
824
1025
  }
825
1026
  getState() {
826
1027
  return this.state;
@@ -833,7 +1034,7 @@ and implement the dispose method to clean up resources when the context stops.
833
1034
  } catch (error) {
834
1035
  errors.push(error);
835
1036
  }
836
- await this.stopEventStream(errors);
1037
+ await this.stopEventSources(errors);
837
1038
  if (this.parent && this.parentEventForwarder) this.parent.eventBus.off("*", this.parentEventForwarder);
838
1039
  for (const service of [...this.services.values()].reverse()) {
839
1040
  if (!isDisposable(service)) continue;
@@ -862,20 +1063,27 @@ and implement the dispose method to clean up resources when the context stops.
862
1063
  for (const timer of this.timers) clearTimeout(timer);
863
1064
  this.timers.clear();
864
1065
  }
865
- async stopEventStream(errors) {
866
- this.resolveReconnectDelay();
867
- const subscription = this.eventSubscription;
868
- if (subscription) try {
869
- await subscription.stop();
870
- } catch (error) {
871
- errors.push(error);
1066
+ async stopEventSources(errors) {
1067
+ for (const [_, runtime] of this.eventSourceRuntimes) {
1068
+ this.resolveReconnectDelay(runtime);
1069
+ const subscription = runtime.subscription;
1070
+ if (!subscription) continue;
1071
+ try {
1072
+ await subscription.stop();
1073
+ } catch (error) {
1074
+ errors.push(error);
1075
+ }
1076
+ runtime.subscription = void 0;
872
1077
  }
873
- if (this.eventStreamTask) try {
874
- await this.eventStreamTask;
875
- } catch (error) {
876
- errors.push(error);
877
- } finally {
878
- this.eventStreamTask = void 0;
1078
+ for (const runtime of this.eventSourceRuntimes.values()) {
1079
+ if (!runtime.task) continue;
1080
+ try {
1081
+ await runtime.task;
1082
+ } catch (error) {
1083
+ errors.push(error);
1084
+ } finally {
1085
+ runtime.task = void 0;
1086
+ }
879
1087
  }
880
1088
  }
881
1089
  acceptsParentEvent(type, event) {
@@ -994,6 +1202,10 @@ and implement the dispose method to clean up resources when the context stops.
994
1202
  }
995
1203
  createProxyContextForPlugin(plugin) {
996
1204
  const proxyLogger = new Logger((message) => this.logHandler?.(message), `plugin:${this.name ? `${this.name}/` : ""}${plugin.name}`);
1205
+ const proxyRouter = this.router.withMeta({
1206
+ context: this.name,
1207
+ plugin: plugin.name
1208
+ });
997
1209
  let proxyInjections;
998
1210
  if (plugin.inject) {
999
1211
  proxyInjections = {};
@@ -1005,17 +1217,23 @@ and implement the dispose method to clean up resources when the context stops.
1005
1217
  }
1006
1218
  return new Proxy(this, { get(target, prop, receiver) {
1007
1219
  if (prop === "logger") return proxyLogger;
1220
+ else if (prop === "router") return proxyRouter;
1008
1221
  else if (proxyInjections && prop in proxyInjections) return proxyInjections[prop];
1009
1222
  else return Reflect.get(target, prop, receiver);
1010
1223
  } });
1011
1224
  }
1012
- async runEventStream() {
1225
+ startEventSource(eventSource) {
1226
+ const runtime = this.eventSourceRuntimes.get(eventSource);
1227
+ if (!runtime) return;
1228
+ runtime.task = this.runEventSource(eventSource, runtime);
1229
+ }
1230
+ async runEventSource(eventSource, runtime) {
1013
1231
  let reconnectDelay = this.initialReconnectDelayMs;
1014
1232
  let reconnectAttempt = 1;
1015
1233
  while (this.state === "started") {
1016
1234
  try {
1017
- this.logger.debug(`Connecting event stream (attempt=${reconnectAttempt})`);
1018
- const subscription = await this.client.startEvents((event) => {
1235
+ this.logger.debug(`Connecting ${eventSource.name ?? "event source"} (attempt=${reconnectAttempt})`);
1236
+ const subscription = await eventSource.start((event) => {
1019
1237
  try {
1020
1238
  if (this.state !== "started") return;
1021
1239
  this.eventBus.emit(event.event_type, event);
@@ -1027,48 +1245,52 @@ and implement the dispose method to clean up resources when the context stops.
1027
1245
  await subscription.stop();
1028
1246
  break;
1029
1247
  }
1030
- this.eventSubscription = subscription;
1031
- this.logger.info("Event stream connected");
1248
+ runtime.subscription = subscription;
1249
+ this.logger.info(`${eventSource.name ?? "Event source"} connected`);
1032
1250
  reconnectDelay = this.initialReconnectDelayMs;
1033
1251
  reconnectAttempt = 1;
1034
1252
  await subscription.closed;
1035
- this.eventSubscription = void 0;
1253
+ if (runtime.subscription === subscription) runtime.subscription = void 0;
1036
1254
  if (this.state !== "started") break;
1037
- this.logger.warn(`Event stream disconnected; reconnecting in ${reconnectDelay}ms`);
1255
+ this.logger.warn(`${eventSource.name ?? "Event source"} disconnected; reconnecting in ${reconnectDelay}ms`);
1038
1256
  } catch (error) {
1039
- this.eventSubscription = void 0;
1040
1257
  if (this.state !== "started") break;
1041
- this.logger.error(`Error connecting event stream; reconnecting in ${reconnectDelay}ms`, error);
1258
+ this.logger.error(`Error connecting ${eventSource.name ?? "event source"}; reconnecting in ${reconnectDelay}ms`, error);
1042
1259
  }
1043
- await this.waitForReconnectDelay(reconnectDelay);
1260
+ await this.waitForReconnectDelay(runtime, reconnectDelay);
1044
1261
  reconnectDelay = Math.min(reconnectDelay * 2, this.maxReconnectDelayMs);
1045
1262
  reconnectAttempt += 1;
1046
1263
  }
1047
1264
  }
1048
- waitForReconnectDelay(delay) {
1265
+ waitForReconnectDelay(runtime, delay) {
1049
1266
  return new Promise((resolve) => {
1050
- this.resolveReconnectTimer = resolve;
1051
- this.reconnectTimer = setTimeout(() => {
1052
- this.reconnectTimer = void 0;
1053
- this.resolveReconnectTimer = void 0;
1267
+ runtime.resolveReconnectTimer = resolve;
1268
+ runtime.reconnectTimer = setTimeout(() => {
1269
+ runtime.reconnectTimer = void 0;
1270
+ runtime.resolveReconnectTimer = void 0;
1054
1271
  resolve();
1055
1272
  }, delay);
1056
1273
  });
1057
1274
  }
1058
- resolveReconnectDelay() {
1059
- if (this.reconnectTimer) {
1060
- clearTimeout(this.reconnectTimer);
1061
- this.reconnectTimer = void 0;
1275
+ resolveReconnectDelay(runtime) {
1276
+ if (runtime.reconnectTimer) {
1277
+ clearTimeout(runtime.reconnectTimer);
1278
+ runtime.reconnectTimer = void 0;
1062
1279
  }
1063
- const resolve = this.resolveReconnectTimer;
1064
- this.resolveReconnectTimer = void 0;
1280
+ const resolve = runtime.resolveReconnectTimer;
1281
+ runtime.resolveReconnectTimer = void 0;
1065
1282
  resolve?.();
1066
1283
  }
1067
1284
  static fromUrl(baseUrl, options) {
1068
- return new Context(createMilkyClient(baseUrl, { accessToken: options?.accessToken }), options);
1285
+ const context = new Context(createMilkyClient(baseUrl, { accessToken: options?.accessToken }), options);
1286
+ if (options?.installEventSource ?? true) context.installEventSource(createMilkyWebSocketEventSource(baseUrl, { accessToken: options?.accessToken }));
1287
+ return context;
1069
1288
  }
1070
1289
  static fromClient(client, options) {
1071
- return new Context(client, options);
1290
+ const context = new Context(client, options);
1291
+ const eventSourceLike = client;
1292
+ if (typeof eventSourceLike.start === "function") context.installEventSource(eventSourceLike);
1293
+ return context;
1072
1294
  }
1073
1295
  };
1074
1296
  //#endregion
@@ -1226,7 +1448,7 @@ function definePlugin(plugin) {
1226
1448
  //#endregion
1227
1449
  //#region src/protocol/types.ts
1228
1450
  const milkyVersion = "1.3";
1229
- const milkyPackageVersion = "1.3.0-rc.1";
1451
+ const milkyPackageVersion = "1.3.0-rc.2";
1230
1452
  //#endregion
1231
1453
  //#region src/routing/parameter.ts
1232
1454
  var Parameter = class Parameter {
@@ -1352,4 +1574,4 @@ let param;
1352
1574
  _param.segment = segment;
1353
1575
  })(param || (param = {}));
1354
1576
  //#endregion
1355
- export { CommandBuilder, Context, Logger, Parameter, Router, combineLogHandlers, createMilkyClient, definePlugin, filter, implementsESNextDisposable, isDisposable, milkyPackageVersion, milkyVersion, msg, param, seg };
1577
+ export { CommandBuilder, Context, Logger, Parameter, Router, combineLogHandlers, createContextRouteActivationResolver, createMilkyClient, createMilkyWebSocketEventSource, defaultRouteActivationResolver, definePlugin, filter, implementsESNextDisposable, isDisposable, mergeRouteMeta, milkyPackageVersion, milkyVersion, msg, param, seg };