mioku-plugin-deerpipe 1.0.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/image.ts ADDED
@@ -0,0 +1,347 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import type { ScreenshotService } from "mioku";
5
+ import { escapeHtml, getAvatarUrl, isNightMode } from "./utils";
6
+
7
+ interface CalendarOptions {
8
+ year: number;
9
+ month: number;
10
+ todayDay: number;
11
+ records: Map<number, number>;
12
+ name: string;
13
+ userId: number;
14
+ }
15
+
16
+ interface RankRow {
17
+ rank: number;
18
+ name: string;
19
+ userId: number;
20
+ count: number;
21
+ }
22
+
23
+ interface DeerTheme {
24
+ pageBg: string;
25
+ textColor: string;
26
+ titleAccent: string;
27
+ countColor: string;
28
+ emptyColor: string;
29
+ dayHalo: string;
30
+ rankCountColor: string;
31
+ avatarBorder: string;
32
+ }
33
+
34
+ const DAY_THEME: DeerTheme = {
35
+ pageBg: "#ffffff",
36
+ textColor: "#000000",
37
+ titleAccent: "#d50000",
38
+ countColor: "#d50000",
39
+ emptyColor: "#888888",
40
+ dayHalo: "#ffffff",
41
+ rankCountColor: "#d50000",
42
+ avatarBorder: "#cccccc",
43
+ };
44
+
45
+ const NIGHT_THEME: DeerTheme = {
46
+ pageBg: "#1d2030",
47
+ textColor: "#f1ecdb",
48
+ titleAccent: "#ff6b6b",
49
+ countColor: "#ff8a65",
50
+ emptyColor: "#9a9a9a",
51
+ dayHalo: "#2a2f44",
52
+ rankCountColor: "#ff8a65",
53
+ avatarBorder: "#5a5f73",
54
+ };
55
+
56
+ function getTheme(): DeerTheme {
57
+ return isNightMode() ? NIGHT_THEME : DAY_THEME;
58
+ }
59
+
60
+ interface AssetUris {
61
+ defaultAvatar: string;
62
+ check: string;
63
+ deerpipe: string;
64
+ }
65
+
66
+ let assetsCache: AssetUris | null = null;
67
+
68
+ async function loadAssets(): Promise<AssetUris> {
69
+ if (assetsCache) return assetsCache;
70
+ const here = path.dirname(fileURLToPath(import.meta.url));
71
+ const assetsDir = path.join(here, "assets");
72
+ const [avatar, check, deerpipe] = await Promise.all([
73
+ fs.readFile(path.join(assetsDir, "akkarin@80x80.png")),
74
+ fs.readFile(path.join(assetsDir, "check@96x100.png")),
75
+ fs.readFile(path.join(assetsDir, "deerpipe@100x82.png")),
76
+ ]);
77
+ assetsCache = {
78
+ defaultAvatar: `data:image/png;base64,${avatar.toString("base64")}`,
79
+ check: `data:image/png;base64,${check.toString("base64")}`,
80
+ deerpipe: `data:image/png;base64,${deerpipe.toString("base64")}`,
81
+ };
82
+ return assetsCache;
83
+ }
84
+
85
+ // Mon–Sun weeks, matching Python's calendar.monthcalendar(year, month)
86
+ function buildMonthGrid(year: number, month: number): number[][] {
87
+ const firstDow = new Date(year, month - 1, 1).getDay(); // 0=Sun
88
+ const daysInMonth = new Date(year, month, 0).getDate();
89
+ // Convert to Monday-first index: Mon=0..Sun=6
90
+ const leadingEmpty = (firstDow + 6) % 7;
91
+ const weeks: number[][] = [];
92
+ let cur: number[] = new Array(leadingEmpty).fill(0);
93
+ for (let d = 1; d <= daysInMonth; d++) {
94
+ cur.push(d);
95
+ if (cur.length === 7) {
96
+ weeks.push(cur);
97
+ cur = [];
98
+ }
99
+ }
100
+ if (cur.length > 0) {
101
+ while (cur.length < 7) cur.push(0);
102
+ weeks.push(cur);
103
+ }
104
+ return weeks;
105
+ }
106
+
107
+ export async function generateCalendarImage(
108
+ screenshotService: ScreenshotService,
109
+ options: CalendarOptions,
110
+ ): Promise<string> {
111
+ const { year, month, records, name, userId } = options;
112
+ const assets = await loadAssets();
113
+ // 日历始终使用白天主题(按用户要求 🦌 / 🦌历 / 补🦌 不适配夜间模式)
114
+ const theme = DAY_THEME;
115
+ const weeks = buildMonthGrid(year, month);
116
+
117
+ const cellSize = 100;
118
+ const headerHeight = 100;
119
+ const imgW = 700;
120
+ const imgH = headerHeight + cellSize * weeks.length;
121
+
122
+ const cellsHtml = weeks
123
+ .map((week, weekIdx) =>
124
+ week
125
+ .map((day, dayIdx) => {
126
+ if (day === 0) return "";
127
+ const x = dayIdx * cellSize;
128
+ const y = headerHeight + weekIdx * cellSize;
129
+ const count = records.get(day) ?? 0;
130
+ const checked = count > 0;
131
+ const countText =
132
+ count > 1 ? (count > 999 ? "x999+" : `x${count}`) : "";
133
+ return `
134
+ <img class="stamp" src="${assets.deerpipe}" style="left:${x}px;top:${y}px" />
135
+ <div class="day-num" style="left:${x + 8}px;top:${y + 50}px">${day}</div>
136
+ ${
137
+ checked
138
+ ? `<img class="check" src="${assets.check}" style="left:${x}px;top:${y}px" />`
139
+ : ""
140
+ }
141
+ ${
142
+ countText
143
+ ? `<div class="count" style="left:${x + cellSize - 5}px;top:${y + cellSize - 25}px">${countText}</div>`
144
+ : ""
145
+ }
146
+ `;
147
+ })
148
+ .join(""),
149
+ )
150
+ .join("");
151
+
152
+ const html = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8" />
153
+ <style>
154
+ ${BASE_CSS}
155
+ .canvas {
156
+ position: relative;
157
+ width: ${imgW}px;
158
+ height: ${imgH}px;
159
+ background: ${theme.pageBg};
160
+ color: ${theme.textColor};
161
+ overflow: hidden;
162
+ }
163
+ .avatar {
164
+ position: absolute;
165
+ left: 10px;
166
+ top: 10px;
167
+ width: 80px;
168
+ height: 80px;
169
+ object-fit: cover;
170
+ border-radius: 50%;
171
+ border: 2px solid ${theme.avatarBorder};
172
+ box-sizing: border-box;
173
+ }
174
+ .title {
175
+ position: absolute;
176
+ left: 100px;
177
+ top: 10px;
178
+ font-size: 25px;
179
+ line-height: 30px;
180
+ color: ${theme.textColor};
181
+ }
182
+ .subtitle {
183
+ position: absolute;
184
+ left: 100px;
185
+ top: 45px;
186
+ font-size: 25px;
187
+ line-height: 30px;
188
+ color: ${theme.textColor};
189
+ }
190
+ .stamp { position: absolute; width: 100px; height: 82px; }
191
+ .check { position: absolute; width: 96px; height: 100px; }
192
+ .day-num {
193
+ position: absolute;
194
+ font-size: 36px;
195
+ line-height: 36px;
196
+ font-weight: 700;
197
+ color: ${theme.textColor};
198
+ text-shadow:
199
+ 1px 0 0 ${theme.dayHalo},
200
+ -1px 0 0 ${theme.dayHalo},
201
+ 0 1px 0 ${theme.dayHalo},
202
+ 0 -1px 0 ${theme.dayHalo},
203
+ 1px 1px 0 ${theme.dayHalo},
204
+ -1px -1px 0 ${theme.dayHalo},
205
+ 1px -1px 0 ${theme.dayHalo},
206
+ -1px 1px 0 ${theme.dayHalo};
207
+ }
208
+ .count {
209
+ position: absolute;
210
+ transform: translateX(-100%);
211
+ font-size: 20px;
212
+ line-height: 20px;
213
+ color: ${theme.countColor};
214
+ font-weight: 700;
215
+ -webkit-text-stroke: 1px ${theme.countColor};
216
+ }
217
+ </style>
218
+ </head>
219
+ <body>
220
+ <div class="canvas">
221
+ <img class="avatar" src="${escapeHtml(getAvatarUrl(userId))}" onerror="this.onerror=null;this.src='${assets.defaultAvatar}'" />
222
+ <div class="title">${year}-${String(month).padStart(2, "0")} 🦌签到日历</div>
223
+ <div class="subtitle">@${escapeHtml(name)}</div>
224
+ ${cellsHtml}
225
+ </div>
226
+ </body></html>`;
227
+
228
+ return screenshotService.screenshot(html, {
229
+ width: imgW,
230
+ height: imgH,
231
+ fullPage: false,
232
+ type: "png",
233
+ });
234
+ }
235
+
236
+ export async function generateRankImage(
237
+ screenshotService: ScreenshotService,
238
+ rows: RankRow[],
239
+ _year: number,
240
+ _month: number,
241
+ ): Promise<string> {
242
+ const assets = await loadAssets();
243
+ const theme = getTheme();
244
+ const imgW = 400;
245
+ const headerHeight = 100;
246
+ const rowHeight = 100;
247
+ // Original: IMG_H = (len(rank) + 1) * 100 — empty rank still gets header + 1 row.
248
+ const visibleRows = rows.length || 1;
249
+ const imgH = headerHeight + rowHeight * visibleRows;
250
+
251
+ const rowsHtml = rows
252
+ .map((row, idx) => {
253
+ const y = headerHeight + idx * rowHeight;
254
+ return `
255
+ <img class="rank-avatar" src="${escapeHtml(getAvatarUrl(row.userId))}" onerror="this.onerror=null;this.src='${assets.defaultAvatar}'" style="top:${y + 10}px" />
256
+ <div class="rank-name" style="top:${y + 10}px">@${escapeHtml(row.name)}</div>
257
+ <div class="rank-count" style="top:${y + 50}px">x${row.count}</div>
258
+ `;
259
+ })
260
+ .join("");
261
+
262
+ const emptyHtml =
263
+ rows.length === 0
264
+ ? `<div class="rank-empty" style="top:${headerHeight + 30}px">本月还没人🦌过呢</div>`
265
+ : "";
266
+
267
+ const html = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8" />
268
+ <style>
269
+ ${BASE_CSS}
270
+ .canvas {
271
+ position: relative;
272
+ width: ${imgW}px;
273
+ height: ${imgH}px;
274
+ background: ${theme.pageBg};
275
+ color: ${theme.textColor};
276
+ overflow: hidden;
277
+ }
278
+ .rank-title {
279
+ position: absolute;
280
+ left: 0;
281
+ top: 25px;
282
+ width: 100%;
283
+ text-align: center;
284
+ font-size: 50px;
285
+ line-height: 50px;
286
+ color: ${theme.titleAccent};
287
+ font-weight: 700;
288
+ -webkit-text-stroke: 1px ${theme.titleAccent};
289
+ }
290
+ .rank-avatar {
291
+ position: absolute;
292
+ left: 10px;
293
+ width: 80px;
294
+ height: 80px;
295
+ object-fit: cover;
296
+ border-radius: 50%;
297
+ border: 2px solid ${theme.avatarBorder};
298
+ box-sizing: border-box;
299
+ }
300
+ .rank-name {
301
+ position: absolute;
302
+ left: 100px;
303
+ font-size: 25px;
304
+ line-height: 25px;
305
+ color: ${theme.textColor};
306
+ }
307
+ .rank-count {
308
+ position: absolute;
309
+ left: 100px;
310
+ font-size: 25px;
311
+ line-height: 25px;
312
+ color: ${theme.rankCountColor};
313
+ }
314
+ .rank-empty {
315
+ position: absolute;
316
+ left: 0;
317
+ width: 100%;
318
+ text-align: center;
319
+ font-size: 22px;
320
+ color: ${theme.emptyColor};
321
+ }
322
+ </style>
323
+ </head>
324
+ <body>
325
+ <div class="canvas">
326
+ <div class="rank-title">本月Top5🦌榜</div>
327
+ ${rowsHtml}
328
+ ${emptyHtml}
329
+ </div>
330
+ </body></html>`;
331
+
332
+ return screenshotService.screenshot(html, {
333
+ width: imgW,
334
+ height: imgH,
335
+ fullPage: false,
336
+ type: "png",
337
+ });
338
+ }
339
+
340
+ const BASE_CSS = `
341
+ * { box-sizing: border-box; margin: 0; padding: 0; }
342
+ body {
343
+ font-family: "MiSans", "PingFang SC", "Microsoft YaHei",
344
+ "Noto Sans CJK SC", "Hiragino Sans GB", "Apple Color Emoji",
345
+ "Noto Color Emoji", sans-serif;
346
+ }
347
+ `;
package/index.ts ADDED
@@ -0,0 +1,86 @@
1
+ import type { ScreenshotService } from "mioku";
2
+ import { definePlugin, type MiokiContext } from "mioki";
3
+ import { initDeerDatabase, type DeerDatabase } from "./db";
4
+ import { handleDeerCommand, parseDeerCommand } from "./commands";
5
+
6
+ const deerpipePlugin = definePlugin({
7
+ name: "deerpipe",
8
+ version: "1.0.0",
9
+ description: "🦌管签到插件,支持自🦌、帮🦌、补🦌、🦌历、🦌榜",
10
+
11
+ async setup(ctx: MiokiContext) {
12
+ ctx.logger.info("deerpipe 插件正在初始化...");
13
+
14
+ const screenshotService = ctx.services?.screenshot as
15
+ | ScreenshotService
16
+ | undefined;
17
+
18
+ if (!screenshotService) {
19
+ ctx.logger.warn("screenshot 服务未加载,deerpipe 插件无法生成图片");
20
+ return () => {
21
+ ctx.logger.info("deerpipe 插件已卸载");
22
+ };
23
+ }
24
+
25
+ let db: DeerDatabase;
26
+ try {
27
+ db = await initDeerDatabase();
28
+ } catch (error) {
29
+ ctx.logger.error(`deerpipe 数据库初始化失败: ${error}`);
30
+ return () => {
31
+ ctx.logger.info("deerpipe 插件已卸载");
32
+ };
33
+ }
34
+
35
+ // 启动时清掉非本月的旧数据,与原版 cleanup 行为一致
36
+ try {
37
+ const now = new Date();
38
+ await db.cleanupOtherMonths(now.getFullYear(), now.getMonth() + 1);
39
+ } catch (error) {
40
+ ctx.logger.warn(`deerpipe 启动期数据清理失败: ${error}`);
41
+ }
42
+
43
+ // 每周一 4:00 清理跨月数据
44
+ ctx.cron("0 4 * * 1", async () => {
45
+ try {
46
+ const now = new Date();
47
+ await db.cleanupOtherMonths(now.getFullYear(), now.getMonth() + 1);
48
+ ctx.logger.info("deerpipe 跨月数据已清理");
49
+ } catch (error) {
50
+ ctx.logger.error(`deerpipe 定时清理失败: ${error}`);
51
+ }
52
+ });
53
+
54
+ ctx.handle("message", async (event: any) => {
55
+ const text = ctx.text(event);
56
+ if (!text) return;
57
+
58
+ const cmd = parseDeerCommand(text, event.message ?? []);
59
+ if (cmd.type === "none") return;
60
+
61
+ try {
62
+ await handleDeerCommand(cmd, {
63
+ ctx,
64
+ db,
65
+ screenshot: screenshotService,
66
+ event,
67
+ });
68
+ } catch (error) {
69
+ ctx.logger.error(`deerpipe 命令执行失败: ${error}`);
70
+ try {
71
+ await event.reply(`🦌管插件出错了: ${error}`, true);
72
+ } catch {
73
+ // 忽略二次失败
74
+ }
75
+ }
76
+ });
77
+
78
+ ctx.logger.info("deerpipe 插件初始化完成");
79
+
80
+ return () => {
81
+ ctx.logger.info("deerpipe 插件已卸载");
82
+ };
83
+ },
84
+ });
85
+
86
+ export default deerpipePlugin;
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "mioku-plugin-deerpipe",
3
+ "version": "1.0.0",
4
+ "description": "🦌管签到插件,支持自🦌、帮🦌、补🦌、🦌历、🦌榜",
5
+ "main": "index.ts",
6
+ "type": "module",
7
+ "keywords": [
8
+ "mioku"
9
+ ],
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/mioku-lab/mioku.git"
13
+ },
14
+ "mioku": {
15
+ "services": [
16
+ "screenshot",
17
+ "help"
18
+ ],
19
+ "help": {
20
+ "title": "🦌管签到",
21
+ "description": "每月🦌管签到,含日历与排行榜",
22
+ "commands": [
23
+ {
24
+ "cmd": "🦌",
25
+ "desc": "签到一次(可加 @某人 帮其签到)",
26
+ "usage": "🦌 / 🦌 @张三",
27
+ "role": "member"
28
+ },
29
+ {
30
+ "cmd": "补🦌 <日>",
31
+ "desc": "补签本月之前未签到的某天",
32
+ "usage": "补🦌 5",
33
+ "role": "member"
34
+ },
35
+ {
36
+ "cmd": "🦌历",
37
+ "desc": "查看本月🦌签到日历(可加 @某人)",
38
+ "usage": "🦌历 / 🦌历 @张三",
39
+ "role": "member"
40
+ },
41
+ {
42
+ "cmd": "🦌榜",
43
+ "desc": "查看本月本群🦌签到排行榜(仅群组)",
44
+ "usage": "🦌榜",
45
+ "role": "member"
46
+ },
47
+ {
48
+ "cmd": "帮🦌 <on|off> [@某人]",
49
+ "desc": "允许/禁止被别人帮🦌;@某人需管理员",
50
+ "usage": "帮🦌 off",
51
+ "role": "member"
52
+ },
53
+ {
54
+ "cmd": "禁🦌 @某人 [时长]",
55
+ "desc": "禁止某人在一段时间内🦌,省略时长视为解禁(仅管理员)",
56
+ "usage": "禁🦌 @张三 1d",
57
+ "role": "admin"
58
+ }
59
+ ]
60
+ }
61
+ },
62
+ "peerDependencies": {
63
+ "mioku": "^0.8.0",
64
+ "mioki": "^0.16.0"
65
+ },
66
+ "devDependencies": {
67
+ "mioku": "workspace:*",
68
+ "mioki": "^0.16.0"
69
+ }
70
+ }
package/types.ts ADDED
@@ -0,0 +1,23 @@
1
+ export interface DeerUser {
2
+ scene: string;
3
+ userId: number;
4
+ canBeHelped: boolean;
5
+ noDeerUntil: number | null;
6
+ }
7
+
8
+ export interface DeerCheckInResult {
9
+ ok: boolean;
10
+ records: Map<number, number>;
11
+ }
12
+
13
+ export interface DeerRankEntry {
14
+ userId: number;
15
+ count: number;
16
+ }
17
+
18
+ export interface DeerScene {
19
+ key: string;
20
+ isGroup: boolean;
21
+ groupId?: number;
22
+ privateUserId?: number;
23
+ }
package/utils.ts ADDED
@@ -0,0 +1,144 @@
1
+ import type { MiokiContext } from "mioki";
2
+ import * as fs from "fs/promises";
3
+ import type { DeerScene } from "./types";
4
+
5
+ export function getAvatarUrl(userId: number): string {
6
+ return `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`;
7
+ }
8
+
9
+ export function getAtUserId(message: any[]): number | undefined {
10
+ if (!Array.isArray(message)) return undefined;
11
+ for (const seg of message) {
12
+ if (seg?.type !== "at") continue;
13
+ const raw = seg?.qq ?? seg?.data?.qq;
14
+ if (raw === "all" || raw == null) continue;
15
+ const qq = Number(raw);
16
+ if (Number.isFinite(qq)) return qq;
17
+ }
18
+ return undefined;
19
+ }
20
+
21
+ export function resolveScene(event: any): DeerScene {
22
+ if (event?.message_type === "group" && event?.group_id != null) {
23
+ return {
24
+ key: `g:${event.group_id}`,
25
+ isGroup: true,
26
+ groupId: Number(event.group_id),
27
+ };
28
+ }
29
+ return {
30
+ key: `p:${event.user_id}`,
31
+ isGroup: false,
32
+ privateUserId: Number(event.user_id),
33
+ };
34
+ }
35
+
36
+ export async function resolveUserName(
37
+ ctx: MiokiContext,
38
+ event: any,
39
+ userId: number,
40
+ ): Promise<string> {
41
+ const selfId = Number(event?.self_id);
42
+ if (event?.message_type === "group" && event?.group_id != null) {
43
+ try {
44
+ const member = await ctx
45
+ .pickBot(selfId)
46
+ .getGroupMemberInfo(Number(event.group_id), userId);
47
+ const name =
48
+ String(member?.card || "").trim() ||
49
+ String(member?.nickname || "").trim();
50
+ if (name) return name;
51
+ } catch {
52
+ // fall through
53
+ }
54
+ }
55
+ if (Number(event?.user_id) === userId && event?.sender) {
56
+ const name =
57
+ String(event.sender?.card || "").trim() ||
58
+ String(event.sender?.nickname || "").trim();
59
+ if (name) return name;
60
+ }
61
+ try {
62
+ const stranger = (await ctx
63
+ .pickBot(selfId)
64
+ .api("get_stranger_info", { user_id: userId })) as
65
+ | { nickname?: string }
66
+ | undefined;
67
+ const name = String(stranger?.nickname || "").trim();
68
+ if (name) return name;
69
+ } catch {
70
+ // ignore
71
+ }
72
+ return String(userId);
73
+ }
74
+
75
+ const DURATION_RE = /(\d+)\s*([smhdwy秒分时天周年]|min|hour|day|week|year)/gi;
76
+
77
+ export function parseDuration(text: string): number | null {
78
+ const t = String(text || "").trim();
79
+ if (!t) return null;
80
+ let total = 0;
81
+ let matched = false;
82
+ for (const m of t.matchAll(DURATION_RE)) {
83
+ matched = true;
84
+ const value = parseInt(m[1], 10);
85
+ const unit = m[2].toLowerCase();
86
+ if (unit === "s" || unit === "秒") total += value;
87
+ else if (unit === "m" || unit === "min" || unit === "分") total += value * 60;
88
+ else if (unit === "h" || unit === "hour" || unit === "时") total += value * 3600;
89
+ else if (unit === "d" || unit === "day" || unit === "天") total += value * 86400;
90
+ else if (unit === "w" || unit === "week" || unit === "周") total += value * 604800;
91
+ else if (unit === "y" || unit === "year" || unit === "年") total += value * 31536000;
92
+ }
93
+ if (!matched) {
94
+ if (/^\d+$/.test(t)) return parseInt(t, 10);
95
+ return null;
96
+ }
97
+ return total;
98
+ }
99
+
100
+ export function formatDateTime(timestamp: number): string {
101
+ const d = new Date(timestamp);
102
+ const pad = (n: number) => String(n).padStart(2, "0");
103
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(
104
+ d.getHours(),
105
+ )}:${pad(d.getMinutes())}`;
106
+ }
107
+
108
+ export function escapeHtml(value: string): string {
109
+ return String(value)
110
+ .replace(/&/g, "&amp;")
111
+ .replace(/</g, "&lt;")
112
+ .replace(/>/g, "&gt;")
113
+ .replace(/"/g, "&quot;")
114
+ .replace(/'/g, "&#39;");
115
+ }
116
+
117
+ // 19:00–07:00 视为夜间模式,与 help 插件保持一致
118
+ export function isNightMode(): boolean {
119
+ const hour = new Date().getHours();
120
+ return hour >= 19 || hour < 7;
121
+ }
122
+
123
+ export async function replyImage(
124
+ event: any,
125
+ segment: { image: (file: string) => any } | undefined,
126
+ imagePath: string,
127
+ text?: any[],
128
+ ): Promise<void> {
129
+ const imageSeg = segment?.image
130
+ ? segment.image(imagePath)
131
+ : { type: "image", file: imagePath };
132
+ const payload = text ? [...text, imageSeg] : [imageSeg];
133
+ try {
134
+ await event.reply(payload, true);
135
+ } catch {
136
+ const buf = await fs.readFile(imagePath);
137
+ const base64 = `base64://${buf.toString("base64")}`;
138
+ const fallbackSeg = segment?.image
139
+ ? segment.image(base64)
140
+ : { type: "image", file: base64 };
141
+ const fallback = text ? [...text, fallbackSeg] : [fallbackSeg];
142
+ await event.reply(fallback, true);
143
+ }
144
+ }