qqbot-elysia 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/logger.ts ADDED
@@ -0,0 +1,197 @@
1
+ // ============================================================
2
+ // QQ Bot SDK — 日志系统
3
+ // ============================================================
4
+
5
+ import log4js from "log4js";
6
+
7
+ // ─── ANSI 颜色码 ─────────────────────────────────────────────
8
+
9
+ const COLORS = {
10
+ reset: "\x1b[0m",
11
+ bold: "\x1b[1m",
12
+ dim: "\x1b[2m",
13
+
14
+ // 前景色
15
+ cyan: "\x1b[36m",
16
+ green: "\x1b[32m",
17
+ yellow: "\x1b[33m",
18
+ red: "\x1b[31m",
19
+ magenta: "\x1b[35m",
20
+ blue: "\x1b[34m",
21
+ white: "\x1b[37m",
22
+ gray: "\x1b[90m",
23
+
24
+ // 背景色
25
+ bgCyan: "\x1b[46m",
26
+ bgGreen: "\x1b[42m",
27
+ bgYellow: "\x1b[43m",
28
+ bgRed: "\x1b[41m",
29
+ bgMagenta: "\x1b[45m",
30
+ bgBlue: "\x1b[44m",
31
+ bgWhite: "\x1b[47m",
32
+ } as const;
33
+
34
+ // ─── 模块标签颜色映射 ────────────────────────────────────────
35
+
36
+ const MODULE_COLORS: Record<string, string> = {
37
+ Bot: `${COLORS.bold}${COLORS.bgMagenta}${COLORS.white}`,
38
+ Auth: `${COLORS.bold}${COLORS.bgBlue}${COLORS.white}`,
39
+ QQAPI: `${COLORS.bold}${COLORS.bgCyan}${COLORS.white}`,
40
+ Webhook: `${COLORS.bold}${COLORS.bgGreen}${COLORS.white}`,
41
+ Event: `${COLORS.bold}${COLORS.bgYellow}${COLORS.white}`,
42
+ Matcher: `${COLORS.bold}${COLORS.bgWhite}${COLORS.magenta}`,
43
+ };
44
+
45
+ // ─── 自定义 Layout ───────────────────────────────────────────
46
+
47
+ log4js.addLayout("qqbot", () => {
48
+ return (logEvent) => {
49
+ const { level, categoryName, data } = logEvent;
50
+ const time = new Date(logEvent.startTime);
51
+
52
+ // 格式化时间 HH:mm:ss.SSS
53
+ const timeStr =
54
+ `${COLORS.gray}${String(time.getHours()).padStart(2, "0")}:` +
55
+ `${String(time.getMinutes()).padStart(2, "0")}:` +
56
+ `${String(time.getSeconds()).padStart(2, "0")}.` +
57
+ `${String(time.getMilliseconds()).padStart(3, "0")}${COLORS.reset}`;
58
+
59
+ // 等级颜色
60
+ let levelColor: string;
61
+ let levelIcon: string;
62
+ switch (level.levelStr) {
63
+ case "TRACE":
64
+ levelColor = COLORS.gray;
65
+ levelIcon = "🔍";
66
+ break;
67
+ case "DEBUG":
68
+ levelColor = COLORS.cyan;
69
+ levelIcon = "🐛";
70
+ break;
71
+ case "INFO":
72
+ levelColor = COLORS.green;
73
+ levelIcon = "✨";
74
+ break;
75
+ case "WARN":
76
+ levelColor = COLORS.yellow;
77
+ levelIcon = "⚠️";
78
+ break;
79
+ case "ERROR":
80
+ levelColor = COLORS.red;
81
+ levelIcon = "❌";
82
+ break;
83
+ case "FATAL":
84
+ levelColor = `${COLORS.bold}${COLORS.red}`;
85
+ levelIcon = "💀";
86
+ break;
87
+ default:
88
+ levelColor = COLORS.white;
89
+ levelIcon = "📝";
90
+ }
91
+
92
+ const levelStr = `${levelColor}${level.levelStr.padEnd(5)}${COLORS.reset}`;
93
+
94
+ // 模块标签
95
+ const modColor = MODULE_COLORS[categoryName] ?? `${COLORS.bold}${COLORS.bgBlue}${COLORS.white}`;
96
+ const modTag = `${modColor} ${categoryName} ${COLORS.reset}`;
97
+
98
+ // 消息内容
99
+ const msg = data
100
+ .map((d: any) => (typeof d === "object" ? JSON.stringify(d, null, 2) : String(d)))
101
+ .join(" ");
102
+
103
+ return `${timeStr} ${levelIcon} ${levelStr} ${modTag} ${msg}`;
104
+ };
105
+ });
106
+
107
+ // ─── log4js 配置 ─────────────────────────────────────────────
108
+
109
+ log4js.configure({
110
+ appenders: {
111
+ console: {
112
+ type: "stdout",
113
+ layout: { type: "qqbot" },
114
+ },
115
+ file: {
116
+ type: "file",
117
+ filename: "logs/qqbot.log",
118
+ maxLogSize: 10485760, // 10MB
119
+ backups: 3,
120
+ layout: { type: "pattern", pattern: "%d{ISO8601} [%p] [%c] %m" },
121
+ },
122
+ },
123
+ categories: {
124
+ default: { appenders: ["console", "file"], level: "trace" },
125
+ },
126
+ });
127
+
128
+ // ─── 导出 Logger 工厂 ────────────────────────────────────────
129
+
130
+ export function getLogger(category: string): log4js.Logger {
131
+ return log4js.getLogger(category);
132
+ }
133
+
134
+ // ─── 预定义模块 Logger ───────────────────────────────────────
135
+
136
+ export const botLogger = getLogger("Bot");
137
+ export const authLogger = getLogger("Auth");
138
+ export const apiLogger = getLogger("QQAPI");
139
+ export const webhookLogger = getLogger("Webhook");
140
+ export const eventLogger = getLogger("Event");
141
+ export const matcherLogger = getLogger("Matcher");
142
+
143
+ // ─── 启动 Banner ─────────────────────────────────────────────
144
+
145
+ export function printBanner(): void {
146
+ const banner = `
147
+ ${COLORS.bold}${COLORS.magenta} ╔══════════════════════════════════════╗
148
+ ║ ║
149
+ ║ 🦊 QQ Bot SDK (Elysia) 🦊 ║
150
+ ║ ║
151
+ ╚══════════════════════════════════════╝${COLORS.reset}
152
+ `;
153
+ console.log(banner);
154
+ }
155
+
156
+ // ─── 事件流日志美化 ──────────────────────────────────────────
157
+
158
+ export function logEventFlow(
159
+ eventType: string,
160
+ source: string,
161
+ detail: string
162
+ ): void {
163
+ const arrow = `${COLORS.cyan}━━▶${COLORS.reset}`;
164
+ const eventColor = `${COLORS.bold}${COLORS.yellow}`;
165
+ eventLogger.info(
166
+ `${arrow} ${eventColor}${eventType}${COLORS.reset} ${COLORS.gray}│${COLORS.reset} ${COLORS.blue}${source}${COLORS.reset} ${arrow} ${detail}`
167
+ );
168
+ }
169
+
170
+ export function logMessageSend(
171
+ targetType: string,
172
+ targetId: string,
173
+ messageId: string
174
+ ): void {
175
+ const arrow = `${COLORS.green}◀━━${COLORS.reset}`;
176
+ apiLogger.info(
177
+ `${arrow} ${COLORS.bold}消息发送${COLORS.reset} ${COLORS.gray}│${COLORS.reset} ` +
178
+ `类型: ${COLORS.cyan}${targetType}${COLORS.reset} ` +
179
+ `目标: ${COLORS.yellow}${targetId}${COLORS.reset} ` +
180
+ `ID: ${COLORS.magenta}${messageId}${COLORS.reset}`
181
+ );
182
+ }
183
+
184
+ export function logRecall(
185
+ targetType: string,
186
+ targetId: string,
187
+ messageId: string,
188
+ success: boolean
189
+ ): void {
190
+ const icon = success ? `${COLORS.green}✔${COLORS.reset}` : `${COLORS.red}✘${COLORS.reset}`;
191
+ apiLogger.info(
192
+ `${icon} ${COLORS.bold}消息撤回${COLORS.reset} ${COLORS.gray}│${COLORS.reset} ` +
193
+ `类型: ${COLORS.cyan}${targetType}${COLORS.reset} ` +
194
+ `目标: ${COLORS.yellow}${targetId}${COLORS.reset} ` +
195
+ `消息: ${COLORS.magenta}${messageId}${COLORS.reset}`
196
+ );
197
+ }
@@ -0,0 +1,77 @@
1
+ // ============================================================
2
+ // QQ Bot SDK — 消息匹配器
3
+ // ============================================================
4
+
5
+ export interface MatchResult {
6
+ matched: boolean;
7
+ /** 匹配后剩余的内容 */
8
+ rest: string;
9
+ }
10
+
11
+ /**
12
+ * 命令匹配器 — 匹配 /cmd 或 cmd 开头的消息
13
+ * 对应 Cocotst 的 QCommandMatcher
14
+ */
15
+ export function matchCommand(content: string, command: string): MatchResult {
16
+ const prefixes = [` /${command}`, `/${command}`, ` ${command}`, command];
17
+
18
+ for (const prefix of prefixes) {
19
+ if (content.startsWith(prefix)) {
20
+ const rest = content.slice(prefix.length).replace(/^ /, "");
21
+ return { matched: true, rest };
22
+ }
23
+ }
24
+
25
+ return { matched: false, rest: content };
26
+ }
27
+
28
+ /**
29
+ * 前缀匹配器 — 匹配消息开头
30
+ * 对应 Cocotst 的 DetectPrefix
31
+ */
32
+ export function matchPrefix(
33
+ content: string,
34
+ prefixes: string | string[]
35
+ ): MatchResult {
36
+ const list = Array.isArray(prefixes) ? prefixes : [prefixes];
37
+
38
+ for (const prefix of list) {
39
+ if (content.startsWith(prefix)) {
40
+ const rest = content.slice(prefix.length).replace(/^ /, "");
41
+ return { matched: true, rest };
42
+ }
43
+ }
44
+
45
+ return { matched: false, rest: content };
46
+ }
47
+
48
+ /**
49
+ * 后缀匹配器 — 匹配消息结尾
50
+ * 对应 Cocotst 的 DetectSuffix
51
+ */
52
+ export function matchSuffix(
53
+ content: string,
54
+ suffixes: string | string[]
55
+ ): MatchResult {
56
+ const list = Array.isArray(suffixes) ? suffixes : [suffixes];
57
+
58
+ for (const suffix of list) {
59
+ if (content.endsWith(suffix)) {
60
+ const rest = content.slice(0, -suffix.length).replace(/ $/, "");
61
+ return { matched: true, rest };
62
+ }
63
+ }
64
+
65
+ return { matched: false, rest: content };
66
+ }
67
+
68
+ /**
69
+ * 关键词匹配器 — 匹配消息中是否包含关键词
70
+ * 对应 Cocotst 的 ContainKeyword
71
+ */
72
+ export function matchKeyword(content: string, keyword: string): MatchResult {
73
+ if (content.includes(keyword)) {
74
+ return { matched: true, rest: content };
75
+ }
76
+ return { matched: false, rest: content };
77
+ }
package/src/module.ts ADDED
@@ -0,0 +1,58 @@
1
+ // ============================================================
2
+ // QQ Bot SDK — BotModule 基类
3
+ // ============================================================
4
+
5
+ import { getHandlerMetas } from "./decorators";
6
+ import type { HandlerMeta } from "./types/types";
7
+ import { botLogger } from "./logger";
8
+
9
+ export interface CollectedHandler {
10
+ meta: HandlerMeta;
11
+ handler: (...args: any[]) => any;
12
+ moduleName: string;
13
+ }
14
+
15
+ /**
16
+ * BotModule — 模块化开发基类
17
+ *
18
+ * 用法:
19
+ * ```ts
20
+ * class MyPlugin extends BotModule {
21
+ * @OnCommand("ping")
22
+ * async onPing(ctx: GroupMessageContext) {
23
+ * await ctx.reply("pong!");
24
+ * }
25
+ * }
26
+ *
27
+ * bot.use(new MyPlugin());
28
+ * ```
29
+ */
30
+ export abstract class BotModule {
31
+ /** 模块加载时的钩子 */
32
+ async onLoad(): Promise<void> { }
33
+
34
+ /** 模块卸载时的钩子 */
35
+ async onUnload(): Promise<void> { }
36
+
37
+ /**
38
+ * 收集当前模块上所有通过装饰器注册的处理器
39
+ */
40
+ collectHandlers(): CollectedHandler[] {
41
+ const metas = getHandlerMetas(Object.getPrototypeOf(this));
42
+ const handlers: CollectedHandler[] = [];
43
+ const moduleName = this.constructor.name;
44
+
45
+ for (const meta of metas) {
46
+ const method = (this as any)[meta.propertyKey];
47
+ if (typeof method === "function") {
48
+ handlers.push({
49
+ meta,
50
+ handler: method.bind(this),
51
+ moduleName,
52
+ });
53
+ }
54
+ }
55
+
56
+ return handlers;
57
+ }
58
+ }
package/src/sign.ts ADDED
@@ -0,0 +1,32 @@
1
+ // ============================================================
2
+ // QQ Bot SDK — Ed25519 签名模块
3
+ // ============================================================
4
+
5
+ import * as ed from "@noble/ed25519";
6
+
7
+ /**
8
+ * QQ 开放平台签名算法
9
+ * 使用 Ed25519 私钥对数据进行签名
10
+ */
11
+ export function sign(secret: string, data: string): string {
12
+ const secretBytes = new TextEncoder().encode(secret);
13
+ // Ed25519 私钥为 32 字节 seed
14
+ const privateKey = secretBytes.slice(0, 32);
15
+ // 同步签名
16
+ const dataBytes = new TextEncoder().encode(data);
17
+ // @noble/ed25519 v2+ 使用 sync 方式时需配置 sha512
18
+ const signature = ed.sign(dataBytes, privateKey);
19
+ return Buffer.from(signature).toString("hex");
20
+ }
21
+
22
+ /**
23
+ * 处理 QQ Webhook 签名验证请求
24
+ */
25
+ export async function handleSignatureVerify(
26
+ secret: string,
27
+ plainToken: string,
28
+ eventTs: string
29
+ ): Promise<{ plain_token: string; signature: string }> {
30
+ const signature = sign(secret, eventTs + plainToken);
31
+ return { plain_token: plainToken, signature };
32
+ }