koishi-plugin-fusheng-count 0.0.1
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/lib/index.d.ts +24 -0
- package/lib/index.js +439 -0
- package/package.json +22 -0
- package/readme.md +5 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
export declare const name = "koishi-plugin-fusheng-count";
|
|
3
|
+
export declare const inject: string[];
|
|
4
|
+
export interface Config {
|
|
5
|
+
allowedGroups: string[];
|
|
6
|
+
imageSavePath: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const Config: Schema<Config>;
|
|
9
|
+
declare module 'koishi' {
|
|
10
|
+
interface Tables {
|
|
11
|
+
mention_stat: MentionStat;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export interface MentionStat {
|
|
15
|
+
id: string;
|
|
16
|
+
channelId: string;
|
|
17
|
+
senderId: string;
|
|
18
|
+
senderName: string;
|
|
19
|
+
targetId: string;
|
|
20
|
+
targetName: string;
|
|
21
|
+
date: string;
|
|
22
|
+
count: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name2 in all)
|
|
10
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
Config: () => Config,
|
|
34
|
+
apply: () => apply,
|
|
35
|
+
inject: () => inject,
|
|
36
|
+
name: () => name
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(src_exports);
|
|
39
|
+
var import_koishi = require("koishi");
|
|
40
|
+
var fs = __toESM(require("fs"));
|
|
41
|
+
var path = __toESM(require("path"));
|
|
42
|
+
var os = __toESM(require("os"));
|
|
43
|
+
var name = "koishi-plugin-fusheng-count";
|
|
44
|
+
var inject = ["database", "puppeteer"];
|
|
45
|
+
var Config = import_koishi.Schema.object({
|
|
46
|
+
allowedGroups: import_koishi.Schema.array(String).default([]).description("允许启用统计的群号列表。如果不填,则默认不在任何群开启统计。"),
|
|
47
|
+
imageSavePath: import_koishi.Schema.string().default("").description("自定义图片保存路径(绝对路径,如 D:\\images)。留空则默认保存在系统临时文件夹。")
|
|
48
|
+
});
|
|
49
|
+
var cooldownCache = /* @__PURE__ */ new Map();
|
|
50
|
+
function getTodayDate() {
|
|
51
|
+
const now = /* @__PURE__ */ new Date();
|
|
52
|
+
const year = now.getFullYear();
|
|
53
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
54
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
55
|
+
return `${year}-${month}-${day}`;
|
|
56
|
+
}
|
|
57
|
+
__name(getTodayDate, "getTodayDate");
|
|
58
|
+
function formatMD(dateString) {
|
|
59
|
+
const parts = dateString.split("-");
|
|
60
|
+
if (parts.length !== 3) return dateString;
|
|
61
|
+
return `${parseInt(parts[1])}.${parseInt(parts[2])}`;
|
|
62
|
+
}
|
|
63
|
+
__name(formatMD, "formatMD");
|
|
64
|
+
function formatDateRanges(dateStrings) {
|
|
65
|
+
if (!dateStrings || dateStrings.length === 0) return "";
|
|
66
|
+
const sorted = Array.from(new Set(dateStrings)).sort();
|
|
67
|
+
const ranges = [];
|
|
68
|
+
let rangeStart = sorted[0];
|
|
69
|
+
let rangeEnd = sorted[0];
|
|
70
|
+
const getNextDay = /* @__PURE__ */ __name((ds) => {
|
|
71
|
+
const [y, m, d] = ds.split("-").map(Number);
|
|
72
|
+
const date = new Date(y, m - 1, d);
|
|
73
|
+
date.setDate(date.getDate() + 1);
|
|
74
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
75
|
+
}, "getNextDay");
|
|
76
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
77
|
+
const current = sorted[i];
|
|
78
|
+
if (current === getNextDay(rangeEnd)) {
|
|
79
|
+
rangeEnd = current;
|
|
80
|
+
} else {
|
|
81
|
+
ranges.push({ start: rangeStart, end: rangeEnd });
|
|
82
|
+
rangeStart = current;
|
|
83
|
+
rangeEnd = current;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
ranges.push({ start: rangeStart, end: rangeEnd });
|
|
87
|
+
return ranges.map((r) => {
|
|
88
|
+
if (r.start === r.end) return formatMD(r.start);
|
|
89
|
+
return `${formatMD(r.start)}-${formatMD(r.end)}`;
|
|
90
|
+
}).join("、");
|
|
91
|
+
}
|
|
92
|
+
__name(formatDateRanges, "formatDateRanges");
|
|
93
|
+
function apply(ctx, config) {
|
|
94
|
+
ctx.model.extend("mention_stat", {
|
|
95
|
+
id: "string",
|
|
96
|
+
channelId: "string",
|
|
97
|
+
senderId: "string",
|
|
98
|
+
senderName: "string",
|
|
99
|
+
targetId: "string",
|
|
100
|
+
targetName: "string",
|
|
101
|
+
date: "string",
|
|
102
|
+
count: "integer"
|
|
103
|
+
}, { primary: "id" });
|
|
104
|
+
const isAllowedGroup = /* @__PURE__ */ __name((guildId) => {
|
|
105
|
+
if (!guildId) return false;
|
|
106
|
+
return config.allowedGroups.includes(guildId);
|
|
107
|
+
}, "isAllowedGroup");
|
|
108
|
+
const checkPermission = /* @__PURE__ */ __name((session) => {
|
|
109
|
+
if (!session.guildId) return false;
|
|
110
|
+
if (!isAllowedGroup(session.guildId)) return false;
|
|
111
|
+
const roles = session.author?.roles || [];
|
|
112
|
+
if (!roles.includes("admin") && !roles.includes("owner")) return false;
|
|
113
|
+
return true;
|
|
114
|
+
}, "checkPermission");
|
|
115
|
+
ctx.on("message", async (session) => {
|
|
116
|
+
if (!session.guildId || !isAllowedGroup(session.guildId)) return;
|
|
117
|
+
const senderId = session.userId;
|
|
118
|
+
const channelId = session.channelId;
|
|
119
|
+
const senderName = session.username || session.author?.nickname || senderId;
|
|
120
|
+
const today = getTodayDate();
|
|
121
|
+
const nowTime = Date.now();
|
|
122
|
+
const atElements = session.elements.filter((el) => el.type === "at");
|
|
123
|
+
if (cooldownCache.size > 1e4) cooldownCache.clear();
|
|
124
|
+
for (const atEl of atElements) {
|
|
125
|
+
const targetId = atEl.attrs.id;
|
|
126
|
+
if (!targetId || targetId === "all" || targetId === senderId) continue;
|
|
127
|
+
const cacheKey = `${channelId}:${senderId}:${targetId}`;
|
|
128
|
+
const lastMentionTime = cooldownCache.get(cacheKey);
|
|
129
|
+
if (lastMentionTime && nowTime - lastMentionTime < 90 * 1e3) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
cooldownCache.set(cacheKey, nowTime);
|
|
133
|
+
const targetName = atEl.attrs.name || targetId;
|
|
134
|
+
const recordId = `${channelId}:${senderId}:${targetId}:${today}`;
|
|
135
|
+
const existing = await ctx.database.get("mention_stat", recordId);
|
|
136
|
+
if (existing.length > 0) {
|
|
137
|
+
await ctx.database.set("mention_stat", recordId, {
|
|
138
|
+
count: existing[0].count + 1,
|
|
139
|
+
senderName,
|
|
140
|
+
targetName: targetName !== targetId ? targetName : existing[0].targetName
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
await ctx.database.create("mention_stat", {
|
|
144
|
+
id: recordId,
|
|
145
|
+
channelId,
|
|
146
|
+
senderId,
|
|
147
|
+
senderName,
|
|
148
|
+
targetId,
|
|
149
|
+
targetName,
|
|
150
|
+
date: today,
|
|
151
|
+
count: 1
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
const buildStatImage = /* @__PURE__ */ __name(async (session, stats, title, isToday) => {
|
|
157
|
+
ctx.logger.info("▶️ [步骤 1] 开始构建统计数据...");
|
|
158
|
+
const guildMembers = /* @__PURE__ */ new Map();
|
|
159
|
+
let guildName = session.guildId;
|
|
160
|
+
let nextToken;
|
|
161
|
+
try {
|
|
162
|
+
const guildInfo = await session.bot.getGuild(session.guildId);
|
|
163
|
+
if (guildInfo && guildInfo.name) {
|
|
164
|
+
guildName = guildInfo.name;
|
|
165
|
+
}
|
|
166
|
+
do {
|
|
167
|
+
const res = await session.bot.getGuildMemberList(session.guildId, nextToken);
|
|
168
|
+
for (const m of res.data) {
|
|
169
|
+
const id = m.user?.id;
|
|
170
|
+
if (id) guildMembers.set(id, m.nick || m.user?.name || id);
|
|
171
|
+
}
|
|
172
|
+
nextToken = res.next;
|
|
173
|
+
} while (nextToken);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
ctx.logger.warn("获取群成员或群信息失败", e);
|
|
176
|
+
}
|
|
177
|
+
const userStats = /* @__PURE__ */ new Map();
|
|
178
|
+
for (const stat of stats) {
|
|
179
|
+
if (!userStats.has(stat.senderId)) userStats.set(stat.senderId, { name: stat.senderName, group: 0, nonGroup: 0, mentioned: 0 });
|
|
180
|
+
const senderData = userStats.get(stat.senderId);
|
|
181
|
+
senderData.name = stat.senderName;
|
|
182
|
+
if (guildMembers.has(stat.targetId)) senderData.group += stat.count;
|
|
183
|
+
else senderData.nonGroup += stat.count;
|
|
184
|
+
if (!userStats.has(stat.targetId)) {
|
|
185
|
+
const tName = guildMembers.get(stat.targetId) || stat.targetName || stat.targetId;
|
|
186
|
+
userStats.set(stat.targetId, { name: tName, group: 0, nonGroup: 0, mentioned: 0 });
|
|
187
|
+
}
|
|
188
|
+
const targetData = userStats.get(stat.targetId);
|
|
189
|
+
targetData.mentioned += stat.count;
|
|
190
|
+
}
|
|
191
|
+
const aggregatedList = Array.from(userStats.values()).sort((a, b) => b.group - a.group || b.nonGroup - a.nonGroup || b.mentioned - a.mentioned);
|
|
192
|
+
const statHtml = aggregatedList.map((user) => `
|
|
193
|
+
<div class="stat-item">
|
|
194
|
+
<span class="stat-icon"></span>
|
|
195
|
+
<span class="stat-name">${user.name}</span>
|
|
196
|
+
<span class="stat-details">群友: <b>${user.group}</b> 次 | 非群友: <b>${user.nonGroup}</b> 次 | 被艾特: <b>${user.mentioned}</b> 次</span>
|
|
197
|
+
</div>
|
|
198
|
+
`).join("") || '<div class="empty">暂无记录。</div>';
|
|
199
|
+
let zeroHtml = "";
|
|
200
|
+
if (isToday && guildMembers.size > 0) {
|
|
201
|
+
const zeroMentioners = [];
|
|
202
|
+
for (const [id, name2] of guildMembers.entries()) {
|
|
203
|
+
if (id === session.bot.selfId) continue;
|
|
204
|
+
const u = userStats.get(id);
|
|
205
|
+
if (!u || u.group === 0) {
|
|
206
|
+
zeroMentioners.push(`<span class="zero-item">${name2}</span>`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (zeroMentioners.length > 0) {
|
|
210
|
+
zeroHtml = `
|
|
211
|
+
<div class="section-title">今日未艾特群友 <span class="badge">${zeroMentioners.length} 人</span></div>
|
|
212
|
+
<div class="zero-list">${zeroMentioners.join("")}</div>
|
|
213
|
+
`;
|
|
214
|
+
} else {
|
|
215
|
+
zeroHtml = `<div class="section-title">今日全员都艾特过群友啦</div>`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
let listTitle = "艾特记录名单";
|
|
219
|
+
let outputFileName = `fusheng-count-${Date.now()}.jpg`;
|
|
220
|
+
const safeGuildName = guildName.replace(/[\\/:*?"<>|]/g, "");
|
|
221
|
+
if (isToday) {
|
|
222
|
+
const mdDate = formatMD(getTodayDate());
|
|
223
|
+
listTitle = `${mdDate}艾特记录名单`;
|
|
224
|
+
outputFileName = `${mdDate}${safeGuildName}.jpg`;
|
|
225
|
+
} else {
|
|
226
|
+
const dates = stats.map((s) => s.date);
|
|
227
|
+
const rangeStr = formatDateRanges(dates);
|
|
228
|
+
if (rangeStr) {
|
|
229
|
+
listTitle = `艾特记录名单(${rangeStr})`;
|
|
230
|
+
outputFileName = `${safeGuildName}(${rangeStr}).jpg`;
|
|
231
|
+
} else {
|
|
232
|
+
outputFileName = `${safeGuildName}总统计.jpg`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const html = `
|
|
236
|
+
<!DOCTYPE html>
|
|
237
|
+
<html>
|
|
238
|
+
<head>
|
|
239
|
+
<style>
|
|
240
|
+
* { box-sizing: border-box; }
|
|
241
|
+
body {
|
|
242
|
+
font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
|
|
243
|
+
background: #e0e6ed;
|
|
244
|
+
padding: 30px;
|
|
245
|
+
width: 800px;
|
|
246
|
+
margin: 0 auto;
|
|
247
|
+
}
|
|
248
|
+
.container {
|
|
249
|
+
width: 100%;
|
|
250
|
+
background: #ffffff;
|
|
251
|
+
border-top: 14px solid #34495e;
|
|
252
|
+
box-shadow: 0 15px 35px rgba(0,0,0,0.1);
|
|
253
|
+
padding-bottom: 40px;
|
|
254
|
+
border-radius: 6px;
|
|
255
|
+
}
|
|
256
|
+
.header {
|
|
257
|
+
padding: 35px 45px 25px;
|
|
258
|
+
text-align: left;
|
|
259
|
+
border-bottom: 1px solid #ecf0f1;
|
|
260
|
+
}
|
|
261
|
+
.header h1 {
|
|
262
|
+
font-size: 38px;
|
|
263
|
+
color: #2c3e50;
|
|
264
|
+
margin: 0;
|
|
265
|
+
font-weight: 900;
|
|
266
|
+
letter-spacing: 1px;
|
|
267
|
+
}
|
|
268
|
+
.header p {
|
|
269
|
+
color: #7f8c8d;
|
|
270
|
+
font-size: 16px;
|
|
271
|
+
margin: 8px 0 0 0;
|
|
272
|
+
text-transform: uppercase;
|
|
273
|
+
letter-spacing: 2px;
|
|
274
|
+
font-weight: 600;
|
|
275
|
+
}
|
|
276
|
+
.content {
|
|
277
|
+
padding: 0 45px;
|
|
278
|
+
}
|
|
279
|
+
.section-title {
|
|
280
|
+
font-size: 22px;
|
|
281
|
+
font-weight: 800;
|
|
282
|
+
color: #2c3e50;
|
|
283
|
+
margin: 35px 0 20px;
|
|
284
|
+
border-bottom: 2px solid #ecf0f1;
|
|
285
|
+
padding-bottom: 12px;
|
|
286
|
+
display: flex;
|
|
287
|
+
align-items: center;
|
|
288
|
+
}
|
|
289
|
+
.section-title::before {
|
|
290
|
+
content: '';
|
|
291
|
+
display: inline-block;
|
|
292
|
+
width: 6px;
|
|
293
|
+
height: 22px;
|
|
294
|
+
background: #34495e;
|
|
295
|
+
margin-right: 12px;
|
|
296
|
+
}
|
|
297
|
+
.badge {
|
|
298
|
+
background: #34495e;
|
|
299
|
+
color: #ffffff;
|
|
300
|
+
font-size: 14px;
|
|
301
|
+
padding: 4px 10px;
|
|
302
|
+
border-radius: 3px;
|
|
303
|
+
margin-left: 12px;
|
|
304
|
+
font-weight: normal;
|
|
305
|
+
letter-spacing: 1px;
|
|
306
|
+
}
|
|
307
|
+
.stat-item {
|
|
308
|
+
display: flex;
|
|
309
|
+
align-items: center;
|
|
310
|
+
padding: 14px 12px;
|
|
311
|
+
border-bottom: 1px dashed #ecf0f1;
|
|
312
|
+
transition: background 0.2s;
|
|
313
|
+
}
|
|
314
|
+
.stat-item:nth-child(even) { background: #fcfcfd; }
|
|
315
|
+
.stat-icon {
|
|
316
|
+
width: 8px;
|
|
317
|
+
height: 8px;
|
|
318
|
+
background-color: #95a5a6;
|
|
319
|
+
border-radius: 50%;
|
|
320
|
+
margin-right: 15px;
|
|
321
|
+
margin-left: 5px;
|
|
322
|
+
}
|
|
323
|
+
.stat-name {
|
|
324
|
+
flex: 1;
|
|
325
|
+
font-size: 18px;
|
|
326
|
+
color: #2c3e50;
|
|
327
|
+
font-weight: 700;
|
|
328
|
+
white-space: nowrap;
|
|
329
|
+
overflow: hidden;
|
|
330
|
+
text-overflow: ellipsis;
|
|
331
|
+
padding-right: 20px;
|
|
332
|
+
}
|
|
333
|
+
.stat-details {
|
|
334
|
+
font-size: 15px;
|
|
335
|
+
color: #7f8c8d;
|
|
336
|
+
font-weight: 500;
|
|
337
|
+
}
|
|
338
|
+
.stat-details b {
|
|
339
|
+
color: #34495e;
|
|
340
|
+
font-size: 18px;
|
|
341
|
+
font-weight: 800;
|
|
342
|
+
padding: 0 4px;
|
|
343
|
+
}
|
|
344
|
+
.zero-list {
|
|
345
|
+
display: flex;
|
|
346
|
+
flex-wrap: wrap;
|
|
347
|
+
gap: 12px;
|
|
348
|
+
background: #f8f9fa;
|
|
349
|
+
padding: 25px;
|
|
350
|
+
border-left: 4px solid #95a5a6;
|
|
351
|
+
border-radius: 0 6px 6px 0;
|
|
352
|
+
}
|
|
353
|
+
.zero-item {
|
|
354
|
+
background: #ffffff;
|
|
355
|
+
border: 1px solid #dcdde1;
|
|
356
|
+
color: #34495e;
|
|
357
|
+
font-size: 14px;
|
|
358
|
+
padding: 6px 14px;
|
|
359
|
+
border-radius: 4px;
|
|
360
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
|
|
361
|
+
white-space: nowrap;
|
|
362
|
+
max-width: 100%;
|
|
363
|
+
overflow: hidden;
|
|
364
|
+
text-overflow: ellipsis;
|
|
365
|
+
}
|
|
366
|
+
.empty {
|
|
367
|
+
text-align: center;
|
|
368
|
+
color: #95a5a6;
|
|
369
|
+
padding: 40px;
|
|
370
|
+
font-size: 16px;
|
|
371
|
+
background: #fdfdfd;
|
|
372
|
+
border: 1px dashed #bdc3c7;
|
|
373
|
+
}
|
|
374
|
+
</style>
|
|
375
|
+
</head>
|
|
376
|
+
<body>
|
|
377
|
+
<div class="container">
|
|
378
|
+
<div class="header">
|
|
379
|
+
<h1>${title}</h1>
|
|
380
|
+
<p>Group Mention Data Report</p>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="content">
|
|
383
|
+
<div class="section-title">${listTitle}</div>
|
|
384
|
+
${statHtml}
|
|
385
|
+
<div style="margin-top: 45px;"></div>
|
|
386
|
+
${zeroHtml}
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
</body>
|
|
390
|
+
</html>
|
|
391
|
+
`;
|
|
392
|
+
ctx.logger.info("▶️ [步骤 2] 正在唤起浏览器进行截图...");
|
|
393
|
+
const page = await ctx.puppeteer.page();
|
|
394
|
+
let imageBuffer;
|
|
395
|
+
try {
|
|
396
|
+
await page.setViewport({ width: 800, height: 100 });
|
|
397
|
+
await page.setContent(html);
|
|
398
|
+
imageBuffer = await page.screenshot({ type: "jpeg", quality: 85, fullPage: true });
|
|
399
|
+
} finally {
|
|
400
|
+
await page.close();
|
|
401
|
+
}
|
|
402
|
+
let saveDir = os.tmpdir();
|
|
403
|
+
if (config.imageSavePath && config.imageSavePath.trim() !== "") {
|
|
404
|
+
saveDir = path.resolve(config.imageSavePath.trim());
|
|
405
|
+
if (!fs.existsSync(saveDir)) {
|
|
406
|
+
fs.mkdirSync(saveDir, { recursive: true });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
ctx.logger.info(`▶️ [步骤 3] 截图成功!准备写入文件...`);
|
|
410
|
+
const finalFilePath = path.join(saveDir, outputFileName);
|
|
411
|
+
fs.writeFileSync(finalFilePath, imageBuffer);
|
|
412
|
+
ctx.logger.info(`▶️ [步骤 4] 文件已落盘: ${finalFilePath}`);
|
|
413
|
+
const fileUrl = `file:///${finalFilePath.replace(/\\/g, "/")}`;
|
|
414
|
+
return import_koishi.h.image(fileUrl);
|
|
415
|
+
}, "buildStatImage");
|
|
416
|
+
ctx.command("今日统计").action(async ({ session }) => {
|
|
417
|
+
if (!checkPermission(session)) return "";
|
|
418
|
+
const stats = await ctx.database.get("mention_stat", { channelId: session.channelId, date: getTodayDate() });
|
|
419
|
+
return await buildStatImage(session, stats, "本群今日艾特统计", true);
|
|
420
|
+
});
|
|
421
|
+
ctx.command("总统计").action(async ({ session }) => {
|
|
422
|
+
if (!checkPermission(session)) return "";
|
|
423
|
+
const stats = await ctx.database.get("mention_stat", { channelId: session.channelId });
|
|
424
|
+
return await buildStatImage(session, stats, "本群历史总计艾特统计", false);
|
|
425
|
+
});
|
|
426
|
+
ctx.command("清空统计", "清除统计").action(async ({ session }) => {
|
|
427
|
+
if (!checkPermission(session)) return "";
|
|
428
|
+
await ctx.database.remove("mention_stat", { channelId: session.channelId });
|
|
429
|
+
return "已清空本群的所有互动统计数据。";
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
__name(apply, "apply");
|
|
433
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
434
|
+
0 && (module.exports = {
|
|
435
|
+
Config,
|
|
436
|
+
apply,
|
|
437
|
+
inject,
|
|
438
|
+
name
|
|
439
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-fusheng-count",
|
|
3
|
+
"description": "",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"scripts": {},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"chatbot",
|
|
15
|
+
"koishi",
|
|
16
|
+
"plugin"
|
|
17
|
+
],
|
|
18
|
+
"devDependencies": {},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"koishi": "4.18.10"
|
|
21
|
+
}
|
|
22
|
+
}
|