mioku-plugin-impact 2.0.2 → 2.0.3

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/README.md CHANGED
@@ -20,22 +20,20 @@
20
20
  npx mioku install plugin impact
21
21
  ```
22
22
 
23
- 启用后请先在群里让管理员/群主/主人发送 `开启淫趴`(也支持 `开启银趴`、`开始银趴`),关闭则用 `关闭银趴` / `禁止淫趴` 等。
23
+ 启用后请先在群里让管理员/群主/主人发送 `.开启淫趴`(也支持 `.开启银趴`、`.开始银趴`),关闭则用 `.关闭银趴` / `.禁止淫趴` 等。
24
24
 
25
25
  ## 命令一览
26
26
 
27
27
  | 命令 | 说明 |
28
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`,群里使用对应的帮助命令即可看到上述命令列表,不再单独提供 `淫趴介绍`。
29
+ | `.开启淫趴` / `.关闭银趴` 等 | 在本群启用/关闭淫趴功能(仅管理/群主/主人) |
30
+ | `.打胶` / `.开导` | 给自己增加随机长度(默认 5 分钟 CD) |
31
+ | `.嗦牛子 [@某人]` | 嗦自己或被 @ 群友的牛子(默认 5 分钟 CD) |
32
+ | `.pk @某人` / `.对决 @某人` | 牛子对决,胜者抢走败者的部分长度(默认 1 分钟 CD) |
33
+ | `.查询 [@某人]` | 查询自己或目标的牛子长度 |
34
+ | `.jj排行榜` / `.jj排名` / `.jj榜单` / `.jjrank` | 输出 Top5 与倒数 5 名图表,并附上自己的排名 |
35
+ | `.日群友` / `.透群友 [@某人]` / `.透群主` / `.透管理` | 随机或定向选取一位幸运儿进行注入(默认 1 小时 CD,主人无 CD) |
36
+ | `.注入查询 [历史/全部] [@某人]` | 查询当日或全部历史被注入量;带 `历史` / `全部` 时附走势图 |
39
37
 
40
38
  ## 数据存储
41
39
 
package/db.ts CHANGED
@@ -10,17 +10,17 @@ import { getToday, nowSeconds } from "./utils";
10
10
  export const DEFAULT_JJ_LENGTH = 10.0;
11
11
 
12
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;
13
+ isUserInTable(userId: string): boolean;
14
+ addNewUser(userId: string): Promise<void>;
15
+ updateActivity(userId: string): Promise<void>;
16
+ getJjLength(userId: string): number;
17
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;
18
+ addJjLength(userId: string, delta: number): Promise<void>;
19
+ isGroupAllowed(groupId: string): boolean;
20
+ setGroupAllow(groupId: string, allow: boolean): Promise<void>;
21
+ insertEjaculation(userId: string, volume: number): Promise<void>;
22
+ getEjaculationData(userId: string): EjaculationRecord[];
23
+ getTodayEjaculationVolume(userId: string): number;
24
24
  /** 一天没活跃的用户随机扣 0–1cm(仅对 jj_length > 1 的用户生效) */
25
25
  punishInactiveUsers(): Promise<number>;
26
26
  /** 全表按 jjLength 倒序排列 */
@@ -29,11 +29,73 @@ export interface ImpactDatabase {
29
29
  }
30
30
 
31
31
  interface UserRow {
32
- user_id: number;
32
+ user_id: string;
33
33
  jj_length: number;
34
34
  last_masturbation_time: number;
35
35
  }
36
36
 
37
+ /**
38
+ * 旧库的 users.user_id / groups.group_id 是 INTEGER PRIMARY KEY(rowid 别名),
39
+ * 写入 openid 会抛 SQLiteError: datatype mismatch,这里重建成 TEXT 并保留原数据。
40
+ */
41
+ function widenLegacyIdColumns(db: Database): void {
42
+ const rebuild = (
43
+ table: string,
44
+ idColumn: string,
45
+ createSql: string,
46
+ columns: string,
47
+ ): void => {
48
+ const info = db
49
+ .query(`PRAGMA table_info(${table})`)
50
+ .all() as Array<{ name?: string; type?: string }>;
51
+ const column = info.find((item) => item.name === idColumn);
52
+ if (!column || String(column.type ?? "").toUpperCase() === "TEXT") return;
53
+
54
+ db.exec("BEGIN");
55
+ try {
56
+ db.exec(`ALTER TABLE ${table} RENAME TO ${table}_legacy`);
57
+ db.exec(createSql);
58
+ db.exec(
59
+ `INSERT INTO ${table} (${columns})
60
+ SELECT ${columns
61
+ .split(",")
62
+ .map((name) =>
63
+ name.trim() === idColumn
64
+ ? `CAST(${idColumn} AS TEXT)`
65
+ : name.trim(),
66
+ )
67
+ .join(", ")}
68
+ FROM ${table}_legacy`,
69
+ );
70
+ db.exec(`DROP TABLE ${table}_legacy`);
71
+ db.exec("COMMIT");
72
+ } catch (err) {
73
+ db.exec("ROLLBACK");
74
+ throw err;
75
+ }
76
+ };
77
+
78
+ rebuild(
79
+ "users",
80
+ "user_id",
81
+ `CREATE TABLE users (
82
+ user_id TEXT PRIMARY KEY,
83
+ jj_length REAL NOT NULL,
84
+ last_masturbation_time INTEGER NOT NULL
85
+ )`,
86
+ "user_id, jj_length, last_masturbation_time",
87
+ );
88
+ rebuild(
89
+ "groups",
90
+ "group_id",
91
+ `CREATE TABLE groups (
92
+ group_id TEXT PRIMARY KEY,
93
+ allow INTEGER NOT NULL DEFAULT 0
94
+ )`,
95
+ "group_id, allow",
96
+ );
97
+ }
98
+
37
99
  export async function initImpactDatabase(): Promise<ImpactDatabase> {
38
100
  const dir = ensureDataDir("impact");
39
101
  const dbPath = path.join(dir, "impact.db");
@@ -43,19 +105,19 @@ export async function initImpactDatabase(): Promise<ImpactDatabase> {
43
105
 
44
106
  db.exec(`
45
107
  CREATE TABLE IF NOT EXISTS users (
46
- user_id INTEGER PRIMARY KEY,
108
+ user_id TEXT PRIMARY KEY,
47
109
  jj_length REAL NOT NULL,
48
110
  last_masturbation_time INTEGER NOT NULL
49
111
  );
50
112
 
51
113
  CREATE TABLE IF NOT EXISTS groups (
52
- group_id INTEGER PRIMARY KEY,
114
+ group_id TEXT PRIMARY KEY,
53
115
  allow INTEGER NOT NULL DEFAULT 0
54
116
  );
55
117
 
56
118
  CREATE TABLE IF NOT EXISTS ejaculations (
57
119
  id INTEGER PRIMARY KEY AUTOINCREMENT,
58
- user_id INTEGER NOT NULL,
120
+ user_id TEXT NOT NULL,
59
121
  date TEXT NOT NULL,
60
122
  volume REAL NOT NULL,
61
123
  UNIQUE(user_id, date)
@@ -63,6 +125,8 @@ export async function initImpactDatabase(): Promise<ImpactDatabase> {
63
125
  CREATE INDEX IF NOT EXISTS idx_ejaculations_user ON ejaculations(user_id, date);
64
126
  `);
65
127
 
128
+ widenLegacyIdColumns(db);
129
+
66
130
  const stmts = {
67
131
  insertUser: db.prepare(`
68
132
  INSERT INTO users (user_id, jj_length, last_masturbation_time)
@@ -214,7 +278,7 @@ export async function initImpactDatabase(): Promise<ImpactDatabase> {
214
278
  getRanking() {
215
279
  const rows = stmts.selectAllUsers.all({}) as UserRow[];
216
280
  return rows
217
- .map((row) => ({ userId: row.user_id, jjLength: row.jj_length }))
281
+ .map((row) => ({ userId: String(row.user_id), jjLength: row.jj_length }))
218
282
  .sort((a, b) => b.jjLength - a.jjLength);
219
283
  },
220
284
 
@@ -4,12 +4,12 @@ import type { HandlerContext } from "./types";
4
4
 
5
5
  export async function handleDajiao(h: HandlerContext): Promise<void> {
6
6
  const { ctx, db, cd, config, event } = h;
7
- const uid = String(event.user_id);
7
+ const uid = String(event.user_id ?? "").trim();
8
8
  const cdState = checkCooldown(cd.dj, uid, config.djCd);
9
9
  if (!cdState.ok) {
10
10
  await event.reply(
11
11
  [
12
- ctx.segment.at(String(event.user_id)),
12
+ ctx.segment.at(uid),
13
13
  ctx.segment.text(
14
14
  `你已经打不动了喵, 请等待${roundTo(cdState.remaining, 3)}秒后再打喵`,
15
15
  ),
@@ -21,25 +21,23 @@ export async function handleDajiao(h: HandlerContext): Promise<void> {
21
21
 
22
22
  markCooldown(cd.dj, uid);
23
23
 
24
- if (db.isUserInTable(Number(uid))) {
24
+ if (db.isUserInTable(uid)) {
25
25
  const delta = getRandomNum();
26
- await db.addJjLength(Number(uid), delta);
26
+ await db.addJjLength(uid, delta);
27
27
  await event.reply(
28
28
  [
29
- ctx.segment.at(String(event.user_id)),
29
+ ctx.segment.at(uid),
30
30
  ctx.segment.text(
31
- `打胶结束喵, 你的${pickJj()}很满意喵, 长了${delta}cm喵, 目前长度为${db.getJjLength(
32
- Number(uid),
33
- )}cm喵`,
31
+ `打胶结束喵, 你的${pickJj()}很满意喵, 长了${delta}cm喵, 目前长度为${db.getJjLength(uid)}cm喵`,
34
32
  ),
35
33
  ],
36
34
  false,
37
35
  );
38
36
  } else {
39
- await db.addNewUser(Number(uid));
37
+ await db.addNewUser(uid);
40
38
  await event.reply(
41
39
  [
42
- ctx.segment.at(String(event.user_id)),
40
+ ctx.segment.at(uid),
43
41
  ctx.segment.text(
44
42
  `你还没有创建${pickJj()}, 咱帮你创建了喵, 目前长度是10cm喵`,
45
43
  ),
@@ -5,7 +5,7 @@ export async function handleOpenModule(
5
5
  enable: boolean,
6
6
  ): Promise<void> {
7
7
  const { ctx, db, event } = h;
8
- await db.setGroupAllow(Number(event.group_id), enable);
8
+ await db.setGroupAllow(String(event.group_id ?? "").trim(), enable);
9
9
  await event.reply(
10
10
  [ctx.segment.text(enable ? "功能已开启喵" : "功能已禁用喵")],
11
11
  false,
package/handlers/pk.ts CHANGED
@@ -4,12 +4,12 @@ import type { HandlerContext } from "./types";
4
4
 
5
5
  export async function handlePk(h: HandlerContext): Promise<void> {
6
6
  const { ctx, db, cd, config, event } = h;
7
- const uid = String(event.user_id);
7
+ const uid = String(event.user_id ?? "").trim();
8
8
  const cdState = checkCooldown(cd.pk, uid, config.pkCd);
9
9
  if (!cdState.ok) {
10
10
  await event.reply(
11
11
  [
12
- ctx.segment.at(String(event.user_id)),
12
+ ctx.segment.at(uid),
13
13
  ctx.segment.text(
14
14
  `你已经pk不动了喵, 请等待${roundTo(cdState.remaining, 3)}秒后再pk喵`,
15
15
  ),
@@ -24,10 +24,10 @@ export async function handlePk(h: HandlerContext): Promise<void> {
24
24
  // pk 必须带 @,无 @ 时不响应
25
25
  return;
26
26
  }
27
- if (at === Number(uid)) {
27
+ if (at === uid) {
28
28
  await event.reply(
29
29
  [
30
- ctx.segment.at(String(event.user_id)),
30
+ ctx.segment.at(uid),
31
31
  ctx.segment.text("你不能pk自己喵"),
32
32
  ],
33
33
  false,
@@ -37,15 +37,15 @@ export async function handlePk(h: HandlerContext): Promise<void> {
37
37
 
38
38
  markCooldown(cd.pk, uid);
39
39
 
40
- const meIn = db.isUserInTable(Number(uid));
40
+ const meIn = db.isUserInTable(uid);
41
41
  const otherIn = db.isUserInTable(at);
42
42
  if (!meIn || !otherIn) {
43
- if (!meIn) await db.addNewUser(Number(uid));
43
+ if (!meIn) await db.addNewUser(uid);
44
44
  if (!otherIn) await db.addNewUser(at);
45
45
  clearCooldown(cd.pk, uid);
46
46
  await event.reply(
47
47
  [
48
- ctx.segment.at(String(event.user_id)),
48
+ ctx.segment.at(uid),
49
49
  ctx.segment.text(
50
50
  `你或对面还没有创建${pickJj()}喵, 咱全帮你创建了喵, 你们的${pickJj()}长度都是10cm喵`,
51
51
  ),
@@ -58,11 +58,11 @@ export async function handlePk(h: HandlerContext): Promise<void> {
58
58
  const win = Math.random() > 0.5;
59
59
  const delta = getRandomNum();
60
60
  if (win) {
61
- await db.addJjLength(Number(uid), delta / 2);
61
+ await db.addJjLength(uid, delta / 2);
62
62
  await db.addJjLength(at, -delta);
63
63
  await event.reply(
64
64
  [
65
- ctx.segment.at(String(event.user_id)),
65
+ ctx.segment.at(uid),
66
66
  ctx.segment.text(
67
67
  `对决胜利喵, 你的${pickJj()}增加了${roundTo(
68
68
  delta / 2,
@@ -73,11 +73,11 @@ export async function handlePk(h: HandlerContext): Promise<void> {
73
73
  false,
74
74
  );
75
75
  } else {
76
- await db.addJjLength(Number(uid), -delta);
76
+ await db.addJjLength(uid, -delta);
77
77
  await db.addJjLength(at, delta / 2);
78
78
  await event.reply(
79
79
  [
80
- ctx.segment.at(String(event.user_id)),
80
+ ctx.segment.at(uid),
81
81
  ctx.segment.text(
82
82
  `对决失败喵, 在对面牛子的阴影笼罩下你的${pickJj()}减小了${delta}cm喵, 对面增加了${roundTo(
83
83
  delta / 2,
@@ -8,7 +8,7 @@ export async function handleQueryInjection(
8
8
  ): Promise<void> {
9
9
  const { ctx, db, screenshot, event } = h;
10
10
  const at = getAtUserId(event.message);
11
- const target = at ?? Number(event.user_id);
11
+ const target = at ?? String(event.user_id ?? "").trim();
12
12
  // 有 @ 用"该用户",无 @ 用"您"
13
13
  const replay1 = at != null ? "该用户" : "您";
14
14
 
@@ -3,14 +3,14 @@ import type { HandlerContext } from "./types";
3
3
 
4
4
  export async function handleQueryJj(h: HandlerContext): Promise<void> {
5
5
  const { ctx, db, event } = h;
6
- const uid = Number(event.user_id);
6
+ const uid = String(event.user_id ?? "").trim();
7
7
  const at = getAtUserId(event.message);
8
8
 
9
9
  if (at == null) {
10
10
  if (db.isUserInTable(uid)) {
11
11
  await event.reply(
12
12
  [
13
- ctx.segment.at(String(uid)),
13
+ ctx.segment.at(uid),
14
14
  ctx.segment.text(`你的${pickJj()}目前长度为${db.getJjLength(uid)}cm喵`),
15
15
  ],
16
16
  false,
@@ -19,7 +19,7 @@ export async function handleQueryJj(h: HandlerContext): Promise<void> {
19
19
  await db.addNewUser(uid);
20
20
  await event.reply(
21
21
  [
22
- ctx.segment.at(String(uid)),
22
+ ctx.segment.at(uid),
23
23
  ctx.segment.text(
24
24
  `你还没有创建${pickJj()}喵, 咱帮你创建了喵, 目前长度是10cm喵`,
25
25
  ),
@@ -33,7 +33,7 @@ export async function handleQueryJj(h: HandlerContext): Promise<void> {
33
33
  if (db.isUserInTable(at)) {
34
34
  await event.reply(
35
35
  [
36
- ctx.segment.at(String(uid)),
36
+ ctx.segment.at(uid),
37
37
  ctx.segment.text(`TA的${pickJj()}目前长度为${db.getJjLength(at)}cm喵`),
38
38
  ],
39
39
  false,
@@ -42,7 +42,7 @@ export async function handleQueryJj(h: HandlerContext): Promise<void> {
42
42
  await db.addNewUser(at);
43
43
  await event.reply(
44
44
  [
45
- ctx.segment.at(String(uid)),
45
+ ctx.segment.at(uid),
46
46
  ctx.segment.text(
47
47
  `TA还没有创建${pickJj()}喵, 咱帮他创建了喵, 目前长度是10cm喵`,
48
48
  ),
package/handlers/rank.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { renderRankChart, type RankRow } from "../image";
2
- import { getStrangerNickname, pickJj } from "../utils";
2
+ import { getStrangerNickname, pickJj, resolveAvatarUrl } from "../utils";
3
3
  import type { HandlerContext } from "./types";
4
4
 
5
5
  export async function handleRank(h: HandlerContext): Promise<void> {
@@ -21,12 +21,14 @@ export async function handleRank(h: HandlerContext): Promise<void> {
21
21
  const top5 = ranking.slice(0, 5);
22
22
  const last5 = ranking.slice(-5);
23
23
 
24
- const myIndex = ranking.findIndex((r) => r.userId === Number(event.user_id));
24
+ const myIndex = ranking.findIndex(
25
+ (r) => r.userId === String(event.user_id ?? "").trim(),
26
+ );
25
27
  if (myIndex < 0) {
26
- await db.addNewUser(Number(event.user_id));
28
+ await db.addNewUser(String(event.user_id ?? "").trim());
27
29
  await event.reply(
28
30
  [
29
- ctx.segment.at(String(event.user_id)),
31
+ ctx.segment.at(String(event.user_id ?? "").trim()),
30
32
  ctx.segment.text(
31
33
  `你还没有创建${pickJj()}看不到rank喵, 咱帮你创建了喵, 目前长度是10cm喵`,
32
34
  ),
@@ -40,6 +42,7 @@ export async function handleRank(h: HandlerContext): Promise<void> {
40
42
  [...top5, ...last5].map(async (r) => ({
41
43
  name: await getStrangerNickname(event.bot, r.userId),
42
44
  userId: r.userId,
45
+ avatar: resolveAvatarUrl(event, r.userId),
43
46
  jjLength: r.jjLength,
44
47
  })),
45
48
  );
@@ -48,7 +51,7 @@ export async function handleRank(h: HandlerContext): Promise<void> {
48
51
 
49
52
  await event.reply(
50
53
  [
51
- ctx.segment.at(String(event.user_id)),
54
+ ctx.segment.at(String(event.user_id ?? "").trim()),
52
55
  ctx.segment.image(imagePath),
53
56
  ctx.segment.text(`你的排名为${myIndex + 1}喵`),
54
57
  ],
package/handlers/suo.ts CHANGED
@@ -4,12 +4,12 @@ import type { HandlerContext } from "./types";
4
4
 
5
5
  export async function handleSuo(h: HandlerContext): Promise<void> {
6
6
  const { ctx, db, cd, config, event } = h;
7
- const uid = String(event.user_id);
7
+ const uid = String(event.user_id ?? "").trim();
8
8
  const cdState = checkCooldown(cd.suo, uid, config.suoCd);
9
9
  if (!cdState.ok) {
10
10
  await event.reply(
11
11
  [
12
- ctx.segment.at(String(event.user_id)),
12
+ ctx.segment.at(uid),
13
13
  ctx.segment.text(
14
14
  `你已经嗦不动了喵, 请等待${roundTo(cdState.remaining, 3)}秒后再嗦喵`,
15
15
  ),
@@ -22,26 +22,24 @@ export async function handleSuo(h: HandlerContext): Promise<void> {
22
22
 
23
23
  const at = getAtUserId(event.message);
24
24
  if (at == null) {
25
- if (db.isUserInTable(Number(uid))) {
25
+ if (db.isUserInTable(uid)) {
26
26
  const delta = getRandomNum();
27
- await db.addJjLength(Number(uid), delta);
27
+ await db.addJjLength(uid, delta);
28
28
  await event.reply(
29
29
  [
30
- ctx.segment.at(String(event.user_id)),
30
+ ctx.segment.at(uid),
31
31
  ctx.segment.text(
32
- `你的${pickJj()}很满意喵, 嗦长了${delta}cm喵, 目前长度为${db.getJjLength(
33
- Number(uid),
34
- )}cm喵`,
32
+ `你的${pickJj()}很满意喵, 嗦长了${delta}cm喵, 目前长度为${db.getJjLength(uid)}cm喵`,
35
33
  ),
36
34
  ],
37
35
  false,
38
36
  );
39
37
  } else {
40
- await db.addNewUser(Number(uid));
38
+ await db.addNewUser(uid);
41
39
  clearCooldown(cd.suo, uid);
42
40
  await event.reply(
43
41
  [
44
- ctx.segment.at(String(event.user_id)),
42
+ ctx.segment.at(uid),
45
43
  ctx.segment.text(
46
44
  `你还没有创建${pickJj()}喵, 咱帮你创建了喵, 目前长度是10cm喵`,
47
45
  ),
@@ -57,7 +55,7 @@ export async function handleSuo(h: HandlerContext): Promise<void> {
57
55
  await db.addJjLength(at, delta);
58
56
  await event.reply(
59
57
  [
60
- ctx.segment.at(String(event.user_id)),
58
+ ctx.segment.at(uid),
61
59
  ctx.segment.text(
62
60
  `对方的${pickJj()}很满意喵, 嗦长了${delta}cm喵, 目前长度为${db.getJjLength(
63
61
  at,
@@ -71,7 +69,7 @@ export async function handleSuo(h: HandlerContext): Promise<void> {
71
69
  clearCooldown(cd.suo, uid);
72
70
  await event.reply(
73
71
  [
74
- ctx.segment.at(String(event.user_id)),
72
+ ctx.segment.at(uid),
75
73
  ctx.segment.text(
76
74
  `TA还没有创建${pickJj()}喵, 咱帮TA创建了喵, 目前长度是10cm喵`,
77
75
  ),
package/handlers/yinpa.ts CHANGED
@@ -2,14 +2,14 @@ import type { MiokuContext } from "mioku";
2
2
  import { checkCooldown, clearCooldown, markCooldown } from "../state";
3
3
  import {
4
4
  getAtUserId,
5
- getAvatarUrl,
6
5
  getSenderName,
6
+ resolveAvatarUrl,
7
7
  roundTo,
8
8
  } from "../utils";
9
9
  import type { HandlerContext } from "./types";
10
10
 
11
11
  interface PrepMember {
12
- user_id: number;
12
+ user_id: string;
13
13
  role: "owner" | "admin" | "member";
14
14
  card?: string;
15
15
  nickname?: string;
@@ -28,15 +28,14 @@ export async function handleYinpa(
28
28
  ): Promise<void> {
29
29
  const { ctx, db, cd, config, event } = h;
30
30
  const miokuCtx = ctx as MiokuContext;
31
- const uid = Number(event.user_id);
32
- const uidKey = String(uid);
31
+ const uid = String(event.user_id ?? "").trim();
33
32
 
34
- const cdState = checkCooldown(cd.fuck, uidKey, config.fuckCd);
33
+ const cdState = checkCooldown(cd.fuck, uid, config.fuckCd);
35
34
  // 主人不受 CD 限制
36
35
  if (!cdState.ok && !miokuCtx.isMaster?.(event)) {
37
36
  await event.reply(
38
37
  [
39
- ctx.segment.at(String(uid)),
38
+ ctx.segment.at(uid),
40
39
  ctx.segment.text(
41
40
  `你已经榨不出来任何东西了, 请先休息${roundTo(cdState.remaining, 3)}秒`,
42
41
  ),
@@ -45,7 +44,7 @@ export async function handleYinpa(
45
44
  );
46
45
  return;
47
46
  }
48
- markCooldown(cd.fuck, uidKey);
47
+ markCooldown(cd.fuck, uid);
49
48
 
50
49
  const reqUserCard = getSenderName(event);
51
50
  let prepList: PrepMember[];
@@ -54,19 +53,19 @@ export async function handleYinpa(
54
53
  if (!bot) throw new Error("bot not found");
55
54
  const members = await bot.getGroupMembers(String(event.group_id));
56
55
  prepList = members.map((m: import("mioku").MemberInfo) => ({
57
- user_id: Number(m.user_id),
56
+ user_id: String(m.user_id ?? "").trim(),
58
57
  role: (m.role as PrepMember["role"]) ?? "member",
59
58
  card: m.card,
60
59
  nickname: m.nickname,
61
60
  }));
62
61
  } catch (err) {
63
- clearCooldown(cd.fuck, uidKey);
62
+ clearCooldown(cd.fuck, uid);
64
63
  ctx.logger.error(`impact: 获取群成员列表失败: ${err}`);
65
64
  await event.reply([ctx.segment.text("喵喵喵? 拿不到群成员列表了!")], false);
66
65
  return;
67
66
  }
68
67
 
69
- let lucky: number | null;
68
+ let lucky: string | null;
70
69
  if (subject === "owner") {
71
70
  lucky = await pickOwner(prepList, uid, reqUserCard, ctx, event);
72
71
  } else if (subject === "admin") {
@@ -76,7 +75,7 @@ export async function handleYinpa(
76
75
  }
77
76
  if (lucky == null) {
78
77
  // pickXxx 内部已 finish 并清掉 CD
79
- clearCooldown(cd.fuck, uidKey);
78
+ clearCooldown(cd.fuck, uid);
80
79
  return;
81
80
  }
82
81
 
@@ -96,13 +95,14 @@ export async function handleYinpa(
96
95
  "群友";
97
96
  const todayTotal = db.getTodayEjaculationVolume(lucky);
98
97
  const seconds = 1 + Math.floor(Math.random() * 20);
98
+ const avatarUrl = resolveAvatarUrl(event, lucky);
99
99
 
100
100
  await event.reply(
101
101
  [
102
102
  ctx.segment.text(
103
103
  `好欸!${reqUserCard}(${uid})用时${seconds}秒 \n给 ${luckyCard}(${lucky}) 注入了${volume}毫升的脱氧核糖核酸, 当日总注入量为:${todayTotal}毫升\n`,
104
104
  ),
105
- ctx.segment.image(getAvatarUrl(lucky)),
105
+ ...(avatarUrl ? [ctx.segment.image(avatarUrl)] : []),
106
106
  ],
107
107
  false,
108
108
  );
@@ -110,11 +110,11 @@ export async function handleYinpa(
110
110
 
111
111
  async function pickOwner(
112
112
  prepList: PrepMember[],
113
- uid: number,
113
+ uid: string,
114
114
  reqUserCard: string,
115
115
  ctx: any,
116
116
  event: any,
117
- ): Promise<number | null> {
117
+ ): Promise<string | null> {
118
118
  // 找不到群主时回退到自己
119
119
  const owner = prepList.find((p) => p.role === "owner");
120
120
  const lucky = owner?.user_id ?? uid;
@@ -131,12 +131,14 @@ async function pickOwner(
131
131
 
132
132
  async function pickAdmin(
133
133
  prepList: PrepMember[],
134
- uid: number,
134
+ uid: string,
135
135
  reqUserCard: string,
136
136
  ctx: any,
137
137
  event: any,
138
- ): Promise<number | null> {
139
- let admins = prepList.filter((p) => p.role === "admin").map((p) => p.user_id);
138
+ ): Promise<string | null> {
139
+ let admins = prepList
140
+ .filter((p) => p.role === "admin")
141
+ .map((p) => p.user_id);
140
142
  if (admins.includes(uid)) {
141
143
  // 自己是管理的话移除自己
142
144
  admins = admins.filter((id) => id !== uid);
@@ -155,11 +157,11 @@ async function pickAdmin(
155
157
 
156
158
  async function pickMember(
157
159
  prepList: PrepMember[],
158
- uid: number,
160
+ uid: string,
159
161
  reqUserCard: string,
160
162
  ctx: any,
161
163
  event: any,
162
- ): Promise<number | null> {
164
+ ): Promise<string | null> {
163
165
  const at = getAtUserId(event.message);
164
166
  if (at != null) {
165
167
  // 有 @ 就直接指定目标,不播报抽取提示
package/image.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import type { ScreenshotService } from "mioku";
2
- import { escapeHtml, getAvatarUrl } from "./utils";
2
+ import { escapeHtml } from "./utils";
3
3
 
4
4
  export interface RankRow {
5
5
  name: string;
6
- userId: number;
6
+ userId: string;
7
+ avatar?: string;
7
8
  jjLength: number;
8
9
  }
9
10
 
@@ -67,9 +68,14 @@ export async function renderRankChart(
67
68
  const center = trackLeft + trackWidth / 2;
68
69
  const barLeft = isPositive ? center : center - barWidth;
69
70
  const labelText = `${row.jjLength.toFixed(2)} cm`;
71
+ const avatar = String(row.avatar ?? "").trim();
70
72
  return `
71
73
  <div class="row" style="top:${headerHeight + i * rowHeight}px;">
72
- <img class="avatar" src="${escapeHtml(getAvatarUrl(row.userId))}" />
74
+ ${
75
+ avatar
76
+ ? `<img class="avatar" src="${escapeHtml(avatar)}" />`
77
+ : ""
78
+ }
73
79
  <div class="name">${escapeHtml(truncate(row.name, 9))}</div>
74
80
  <div class="track" style="left:${trackLeft}px; width:${trackWidth}px;">
75
81
  <div class="axis"></div>
package/index.ts CHANGED
@@ -89,9 +89,9 @@ const impactPlugin = definePlugin({
89
89
  await handleOpenModule(h, extra as boolean);
90
90
  return;
91
91
  }
92
- if (!db.isGroupAllowed(Number(event.group_id))) {
92
+ if (!db.isGroupAllowed(String(event.group_id ?? "").trim())) {
93
93
  await event.reply(
94
- [ctx.segment.at(String(event.user_id)), ctx.segment.text(NOT_ALLOW_TEXT)],
94
+ [ctx.segment.at(String(event.user_id ?? "").trim()), ctx.segment.text(NOT_ALLOW_TEXT)],
95
95
  false,
96
96
  );
97
97
  return;
@@ -125,6 +125,7 @@ const impactPlugin = definePlugin({
125
125
  }
126
126
  };
127
127
 
128
+ // 裸触发设计:开启淫趴 / .开启淫趴 都可用
128
129
  const cmd = (command: CommandDefinition) =>
129
130
  ctx.command({ ...command, prefixes: false });
130
131
 
@@ -134,7 +135,7 @@ const impactPlugin = definePlugin({
134
135
  match: /^(?:开始银趴|关闭银趴|开启淫趴|禁止淫趴|开启银趴|禁止银趴)\s*$/,
135
136
  permission: "admin",
136
137
  description: "在本群开启或关闭淫趴功能",
137
- usage: "开启淫趴 / 关闭银趴",
138
+ usage: ".开启淫趴 / .关闭银趴",
138
139
  handler: ({ event, match }) =>
139
140
  run("open_module")(event, match![0].startsWith("开启") || match![0].startsWith("开始")),
140
141
  });
@@ -143,7 +144,7 @@ const impactPlugin = definePlugin({
143
144
  name: "日群友",
144
145
  match: /^(日群友|透群友|日群主|透群主|日管理|透管理)(?:\s|$|@)/,
145
146
  description: "随机或定向选取一位幸运儿进行注入,按命令决定身份",
146
- usage: "日群友 / 透群友 @某人 / 透群主 / 透管理",
147
+ usage: ".日群友 / .透群友 @某人 / .透群主 / .透管理",
147
148
  handler: ({ event, match }) => {
148
149
  const word = match![1];
149
150
  const subject = word.includes("群主") ? "owner" : word.includes("管理") ? "admin" : "member";
@@ -155,7 +156,7 @@ const impactPlugin = definePlugin({
155
156
  name: "pk",
156
157
  match: /^(?:pk|PK|对决)(?:\s|@|$)/,
157
158
  description: "和 @ 的群友进行牛子对决,胜者抢走败者的随机长度",
158
- usage: "pk @某人",
159
+ usage: ".pk @某人",
159
160
  handler: ({ event }) => run("pk")(event),
160
161
  });
161
162
  cmd({
@@ -170,7 +171,7 @@ const impactPlugin = definePlugin({
170
171
  name: "嗦牛子",
171
172
  match: /^嗦牛子(?:\s|@|$)/,
172
173
  description: "嗦自己或被 @ 的群友的牛子,增加随机长度",
173
- usage: "嗦牛子 / 嗦牛子 @某人",
174
+ usage: ".嗦牛子 / .嗦牛子 @某人",
174
175
  handler: ({ event }) => run("suo")(event),
175
176
  });
176
177
  cmd({
@@ -178,7 +179,7 @@ const impactPlugin = definePlugin({
178
179
  name: "查询",
179
180
  match: /^查询(?:\s|@|$)/,
180
181
  description: "查询自己或 @ 的群友的牛子长度",
181
- usage: "查询 / 查询 @某人",
182
+ usage: ".查询 / .查询 @某人",
182
183
  handler: ({ event }) => run("queryjj")(event),
183
184
  });
184
185
  cmd({
@@ -186,7 +187,7 @@ const impactPlugin = definePlugin({
186
187
  name: "注入查询",
187
188
  match: /^(?:注入查询|摄入查询|射入查询)(.*)$/,
188
189
  description: "查询当日或全部历史被注入量,加 历史/全部 看走势图",
189
- usage: "注入查询 / 注入查询 历史 / 注入查询 @某人 全部",
190
+ usage: ".注入查询 / .注入查询 历史 / .注入查询 @某人 全部",
190
191
  handler: ({ event, match }) =>
191
192
  run("query_injection")(event, /历史|全部/.test(match![1] || "") ? "history" : "today"),
192
193
  });
@@ -195,7 +196,7 @@ const impactPlugin = definePlugin({
195
196
  name: "jj排行榜",
196
197
  match: /^(?:jj|JJ)(?:排行榜|排名|榜单|rank|RANK)(?:\s|$)/,
197
198
  description: "查看牛子长度前五与倒数五名的图表",
198
- usage: "jj排行榜 / jjrank",
199
+ usage: ".jj排行榜 / .jjrank",
199
200
  handler: ({ event }) => run("rank")(event),
200
201
  });
201
202
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-impact",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "让群友们眼前一黑的淫趴插件",
5
5
  "main": "index.ts",
6
6
  "type": "module",
package/types.ts CHANGED
@@ -5,7 +5,7 @@ export interface EjaculationRecord {
5
5
  }
6
6
 
7
7
  export interface RankEntry {
8
- userId: number;
8
+ userId: string;
9
9
  jjLength: number;
10
10
  }
11
11
 
package/utils.ts CHANGED
@@ -35,17 +35,18 @@ export function roundTo(value: number, digits: number): number {
35
35
  }
36
36
 
37
37
  /**
38
- * 提取消息里第一个 @ 的 QQ 号,忽略 @全体。
39
- * 找不到返回 undefined。
38
+ * 提取消息里第一个 @ 的目标 id,忽略 @全体。
39
+ * 兼容 qq-official 的 target 段。找不到返回 undefined。
40
40
  */
41
- export function getAtUserId(message: any): number | undefined {
41
+ export function getAtUserId(message: any): string | undefined {
42
42
  if (!Array.isArray(message)) return undefined;
43
43
  for (const seg of message) {
44
44
  if (seg?.type !== "at") continue;
45
- const raw = seg?.qq ?? seg?.data?.qq;
46
- if (raw == null || raw === "all") continue;
47
- const qq = Number(raw);
48
- if (Number.isFinite(qq)) return qq;
45
+ const raw = seg?.data?.qq ?? seg?.qq ?? seg?.data?.target ?? seg?.target;
46
+ if (raw == null) continue;
47
+ const id = String(raw).trim();
48
+ if (!id || id === "all") continue;
49
+ return id;
49
50
  }
50
51
  return undefined;
51
52
  }
@@ -64,7 +65,7 @@ export function getSenderName(event: any): string {
64
65
 
65
66
  export async function getStrangerNickname(
66
67
  bot: import("mioku").Bot | undefined,
67
- userId: number,
68
+ userId: string,
68
69
  ): Promise<string> {
69
70
  try {
70
71
  const info = bot ? await bot.getFriendInfo(userId) : undefined;
@@ -76,8 +77,30 @@ export async function getStrangerNickname(
76
77
  return String(userId);
77
78
  }
78
79
 
79
- export function getAvatarUrl(userId: number): string {
80
- return `https://q1.qlogo.cn/g?b=qq&nk=${userId}&s=640`;
80
+ const QQ_AVATAR_ADAPTERS = new Set(["onebotv11", "icqq"]);
81
+ const QQ_ID_RE = /^\d{4,12}$/;
82
+
83
+ /**
84
+ * 生成头像地址。仅在 QQ 系适配器且 id 是 QQ 号时才回退到 qlogo,
85
+ * 其他平台(如 qq-official 的 openid)没有通用头像接口,返回空串由调用方降级。
86
+ */
87
+ export function getAvatarUrl(
88
+ userId: string,
89
+ options?: { avatar?: unknown; adapter?: unknown },
90
+ ): string {
91
+ const direct = String(options?.avatar ?? "").trim();
92
+ if (direct) return direct;
93
+ const id = String(userId ?? "").trim();
94
+ const adapter = String(options?.adapter ?? "").trim().toLowerCase();
95
+ if (adapter && !QQ_AVATAR_ADAPTERS.has(adapter)) return "";
96
+ return QQ_ID_RE.test(id) ? `https://q1.qlogo.cn/g?b=qq&nk=${id}&s=640` : "";
97
+ }
98
+
99
+ export function resolveAvatarUrl(event: any, userId: string): string {
100
+ const id = String(userId ?? "").trim();
101
+ const isSelf = id !== "" && id === String(event?.user_id ?? "").trim();
102
+ const avatar = isSelf ? event?.sender?.avatar ?? event?.avatar : undefined;
103
+ return getAvatarUrl(id, { avatar, adapter: event?.bot?.adapter });
81
104
  }
82
105
 
83
106
  export function escapeHtml(value: string): string {