koishi-plugin-ll-mc 1.0.7 → 2.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/index.js ADDED
@@ -0,0 +1,341 @@
1
+ 'use strict'
2
+
3
+ // koishi-plugin-ll-mc v2.0.0(重做版)
4
+ // 消息统计插件:群成员发言排行 + 跨群发言排行(群名正确)
5
+ // 官方QQ机器人(官机)原生适配:
6
+ // - 昵称:优先取消息自带昵称(官机消息自带 member.nick / author.username,零API)
7
+ // - 群名:getGuild(需在QQ开放平台申请"获取群信息"权限)+ 配置手动指定兜底
8
+ // 纯文本排行,不依赖 puppeteer/canvas/图片生成,任何环境都能跑
9
+
10
+ const { Schema } = require('koishi')
11
+
12
+ exports.name = 'll-mc'
13
+
14
+ exports.usage = `消息统计插件:群成员发言排行 + 跨群发言排行。
15
+
16
+ ## 命令
17
+
18
+ - \`mc.排行 [周期] [人数]\`:本群成员发言排行。周期:今日/本周/本月/总榜(默认总榜),如 \`mc.排行 今日 10\`
19
+ - \`mc.群排行 [周期] [人数]\`:所有群的发言排行(群名正确显示),当前群带 ★
20
+ - \`mc.我的\`:自己的发言统计(今日/本周/本月/总榜)
21
+
22
+ ## 官机(官方QQ机器人)说明
23
+
24
+ - **群名**:官机消息里不带群名,插件通过 \`getGuild\` 接口获取。需要在 QQ 开放平台申请「获取群信息」权限(否则群名显示为 openid);也可以在配置 \`groupNameMap\` 里手动指定(键=群openid,值=群名),一次填好永久正确
25
+ - **昵称**:官机消息自带昵称,无需接口权限;申请「获取群成员信息」后更完整
26
+ - **群ID**:官机显示的是 openid,非数字群号,属正常现象
27
+
28
+ ## 配置
29
+
30
+ - \`rankLimit\`:排行默认显示人数
31
+ - \`ignoredGroups\`:不统计的群(填群ID)
32
+ - \`groupNameMap\`:手动指定群名(群openid → 群名)
33
+ - \`refreshGroupNameInterval\`:群名刷新间隔(秒,默认3600)
34
+ - \`countBots\`:是否统计机器人自己的发言(默认否)
35
+ `
36
+
37
+ const Config = Schema.object({
38
+ rankLimit: Schema.number()
39
+ .min(1).max(100).default(20)
40
+ .description('排行默认显示人数'),
41
+ ignoredGroups: Schema.array(String)
42
+ .default([])
43
+ .description('不统计的群(填群ID,官机填 openid)'),
44
+ groupNameMap: Schema.dict(String)
45
+ .default({})
46
+ .description('手动指定群名:键=群ID(官机填 openid),值=群名。用于官机无法自动获取群名时保证"群排行"群名正确'),
47
+ refreshGroupNameInterval: Schema.number()
48
+ .min(60).default(3600)
49
+ .description('群名自动刷新间隔(秒)。官机获取群名需要"获取群信息"权限,无权限时只会尝试一次并降级'),
50
+ countBots: Schema.boolean()
51
+ .default(false)
52
+ .description('是否统计机器人自己的发言'),
53
+ verbose: Schema.boolean()
54
+ .default(false)
55
+ .description('调试:控制台打印统计详情'),
56
+ })
57
+
58
+ exports.Config = Config
59
+
60
+ exports.inject = { required: ['database'] }
61
+
62
+ function apply(ctx, config) {
63
+ const logger = ctx.logger('ll-mc')
64
+
65
+ // ===== 数据表 =====
66
+ // ll_mc_stats: 每群每人每天计数
67
+ ctx.model.extend('ll_mc_stats', {
68
+ channelId: 'string',
69
+ userId: 'string',
70
+ nick: 'string',
71
+ date: 'string',
72
+ count: 'unsigned',
73
+ }, { primary: ['channelId', 'userId', 'date'] })
74
+ // ll_mc_groups: 群名缓存(channelId -> 群名)
75
+ ctx.model.extend('ll_mc_groups', {
76
+ channelId: 'string',
77
+ name: 'string',
78
+ platform: 'string',
79
+ updatedAt: 'timestamp',
80
+ }, { primary: ['channelId'] })
81
+
82
+ // ===== 内存缓存 =====
83
+ const nameCache = new Map() // channelId -> { name, t }
84
+ const lastNameRefresh = new Map() // channelId -> timestamp
85
+ const warnThrottle = new Map() // key -> timestamp(同类警告10分钟一次)
86
+
87
+ function throttledWarn(key, message) {
88
+ const now = Date.now()
89
+ if ((warnThrottle.get(key) || 0) > now - 10 * 60 * 1000) return
90
+ warnThrottle.set(key, now)
91
+ logger.warn(message)
92
+ }
93
+
94
+ // ===== 日期工具 =====
95
+ function dateStr(d) {
96
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
97
+ }
98
+ function todayStr() {
99
+ return dateStr(new Date())
100
+ }
101
+ // 解析周期:返回 { key, label, start },start 为日期范围起点(含当天),null=全部
102
+ function parsePeriod(period) {
103
+ const p = String(period || '').trim()
104
+ const now = new Date()
105
+ if (!p || /总|全部|all/i.test(p)) return { key: 'all', label: '总榜', start: null }
106
+ if (/周/.test(p)) {
107
+ const d = new Date(now)
108
+ d.setDate(d.getDate() - ((d.getDay() + 6) % 7)) // 周一
109
+ return { key: 'week', label: '本周', start: dateStr(d) }
110
+ }
111
+ if (/月/.test(p)) {
112
+ return { key: 'month', label: '本月', start: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01` }
113
+ }
114
+ if (/日|天/.test(p)) return { key: 'day', label: '今日', start: todayStr() }
115
+ return null
116
+ }
117
+
118
+ // ===== 昵称解析(官机优先取消息自带) =====
119
+ async function resolveNick(session, userId) {
120
+ const author = session.author
121
+ if (author?.nick) return String(author.nick)
122
+ if (author?.name) return String(author.name)
123
+ // 官机:原始事件自带 member.nick / author.username,零 API
124
+ const raw = session.event?._data?.d
125
+ if (raw?.member?.nick) return String(raw.member.nick)
126
+ if (raw?.author?.username) return String(raw.author.username)
127
+ // 兜底:getGuildMember(需"获取群成员信息"权限,失败降级为 ID)
128
+ if (session.guildId && !session.isDirect && typeof session.bot?.getGuildMember === 'function') {
129
+ try {
130
+ const member = await session.bot.getGuildMember(session.guildId, userId)
131
+ const name = member?.name || member?.user?.name
132
+ if (name) return String(name)
133
+ }
134
+ catch (e) {
135
+ throttledWarn(`member:${session.guildId}:${userId}`, `[ll-mc] 获取成员昵称失败 ${userId}: ${e?.message}(官机需申请"获取群成员信息"权限)`)
136
+ }
137
+ }
138
+ return String(userId)
139
+ }
140
+
141
+ // ===== 群名 =====
142
+ async function setGroupName(guildId, name) {
143
+ if (!name || name === guildId) return
144
+ nameCache.set(guildId, { name, t: Date.now() })
145
+ try {
146
+ await ctx.database.upsert('ll_mc_groups', [{
147
+ channelId: guildId, name, platform: '', updatedAt: Date.now(),
148
+ }], ['channelId'])
149
+ }
150
+ catch { /* 忽略写入失败 */ }
151
+ }
152
+ async function getGroupName(guildId) {
153
+ if (config.groupNameMap[guildId]) return config.groupNameMap[guildId]
154
+ const cached = nameCache.get(guildId)
155
+ if (cached?.name) return cached.name
156
+ try {
157
+ const rows = await ctx.database.get('ll_mc_groups', { channelId: guildId })
158
+ if (rows[0]?.name) {
159
+ nameCache.set(guildId, { name: rows[0].name, t: Date.now() })
160
+ return rows[0].name
161
+ }
162
+ }
163
+ catch { /* 忽略 */ }
164
+ return guildId
165
+ }
166
+ // 收到消息时刷新群名(节流;官机走 getGuild)
167
+ async function refreshGroupName(session) {
168
+ const guildId = session.guildId
169
+ const now = Date.now()
170
+ if ((lastNameRefresh.get(guildId) || 0) > now - config.refreshGroupNameInterval * 1000) return
171
+ lastNameRefresh.set(guildId, now)
172
+ // 1. OneBot:消息自带群名
173
+ if (session.event?.channel?.name) {
174
+ await setGroupName(guildId, session.event.channel.name)
175
+ return
176
+ }
177
+ // 2. 手动指定
178
+ if (config.groupNameMap[guildId]) {
179
+ await setGroupName(guildId, config.groupNameMap[guildId])
180
+ return
181
+ }
182
+ // 3. 官机:getGuild(内部 getGuildInfo,需"获取群信息"权限)
183
+ if (typeof session.bot?.getGuild === 'function') {
184
+ try {
185
+ const g = await session.bot.getGuild(guildId)
186
+ if (g?.name) {
187
+ await setGroupName(guildId, g.name)
188
+ return
189
+ }
190
+ }
191
+ catch (e) {
192
+ throttledWarn(`guild:${guildId}`, `[ll-mc] 获取群名失败 ${guildId}: ${e?.message}(官机需在QQ开放平台申请"获取群信息"权限,或在配置 groupNameMap 手动指定群名)`)
193
+ }
194
+ }
195
+ }
196
+
197
+ // ===== 消息统计 =====
198
+ ctx.on('message', async (session) => {
199
+ try {
200
+ if (session.isDirect || !session.guildId || !session.userId) return
201
+ const guildId = session.guildId
202
+ const userId = session.userId
203
+ if (!config.countBots && session.selfId === session.userId) return
204
+ if (config.ignoredGroups.includes(guildId)) return
205
+
206
+ const date = todayStr()
207
+ const rows = await ctx.database.get('ll_mc_stats', { channelId: guildId, userId, date })
208
+ const count = (rows[0]?.count || 0) + 1
209
+ const nick = await resolveNick(session, userId)
210
+ await ctx.database.upsert('ll_mc_stats', [{
211
+ channelId: guildId, userId, date, nick, count,
212
+ }], ['channelId', 'userId', 'date'])
213
+ if (config.verbose) logger.info(`[ll-mc] ${guildId} ${userId} +1 (${count})`)
214
+ }
215
+ catch (e) {
216
+ logger.warn('[ll-mc] 消息统计失败:', e?.message)
217
+ }
218
+ // 群名刷新独立于计数,失败不影响统计
219
+ refreshGroupName(session).catch(() => {})
220
+ })
221
+
222
+ // ===== 排行聚合 =====
223
+ // 返回 [{ key, nick/name, count }] 按 count 降序
224
+ async function aggregateStats(range, groupBy) {
225
+ const query = range.start ? { date: { $gte: range.start } } : {}
226
+ const rows = await ctx.database.get('ll_mc_stats', query)
227
+ const map = new Map()
228
+ for (const row of rows) {
229
+ const key = groupBy === 'user' ? `${row.channelId}:${row.userId}` : row.channelId
230
+ const item = map.get(key) || { count: 0, nick: row.nick }
231
+ item.count += row.count || 0
232
+ if (row.nick) item.nick = row.nick
233
+ map.set(key, item)
234
+ }
235
+ return [...map.entries()].map(([key, item]) => ({ key, ...item }))
236
+ .sort((a, b) => b.count - a.count)
237
+ }
238
+
239
+ // ===== 命令:本群成员排行 =====
240
+ async function memberRanking(session, period, limit) {
241
+ if (session.isDirect || !session.guildId) return '❌ 请在群聊中使用'
242
+ const range = parsePeriod(period)
243
+ if (!range) return '❌ 周期参数无效,可用:今日 / 本周 / 本月 / 总榜'
244
+ const n = Math.min(Math.max(limit || config.rankLimit, 1), 100)
245
+ const list = await aggregateStats(range, 'user')
246
+ const inRange = list.filter(x => x.key.startsWith(`${session.guildId}:`))
247
+ const mine = inRange.find(x => x.key === `${session.guildId}:${session.userId}`)
248
+ const mineRank = mine ? inRange.indexOf(mine) + 1 : 0
249
+ const groupRows = await ctx.database.get('ll_mc_stats', { channelId: session.guildId })
250
+ const total = groupRows.reduce((s, r) => s + (r.count || 0), 0)
251
+ const totalInRange = inRange.reduce((s, x) => s + x.count, 0)
252
+
253
+ const lines = [`📊 ${range.label}发言排行 TOP ${Math.min(n, inRange.length)}(本群)`]
254
+ inRange.slice(0, n).forEach((x, i) => {
255
+ const mark = x.key === `${session.guildId}:${session.userId}` ? ' ★' : ''
256
+ lines.push(`${i + 1}. ${x.nick || x.key.split(':')[1]} · ${x.count}条${mark}`)
257
+ })
258
+ lines.push(`—— 本群${range.label}共 ${totalInRange} 条发言,历史累计 ${total} 条`)
259
+ if (mineRank) lines.push(`我的排名:第 ${mineRank} 名(${mine.count}条)`)
260
+ return lines.join('\n')
261
+ }
262
+
263
+ // ===== 命令:跨群排行(群名正确) =====
264
+ async function groupRanking(session, period, limit) {
265
+ const range = parsePeriod(period)
266
+ if (!range) return '❌ 周期参数无效,可用:今日 / 本周 / 本月 / 总榜'
267
+ const n = Math.min(Math.max(limit || config.rankLimit, 1), 100)
268
+ const list = await aggregateStats(range, 'group')
269
+ const current = session.guildId
270
+ const lines = [`🏆 群${range.label}发言排行 TOP ${Math.min(n, list.length)}`]
271
+ for (let i = 0; i < Math.min(n, list.length); i++) {
272
+ const x = list[i]
273
+ const name = await getGroupName(x.key)
274
+ const mark = x.key === current ? ' ★当前群' : ''
275
+ lines.push(`${i + 1}. ${name} · ${x.count}条${mark}`)
276
+ }
277
+ lines.push('(群名来自 getGuild 接口,需申请"获取群信息"权限;或用 groupNameMap 手动指定)')
278
+ return lines.join('\n')
279
+ }
280
+
281
+ // ===== 命令:我的统计 =====
282
+ async function myStats(session) {
283
+ if (session.isDirect || !session.guildId) return '❌ 请在群聊中使用'
284
+ const guildId = session.guildId
285
+ const userId = session.userId
286
+ const periods = [
287
+ { key: 'day', label: '今日' },
288
+ { key: 'week', label: '本周' },
289
+ { key: 'month', label: '本月' },
290
+ { key: 'all', label: '总榜' },
291
+ ]
292
+ const lines = ['👤 我的发言统计(本群)']
293
+ for (const p of periods) {
294
+ const range = parsePeriod(p.label)
295
+ const query = range.start
296
+ ? { channelId: guildId, userId, date: { $gte: range.start } }
297
+ : { channelId: guildId, userId }
298
+ const rows = await ctx.database.get('ll_mc_stats', query)
299
+ const total = rows.reduce((s, r) => s + (r.count || 0), 0)
300
+ lines.push(`${p.label}:${total} 条`)
301
+ }
302
+ return lines.join('\n')
303
+ }
304
+
305
+ // ===== 命令注册 =====
306
+ // 解析 "周期 [人数]" 或 "[人数]",两种顺序都支持
307
+ function parseArgs(text) {
308
+ const parts = String(text || '').trim().split(/\s+/).filter(Boolean)
309
+ let period = ''
310
+ let limit = 0
311
+ for (const part of parts) {
312
+ if (/^\d+$/.test(part)) limit = parseInt(part, 10)
313
+ else period = part
314
+ }
315
+ return { period, limit }
316
+ }
317
+
318
+ ctx.command('mc.排行 [args:text]', '本群成员发言排行(今日/本周/本月/总榜)')
319
+ .alias('排行')
320
+ .action(async ({ session }, args) => {
321
+ const { period, limit } = parseArgs(args)
322
+ return await memberRanking(session, period, limit)
323
+ })
324
+
325
+ ctx.command('mc.群排行 [args:text]', '跨群发言排行(群名正确)')
326
+ .alias('群排行')
327
+ .action(async ({ session }, args) => {
328
+ const { period, limit } = parseArgs(args)
329
+ return await groupRanking(session, period, limit)
330
+ })
331
+
332
+ ctx.command('mc.我的', '我的发言统计')
333
+ .alias('我的发言')
334
+ .action(async ({ session }) => {
335
+ return await myStats(session)
336
+ })
337
+
338
+ logger.info('[ll-mc] 已启动:mc.排行 / mc.群排行 / mc.我的')
339
+ }
340
+
341
+ exports.apply = apply
package/package.json CHANGED
@@ -1,45 +1,26 @@
1
- {
2
- "name": "koishi-plugin-ll-mc",
3
- "description": "消息统计插件(克隆自 message-counter),支持排除指定群聊",
4
- "version": "1.0.7",
5
- "main": "lib/index.js",
6
- "typings": "lib/index.d.ts",
7
- "files": [
8
- "lib",
9
- "assets"
10
- ],
11
- "koishi": {
12
- "description": {
13
- "zh": "消息统计插件:发言排行榜、群排行榜,支持排除指定群聊"
14
- },
15
- "service": {
16
- "required": [
17
- "database",
18
- "cron"
19
- ],
20
- "optional": [
21
- "markdownToImage",
22
- "puppeteer",
23
- "canvas"
24
- ]
25
- }
26
- },
27
- "author": "lee",
28
- "license": "MIT",
29
- "keywords": [
30
- "koishi",
31
- "koishi-plugin",
32
- "message-counter",
33
- "rank"
34
- ],
35
- "peerDependencies": {
36
- "koishi": "^4.18.9"
37
- },
38
- "devDependencies": {
39
- "koishi": "^4.18.11",
40
- "koishi-plugin-cron": "^3.1.0",
41
- "koishi-plugin-markdown-to-image-service": "^1.3.6",
42
- "koishi-plugin-puppeteer": "^3.9.0",
43
- "typescript": "^5.4.0"
44
- }
45
- }
1
+ {
2
+ "name": "koishi-plugin-ll-mc",
3
+ "description": "消息统计插件(重做版):群成员发言排行 + 跨群发言排行,官方QQ机器人原生适配",
4
+ "version": "2.0.0",
5
+ "main": "index.js",
6
+ "files": [
7
+ "index.js"
8
+ ],
9
+ "koishi": {
10
+ "description": {
11
+ "zh": "消息统计插件:群成员发言排行 + 跨群发言排行(群名正确),官机原生适配,纯文本无需图片依赖"
12
+ }
13
+ },
14
+ "author": "lee",
15
+ "license": "MIT",
16
+ "keywords": [
17
+ "koishi",
18
+ "koishi-plugin",
19
+ "message-counter",
20
+ "rank",
21
+ "stats"
22
+ ],
23
+ "peerDependencies": {
24
+ "koishi": "^4.18.0"
25
+ }
26
+ }
@@ -1,3 +0,0 @@
1
- [
2
- "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"
3
- ]
package/lib/index.d.ts DELETED
@@ -1,136 +0,0 @@
1
- import { Context, Schema } from "koishi";
2
- export declare const name = "mc";
3
- export declare const inject: {
4
- required: string[];
5
- optional: string[];
6
- };
7
- export interface Config {
8
- /** 是否统计 Bot 自己发送的消息。 */
9
- isBotMessageTrackingEnabled: boolean;
10
- /** 排行榜默认显示的人数。 */
11
- defaultMaxDisplayCount: number;
12
- /** 是否在显示排行榜时补充时间信息。 */
13
- isTimeInfoSupplementEnabled: boolean;
14
- /** 是否在排行榜中显示用户消息占比。 */
15
- isUserMessagePercentageVisible: boolean;
16
- /** 在排行榜中全局隐藏的用户 ID 列表。 */
17
- hiddenUserIdsInLeaderboard: string[];
18
- /** 在群排行榜中全局隐藏的频道 ID 列表。 */
19
- hiddenChannelIdsInLeaderboard: string[];
20
- /** 排除统计的频道 ID 列表,这些群的消息不会被计数。 */
21
- excludedChannelIds: string[];
22
- /** 排除统计的用户 ID 列表,这些用户的消息不会被计数。 */
23
- excludedUserIds: string[];
24
- /** 是否将文本排行榜转为 Markdown 图片。 */
25
- isTextToImageConversionEnabled: boolean;
26
- /** 是否将排行榜渲染为水平柱状图。 */
27
- isLeaderboardToHorizontalBarChartConversionEnabled: boolean;
28
- /** 生成的柱状图图片类型。 */
29
- imageType: "png" | "jpeg" | "webp";
30
- /** 头像缓存的有效期(秒)。设置为 0 可禁用缓存刷新。 */
31
- avatarCacheTTL: number;
32
- /** 头像获取失败后的重试间隔(秒)。 */
33
- avatarFailureCacheTTL: number;
34
- /** 页面加载等待事件,影响图片生成速度和稳定性。 */
35
- waitUntil: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
36
- /**
37
- * 生成水平柱状图时,渲染页面的视口宽度(像素)。
38
- * @default 1080
39
- */
40
- chartViewportWidth: number;
41
- /**
42
- * 渲染时的设备像素比 (DPR)。
43
- * 更高的值可以生成更清晰的图片(例如,设置为 2 相当于 2x 图),但也会增加图片文件体积。
44
- * @default 1
45
- */
46
- deviceScaleFactor: number;
47
- /** 是否将自定义图标显示在柱状条的末端。 */
48
- shouldMoveIconToBarEndLeft: boolean;
49
- /** 是否在生成水平柱状图时,在当前用户/群聊名称前显示★以高亮。 */
50
- showStarInChart: boolean;
51
- /** 自定义背景图在进度条区域的不透明度。 */
52
- horizontalBarBackgroundOpacity: number;
53
- /** 自定义背景图在整行背景的不透明度。 */
54
- horizontalBarBackgroundFullOpacity: number;
55
- /** 允许上传的背景图最大宽度(像素)。 */
56
- maxBarBgWidth: number;
57
- /** 允许上传的背景图最大高度(像素)。 */
58
- maxBarBgHeight: number;
59
- /** 允许上传的背景图最大体积(MB)。 */
60
- maxBarBgSize: number;
61
- /** 图片整体背景的类型。 */
62
- backgroundType: string;
63
- /** API 背景配置。 */
64
- apiBackgroundConfig: apiBackgroundConfig;
65
- /** 自定义背景的 CSS 代码。 */
66
- backgroundValue: string;
67
- /** 水平柱状图 - 标题的字体。 */
68
- chartTitleFont: string;
69
- /** 水平柱状图 - 成员昵称和发言次数的字体。 */
70
- chartNicknameFont: string;
71
- /** 是否启用定时自动推送排行榜功能。 */
72
- autoPush: boolean;
73
- /** 是否在每日 0 点自动发送昨日排行榜。 */
74
- shouldSendDailyLeaderboardAtMidnight: boolean;
75
- /** 是否在每周一 0 点自动发送上周排行榜。 */
76
- shouldSendWeeklyLeaderboard: boolean;
77
- /** 是否在每月第一天 0 点自动发送上月排行榜。 */
78
- shouldSendMonthlyLeaderboard: boolean;
79
- /** 是否在每年第一天 0 点自动发送去年排行榜。 */
80
- shouldSendYearlyLeaderboard: boolean;
81
- /** 其他定时发送今日排行榜的时间点(24小时制)。 */
82
- dailyScheduledTimers: string[];
83
- /** 发送排行榜前是否发送提示消息。 */
84
- isGeneratingRankingListPromptVisible: boolean;
85
- /** 发送提示后等待多少秒再发送图片。 */
86
- leaderboardGenerationWaitTime: number;
87
- /** 需要接收自动推送的频道 ID 列表。 */
88
- pushChannelIds: string[];
89
- /** 是否向机器人所在的所有群聊推送。 */
90
- shouldSendLeaderboardNotificationsToAllChannels: boolean;
91
- /** "向所有群聊推送" 开启时的排除列表。 */
92
- excludedLeaderboardChannels: string[];
93
- /** 批量推送时,每个群之间的发送延迟(秒)。 */
94
- delayBetweenGroupPushesInSeconds: number;
95
- /** 延迟时间的随机波动范围(秒)。 */
96
- groupPushDelayRandomizationSeconds: number;
97
- /** 是否在每日 0 点自动禁言昨日发言最多的人。 */
98
- enableMostActiveUserMuting: boolean;
99
- /** 0 点后,等待多少秒再执行禁言操作。 */
100
- dragonKingDetainmentTime: number;
101
- /** 禁言时长(天)。 */
102
- detentionDuration: number;
103
- /** 在哪些频道中执行“抓龙王”操作。 */
104
- muteChannelIds: string[];
105
- }
106
- export declare const Config: Schema<Config>;
107
- declare module "koishi" {
108
- interface Tables {
109
- message_counter_records: MessageCounterRecord;
110
- message_counter_state: MessageCounterState;
111
- }
112
- }
113
- interface apiBackgroundConfig {
114
- apiUrl: string;
115
- apiKey: string;
116
- responseType: string;
117
- }
118
- interface MessageCounterRecord {
119
- channelId: string;
120
- channelName: string;
121
- userId: string;
122
- username: string;
123
- userAvatar: string;
124
- todayPostCount: number;
125
- thisWeekPostCount: number;
126
- thisMonthPostCount: number;
127
- thisYearPostCount: number;
128
- totalPostCount: number;
129
- yesterdayPostCount: number;
130
- }
131
- interface MessageCounterState {
132
- key: string;
133
- value: Date;
134
- }
135
- export declare function apply(ctx: Context, config: Config): Promise<void>;
136
- export {};