@fraqjs/fraq 0.1.0 → 0.3.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 +54 -1
- package/dist/index.d.mts +68 -28
- package/dist/index.mjs +130 -67
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
1
|
# Fraq
|
|
2
2
|
|
|
3
|
-
|
|
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.fromUrl('<替换成你的 Milky 协议端地址>', {
|
|
27
|
+
// accessToken: '<如果你的 Milky 协议端配置了 token,请在这里添加>',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
ctx.router.command(
|
|
31
|
+
'echo',
|
|
32
|
+
{
|
|
33
|
+
content: param.greedy(),
|
|
34
|
+
},
|
|
35
|
+
(session, { content }) => {
|
|
36
|
+
session.reply(msg`You said: ${content}`);
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
ctx.start();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
在一切完成之后,用 `tsx` 执行你的 `index.ts` 文件:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx tsx index.ts
|
|
47
|
+
# 或 tsx index.ts, 如果你全局安装了 tsx
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
你可以在任何一个包含它的会话中发送 `echo Hello` 来测试它是否正常工作。
|
|
51
|
+
|
|
52
|
+
## 下一步
|
|
53
|
+
|
|
54
|
+
上面的例子只是 Fraq 的最小使用方式,你还可以继续了解如何构造更丰富的消息、定义更复杂的指令参数、编写可复用的插件等。
|
|
55
|
+
|
|
56
|
+
完整文档请见 [fraq.ntqqrev.org](https://fraq.ntqqrev.org/)。
|
package/dist/index.d.mts
CHANGED
|
@@ -5396,21 +5396,27 @@ type EventMap = {
|
|
|
5396
5396
|
};
|
|
5397
5397
|
//#endregion
|
|
5398
5398
|
//#region src/protocol/client.d.ts
|
|
5399
|
+
interface MilkyEventSubscription {
|
|
5400
|
+
closed: Promise<void>;
|
|
5401
|
+
stop(): void | Promise<void>;
|
|
5402
|
+
}
|
|
5403
|
+
interface MilkyClient extends ApiEndpoints {
|
|
5404
|
+
startEvents(onEvent: (event: Event) => void | Promise<void>): Promise<MilkyEventSubscription>;
|
|
5405
|
+
}
|
|
5399
5406
|
declare class MilkyClientBase {
|
|
5400
5407
|
readonly baseUrl: string;
|
|
5401
5408
|
readonly wsBaseUrl: string;
|
|
5402
5409
|
private readonly accessToken?;
|
|
5403
5410
|
private readonly baseHeaders;
|
|
5404
5411
|
constructor(
|
|
5405
|
-
baseUrl: string,
|
|
5412
|
+
baseUrl: string | URL,
|
|
5406
5413
|
options?: {
|
|
5407
5414
|
accessToken?: string;
|
|
5408
5415
|
},
|
|
5409
5416
|
);
|
|
5410
5417
|
callApi(endpoint: string, params?: unknown): Promise<unknown>;
|
|
5411
|
-
|
|
5418
|
+
startEvents(onEvent: (event: Event) => void | Promise<void>): Promise<MilkyEventSubscription>;
|
|
5412
5419
|
}
|
|
5413
|
-
type MilkyClient = MilkyClientBase & ApiEndpoints;
|
|
5414
5420
|
declare function createMilkyClient(...params: ConstructorParameters<typeof MilkyClientBase>): MilkyClient;
|
|
5415
5421
|
//#endregion
|
|
5416
5422
|
//#region src/routing/tokenizer.d.ts
|
|
@@ -5526,13 +5532,32 @@ type Filter = { [K in keyof EventMap]?: (event: EventMap[K]) => boolean };
|
|
|
5526
5532
|
declare namespace filter {
|
|
5527
5533
|
function define(filter: Filter): Filter;
|
|
5528
5534
|
function allPass(): Filter;
|
|
5529
|
-
function friend(uinList: number[]): Filter;
|
|
5530
|
-
function group(uinList: number[]): Filter;
|
|
5535
|
+
function friend(...uinList: number[]): Filter;
|
|
5536
|
+
function group(...uinList: number[]): Filter;
|
|
5531
5537
|
function admin(): Filter;
|
|
5532
5538
|
function or(...filters: Filter[]): Filter;
|
|
5533
5539
|
function and(...filters: Filter[]): Filter;
|
|
5534
5540
|
}
|
|
5535
5541
|
//#endregion
|
|
5542
|
+
//#region src/core/logging.d.ts
|
|
5543
|
+
interface LogMessage {
|
|
5544
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
5545
|
+
module: string;
|
|
5546
|
+
message: string;
|
|
5547
|
+
error?: unknown;
|
|
5548
|
+
time: number;
|
|
5549
|
+
}
|
|
5550
|
+
declare class Logger {
|
|
5551
|
+
private readonly logHandler;
|
|
5552
|
+
private readonly module;
|
|
5553
|
+
constructor(logHandler: (message: LogMessage) => void, module: string);
|
|
5554
|
+
private log;
|
|
5555
|
+
debug(message: string): void;
|
|
5556
|
+
info(message: string): void;
|
|
5557
|
+
warn(message: string, error?: unknown): void;
|
|
5558
|
+
error(message: string, error?: unknown): void;
|
|
5559
|
+
}
|
|
5560
|
+
//#endregion
|
|
5536
5561
|
//#region src/core/service.d.ts
|
|
5537
5562
|
/** biome-ignore-all lint/suspicious/noExplicitAny: Service constructors may accept any arguments. */
|
|
5538
5563
|
type ServiceClass<T extends object = object> = abstract new (...args: any[]) => T;
|
|
@@ -5540,21 +5565,38 @@ type ServiceClass<T extends object = object> = abstract new (...args: any[]) =>
|
|
|
5540
5565
|
//#region src/core/plugin.d.ts
|
|
5541
5566
|
type ParameterList = Array<any>;
|
|
5542
5567
|
interface Plugin<T extends ParameterList> {
|
|
5568
|
+
name: string;
|
|
5543
5569
|
requires?: readonly ServiceClass[];
|
|
5544
5570
|
provides?: readonly ServiceClass[];
|
|
5545
|
-
apply(
|
|
5571
|
+
apply(ctx: Context, ...args: T): void | Promise<void>;
|
|
5546
5572
|
}
|
|
5547
5573
|
declare function definePlugin<T extends ParameterList>(plugin: Plugin<T>): Plugin<T>;
|
|
5548
5574
|
//#endregion
|
|
5549
5575
|
//#region src/core/context.d.ts
|
|
5576
|
+
interface ContextOptions {
|
|
5577
|
+
reconnect?: {
|
|
5578
|
+
initialDelayMs?: number;
|
|
5579
|
+
maxDelayMs?: number;
|
|
5580
|
+
};
|
|
5581
|
+
logHandler?: (message: LogMessage) => void;
|
|
5582
|
+
}
|
|
5583
|
+
interface ContextUrlOptions {
|
|
5584
|
+
accessToken?: string;
|
|
5585
|
+
}
|
|
5550
5586
|
declare class Context {
|
|
5551
5587
|
readonly client: MilkyClient;
|
|
5552
|
-
private readonly parent?;
|
|
5553
5588
|
readonly router: Router;
|
|
5589
|
+
readonly logger: Logger;
|
|
5590
|
+
readonly name: string;
|
|
5591
|
+
private readonly parent?;
|
|
5592
|
+
private readonly filter?;
|
|
5554
5593
|
private readonly eventBus;
|
|
5555
5594
|
private readonly plugins;
|
|
5556
5595
|
private readonly services;
|
|
5557
5596
|
private readonly subContexts;
|
|
5597
|
+
private readonly initialReconnectDelayMs;
|
|
5598
|
+
private readonly maxReconnectDelayMs;
|
|
5599
|
+
private readonly logHandler?;
|
|
5558
5600
|
private isStarted;
|
|
5559
5601
|
private constructor();
|
|
5560
5602
|
on<K extends keyof EventMap>(type: K, handler: (event: EventMap[K]) => void | Promise<void>): void;
|
|
@@ -5563,25 +5605,18 @@ declare class Context {
|
|
|
5563
5605
|
resolve<T extends object>(service: ServiceClass<T>): T;
|
|
5564
5606
|
tryResolve<T extends object>(service: ServiceClass<T>): T | undefined;
|
|
5565
5607
|
isProvided<T extends object>(service: ServiceClass<T>): boolean;
|
|
5566
|
-
fork(filter?: Filter): Context;
|
|
5608
|
+
fork(name: string, filter?: Filter): Context;
|
|
5567
5609
|
start(): Promise<void>;
|
|
5610
|
+
private acceptsParentEvent;
|
|
5568
5611
|
private applyPlugins;
|
|
5569
5612
|
private sortPlugins;
|
|
5570
|
-
private validateUniqueProvidedServices;
|
|
5571
5613
|
private collectAvailableServices;
|
|
5572
|
-
private areRequirementsAvailable;
|
|
5573
5614
|
private createUnresolvablePluginError;
|
|
5574
|
-
private
|
|
5575
|
-
private
|
|
5576
|
-
private handleWebSocketMessage;
|
|
5577
|
-
private delay;
|
|
5615
|
+
private createProxyContextForPlugin;
|
|
5616
|
+
private runEventStream;
|
|
5578
5617
|
private createSession;
|
|
5579
|
-
static
|
|
5580
|
-
|
|
5581
|
-
baseUrl: string;
|
|
5582
|
-
accessToken?: string;
|
|
5583
|
-
};
|
|
5584
|
-
}): Context;
|
|
5618
|
+
static fromUrl(baseUrl: string | URL, options?: ContextOptions & ContextUrlOptions): Context;
|
|
5619
|
+
static fromClient(client: MilkyClient, options?: ContextOptions): Context;
|
|
5585
5620
|
}
|
|
5586
5621
|
//#endregion
|
|
5587
5622
|
//#region src/protocol/segment.d.ts
|
|
@@ -5625,24 +5660,29 @@ declare namespace seg {
|
|
|
5625
5660
|
}
|
|
5626
5661
|
//#endregion
|
|
5627
5662
|
export {
|
|
5628
|
-
|
|
5663
|
+
ApiEndpoints,
|
|
5629
5664
|
Context,
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5665
|
+
ContextOptions,
|
|
5666
|
+
ContextUrlOptions,
|
|
5667
|
+
EventMap,
|
|
5668
|
+
Filter,
|
|
5669
|
+
LogMessage,
|
|
5670
|
+
Logger,
|
|
5671
|
+
MilkyClient,
|
|
5672
|
+
MilkyEventSubscription,
|
|
5633
5673
|
Parameter,
|
|
5634
|
-
|
|
5635
|
-
|
|
5674
|
+
ParameterList,
|
|
5675
|
+
Plugin,
|
|
5636
5676
|
Router,
|
|
5637
5677
|
type ServiceClass,
|
|
5638
|
-
|
|
5678
|
+
Session,
|
|
5639
5679
|
createMilkyClient,
|
|
5640
5680
|
definePlugin,
|
|
5641
5681
|
filter,
|
|
5682
|
+
type types_d_exports as milky,
|
|
5642
5683
|
milkyPackageVersion,
|
|
5643
5684
|
milkyVersion,
|
|
5644
5685
|
msg,
|
|
5645
5686
|
param,
|
|
5646
5687
|
seg,
|
|
5647
|
-
type types_d_exports as types,
|
|
5648
5688
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -6,7 +6,8 @@ var MilkyClientBase = class {
|
|
|
6
6
|
accessToken;
|
|
7
7
|
baseHeaders;
|
|
8
8
|
constructor(baseUrl, options) {
|
|
9
|
-
|
|
9
|
+
const normalizedBaseUrl = baseUrl.toString();
|
|
10
|
+
this.baseUrl = normalizedBaseUrl.endsWith("/") ? normalizedBaseUrl.slice(0, -1) : normalizedBaseUrl;
|
|
10
11
|
this.wsBaseUrl = this.baseUrl.replace(/^http/, "ws");
|
|
11
12
|
this.accessToken = options?.accessToken;
|
|
12
13
|
this.baseHeaders = { "Content-Type": "application/json" };
|
|
@@ -18,18 +19,41 @@ var MilkyClientBase = class {
|
|
|
18
19
|
body: JSON.stringify(params ?? {}),
|
|
19
20
|
headers: this.baseHeaders
|
|
20
21
|
});
|
|
21
|
-
if (!response.ok) throw new Error(`API call failed with status ${response.status}`);
|
|
22
|
+
if (!response.ok) throw new Error(`API call ${endpoint} failed with HTTP status ${response.status}`);
|
|
22
23
|
const json = await response.json();
|
|
23
|
-
if (json.status === "failed") throw new Error(`API call failed with retcode ${json.retcode}: ${json.message}`);
|
|
24
|
+
if (json.status === "failed") throw new Error(`API call ${endpoint} failed with retcode ${json.retcode}: ${json.message}`);
|
|
24
25
|
return json.data;
|
|
25
26
|
}
|
|
26
|
-
async
|
|
27
|
+
async startEvents(onEvent) {
|
|
27
28
|
const ws = new WebSocket(`${this.wsBaseUrl}/event${this.accessToken ? `?access_token=${this.accessToken}` : ""}`);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 });
|
|
31
47
|
ws.addEventListener("error", (event) => reject(/* @__PURE__ */ new Error(`WebSocket error: ${event}`)));
|
|
32
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
|
+
};
|
|
33
57
|
}
|
|
34
58
|
};
|
|
35
59
|
function createMilkyClient(...params) {
|
|
@@ -246,38 +270,78 @@ var Router = class Router {
|
|
|
246
270
|
}
|
|
247
271
|
};
|
|
248
272
|
//#endregion
|
|
273
|
+
//#region src/core/logging.ts
|
|
274
|
+
var Logger = class {
|
|
275
|
+
logHandler;
|
|
276
|
+
module;
|
|
277
|
+
constructor(logHandler, module) {
|
|
278
|
+
this.logHandler = logHandler;
|
|
279
|
+
this.module = module;
|
|
280
|
+
}
|
|
281
|
+
log(level, message, error) {
|
|
282
|
+
this.logHandler({
|
|
283
|
+
level,
|
|
284
|
+
module: this.module,
|
|
285
|
+
message,
|
|
286
|
+
error,
|
|
287
|
+
time: Date.now()
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
debug(message) {
|
|
291
|
+
this.log("debug", message);
|
|
292
|
+
}
|
|
293
|
+
info(message) {
|
|
294
|
+
this.log("info", message);
|
|
295
|
+
}
|
|
296
|
+
warn(message, error) {
|
|
297
|
+
this.log("warn", message, error);
|
|
298
|
+
}
|
|
299
|
+
error(message, error) {
|
|
300
|
+
this.log("error", message, error);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
//#endregion
|
|
249
304
|
//#region src/core/service.ts
|
|
250
305
|
function getServiceName(service) {
|
|
251
306
|
return service.name || "<anonymous service>";
|
|
252
307
|
}
|
|
253
308
|
//#endregion
|
|
254
309
|
//#region src/core/context.ts
|
|
255
|
-
const
|
|
256
|
-
const
|
|
310
|
+
const DEFAULT_INITIAL_RECONNECT_DELAY_MS = 1e3;
|
|
311
|
+
const DEFAULT_MAX_RECONNECT_DELAY_MS = 3e4;
|
|
257
312
|
var Context = class Context {
|
|
258
313
|
client;
|
|
259
|
-
parent;
|
|
260
314
|
router = new Router();
|
|
315
|
+
logger;
|
|
316
|
+
name;
|
|
317
|
+
parent;
|
|
318
|
+
filter;
|
|
261
319
|
eventBus = mitt();
|
|
262
320
|
plugins = [];
|
|
263
321
|
services = /* @__PURE__ */ new Map();
|
|
264
322
|
subContexts = [];
|
|
323
|
+
initialReconnectDelayMs;
|
|
324
|
+
maxReconnectDelayMs;
|
|
325
|
+
logHandler;
|
|
265
326
|
isStarted = false;
|
|
266
|
-
constructor(client, parent, filter) {
|
|
327
|
+
constructor(client, options, name, parent, filter) {
|
|
267
328
|
this.client = client;
|
|
329
|
+
this.initialReconnectDelayMs = options?.reconnect?.initialDelayMs ?? DEFAULT_INITIAL_RECONNECT_DELAY_MS;
|
|
330
|
+
this.maxReconnectDelayMs = options?.reconnect?.maxDelayMs ?? DEFAULT_MAX_RECONNECT_DELAY_MS;
|
|
331
|
+
this.logHandler = options?.logHandler ?? parent?.logHandler;
|
|
332
|
+
this.name = name ?? "root";
|
|
333
|
+
this.logger = new Logger((message) => this.logHandler?.(message), this.name);
|
|
268
334
|
this.parent = parent;
|
|
335
|
+
this.filter = filter;
|
|
269
336
|
parent?.eventBus.on("*", (type, event) => {
|
|
270
|
-
if (
|
|
271
|
-
const predicate = filter[type];
|
|
272
|
-
if (predicate && !predicate(event)) return;
|
|
273
|
-
}
|
|
337
|
+
if (!this.acceptsParentEvent(type, event)) return;
|
|
274
338
|
this.eventBus.emit(type, event);
|
|
275
339
|
});
|
|
276
340
|
this.eventBus.on("message_receive", async ({ data: message }) => {
|
|
277
341
|
try {
|
|
278
|
-
this.router.dispatch(this.createSession(message), message);
|
|
342
|
+
await this.router.dispatch(this.createSession(message), message);
|
|
279
343
|
} catch (error) {
|
|
280
|
-
|
|
344
|
+
this.logger.error(`Error routing command (scene=${message.message_scene} peer=${message.peer_id} sender=${message.sender_id} seq=${message.message_seq})`, error);
|
|
281
345
|
}
|
|
282
346
|
});
|
|
283
347
|
}
|
|
@@ -286,7 +350,7 @@ var Context = class Context {
|
|
|
286
350
|
try {
|
|
287
351
|
await handler(event);
|
|
288
352
|
} catch (error) {
|
|
289
|
-
|
|
353
|
+
this.logger.error(`Error handling event ${type}`, error);
|
|
290
354
|
}
|
|
291
355
|
});
|
|
292
356
|
}
|
|
@@ -312,8 +376,8 @@ var Context = class Context {
|
|
|
312
376
|
isProvided(service) {
|
|
313
377
|
return this.tryResolve(service) !== void 0;
|
|
314
378
|
}
|
|
315
|
-
fork(filter) {
|
|
316
|
-
const subContext = new Context(this.client, this, filter);
|
|
379
|
+
fork(name, filter) {
|
|
380
|
+
const subContext = new Context(this.client, void 0, name, this, filter);
|
|
317
381
|
this.subContexts.push(subContext);
|
|
318
382
|
return subContext;
|
|
319
383
|
}
|
|
@@ -322,23 +386,35 @@ var Context = class Context {
|
|
|
322
386
|
await this.applyPlugins();
|
|
323
387
|
for (const subContext of this.subContexts) await subContext.start();
|
|
324
388
|
this.isStarted = true;
|
|
325
|
-
if (!this.parent) this.
|
|
389
|
+
if (!this.parent) this.runEventStream();
|
|
390
|
+
}
|
|
391
|
+
acceptsParentEvent(type, event) {
|
|
392
|
+
if (!this.filter) return true;
|
|
393
|
+
const predicate = this.filter[type];
|
|
394
|
+
return predicate?.(event) === true;
|
|
326
395
|
}
|
|
327
396
|
async applyPlugins() {
|
|
328
397
|
for (const { plugin, args } of this.sortPlugins()) {
|
|
329
398
|
const providedBeforeApply = new Set(this.services.keys());
|
|
330
|
-
|
|
331
|
-
|
|
399
|
+
this.logger.info(`Applying plugin ${plugin.name}`);
|
|
400
|
+
await plugin.apply(this.createProxyContextForPlugin(plugin), ...args);
|
|
401
|
+
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.`);
|
|
402
|
+
this.logger.debug(`Applied plugin ${plugin.name}`);
|
|
332
403
|
}
|
|
333
404
|
}
|
|
334
405
|
sortPlugins() {
|
|
335
|
-
|
|
406
|
+
const providers = /* @__PURE__ */ new Map();
|
|
407
|
+
for (const { plugin } of this.plugins) for (const service of plugin.provides ?? []) {
|
|
408
|
+
const existingProvider = providers.get(service);
|
|
409
|
+
if (existingProvider) throw new Error(`Service ${getServiceName(service)} is declared by multiple plugins: ${existingProvider.name} and ${plugin.name}.`);
|
|
410
|
+
providers.set(service, plugin);
|
|
411
|
+
}
|
|
336
412
|
const pending = [...this.plugins];
|
|
337
413
|
const sorted = [];
|
|
338
414
|
const available = /* @__PURE__ */ new Set();
|
|
339
415
|
for (const service of this.collectAvailableServices()) available.add(service);
|
|
340
416
|
while (pending.length > 0) {
|
|
341
|
-
const nextIndex = pending.findIndex(({ plugin }) =>
|
|
417
|
+
const nextIndex = pending.findIndex(({ plugin }) => (plugin.requires ?? []).every((service) => available.has(service)));
|
|
342
418
|
if (nextIndex === -1) throw this.createUnresolvablePluginError(pending, available);
|
|
343
419
|
const [next] = pending.splice(nextIndex, 1);
|
|
344
420
|
sorted.push(next);
|
|
@@ -346,22 +422,11 @@ var Context = class Context {
|
|
|
346
422
|
}
|
|
347
423
|
return sorted;
|
|
348
424
|
}
|
|
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
425
|
collectAvailableServices() {
|
|
358
426
|
const services = [...this.services.keys()];
|
|
359
427
|
if (this.parent) services.push(...this.parent.collectAvailableServices());
|
|
360
428
|
return services;
|
|
361
429
|
}
|
|
362
|
-
areRequirementsAvailable(plugin, available) {
|
|
363
|
-
return (plugin.requires ?? []).every((service) => available.has(service));
|
|
364
|
-
}
|
|
365
430
|
createUnresolvablePluginError(pending, available) {
|
|
366
431
|
const missingRequirements = /* @__PURE__ */ new Map();
|
|
367
432
|
const pendingProviders = /* @__PURE__ */ new Set();
|
|
@@ -372,44 +437,39 @@ var Context = class Context {
|
|
|
372
437
|
missingRequirements.set(service, plugins);
|
|
373
438
|
}
|
|
374
439
|
const lines = [...missingRequirements].map(([service, plugins]) => {
|
|
375
|
-
const dependents = plugins.map((plugin) =>
|
|
440
|
+
const dependents = plugins.map((plugin) => plugin.name).join(", ");
|
|
376
441
|
const reason = pendingProviders.has(service) ? "blocked by a dependency cycle" : "no installed plugin provides it";
|
|
377
442
|
return `${getServiceName(service)} required by ${dependents} (${reason})`;
|
|
378
443
|
});
|
|
379
444
|
return /* @__PURE__ */ new Error(`Unable to resolve plugin service dependencies: ${lines.join("; ")}.`);
|
|
380
445
|
}
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
return
|
|
446
|
+
createProxyContextForPlugin(plugin) {
|
|
447
|
+
const proxyLogger = new Logger((message) => this.logHandler?.(message), plugin.name);
|
|
448
|
+
return new Proxy(this, { get(target, prop) {
|
|
449
|
+
if (prop === "logger") return proxyLogger;
|
|
450
|
+
else return target[prop];
|
|
451
|
+
} });
|
|
384
452
|
}
|
|
385
|
-
async
|
|
386
|
-
let reconnectDelay =
|
|
453
|
+
async runEventStream() {
|
|
454
|
+
let reconnectDelay = this.initialReconnectDelayMs;
|
|
387
455
|
while (this.isStarted) {
|
|
388
456
|
try {
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
457
|
+
const subscription = await this.client.startEvents((event) => {
|
|
458
|
+
try {
|
|
459
|
+
this.eventBus.emit(event.event_type, event);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
this.logger.error("Error handling event stream event", error);
|
|
462
|
+
}
|
|
393
463
|
});
|
|
464
|
+
reconnectDelay = this.initialReconnectDelayMs;
|
|
465
|
+
await subscription.closed;
|
|
394
466
|
} catch (error) {
|
|
395
|
-
|
|
467
|
+
this.logger.error("Error connecting event stream", error);
|
|
396
468
|
}
|
|
397
|
-
await
|
|
398
|
-
reconnectDelay = Math.min(reconnectDelay * 2,
|
|
469
|
+
await new Promise((resolve) => setTimeout(resolve, reconnectDelay));
|
|
470
|
+
reconnectDelay = Math.min(reconnectDelay * 2, this.maxReconnectDelayMs);
|
|
399
471
|
}
|
|
400
472
|
}
|
|
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
473
|
createSession(message) {
|
|
414
474
|
return {
|
|
415
475
|
raw: message,
|
|
@@ -430,13 +490,16 @@ var Context = class Context {
|
|
|
430
490
|
break;
|
|
431
491
|
}
|
|
432
492
|
} catch (error) {
|
|
433
|
-
|
|
493
|
+
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);
|
|
434
494
|
}
|
|
435
495
|
}
|
|
436
496
|
};
|
|
437
497
|
}
|
|
438
|
-
static
|
|
439
|
-
return new Context(createMilkyClient(
|
|
498
|
+
static fromUrl(baseUrl, options) {
|
|
499
|
+
return new Context(createMilkyClient(baseUrl, { accessToken: options?.accessToken }), options);
|
|
500
|
+
}
|
|
501
|
+
static fromClient(client, options) {
|
|
502
|
+
return new Context(client, options);
|
|
440
503
|
}
|
|
441
504
|
};
|
|
442
505
|
//#endregion
|
|
@@ -476,7 +539,7 @@ let filter;
|
|
|
476
539
|
return result;
|
|
477
540
|
}
|
|
478
541
|
_filter.allPass = allPass;
|
|
479
|
-
function friend(uinList) {
|
|
542
|
+
function friend(...uinList) {
|
|
480
543
|
const uinSet = new Set(uinList);
|
|
481
544
|
return {
|
|
482
545
|
message_receive: ({ data }) => data.message_scene === "friend" && uinSet.has(data.peer_id),
|
|
@@ -488,7 +551,7 @@ let filter;
|
|
|
488
551
|
};
|
|
489
552
|
}
|
|
490
553
|
_filter.friend = friend;
|
|
491
|
-
function group(uinList) {
|
|
554
|
+
function group(...uinList) {
|
|
492
555
|
const uinSet = new Set(uinList);
|
|
493
556
|
return {
|
|
494
557
|
message_receive: ({ data }) => data.message_scene === "group" && uinSet.has(data.peer_id),
|
|
@@ -774,4 +837,4 @@ let param;
|
|
|
774
837
|
_param.segment = segment;
|
|
775
838
|
})(param || (param = {}));
|
|
776
839
|
//#endregion
|
|
777
|
-
export { Context, Parameter, Router, createMilkyClient, definePlugin, filter, milkyPackageVersion, milkyVersion, msg, param, seg };
|
|
840
|
+
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.
|
|
4
|
+
"version": "0.3.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"
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
"node": ">=22"
|
|
23
24
|
},
|
|
24
25
|
"scripts": {
|
|
25
|
-
"build": "tsdown &&
|
|
26
|
+
"build": "tsdown && tsx scripts/format-dts-comments.ts && biome format --vcs-enabled=false --write dist/index.d.mts",
|
|
26
27
|
"test": "tsx --test test/*.test.ts"
|
|
27
28
|
}
|
|
28
29
|
}
|