@fraqjs/fraq 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -5514,20 +5514,36 @@ interface Command<P extends Pattern> {
5514
5514
  name: string;
5515
5515
  pattern: P;
5516
5516
  execute: Executor<P>;
5517
+ description?: string;
5518
+ aliases?: string[];
5519
+ hidden?: boolean;
5517
5520
  }
5518
5521
  interface RawPattern<P extends Pattern> {
5519
5522
  pattern: P;
5520
5523
  execute: Executor<P>;
5521
5524
  }
5525
+ interface SessionReplyOptions {
5526
+ withQuote?: boolean;
5527
+ withMention?: boolean;
5528
+ }
5522
5529
  interface Session {
5523
5530
  raw: IncomingMessage;
5524
- reply(segments: OutgoingSegment_ZodInput[]): Promise<void>;
5531
+ reply(
5532
+ segments: OutgoingSegment_ZodInput[],
5533
+ options?: SessionReplyOptions,
5534
+ ): Promise<{
5535
+ messageSeq: number;
5536
+ }>;
5537
+ reaction(type: 'face' | 'emoji', reactionId: string): Promise<void>;
5525
5538
  }
5526
5539
  declare class CommandBuilder<P extends Pattern = {}, S = Command<P>> {
5527
5540
  readonly name: string;
5528
5541
  private readonly sink;
5529
5542
  private readonly pattern;
5530
5543
  private executor?;
5544
+ private description?;
5545
+ private aliases?;
5546
+ private hidden?;
5531
5547
  constructor(name: string, sink?: (command: Command<P>) => S);
5532
5548
  arg<K extends string, T>(
5533
5549
  key: K,
@@ -5540,6 +5556,9 @@ declare class CommandBuilder<P extends Pattern = {}, S = Command<P>> {
5540
5556
  ? RawPattern<P & { [K2 in K]: Parameter<T> }>
5541
5557
  : S
5542
5558
  >;
5559
+ describe(description: string): CommandBuilder<P, S>;
5560
+ alias(...aliases: string[]): CommandBuilder<P, S>;
5561
+ hide(): CommandBuilder<P, S>;
5543
5562
  execute(executor: Executor<P>): S;
5544
5563
  }
5545
5564
  //#endregion
@@ -5598,6 +5617,7 @@ declare class Router {
5598
5617
  group(name: string): Router;
5599
5618
  filter(predicate: SessionPredicate): Router;
5600
5619
  routes(): RouteEntry[];
5620
+ aliasesOf(name: string): string[];
5601
5621
  branches(session: Session): RouteBranch[];
5602
5622
  match(session: Session, message: IncomingMessage): RouteMatchResult | undefined;
5603
5623
  dispatch(session: Session, message: IncomingMessage): Promise<boolean>;
@@ -5607,6 +5627,7 @@ declare class Router {
5607
5627
  private matchGroup;
5608
5628
  private matchRawPattern;
5609
5629
  private branchesFrom;
5630
+ private resolveAliasConflicts;
5610
5631
  private validatePattern;
5611
5632
  private capturePattern;
5612
5633
  }
@@ -5835,6 +5856,7 @@ export {
5835
5856
  ServiceClass,
5836
5857
  Session,
5837
5858
  SessionPredicate,
5859
+ SessionReplyOptions,
5838
5860
  TypeInstruction,
5839
5861
  combineLogHandlers,
5840
5862
  createMilkyClient,
package/dist/index.mjs CHANGED
@@ -64,12 +64,147 @@ function createMilkyClient(...params) {
64
64
  } });
65
65
  }
66
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
67
199
  //#region src/routing/command.ts
68
200
  var CommandBuilder = class {
69
201
  name;
70
202
  sink;
71
203
  pattern = {};
72
204
  executor;
205
+ description;
206
+ aliases;
207
+ hidden;
73
208
  constructor(name, sink = (command) => command) {
74
209
  this.name = name;
75
210
  this.sink = sink;
@@ -78,6 +213,19 @@ var CommandBuilder = class {
78
213
  this.pattern[key] = parameter;
79
214
  return this;
80
215
  }
216
+ describe(description) {
217
+ this.description = description;
218
+ return this;
219
+ }
220
+ alias(...aliases) {
221
+ if (!this.aliases) this.aliases = [];
222
+ this.aliases.push(...aliases);
223
+ return this;
224
+ }
225
+ hide() {
226
+ this.hidden = true;
227
+ return this;
228
+ }
81
229
  execute(executor) {
82
230
  this.executor = executor;
83
231
  const command = {
@@ -85,6 +233,9 @@ var CommandBuilder = class {
85
233
  pattern: this.pattern,
86
234
  execute: this.executor
87
235
  };
236
+ if (this.description !== void 0) command.description = this.description;
237
+ if (this.aliases !== void 0) command.aliases = this.aliases;
238
+ if (this.hidden !== void 0) command.hidden = this.hidden;
88
239
  return this.sink(command);
89
240
  }
90
241
  };
@@ -222,6 +373,7 @@ var Router = class Router {
222
373
  return builtCommand;
223
374
  });
224
375
  this.validatePattern(command.pattern);
376
+ this.resolveAliasConflicts(command);
225
377
  this.entries.push({
226
378
  type: "command",
227
379
  command
@@ -265,6 +417,10 @@ var Router = class Router {
265
417
  routes() {
266
418
  return this.entries;
267
419
  }
420
+ aliasesOf(name) {
421
+ for (const entry of this.entries) if (entry.type === "command" && entry.command.name === name) return entry.command.aliases ? [...entry.command.aliases] : [];
422
+ return [];
423
+ }
268
424
  branches(session) {
269
425
  return this.branchesFrom(session, []);
270
426
  }
@@ -306,7 +462,7 @@ var Router = class Router {
306
462
  }
307
463
  matchCommand(command, tokenizer, path) {
308
464
  const token = tokenizer.peek();
309
- if (typeof token !== "string" || token !== command.name) return;
465
+ if (typeof token !== "string" || token !== command.name && !command.aliases?.includes(token)) return;
310
466
  tokenizer.next();
311
467
  const params = this.capturePattern(command.pattern, tokenizer);
312
468
  if (params === void 0 || tokenizer.hasNext()) return;
@@ -337,7 +493,7 @@ var Router = class Router {
337
493
  const branches = [];
338
494
  for (const entry of this.entries) switch (entry.type) {
339
495
  case "command":
340
- branches.push({
496
+ if (!entry.command.hidden) branches.push({
341
497
  type: "command",
342
498
  path: [...path],
343
499
  command: entry.command
@@ -359,6 +515,27 @@ var Router = class Router {
359
515
  }
360
516
  return branches;
361
517
  }
518
+ resolveAliasConflicts(command) {
519
+ for (const entry of this.entries) {
520
+ if (entry.type !== "command") continue;
521
+ const existing = entry.command;
522
+ if (existing.aliases?.includes(command.name)) {
523
+ existing.aliases = existing.aliases.filter((a) => a !== command.name);
524
+ console.warn(`Command "${command.name}" conflicts with alias of existing command "${existing.name}". The alias "${command.name}" has been removed from "${existing.name}".`);
525
+ }
526
+ if (command.aliases) {
527
+ const dropped = [];
528
+ for (const alias of command.aliases) if (existing.name === alias) {
529
+ dropped.push(alias);
530
+ console.warn(`Alias "${alias}" of command "${command.name}" conflicts with existing command name "${existing.name}". The alias has been dropped.`);
531
+ } else if (existing.aliases?.includes(alias)) {
532
+ existing.aliases = existing.aliases.filter((a) => a !== alias);
533
+ console.warn(`Alias "${alias}" of command "${command.name}" conflicts with alias of existing command "${existing.name}". The alias has been removed from "${existing.name}".`);
534
+ }
535
+ if (dropped.length > 0) command.aliases = command.aliases.filter((a) => !dropped.includes(a));
536
+ }
537
+ }
538
+ }
362
539
  validatePattern(pattern, options) {
363
540
  const entries = Object.entries(pattern);
364
541
  if (options?.rawPattern && entries.length === 0) throw new Error("Raw pattern must have at least one parameter.");
@@ -547,25 +724,35 @@ and implement the dispose method to clean up resources when the context stops.
547
724
  createSession(message) {
548
725
  return {
549
726
  raw: message,
550
- reply: async (segments) => {
551
- try {
552
- switch (message.message_scene) {
553
- case "friend":
554
- await this.client.send_private_message({
555
- user_id: message.peer_id,
556
- message: segments
557
- });
558
- break;
559
- case "group":
560
- await this.client.send_group_message({
561
- group_id: message.peer_id,
562
- message: segments
563
- });
564
- break;
727
+ reply: async (segments, options) => {
728
+ const actualSegments = [...segments];
729
+ if (options?.withMention && message.message_scene === "group") actualSegments.unshift(seg.mention(message.sender_id));
730
+ if (options?.withQuote) actualSegments.unshift(seg.reply(message.message_seq));
731
+ switch (message.message_scene) {
732
+ case "friend": {
733
+ const { message_seq } = await this.client.send_private_message({
734
+ user_id: message.peer_id,
735
+ message: actualSegments
736
+ });
737
+ return { messageSeq: message_seq };
738
+ }
739
+ case "group": {
740
+ const { message_seq } = await this.client.send_group_message({
741
+ group_id: message.peer_id,
742
+ message: actualSegments
743
+ });
744
+ return { messageSeq: message_seq };
565
745
  }
566
- } catch (error) {
567
- this.logger.error(`Error sending reply (source msg: scene=${message.message_scene} peer=${message.peer_id} sender=${message.sender_id} seq=${message.message_seq})`, error);
568
746
  }
747
+ return { messageSeq: 0 };
748
+ },
749
+ reaction: async (type, reactionId) => {
750
+ if (message.message_scene === "group") await this.client.send_group_message_reaction({
751
+ group_id: message.peer_id,
752
+ message_seq: message.message_seq,
753
+ reaction_type: type,
754
+ reaction: reactionId
755
+ });
569
756
  }
570
757
  };
571
758
  }
@@ -681,7 +868,15 @@ and implement the dispose method to clean up resources when the context stops.
681
868
  for (const installedPlugin of sortedPlugins) {
682
869
  const { plugin, args } = installedPlugin;
683
870
  const providedBeforeApply = new Set(this.services.keys());
684
- this.logger.debug(`Applying plugin ${plugin.name}`);
871
+ let applyingMessage = `Applying plugin ${plugin.name}`;
872
+ const requiredServices = [];
873
+ const providedServices = [];
874
+ if (plugin.requires) for (const service of plugin.requires) requiredServices.push(service.name);
875
+ if (plugin.optionalRequires) for (const service of plugin.optionalRequires) requiredServices.push(`${service.name}?`);
876
+ if (plugin.provides) for (const service of plugin.provides) providedServices.push(service.name);
877
+ if (requiredServices.length > 0) applyingMessage += `, requires: [${requiredServices.join(", ")}]`;
878
+ if (providedServices.length > 0) applyingMessage += `, provides: [${providedServices.join(", ")}]`;
879
+ this.logger.info(applyingMessage);
685
880
  await plugin.apply(this.getPluginContext(installedPlugin), ...args);
686
881
  for (const service of plugin.provides ?? []) if (!this.services.has(service) || providedBeforeApply.has(service)) throw new Error(`${plugin.name} declares service ${service.name} but did not provide it.`);
687
882
  }
@@ -780,10 +975,6 @@ and implement the dispose method to clean up resources when the context stops.
780
975
  }
781
976
  createProxyContextForPlugin(plugin) {
782
977
  const proxyLogger = new Logger((message) => this.logHandler?.(message), plugin.name);
783
- const proxyProvide = (service, instance) => {
784
- this.provide(service, instance);
785
- this.logger.debug(`Plugin ${plugin.name} provided ${service.name}`);
786
- };
787
978
  let proxyInjections;
788
979
  if (plugin.inject) {
789
980
  proxyInjections = {};
@@ -795,7 +986,6 @@ and implement the dispose method to clean up resources when the context stops.
795
986
  }
796
987
  return new Proxy(this, { get(target, prop, receiver) {
797
988
  if (prop === "logger") return proxyLogger;
798
- else if (prop === "provide") return proxyProvide;
799
989
  else if (proxyInjections && prop in proxyInjections) return proxyInjections[prop];
800
990
  else return Reflect.get(target, prop, receiver);
801
991
  } });
@@ -1015,138 +1205,6 @@ function definePlugin(plugin) {
1015
1205
  return plugin;
1016
1206
  }
1017
1207
  //#endregion
1018
- //#region src/protocol/segment.ts
1019
- function msg(strings, ...values) {
1020
- let buffer = "";
1021
- const segments = [];
1022
- for (let i = 0; i < strings.length; i++) {
1023
- buffer += strings[i];
1024
- if (i < values.length) {
1025
- const value = values[i];
1026
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") buffer += value.toString();
1027
- else {
1028
- if (buffer) {
1029
- segments.push({
1030
- type: "text",
1031
- data: { text: buffer }
1032
- });
1033
- buffer = "";
1034
- }
1035
- segments.push(value);
1036
- }
1037
- }
1038
- }
1039
- if (buffer) segments.push({
1040
- type: "text",
1041
- data: { text: buffer }
1042
- });
1043
- return trimBoundaryTextSegments(segments);
1044
- }
1045
- function isTextSegment(segment) {
1046
- return segment.type === "text";
1047
- }
1048
- function withText(segment, text) {
1049
- return {
1050
- ...segment,
1051
- data: {
1052
- ...segment.data,
1053
- text
1054
- }
1055
- };
1056
- }
1057
- function trimBoundaryTextSegments(segments) {
1058
- const result = [...segments];
1059
- const first = result[0];
1060
- if (first && isTextSegment(first)) {
1061
- const text = first.data.text.trimStart();
1062
- if (text) result[0] = withText(first, text);
1063
- else result.shift();
1064
- }
1065
- const lastIndex = result.length - 1;
1066
- const last = result[lastIndex];
1067
- if (last && isTextSegment(last)) {
1068
- const text = last.data.text.trimEnd();
1069
- if (text) result[lastIndex] = withText(last, text);
1070
- else result.pop();
1071
- }
1072
- return result;
1073
- }
1074
- let seg;
1075
- (function(_seg) {
1076
- function mention(userId) {
1077
- return {
1078
- type: "mention",
1079
- data: { user_id: userId }
1080
- };
1081
- }
1082
- _seg.mention = mention;
1083
- function mentionAll() {
1084
- return {
1085
- type: "mention_all",
1086
- data: {}
1087
- };
1088
- }
1089
- _seg.mentionAll = mentionAll;
1090
- function face(faceId, options) {
1091
- return {
1092
- type: "face",
1093
- data: {
1094
- face_id: faceId.toString(),
1095
- is_large: options?.isLarge ?? false
1096
- }
1097
- };
1098
- }
1099
- _seg.face = face;
1100
- function reply(messageSeq) {
1101
- return {
1102
- type: "reply",
1103
- data: { message_seq: messageSeq }
1104
- };
1105
- }
1106
- _seg.reply = reply;
1107
- function image(uri, options) {
1108
- return {
1109
- type: "image",
1110
- data: {
1111
- uri,
1112
- sub_type: options?.subType,
1113
- summary: options?.summary
1114
- }
1115
- };
1116
- }
1117
- _seg.image = image;
1118
- function record(uri) {
1119
- return {
1120
- type: "record",
1121
- data: { uri }
1122
- };
1123
- }
1124
- _seg.record = record;
1125
- function video(uri, options) {
1126
- return {
1127
- type: "video",
1128
- data: {
1129
- uri,
1130
- thumb_uri: options?.thumbUri
1131
- }
1132
- };
1133
- }
1134
- _seg.video = video;
1135
- function forward(messages, options) {
1136
- return {
1137
- type: "forward",
1138
- data: {
1139
- messages,
1140
- title: options?.title,
1141
- preview: options?.preview,
1142
- summary: options?.summary,
1143
- prompt: options?.prompt
1144
- }
1145
- };
1146
- }
1147
- _seg.forward = forward;
1148
- })(seg || (seg = {}));
1149
- //#endregion
1150
1208
  //#region src/protocol/types.ts
1151
1209
  const milkyVersion = "1.3";
1152
1210
  const milkyPackageVersion = "1.3.0-rc.1";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fraqjs/fraq",
3
3
  "type": "module",
4
- "version": "0.9.0",
4
+ "version": "0.9.2",
5
5
  "description": "Milky Bot framework",
6
6
  "files": [
7
7
  "dist"