mioku-plugin-impact 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mioku Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # mioku-plugin-impact
2
+
3
+ > 本插件移植自 https://github.com/Special-Week/nonebot_plugin_impact ,欢迎原作者接手维护
4
+ > 侵权请发 issue 联系删除,谢谢 :)
5
+
6
+ 让群友们眼前一黑的 mioku 淫趴插件(牛子比拼),原版基于 NoneBot2,本仓库使用 TypeScript 重写并适配 mioku 框架。
7
+
8
+ ## 警告
9
+
10
+ - 在群内大量使用本插件可能被网警封群,请慎用。
11
+ - 使用插件是你自己的事情, 产生的一切后果作者概不负责。
12
+
13
+ ## 安装与启用
14
+
15
+ 本插件作为 mioku monorepo 内的工作区包发布,使用方式有两种:
16
+
17
+ ```bash
18
+ # 1. monorepo 内开发:在 example/package.json 里 "workspace:*" 引入即可
19
+ # 2. 已发布版本:
20
+ npx mioku install plugin impact
21
+ ```
22
+
23
+ 启用后请先在群里让管理员/群主/主人发送 `开启淫趴`(也支持 `开启银趴`、`开始银趴`),关闭则用 `关闭银趴` / `禁止淫趴` 等。
24
+
25
+ ## 命令一览
26
+
27
+ | 命令 | 说明 |
28
+ | --- | --- |
29
+ | `开启淫趴` / `关闭银趴` 等 | 在本群启用/关闭淫趴功能(仅管理/群主/主人) |
30
+ | `打胶` / `开导` | 给自己增加随机长度(默认 5 分钟 CD) |
31
+ | `嗦牛子 [@某人]` | 嗦自己或被 @ 群友的牛子(默认 5 分钟 CD) |
32
+ | `pk @某人` / `对决 @某人` | 牛子对决,胜者抢走败者的部分长度(默认 1 分钟 CD) |
33
+ | `查询 [@某人]` | 查询自己或目标的牛子长度 |
34
+ | `jj排行榜` / `jj排名` / `jj榜单` / `jjrank` | 输出 Top5 与倒数 5 名图表,并附上自己的排名 |
35
+ | `日群友` / `透群友 [@某人]` / `透群主` / `透管理` | 随机或定向选取一位幸运儿进行注入(默认 1 小时 CD,主人无 CD) |
36
+ | `注入查询 [历史/全部] [@某人]` | 查询当日或全部历史被注入量;带 `历史` / `全部` 时附走势图 |
37
+
38
+ > 帮助信息已经接入 mioku 自带的 `mioku-plugin-help`,群里使用对应的帮助命令即可看到上述命令列表,不再单独提供 `淫趴介绍`。
39
+
40
+ ## 数据存储
41
+
42
+ 数据通过 mioki 的 `createDB` 落盘到 `data/impact/impact.json`,包含:
43
+
44
+ - `users` —— 每位用户的牛子长度与最后活跃时间
45
+ - `groups` —— 各群是否开启淫趴
46
+ - `ejaculations` —— 每位用户按日期累计的被注入量
47
+
48
+ 如果重命名 / 重装插件,请将 `data/impact/` 一并迁移即可保留进度。
49
+
50
+ 欢迎 PR / issue 反馈问题。
package/config.md ADDED
@@ -0,0 +1,42 @@
1
+ ---
2
+ title: Impact 淫趴插件配置
3
+ description: 配置淫趴(牛子比拼)插件各命令的冷却时间与每日不活跃惩罚。
4
+ fields:
5
+ - key: base.djCd
6
+ label: 打胶 / 开导 冷却
7
+ type: number
8
+ description: 执行 `打胶` 或 `开导` 后再次执行的冷却时间,单位为秒。
9
+ placeholder: 300
10
+
11
+ - key: base.pkCd
12
+ label: pk / 对决 冷却
13
+ type: number
14
+ description: 执行 `pk` / `对决` 后再次执行的冷却时间,单位为秒。
15
+ placeholder: 60
16
+
17
+ - key: base.suoCd
18
+ label: 嗦牛子 冷却
19
+ type: number
20
+ description: 执行 `嗦牛子` 后再次执行的冷却时间,单位为秒。
21
+ placeholder: 300
22
+
23
+ - key: base.fuckCd
24
+ label: 透群友 / 日群友 冷却
25
+ type: number
26
+ description: 执行 `透群友` / `日群友` / `透群主` / `透管理` 后再次执行的冷却时间,单位为秒。主(owner)发送该系列命令不受此冷却限制。
27
+ placeholder: 3600
28
+
29
+ - key: base.isalive
30
+ label: 不活跃惩罚
31
+ type: switch
32
+ description: 开启后每天 0 点对所有「上次打胶超过一天」且「长度大于 1cm」的用户随机扣减 0–1cm。
33
+ ---
34
+
35
+ ```mioku-fields
36
+ keys:
37
+ - base.djCd
38
+ - base.pkCd
39
+ - base.suoCd
40
+ - base.fuckCd
41
+ - base.isalive
42
+ ```
@@ -0,0 +1,40 @@
1
+ export interface ImpactConfig {
2
+ /** 打胶 / 开导 冷却时间(秒) */
3
+ djCd: number;
4
+ /** pk / 对决 冷却时间(秒) */
5
+ pkCd: number;
6
+ /** 嗦牛子 冷却时间(秒) */
7
+ suoCd: number;
8
+ /** 透群友 / 日群友 冷却时间(秒) */
9
+ fuckCd: number;
10
+ /** 开启后每天 0 点对超过一天没活跃的用户随机扣 0–1cm */
11
+ isalive: boolean;
12
+ }
13
+
14
+ export const DEFAULT_CONFIG: ImpactConfig = {
15
+ djCd: 300,
16
+ pkCd: 60,
17
+ suoCd: 300,
18
+ fuckCd: 3600,
19
+ isalive: false,
20
+ };
21
+
22
+ export function cloneConfig<T>(value: T): T {
23
+ return JSON.parse(JSON.stringify(value)) as T;
24
+ }
25
+
26
+ function numOrDefault(value: unknown, fallback: number): number {
27
+ const n = Number(value);
28
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
29
+ }
30
+
31
+ export function normalizeImpactConfig(config: any): ImpactConfig {
32
+ const c = config || {};
33
+ return {
34
+ djCd: numOrDefault(c.djCd, DEFAULT_CONFIG.djCd),
35
+ pkCd: numOrDefault(c.pkCd, DEFAULT_CONFIG.pkCd),
36
+ suoCd: numOrDefault(c.suoCd, DEFAULT_CONFIG.suoCd),
37
+ fuckCd: numOrDefault(c.fuckCd, DEFAULT_CONFIG.fuckCd),
38
+ isalive: typeof c.isalive === "boolean" ? c.isalive : DEFAULT_CONFIG.isalive,
39
+ };
40
+ }
package/db.ts ADDED
@@ -0,0 +1,229 @@
1
+ import { Database } from "bun:sqlite";
2
+ import * as path from "path";
3
+ import { ensureDataDir } from "mioku";
4
+ import type {
5
+ EjaculationRecord,
6
+ RankEntry,
7
+ } from "./types";
8
+ import { getToday, nowSeconds } from "./utils";
9
+
10
+ export const DEFAULT_JJ_LENGTH = 10.0;
11
+
12
+ export interface ImpactDatabase {
13
+ isUserInTable(userId: number): boolean;
14
+ addNewUser(userId: number): Promise<void>;
15
+ updateActivity(userId: number): Promise<void>;
16
+ getJjLength(userId: number): number;
17
+ /** 累加长度(可负)。先确保用户存在再调用。 */
18
+ addJjLength(userId: number, delta: number): Promise<void>;
19
+ isGroupAllowed(groupId: number): boolean;
20
+ setGroupAllow(groupId: number, allow: boolean): Promise<void>;
21
+ insertEjaculation(userId: number, volume: number): Promise<void>;
22
+ getEjaculationData(userId: number): EjaculationRecord[];
23
+ getTodayEjaculationVolume(userId: number): number;
24
+ /** 一天没活跃的用户随机扣 0–1cm(仅对 jj_length > 1 的用户生效) */
25
+ punishInactiveUsers(): Promise<number>;
26
+ /** 全表按 jjLength 倒序排列 */
27
+ getRanking(): RankEntry[];
28
+ close(): void;
29
+ }
30
+
31
+ interface UserRow {
32
+ user_id: number;
33
+ jj_length: number;
34
+ last_masturbation_time: number;
35
+ }
36
+
37
+ export async function initImpactDatabase(): Promise<ImpactDatabase> {
38
+ const dir = ensureDataDir("impact");
39
+ const dbPath = path.join(dir, "impact.db");
40
+ const db = new Database(dbPath);
41
+
42
+ db.exec("PRAGMA journal_mode = WAL");
43
+
44
+ db.exec(`
45
+ CREATE TABLE IF NOT EXISTS users (
46
+ user_id INTEGER PRIMARY KEY,
47
+ jj_length REAL NOT NULL,
48
+ last_masturbation_time INTEGER NOT NULL
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS groups (
52
+ group_id INTEGER PRIMARY KEY,
53
+ allow INTEGER NOT NULL DEFAULT 0
54
+ );
55
+
56
+ CREATE TABLE IF NOT EXISTS ejaculations (
57
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
58
+ user_id INTEGER NOT NULL,
59
+ date TEXT NOT NULL,
60
+ volume REAL NOT NULL,
61
+ UNIQUE(user_id, date)
62
+ );
63
+ CREATE INDEX IF NOT EXISTS idx_ejaculations_user ON ejaculations(user_id, date);
64
+ `);
65
+
66
+ const stmts = {
67
+ insertUser: db.prepare(`
68
+ INSERT INTO users (user_id, jj_length, last_masturbation_time)
69
+ VALUES ($userId, $jjLength, $lastMasturbationTime)
70
+ ON CONFLICT(user_id) DO NOTHING
71
+ `),
72
+ upsertActivity: db.prepare(`
73
+ INSERT INTO users (user_id, jj_length, last_masturbation_time)
74
+ VALUES ($userId, $jjLength, $lastMasturbationTime)
75
+ ON CONFLICT(user_id) DO UPDATE SET
76
+ last_masturbation_time = $lastMasturbationTime
77
+ `),
78
+ getUser: db.prepare(
79
+ `SELECT jj_length, last_masturbation_time FROM users WHERE user_id = $userId`,
80
+ ),
81
+ updateJjLength: db.prepare(`
82
+ UPDATE users
83
+ SET jj_length = $jjLength, last_masturbation_time = $lastMasturbationTime
84
+ WHERE user_id = $userId
85
+ `),
86
+ getGroup: db.prepare(
87
+ `SELECT allow FROM groups WHERE group_id = $groupId`,
88
+ ),
89
+ upsertGroup: db.prepare(`
90
+ INSERT INTO groups (group_id, allow) VALUES ($groupId, $allow)
91
+ ON CONFLICT(group_id) DO UPDATE SET allow = $allow
92
+ `),
93
+ upsertEjaculation: db.prepare(`
94
+ INSERT INTO ejaculations (user_id, date, volume)
95
+ VALUES ($userId, $date, $volume)
96
+ ON CONFLICT(user_id, date) DO UPDATE SET volume = volume + $volume
97
+ `),
98
+ getEjaculationData: db.prepare(`
99
+ SELECT date, volume FROM ejaculations
100
+ WHERE user_id = $userId
101
+ ORDER BY date ASC
102
+ `),
103
+ getTodayEjaculation: db.prepare(`
104
+ SELECT volume FROM ejaculations
105
+ WHERE user_id = $userId AND date = $date
106
+ `),
107
+ selectAllUsers: db.prepare(
108
+ `SELECT user_id, jj_length, last_masturbation_time FROM users`,
109
+ ),
110
+ decrementJjLength: db.prepare(
111
+ `UPDATE users SET jj_length = $jjLength WHERE user_id = $userId`,
112
+ ),
113
+ };
114
+
115
+ return {
116
+ isUserInTable(userId) {
117
+ return stmts.getUser.get({ $userId: userId }) != null;
118
+ },
119
+
120
+ async addNewUser(userId) {
121
+ stmts.insertUser.run({
122
+ $userId: userId,
123
+ $jjLength: DEFAULT_JJ_LENGTH,
124
+ $lastMasturbationTime: nowSeconds(),
125
+ });
126
+ },
127
+
128
+ async updateActivity(userId) {
129
+ stmts.upsertActivity.run({
130
+ $userId: userId,
131
+ $jjLength: DEFAULT_JJ_LENGTH,
132
+ $lastMasturbationTime: nowSeconds(),
133
+ });
134
+ },
135
+
136
+ getJjLength(userId) {
137
+ const row = stmts.getUser.get({ $userId: userId }) as
138
+ | Pick<UserRow, "jj_length">
139
+ | null;
140
+ return row?.jj_length ?? 0;
141
+ },
142
+
143
+ async addJjLength(userId, delta) {
144
+ const row = stmts.getUser.get({ $userId: userId }) as
145
+ | Pick<UserRow, "jj_length">
146
+ | null;
147
+ if (!row) return;
148
+ stmts.updateJjLength.run({
149
+ $userId: userId,
150
+ $jjLength: roundTo3(row.jj_length + delta),
151
+ $lastMasturbationTime: nowSeconds(),
152
+ });
153
+ },
154
+
155
+ isGroupAllowed(groupId) {
156
+ const row = stmts.getGroup.get({ $groupId: groupId }) as
157
+ | { allow: number }
158
+ | null;
159
+ return Boolean(row?.allow);
160
+ },
161
+
162
+ async setGroupAllow(groupId, allow) {
163
+ stmts.upsertGroup.run({
164
+ $groupId: groupId,
165
+ $allow: allow ? 1 : 0,
166
+ });
167
+ },
168
+
169
+ async insertEjaculation(userId, volume) {
170
+ stmts.upsertEjaculation.run({
171
+ $userId: userId,
172
+ $date: getToday(),
173
+ $volume: roundTo3(volume),
174
+ });
175
+ },
176
+
177
+ getEjaculationData(userId) {
178
+ const rows = stmts.getEjaculationData.all({ $userId: userId }) as Array<
179
+ Pick<EjaculationRecord, "date" | "volume">
180
+ >;
181
+ return rows.map((row) => ({ date: row.date, volume: row.volume }));
182
+ },
183
+
184
+ getTodayEjaculationVolume(userId) {
185
+ const row = stmts.getTodayEjaculation.get({
186
+ $userId: userId,
187
+ $date: getToday(),
188
+ }) as { volume: number } | null;
189
+ return row?.volume ?? 0;
190
+ },
191
+
192
+ async punishInactiveUsers() {
193
+ const now = nowSeconds();
194
+ const rows = stmts.selectAllUsers.all({}) as UserRow[];
195
+ let touched = 0;
196
+ const tx = db.transaction(() => {
197
+ for (const row of rows) {
198
+ if (
199
+ now - row.last_masturbation_time > 86400 &&
200
+ row.jj_length > 1
201
+ ) {
202
+ stmts.decrementJjLength.run({
203
+ $userId: row.user_id,
204
+ $jjLength: roundTo3(row.jj_length - Math.random()),
205
+ });
206
+ touched += 1;
207
+ }
208
+ }
209
+ });
210
+ tx();
211
+ return touched;
212
+ },
213
+
214
+ getRanking() {
215
+ const rows = stmts.selectAllUsers.all({}) as UserRow[];
216
+ return rows
217
+ .map((row) => ({ userId: row.user_id, jjLength: row.jj_length }))
218
+ .sort((a, b) => b.jjLength - a.jjLength);
219
+ },
220
+
221
+ close() {
222
+ db.close();
223
+ },
224
+ };
225
+ }
226
+
227
+ function roundTo3(n: number): number {
228
+ return Math.round(n * 1000) / 1000;
229
+ }
@@ -0,0 +1,50 @@
1
+ import { checkCooldown, markCooldown } from "../state";
2
+ import { getRandomNum, pickJj, roundTo } from "../utils";
3
+ import type { HandlerContext } from "./types";
4
+
5
+ export async function handleDajiao(h: HandlerContext): Promise<void> {
6
+ const { ctx, db, cd, config, event } = h;
7
+ const uid = String(event.user_id);
8
+ const cdState = checkCooldown(cd.dj, uid, config.djCd);
9
+ if (!cdState.ok) {
10
+ await event.reply(
11
+ [
12
+ ctx.segment.at(Number(event.user_id)),
13
+ ctx.segment.text(
14
+ `你已经打不动了喵, 请等待${roundTo(cdState.remaining, 3)}秒后再打喵`,
15
+ ),
16
+ ],
17
+ false,
18
+ );
19
+ return;
20
+ }
21
+
22
+ markCooldown(cd.dj, uid);
23
+
24
+ if (db.isUserInTable(Number(uid))) {
25
+ const delta = getRandomNum();
26
+ await db.addJjLength(Number(uid), delta);
27
+ await event.reply(
28
+ [
29
+ ctx.segment.at(Number(event.user_id)),
30
+ ctx.segment.text(
31
+ `打胶结束喵, 你的${pickJj()}很满意喵, 长了${delta}cm喵, 目前长度为${db.getJjLength(
32
+ Number(uid),
33
+ )}cm喵`,
34
+ ),
35
+ ],
36
+ false,
37
+ );
38
+ } else {
39
+ await db.addNewUser(Number(uid));
40
+ await event.reply(
41
+ [
42
+ ctx.segment.at(Number(event.user_id)),
43
+ ctx.segment.text(
44
+ `你还没有创建${pickJj()}, 咱帮你创建了喵, 目前长度是10cm喵`,
45
+ ),
46
+ ],
47
+ false,
48
+ );
49
+ }
50
+ }
@@ -0,0 +1,22 @@
1
+ import { isOwner } from "mioki";
2
+ import { isGroupAdmin } from "../utils";
3
+ import type { HandlerContext } from "./types";
4
+
5
+ export async function handleOpenModule(
6
+ h: HandlerContext,
7
+ enable: boolean,
8
+ ): Promise<void> {
9
+ const { ctx, db, event } = h;
10
+ if (!isOwner(event) && !isGroupAdmin(event)) {
11
+ await event.reply(
12
+ [ctx.segment.text("权限不足喵, 仅管理员/群主/主人可以开关淫趴")],
13
+ false,
14
+ );
15
+ return;
16
+ }
17
+ await db.setGroupAllow(Number(event.group_id), enable);
18
+ await event.reply(
19
+ [ctx.segment.text(enable ? "功能已开启喵" : "功能已禁用喵")],
20
+ false,
21
+ );
22
+ }
package/handlers/pk.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { checkCooldown, clearCooldown, markCooldown } from "../state";
2
+ import { getAtUserId, getRandomNum, pickJj, roundTo } from "../utils";
3
+ import type { HandlerContext } from "./types";
4
+
5
+ export async function handlePk(h: HandlerContext): Promise<void> {
6
+ const { ctx, db, cd, config, event } = h;
7
+ const uid = String(event.user_id);
8
+ const cdState = checkCooldown(cd.pk, uid, config.pkCd);
9
+ if (!cdState.ok) {
10
+ await event.reply(
11
+ [
12
+ ctx.segment.at(Number(event.user_id)),
13
+ ctx.segment.text(
14
+ `你已经pk不动了喵, 请等待${roundTo(cdState.remaining, 3)}秒后再pk喵`,
15
+ ),
16
+ ],
17
+ false,
18
+ );
19
+ return;
20
+ }
21
+
22
+ const at = getAtUserId(event.message);
23
+ if (at == null) {
24
+ // pk 必须带 @,无 @ 时不响应
25
+ return;
26
+ }
27
+ if (at === Number(uid)) {
28
+ await event.reply(
29
+ [
30
+ ctx.segment.at(Number(event.user_id)),
31
+ ctx.segment.text("你不能pk自己喵"),
32
+ ],
33
+ false,
34
+ );
35
+ return;
36
+ }
37
+
38
+ markCooldown(cd.pk, uid);
39
+
40
+ const meIn = db.isUserInTable(Number(uid));
41
+ const otherIn = db.isUserInTable(at);
42
+ if (!meIn || !otherIn) {
43
+ if (!meIn) await db.addNewUser(Number(uid));
44
+ if (!otherIn) await db.addNewUser(at);
45
+ clearCooldown(cd.pk, uid);
46
+ await event.reply(
47
+ [
48
+ ctx.segment.at(Number(event.user_id)),
49
+ ctx.segment.text(
50
+ `你或对面还没有创建${pickJj()}喵, 咱全帮你创建了喵, 你们的${pickJj()}长度都是10cm喵`,
51
+ ),
52
+ ],
53
+ false,
54
+ );
55
+ return;
56
+ }
57
+
58
+ const win = Math.random() > 0.5;
59
+ const delta = getRandomNum();
60
+ if (win) {
61
+ await db.addJjLength(Number(uid), delta / 2);
62
+ await db.addJjLength(at, -delta);
63
+ await event.reply(
64
+ [
65
+ ctx.segment.at(Number(event.user_id)),
66
+ ctx.segment.text(
67
+ `对决胜利喵, 你的${pickJj()}增加了${roundTo(
68
+ delta / 2,
69
+ 3,
70
+ )}cm喵, 对面则在你的阴影笼罩下减小了${delta}cm喵`,
71
+ ),
72
+ ],
73
+ false,
74
+ );
75
+ } else {
76
+ await db.addJjLength(Number(uid), -delta);
77
+ await db.addJjLength(at, delta / 2);
78
+ await event.reply(
79
+ [
80
+ ctx.segment.at(Number(event.user_id)),
81
+ ctx.segment.text(
82
+ `对决失败喵, 在对面牛子的阴影笼罩下你的${pickJj()}减小了${delta}cm喵, 对面增加了${roundTo(
83
+ delta / 2,
84
+ 3,
85
+ )}cm喵`,
86
+ ),
87
+ ],
88
+ false,
89
+ );
90
+ }
91
+ }
@@ -0,0 +1,74 @@
1
+ import { renderInjectionChart } from "../image";
2
+ import { getAtUserId } from "../utils";
3
+ import type { HandlerContext } from "./types";
4
+
5
+ export async function handleQueryInjection(
6
+ h: HandlerContext,
7
+ mode: "today" | "history",
8
+ ): Promise<void> {
9
+ const { ctx, db, screenshot, event } = h;
10
+ const at = getAtUserId(event.message);
11
+ const target = at ?? Number(event.user_id);
12
+ // 有 @ 用"该用户",无 @ 用"您"
13
+ const replay1 = at != null ? "该用户" : "您";
14
+
15
+ if (mode === "today") {
16
+ const today = db.getTodayEjaculationVolume(target);
17
+ await event.reply(
18
+ [ctx.segment.text(`${replay1}当日总被注射量为${today}ml`)],
19
+ false,
20
+ );
21
+ return;
22
+ }
23
+
24
+ // history / 全部
25
+ const records = db.getEjaculationData(target);
26
+ if (records.length === 0) {
27
+ await event.reply(
28
+ [ctx.segment.text(`${replay1}历史总被注射量为0ml`)],
29
+ false,
30
+ );
31
+ return;
32
+ }
33
+
34
+ const injectData = new Map<string, number>();
35
+ let total = 0;
36
+ for (const item of records) {
37
+ total += item.volume;
38
+ injectData.set(item.date, item.volume);
39
+ }
40
+
41
+ // 记录少于 2 天,只返回总量
42
+ if (injectData.size < 2) {
43
+ await event.reply(
44
+ [ctx.segment.text(`${replay1}历史总被注射量为${total}ml`)],
45
+ false,
46
+ );
47
+ return;
48
+ }
49
+
50
+ if (!screenshot) {
51
+ ctx.logger.warn(
52
+ "impact: screenshot 服务未启用,无法绘制历史走势图,仅返回总量",
53
+ );
54
+ await event.reply(
55
+ [ctx.segment.text(`${replay1}历史总被注射量为${total}ml`)],
56
+ false,
57
+ );
58
+ return;
59
+ }
60
+
61
+ // 折线图按日期排序
62
+ const sorted = Array.from(injectData.entries())
63
+ .sort((a, b) => a[0].localeCompare(b[0]))
64
+ .map(([date, volume]) => ({ date, volume }));
65
+
66
+ const imagePath = await renderInjectionChart(screenshot, sorted, total);
67
+ await event.reply(
68
+ [
69
+ ctx.segment.text(`${replay1}历史总被注射量为${total}ml`),
70
+ ctx.segment.image(imagePath),
71
+ ],
72
+ false,
73
+ );
74
+ }
@@ -0,0 +1,53 @@
1
+ import { getAtUserId, pickJj } from "../utils";
2
+ import type { HandlerContext } from "./types";
3
+
4
+ export async function handleQueryJj(h: HandlerContext): Promise<void> {
5
+ const { ctx, db, event } = h;
6
+ const uid = Number(event.user_id);
7
+ const at = getAtUserId(event.message);
8
+
9
+ if (at == null) {
10
+ if (db.isUserInTable(uid)) {
11
+ await event.reply(
12
+ [
13
+ ctx.segment.at(uid),
14
+ ctx.segment.text(`你的${pickJj()}目前长度为${db.getJjLength(uid)}cm喵`),
15
+ ],
16
+ false,
17
+ );
18
+ } else {
19
+ await db.addNewUser(uid);
20
+ await event.reply(
21
+ [
22
+ ctx.segment.at(uid),
23
+ ctx.segment.text(
24
+ `你还没有创建${pickJj()}喵, 咱帮你创建了喵, 目前长度是10cm喵`,
25
+ ),
26
+ ],
27
+ false,
28
+ );
29
+ }
30
+ return;
31
+ }
32
+
33
+ if (db.isUserInTable(at)) {
34
+ await event.reply(
35
+ [
36
+ ctx.segment.at(uid),
37
+ ctx.segment.text(`TA的${pickJj()}目前长度为${db.getJjLength(at)}cm喵`),
38
+ ],
39
+ false,
40
+ );
41
+ } else {
42
+ await db.addNewUser(at);
43
+ await event.reply(
44
+ [
45
+ ctx.segment.at(uid),
46
+ ctx.segment.text(
47
+ `TA还没有创建${pickJj()}喵, 咱帮他创建了喵, 目前长度是10cm喵`,
48
+ ),
49
+ ],
50
+ false,
51
+ );
52
+ }
53
+ }