@fraqjs/fraq 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,3 +1,59 @@
1
1
  # Fraq
2
2
 
3
- - [官方网站](https://fraq.ntqqrev.org/)
3
+ Fraq 是一个 [Milky](https://milky.ntqqrev.org/) 协议的聊天机器人框架,在协议之上提供了更方便的上下文、路由、消息构造、插件和服务依赖注入能力,让你可以用 TypeScript 更轻松地组织一个机器人应用。
4
+
5
+ ## 安装 Fraq
6
+
7
+ 可以通过任意的包管理器安装 Fraq:
8
+
9
+ ```bash
10
+ npm install @fraqjs/fraq
11
+ # or
12
+ yarn add @fraqjs/fraq
13
+ # or
14
+ pnpm add @fraqjs/fraq
15
+ ```
16
+
17
+ Fraq 需要在 Node.js 22+ 环境(或其他的支持 WebSocket API 的服务端 JS 环境)中运行。强烈建议使用 TypeScript 来编写 Fraq 代码,并使用 [tsx](https://www.npmjs.com/package/tsx) 来运行你的项目。
18
+
19
+ ## 第一个 Fraq 机器人
20
+
21
+ 我们从一个 Echo 机器人开始。首先,启动你的 Milky 协议端,并确认它监听的地址与端口。然后,在你的项目中创建一个 `index.ts` 文件,并添加以下代码:
22
+
23
+ ```typescript
24
+ import { Context, msg, param } from '@fraqjs/fraq';
25
+
26
+ const ctx = Context.create({
27
+ connect: {
28
+ baseUrl: '<替换成你的 Milky 协议端地址>',
29
+ // accessToken: '<如果你的 Milky 协议端配置了 token,请在这里添加>',
30
+ },
31
+ });
32
+
33
+ ctx.router.command(
34
+ 'echo',
35
+ {
36
+ content: param.greedy(),
37
+ },
38
+ (session, { content }) => {
39
+ session.reply(msg`You said: ${content}`);
40
+ },
41
+ );
42
+
43
+ ctx.start();
44
+ ```
45
+
46
+ 在一切完成之后,用 `tsx` 执行你的 `index.ts` 文件:
47
+
48
+ ```bash
49
+ npx tsx index.ts
50
+ # 或 tsx index.ts, 如果你全局安装了 tsx
51
+ ```
52
+
53
+ 你可以在任何一个包含它的会话中发送 `echo Hello` 来测试它是否正常工作。
54
+
55
+ ## 下一步
56
+
57
+ 上面的例子只是 Fraq 的最小使用方式,你还可以继续了解如何构造更丰富的消息、定义更复杂的指令参数、编写可复用的插件等。
58
+
59
+ 完整文档请见 [fraq.ntqqrev.org](https://fraq.ntqqrev.org/)。
package/dist/index.d.mts CHANGED
@@ -5526,13 +5526,32 @@ type Filter = { [K in keyof EventMap]?: (event: EventMap[K]) => boolean };
5526
5526
  declare namespace filter {
5527
5527
  function define(filter: Filter): Filter;
5528
5528
  function allPass(): Filter;
5529
- function friend(uinList: number[]): Filter;
5530
- function group(uinList: number[]): Filter;
5529
+ function friend(...uinList: number[]): Filter;
5530
+ function group(...uinList: number[]): Filter;
5531
5531
  function admin(): Filter;
5532
5532
  function or(...filters: Filter[]): Filter;
5533
5533
  function and(...filters: Filter[]): Filter;
5534
5534
  }
5535
5535
  //#endregion
5536
+ //#region src/core/logging.d.ts
5537
+ interface LogMessage {
5538
+ level: 'debug' | 'info' | 'warn' | 'error';
5539
+ module: string;
5540
+ message: string;
5541
+ error?: unknown;
5542
+ time: number;
5543
+ }
5544
+ declare class Logger {
5545
+ private readonly logHandler;
5546
+ private readonly module;
5547
+ constructor(logHandler: (message: LogMessage) => void, module: string);
5548
+ private log;
5549
+ debug(message: string): void;
5550
+ info(message: string): void;
5551
+ warn(message: string, error?: unknown): void;
5552
+ error(message: string, error?: unknown): void;
5553
+ }
5554
+ //#endregion
5536
5555
  //#region src/core/service.d.ts
5537
5556
  /** biome-ignore-all lint/suspicious/noExplicitAny: Service constructors may accept any arguments. */
5538
5557
  type ServiceClass<T extends object = object> = abstract new (...args: any[]) => T;
@@ -5540,21 +5559,35 @@ type ServiceClass<T extends object = object> = abstract new (...args: any[]) =>
5540
5559
  //#region src/core/plugin.d.ts
5541
5560
  type ParameterList = Array<any>;
5542
5561
  interface Plugin<T extends ParameterList> {
5562
+ name: string;
5543
5563
  requires?: readonly ServiceClass[];
5544
5564
  provides?: readonly ServiceClass[];
5545
- apply(context: Context, ...args: T): void | Promise<void>;
5565
+ apply(ctx: Context, ...args: T): void | Promise<void>;
5546
5566
  }
5547
5567
  declare function definePlugin<T extends ParameterList>(plugin: Plugin<T>): Plugin<T>;
5548
5568
  //#endregion
5549
5569
  //#region src/core/context.d.ts
5570
+ interface ContextOptions {
5571
+ connect: {
5572
+ baseUrl: string;
5573
+ accessToken?: string;
5574
+ initialReconnectDelay?: number;
5575
+ maxReconnectDelay?: number;
5576
+ };
5577
+ logHandler?: (message: LogMessage) => void;
5578
+ }
5550
5579
  declare class Context {
5551
5580
  readonly client: MilkyClient;
5552
- private readonly parent?;
5553
5581
  readonly router: Router;
5582
+ readonly logger: Logger;
5583
+ private readonly parent?;
5554
5584
  private readonly eventBus;
5555
5585
  private readonly plugins;
5556
5586
  private readonly services;
5557
5587
  private readonly subContexts;
5588
+ private readonly initialReconnectDelayMs;
5589
+ private readonly maxReconnectDelayMs;
5590
+ private readonly logHandler?;
5558
5591
  private isStarted;
5559
5592
  private constructor();
5560
5593
  on<K extends keyof EventMap>(type: K, handler: (event: EventMap[K]) => void | Promise<void>): void;
@@ -5567,21 +5600,12 @@ declare class Context {
5567
5600
  start(): Promise<void>;
5568
5601
  private applyPlugins;
5569
5602
  private sortPlugins;
5570
- private validateUniqueProvidedServices;
5571
5603
  private collectAvailableServices;
5572
- private areRequirementsAvailable;
5573
5604
  private createUnresolvablePluginError;
5574
- private describePlugin;
5605
+ private createProxyContextForPlugin;
5575
5606
  private connectEventWebSocket;
5576
- private handleWebSocketMessage;
5577
- private delay;
5578
5607
  private createSession;
5579
- static create(options: {
5580
- connect: {
5581
- baseUrl: string;
5582
- accessToken?: string;
5583
- };
5584
- }): Context;
5608
+ static create(options: ContextOptions): Context;
5585
5609
  }
5586
5610
  //#endregion
5587
5611
  //#region src/protocol/segment.d.ts
@@ -5625,24 +5649,27 @@ declare namespace seg {
5625
5649
  }
5626
5650
  //#endregion
5627
5651
  export {
5628
- type ApiEndpoints,
5652
+ ApiEndpoints,
5629
5653
  Context,
5630
- type EventMap,
5631
- type Filter,
5632
- type MilkyClient,
5654
+ ContextOptions,
5655
+ EventMap,
5656
+ Filter,
5657
+ LogMessage,
5658
+ Logger,
5659
+ MilkyClient,
5633
5660
  Parameter,
5634
- type ParameterList,
5635
- type Plugin,
5661
+ ParameterList,
5662
+ Plugin,
5636
5663
  Router,
5637
5664
  type ServiceClass,
5638
- type Session,
5665
+ Session,
5639
5666
  createMilkyClient,
5640
5667
  definePlugin,
5641
5668
  filter,
5669
+ type types_d_exports as milky,
5642
5670
  milkyPackageVersion,
5643
5671
  milkyVersion,
5644
5672
  msg,
5645
5673
  param,
5646
5674
  seg,
5647
- type types_d_exports as types,
5648
5675
  };
package/dist/index.mjs CHANGED
@@ -246,25 +246,63 @@ var Router = class Router {
246
246
  }
247
247
  };
248
248
  //#endregion
249
+ //#region src/core/logging.ts
250
+ var Logger = class {
251
+ logHandler;
252
+ module;
253
+ constructor(logHandler, module) {
254
+ this.logHandler = logHandler;
255
+ this.module = module;
256
+ }
257
+ log(level, message, error) {
258
+ this.logHandler({
259
+ level,
260
+ module: this.module,
261
+ message,
262
+ error,
263
+ time: Date.now()
264
+ });
265
+ }
266
+ debug(message) {
267
+ this.log("debug", message);
268
+ }
269
+ info(message) {
270
+ this.log("info", message);
271
+ }
272
+ warn(message, error) {
273
+ this.log("warn", message, error);
274
+ }
275
+ error(message, error) {
276
+ this.log("error", message, error);
277
+ }
278
+ };
279
+ //#endregion
249
280
  //#region src/core/service.ts
250
281
  function getServiceName(service) {
251
282
  return service.name || "<anonymous service>";
252
283
  }
253
284
  //#endregion
254
285
  //#region src/core/context.ts
255
- const INITIAL_RECONNECT_DELAY_MS = 1e3;
256
- const MAX_RECONNECT_DELAY_MS = 3e4;
286
+ const DEFAULT_INITIAL_RECONNECT_DELAY_MS = 1e3;
287
+ const DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
257
288
  var Context = class Context {
258
289
  client;
259
- parent;
260
290
  router = new Router();
291
+ logger = new Logger((message) => this.logHandler?.(message), "context");
292
+ parent;
261
293
  eventBus = mitt();
262
294
  plugins = [];
263
295
  services = /* @__PURE__ */ new Map();
264
296
  subContexts = [];
297
+ initialReconnectDelayMs;
298
+ maxReconnectDelayMs;
299
+ logHandler;
265
300
  isStarted = false;
266
- constructor(client, parent, filter) {
301
+ constructor(client, options, parent, filter) {
267
302
  this.client = client;
303
+ this.initialReconnectDelayMs = options?.connect.initialReconnectDelay ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
304
+ this.maxReconnectDelayMs = options?.connect.maxReconnectDelay ?? DEFAULT_MAX_RECONNECT_DELAY_MS;
305
+ this.logHandler = options?.logHandler ?? parent?.logHandler;
268
306
  this.parent = parent;
269
307
  parent?.eventBus.on("*", (type, event) => {
270
308
  if (filter) {
@@ -313,7 +351,7 @@ var Context = class Context {
313
351
  return this.tryResolve(service) !== void 0;
314
352
  }
315
353
  fork(filter) {
316
- const subContext = new Context(this.client, this, filter);
354
+ const subContext = new Context(this.client, void 0, this, filter);
317
355
  this.subContexts.push(subContext);
318
356
  return subContext;
319
357
  }
@@ -327,18 +365,23 @@ var Context = class Context {
327
365
  async applyPlugins() {
328
366
  for (const { plugin, args } of this.sortPlugins()) {
329
367
  const providedBeforeApply = new Set(this.services.keys());
330
- await plugin.apply(this, ...args);
331
- for (const service of plugin.provides ?? []) if (!this.services.has(service) || providedBeforeApply.has(service)) throw new Error(`${this.describePlugin(plugin)} declares service ${getServiceName(service)} but did not provide it.`);
368
+ await plugin.apply(this.createProxyContextForPlugin(plugin), ...args);
369
+ for (const service of plugin.provides ?? []) if (!this.services.has(service) || providedBeforeApply.has(service)) throw new Error(`${plugin.name} declares service ${getServiceName(service)} but did not provide it.`);
332
370
  }
333
371
  }
334
372
  sortPlugins() {
335
- this.validateUniqueProvidedServices();
373
+ const providers = /* @__PURE__ */ new Map();
374
+ for (const { plugin } of this.plugins) for (const service of plugin.provides ?? []) {
375
+ const existingProvider = providers.get(service);
376
+ if (existingProvider) throw new Error(`Service ${getServiceName(service)} is declared by multiple plugins: ${existingProvider.name} and ${plugin.name}.`);
377
+ providers.set(service, plugin);
378
+ }
336
379
  const pending = [...this.plugins];
337
380
  const sorted = [];
338
381
  const available = /* @__PURE__ */ new Set();
339
382
  for (const service of this.collectAvailableServices()) available.add(service);
340
383
  while (pending.length > 0) {
341
- const nextIndex = pending.findIndex(({ plugin }) => this.areRequirementsAvailable(plugin, available));
384
+ const nextIndex = pending.findIndex(({ plugin }) => (plugin.requires ?? []).every((service) => available.has(service)));
342
385
  if (nextIndex === -1) throw this.createUnresolvablePluginError(pending, available);
343
386
  const [next] = pending.splice(nextIndex, 1);
344
387
  sorted.push(next);
@@ -346,22 +389,11 @@ var Context = class Context {
346
389
  }
347
390
  return sorted;
348
391
  }
349
- validateUniqueProvidedServices() {
350
- const providers = /* @__PURE__ */ new Map();
351
- for (const { plugin } of this.plugins) for (const service of plugin.provides ?? []) {
352
- const existingProvider = providers.get(service);
353
- if (existingProvider) throw new Error(`Service ${getServiceName(service)} is declared by multiple plugins: ${this.describePlugin(existingProvider)} and ${this.describePlugin(plugin)}.`);
354
- providers.set(service, plugin);
355
- }
356
- }
357
392
  collectAvailableServices() {
358
393
  const services = [...this.services.keys()];
359
394
  if (this.parent) services.push(...this.parent.collectAvailableServices());
360
395
  return services;
361
396
  }
362
- areRequirementsAvailable(plugin, available) {
363
- return (plugin.requires ?? []).every((service) => available.has(service));
364
- }
365
397
  createUnresolvablePluginError(pending, available) {
366
398
  const missingRequirements = /* @__PURE__ */ new Map();
367
399
  const pendingProviders = /* @__PURE__ */ new Set();
@@ -372,44 +404,43 @@ var Context = class Context {
372
404
  missingRequirements.set(service, plugins);
373
405
  }
374
406
  const lines = [...missingRequirements].map(([service, plugins]) => {
375
- const dependents = plugins.map((plugin) => this.describePlugin(plugin)).join(", ");
407
+ const dependents = plugins.map((plugin) => plugin.name).join(", ");
376
408
  const reason = pendingProviders.has(service) ? "blocked by a dependency cycle" : "no installed plugin provides it";
377
409
  return `${getServiceName(service)} required by ${dependents} (${reason})`;
378
410
  });
379
411
  return /* @__PURE__ */ new Error(`Unable to resolve plugin service dependencies: ${lines.join("; ")}.`);
380
412
  }
381
- describePlugin(plugin) {
382
- const pluginIndex = this.plugins.findIndex((installed) => installed.plugin === plugin);
383
- return plugin.constructor.name && plugin.constructor.name !== "Object" ? plugin.constructor.name : `plugin #${pluginIndex + 1}`;
413
+ createProxyContextForPlugin(plugin) {
414
+ const proxyLogger = new Logger((message) => this.logHandler?.(message), plugin.name);
415
+ return new Proxy(this, { get(target, prop) {
416
+ if (prop === "logger") return proxyLogger;
417
+ else return target[prop];
418
+ } });
384
419
  }
385
420
  async connectEventWebSocket() {
386
- let reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
421
+ let reconnectDelay = this.initialReconnectDelayMs;
387
422
  while (this.isStarted) {
388
423
  try {
389
- const ws = await this.client.openEventWebSocket((event) => this.handleWebSocketMessage(event));
390
- reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
424
+ const ws = await this.client.openEventWebSocket((event) => {
425
+ try {
426
+ if (typeof event.data !== "string") throw new Error(`Expected text frame, got ${typeof event.data}`);
427
+ const payload = JSON.parse(event.data);
428
+ this.eventBus.emit(payload.event_type, payload);
429
+ } catch (error) {
430
+ console.error("Error handling WebSocket event:", error);
431
+ }
432
+ });
433
+ reconnectDelay = this.initialReconnectDelayMs;
391
434
  await new Promise((resolve) => {
392
435
  ws.addEventListener("close", () => resolve(), { once: true });
393
436
  });
394
437
  } catch (error) {
395
438
  console.error("Error connecting event WebSocket:", error);
396
439
  }
397
- await this.delay(reconnectDelay);
398
- reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
440
+ await new Promise((resolve) => setTimeout(resolve, reconnectDelay));
441
+ reconnectDelay = Math.min(reconnectDelay * 2, this.maxReconnectDelayMs);
399
442
  }
400
443
  }
401
- handleWebSocketMessage(event) {
402
- try {
403
- if (typeof event.data !== "string") throw new Error(`Expected text frame, got ${typeof event.data}`);
404
- const payload = JSON.parse(event.data);
405
- this.eventBus.emit(payload.event_type, payload);
406
- } catch (error) {
407
- console.error("Error handling WebSocket event:", error);
408
- }
409
- }
410
- delay(ms) {
411
- return new Promise((resolve) => setTimeout(resolve, ms));
412
- }
413
444
  createSession(message) {
414
445
  return {
415
446
  raw: message,
@@ -436,7 +467,7 @@ var Context = class Context {
436
467
  };
437
468
  }
438
469
  static create(options) {
439
- return new Context(createMilkyClient(options.connect.baseUrl, { accessToken: options.connect.accessToken }));
470
+ return new Context(createMilkyClient(options.connect.baseUrl, { accessToken: options.connect.accessToken }), options);
440
471
  }
441
472
  };
442
473
  //#endregion
@@ -476,7 +507,7 @@ let filter;
476
507
  return result;
477
508
  }
478
509
  _filter.allPass = allPass;
479
- function friend(uinList) {
510
+ function friend(...uinList) {
480
511
  const uinSet = new Set(uinList);
481
512
  return {
482
513
  message_receive: ({ data }) => data.message_scene === "friend" && uinSet.has(data.peer_id),
@@ -488,7 +519,7 @@ let filter;
488
519
  };
489
520
  }
490
521
  _filter.friend = friend;
491
- function group(uinList) {
522
+ function group(...uinList) {
492
523
  const uinSet = new Set(uinList);
493
524
  return {
494
525
  message_receive: ({ data }) => data.message_scene === "group" && uinSet.has(data.peer_id),
@@ -774,4 +805,4 @@ let param;
774
805
  _param.segment = segment;
775
806
  })(param || (param = {}));
776
807
  //#endregion
777
- export { Context, Parameter, Router, createMilkyClient, definePlugin, filter, milkyPackageVersion, milkyVersion, msg, param, seg };
808
+ export { Context, Logger, Parameter, Router, createMilkyClient, definePlugin, filter, milkyPackageVersion, milkyVersion, msg, param, seg };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fraqjs/fraq",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "description": "Milky Bot framework",
6
6
  "files": [
7
7
  "dist"
@@ -14,6 +14,7 @@
14
14
  "type": "git",
15
15
  "url": "git+https://github.com/Wesley-Young/fraq.git"
16
16
  },
17
+ "homepage": "https://fraq.ntqqrev.org/",
17
18
  "license": "MIT",
18
19
  "dependencies": {
19
20
  "mitt": "^3.0.1"