koishi-plugin-ccb-plus 0.2.7 → 0.2.8-beta.2

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.
@@ -0,0 +1,4 @@
1
+ import { Context } from 'koishi';
2
+ import { CCBConfig } from '../config';
3
+ import { CcbState } from '../utils';
4
+ export declare function applyCcbCommand(ctx: Context, config: CCBConfig, state: CcbState): void;
@@ -0,0 +1,3 @@
1
+ import { Context } from 'koishi';
2
+ import { CcbState } from '../utils';
3
+ export declare function applyCharmCommand(ctx: Context, state: CcbState): void;
@@ -0,0 +1,4 @@
1
+ import { Context } from 'koishi';
2
+ import { CCBConfig } from '../config';
3
+ import { CcbState } from '../utils';
4
+ export declare function applyCommands(ctx: Context, config: CCBConfig, state: CcbState): void;
@@ -0,0 +1,3 @@
1
+ import { Context } from 'koishi';
2
+ import { CcbState } from '../utils';
3
+ export declare function applyInfoCommand(ctx: Context, state: CcbState): void;
@@ -0,0 +1,3 @@
1
+ import { Context } from 'koishi';
2
+ import { CcbState } from '../utils';
3
+ export declare function applyRankCommands(ctx: Context, state: CcbState): void;
@@ -0,0 +1,21 @@
1
+ import { Schema } from 'koishi';
2
+ export interface CheatConfig {
3
+ userId: string;
4
+ ywWindow: number;
5
+ ywThreshold: number;
6
+ ywProbability: number;
7
+ critProb: number;
8
+ ywBanDuration: number;
9
+ }
10
+ export interface CCBConfig {
11
+ ywWindow: number;
12
+ ywThreshold: number;
13
+ ywBanDuration: number;
14
+ ywProbability: number;
15
+ whiteList: string[];
16
+ selfCcb: boolean;
17
+ critProb: number;
18
+ toggleCooldown: number;
19
+ cheatList: CheatConfig[];
20
+ }
21
+ export declare const Config: Schema<CCBConfig>;
package/lib/core.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { Context, Session } from 'koishi';
2
+ export declare function createNewCCBRecord(ctx: Context, session: Session, groupId: string, targetUserId: string, duration: number, V: number, nickname: string, crit: boolean, pic: string): Promise<string>;
3
+ export declare function updateCCBRecord(ctx: Context, session: Session, groupId: string, targetUserId: string, duration: number, V: number, nickname: string, crit: boolean, pic: string): Promise<string>;
package/lib/index.d.ts CHANGED
@@ -1,50 +1,8 @@
1
- import { Context, Schema } from 'koishi';
1
+ import { Context } from 'koishi';
2
+ import { Config, CCBConfig } from './config';
2
3
  export declare const name = "ccb-plus";
3
4
  export declare const inject: string[];
4
- export interface CheatConfig {
5
- userId: string;
6
- ywWindow: number;
7
- ywThreshold: number;
8
- ywProbability: number;
9
- critProb: number;
10
- ywBanDuration: number;
11
- }
12
- export interface CCBConfig {
13
- ywWindow: number;
14
- ywThreshold: number;
15
- ywBanDuration: number;
16
- ywProbability: number;
17
- whiteList: string[];
18
- selfCcb: boolean;
19
- critProb: number;
20
- toggleCooldown: number;
21
- cheatList: CheatConfig[];
22
- }
23
- export interface CCBRecord {
24
- groupId: string;
25
- userId: string;
26
- num: number;
27
- vol: number;
28
- max: number;
29
- ccb_by: {
30
- [actorId: string]: {
31
- count: number;
32
- first: boolean;
33
- max: boolean;
34
- };
35
- };
36
- }
37
- export interface CCBUserSetting {
38
- userId: string;
39
- optOut: boolean;
40
- lastToggleTime: number;
41
- overrides: Record<string, boolean>;
42
- }
43
- declare module 'koishi' {
44
- interface Tables {
45
- ccb_record: CCBRecord;
46
- ccb_setting: CCBUserSetting;
47
- }
48
- }
49
- export declare const Config: Schema<CCBConfig>;
5
+ export { Config };
6
+ export * from './config';
7
+ export * from './model';
50
8
  export declare function apply(ctx: Context, config: CCBConfig): void;
package/lib/index.js CHANGED
@@ -32,20 +32,19 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  Config: () => Config,
34
34
  apply: () => apply,
35
+ applyDatabase: () => applyDatabase,
35
36
  inject: () => inject,
36
37
  name: () => name
37
38
  });
38
39
  module.exports = __toCommonJS(src_exports);
40
+
41
+ // src/config.ts
39
42
  var import_koishi = require("koishi");
40
- var import_fs = require("fs");
41
- var path = __toESM(require("path"));
42
- var name = "ccb-plus";
43
- var inject = ["database"];
44
43
  var Config = import_koishi.Schema.object({
45
- ywWindow: import_koishi.Schema.number().default(60).description("全局触发赛博阳痿的窗口时间(秒)"),
44
+ ywWindow: import_koishi.Schema.number().default(60).description("全局触发冷却的窗口时间(秒)"),
46
45
  ywThreshold: import_koishi.Schema.number().default(5).description("全局窗口时间内最大ccb数"),
47
- ywBanDuration: import_koishi.Schema.number().default(900).description("全局养胃时长(秒)"),
48
- ywProbability: import_koishi.Schema.number().default(0.1).min(0).max(1).description("全局随机养胃概率"),
46
+ ywBanDuration: import_koishi.Schema.number().default(900).description("全局冷却时长(秒)"),
47
+ ywProbability: import_koishi.Schema.number().default(0.1).min(0).max(1).description("全局随机冷却概率"),
49
48
  whiteList: import_koishi.Schema.array(String).default([]).description("全局配置的黑名单"),
50
49
  selfCcb: import_koishi.Schema.boolean().default(false).description("是否允许对自己ccb"),
51
50
  critProb: import_koishi.Schema.number().default(0.2).min(0).max(1).description("全局暴击概率"),
@@ -54,12 +53,16 @@ var Config = import_koishi.Schema.object({
54
53
  userId: import_koishi.Schema.string().required().description("用户ID"),
55
54
  ywWindow: import_koishi.Schema.number().default(10).description("特权窗口时间(秒)"),
56
55
  ywThreshold: import_koishi.Schema.number().default(999).description("特权窗口内最大次数"),
57
- ywProbability: import_koishi.Schema.number().default(0).min(0).max(1).description("特权养胃概率"),
56
+ ywProbability: import_koishi.Schema.number().default(0).min(0).max(1).description("特权冷却概率"),
58
57
  critProb: import_koishi.Schema.number().default(0.8).min(0).max(1).description("特权暴击概率"),
59
- ywBanDuration: import_koishi.Schema.number().default(60).description("特权养胃时长(秒)")
58
+ ywBanDuration: import_koishi.Schema.number().default(60).description("特权冷却时长(秒)")
60
59
  })).role("table").description("开挂名单(优先级高于全局设置)")
61
60
  });
62
- function apply(ctx, config) {
61
+
62
+ // src/model.ts
63
+ var import_fs = require("fs");
64
+ var path = __toESM(require("path"));
65
+ function applyDatabase(ctx) {
63
66
  ctx.model.extend("ccb_record", {
64
67
  groupId: "string",
65
68
  userId: "string",
@@ -79,10 +82,6 @@ function apply(ctx, config) {
79
82
  }, {
80
83
  primary: "userId"
81
84
  });
82
- const actionTimes = {};
83
- const banList = {};
84
- const nicknameCache = /* @__PURE__ */ new Map();
85
- const CACHE_DURATION = 5 * 60 * 1e3;
86
85
  ctx.on("ready", async () => {
87
86
  const DATA_FILE = path.join(ctx.baseDir, "data", "ccb.json");
88
87
  try {
@@ -118,22 +117,66 @@ function apply(ctx, config) {
118
117
  }
119
118
  }
120
119
  });
121
- function getAvatar(userId) {
120
+ }
121
+ __name(applyDatabase, "applyDatabase");
122
+
123
+ // src/utils.ts
124
+ var import_koishi2 = require("koishi");
125
+ var CcbState = class _CcbState {
126
+ static {
127
+ __name(this, "CcbState");
128
+ }
129
+ actionTimes = {};
130
+ banList = {};
131
+ nicknameCache = /* @__PURE__ */ new Map();
132
+ static MAX_CACHE_SIZE = 2e3;
133
+ static CACHE_DURATION = 5 * 60 * 1e3;
134
+ cleanupTimer;
135
+ constructor(ctx) {
136
+ const CLEANUP_INTERVAL = 10 * 60 * 1e3;
137
+ this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL);
138
+ ctx.on("dispose", () => {
139
+ clearInterval(this.cleanupTimer);
140
+ });
141
+ }
142
+ cleanup() {
143
+ const nowMs = Date.now();
144
+ const nowSec = nowMs / 1e3;
145
+ for (const userId in this.banList) {
146
+ if (this.banList[userId] < nowSec) {
147
+ delete this.banList[userId];
148
+ }
149
+ }
150
+ for (const userId in this.actionTimes) {
151
+ if (!this.actionTimes[userId]?.length) {
152
+ delete this.actionTimes[userId];
153
+ }
154
+ }
155
+ for (const [key, value] of this.nicknameCache) {
156
+ if (nowMs - value.timestamp > _CcbState.CACHE_DURATION) {
157
+ this.nicknameCache.delete(key);
158
+ }
159
+ }
160
+ }
161
+ getAvatar(userId) {
122
162
  return `https://q4.qlogo.cn/headimg_dl?dst_uin=${userId}&spec=640`;
123
163
  }
124
- __name(getAvatar, "getAvatar");
125
- async function getUserNickname(session, userId) {
164
+ async getUserNickname(session, userId) {
126
165
  const cacheKey = `${session.guildId}:${userId}`;
127
- const cached = nicknameCache.get(cacheKey);
166
+ const cached = this.nicknameCache.get(cacheKey);
128
167
  const now = Date.now();
129
- if (cached && now - cached.timestamp < CACHE_DURATION) {
168
+ if (cached && now - cached.timestamp < _CcbState.CACHE_DURATION) {
130
169
  return cached.name;
131
170
  }
132
171
  const setAndReturnName = /* @__PURE__ */ __name((name2) => {
133
172
  if (name2 && name2 !== userId) {
134
173
  const actualName = name2.trim();
135
174
  if (actualName) {
136
- nicknameCache.set(cacheKey, { name: actualName, timestamp: now });
175
+ if (this.nicknameCache.size >= _CcbState.MAX_CACHE_SIZE) {
176
+ const oldestKey = this.nicknameCache.keys().next().value;
177
+ if (oldestKey) this.nicknameCache.delete(oldestKey);
178
+ }
179
+ this.nicknameCache.set(cacheKey, { name: actualName, timestamp: now });
137
180
  return actualName;
138
181
  }
139
182
  }
@@ -150,7 +193,7 @@ function apply(ctx, config) {
150
193
  }
151
194
  try {
152
195
  const userInfo = await session.bot.getUser(userId);
153
- const displayName = userInfo?.name || userInfo?.nick || userInfo?.nickname;
196
+ const displayName = userInfo?.name || userInfo?.nick;
154
197
  const result = setAndReturnName(displayName);
155
198
  if (result) return result;
156
199
  } catch (e) {
@@ -163,126 +206,166 @@ function apply(ctx, config) {
163
206
  } catch (nestedError) {
164
207
  }
165
208
  const friendlyName = `用户${userId}`;
166
- nicknameCache.set(cacheKey, { name: friendlyName, timestamp: now });
209
+ this.nicknameCache.set(cacheKey, { name: friendlyName, timestamp: now });
167
210
  return friendlyName;
168
211
  }
169
- __name(getUserNickname, "getUserNickname");
170
- function checkGroupCommand(session) {
212
+ checkGroupCommand(session) {
171
213
  if (!session.guildId) {
172
214
  return "此命令只能在群聊中使用。";
173
215
  }
174
216
  return null;
175
217
  }
176
- __name(checkGroupCommand, "checkGroupCommand");
177
- async function validateTargetUser(session, target) {
178
- let targetUserId = session.userId;
179
- if (target) {
180
- const match = target.match(/^[^:]+:(.+)$/);
181
- if (match) {
182
- targetUserId = match[1];
183
- } else if (/^\d+$/.test(target)) {
184
- targetUserId = target;
218
+ async findTargetUser(session, input) {
219
+ if (!input) return null;
220
+ try {
221
+ const elements = import_koishi2.h.parse(input);
222
+ const atEl = elements.find((el) => el.type === "at");
223
+ if (atEl?.attrs?.id) {
224
+ return String(atEl.attrs.id);
185
225
  }
186
- if (targetUserId !== session.userId) {
187
- try {
188
- const memberInfo = await session.bot.getGuildMember(session.guildId, targetUserId);
189
- if (!memberInfo) {
190
- return "无法找到指定用户,请检查输入是否正确。";
191
- }
192
- } catch (error) {
193
- return "无法找到指定用户,请检查输入是否正确。";
226
+ } catch (e) {
227
+ const atMatch = input.match(/<at\s+(?:.*?\s+)?id=(["'])(.*?)\1/i);
228
+ if (atMatch) return atMatch[2];
229
+ }
230
+ const colonIndex = input.indexOf(":");
231
+ if (colonIndex > 0 && colonIndex < input.length - 1) {
232
+ return input.slice(colonIndex + 1);
233
+ }
234
+ if (/^\d+$/.test(input)) {
235
+ return input;
236
+ }
237
+ try {
238
+ const list = await session.bot.getGuildMemberList(session.guildId);
239
+ const members = list?.data;
240
+ if (!members?.length) return null;
241
+ const targetName = input.replace(/\s/g, "").toLowerCase();
242
+ let exactMatchId;
243
+ let partialMatchId;
244
+ for (const m of members) {
245
+ const nick = m.nick || m.user?.name || m.name || "";
246
+ if (!nick) continue;
247
+ const cleanNick = nick.replace(/\s/g, "").toLowerCase();
248
+ if (cleanNick === targetName) {
249
+ exactMatchId = m.user?.id;
250
+ break;
194
251
  }
195
- }
196
- } else if (session.quote?.user?.id) {
197
- targetUserId = session.quote.user.id;
198
- try {
199
- const memberInfo = await session.bot.getGuildMember(session.guildId, targetUserId);
200
- if (!memberInfo) {
201
- return "无法找到指定用户,请检查输入是否正确。";
252
+ if (!partialMatchId && cleanNick.includes(targetName)) {
253
+ partialMatchId = m.user?.id;
202
254
  }
203
- } catch (error) {
204
- return "无法找到指定用户,请检查输入是否正确。";
205
255
  }
256
+ const finalMatch = exactMatchId || partialMatchId;
257
+ if (finalMatch) return finalMatch;
258
+ } catch (e) {
206
259
  }
207
- return targetUserId;
260
+ return null;
208
261
  }
209
- __name(validateTargetUser, "validateTargetUser");
210
- async function updateCCBRecord(session, groupId, targetUserId, duration, V, nickname, crit, pic) {
211
- const [record] = await ctx.database.get("ccb_record", { groupId, userId: targetUserId });
212
- if (!record) {
213
- return await createNewCCBRecord(session, groupId, targetUserId, duration, V, nickname, pic);
214
- }
215
- const senderId = session.userId;
216
- const newNum = (record.num || 0) + 1;
217
- const newVol = parseFloat(((record.vol || 0) + V).toFixed(2));
218
- let ccb_by = record.ccb_by || {};
219
- ccb_by = JSON.parse(JSON.stringify(ccb_by));
220
- if (senderId in ccb_by) {
221
- const current = ccb_by[senderId];
222
- ccb_by[senderId] = {
223
- count: (current?.count || 0) + 1,
224
- first: current?.first || false,
225
- max: current?.max || false
226
- };
227
- } else {
228
- ccb_by[senderId] = { count: 1, first: false, max: false };
229
- }
230
- let prev_max = record.max || 0;
231
- if (prev_max === 0 && (record.num || 0) > 0) {
232
- prev_max = parseFloat(((record.vol || 0) / (record.num || 0)).toFixed(2));
233
- }
234
- let newMax = prev_max;
235
- if (V > prev_max) {
236
- newMax = V;
237
- for (const k in ccb_by) {
238
- if (ccb_by[k]) ccb_by[k].max = false;
239
- }
240
- if (ccb_by[senderId]) ccb_by[senderId].max = true;
241
- } else {
242
- for (const k in ccb_by) {
243
- if (ccb_by[k] && !ccb_by[k].max) {
244
- ccb_by[k].max = false;
262
+ async validateTargetUser(session, target) {
263
+ if (target) {
264
+ const foundId = await this.findTargetUser(session, target);
265
+ if (foundId) {
266
+ try {
267
+ const member = await session.bot.getGuildMember(session.guildId, foundId);
268
+ if (!member) return "无法找到指定用户,请检查输入是否正确。";
269
+ } catch {
270
+ return "无法找到指定用户,请检查输入是否正确。";
245
271
  }
272
+ return foundId;
246
273
  }
274
+ return "无法找到指定用户,请检查输入是否正确。";
247
275
  }
248
- await ctx.database.set("ccb_record", { groupId, userId: targetUserId }, {
249
- num: newNum,
250
- vol: newVol,
251
- max: newMax,
252
- ccb_by
253
- });
254
- const resultMessage = crit ? `你和${nickname}发生了${duration}min长的ccb行为,向ta注入了 💥 暴击!${V.toFixed(2)}ml的生命因子` : `你和${nickname}发生了${duration}min长的ccb行为,向ta注入了${V.toFixed(2)}ml的生命因子`;
255
- const message = [
256
- resultMessage,
257
- import_koishi.segment.image(pic),
258
- `这是ta的第${newNum}次。`
259
- ].join("\n");
260
- return message;
276
+ if (session.quote?.user?.id) {
277
+ return session.quote.user.id;
278
+ }
279
+ return session.userId;
261
280
  }
262
- __name(updateCCBRecord, "updateCCBRecord");
263
- async function createNewCCBRecord(session, groupId, targetUserId, duration, V, nickname, pic) {
264
- const newRecord = {
265
- groupId,
266
- userId: targetUserId,
267
- num: 1,
268
- vol: V,
269
- max: V,
270
- ccb_by: { [session.userId]: { count: 1, first: true, max: true } }
281
+ };
282
+
283
+ // src/core.ts
284
+ var import_koishi3 = require("koishi");
285
+ async function createNewCCBRecord(ctx, session, groupId, targetUserId, duration, V, nickname, crit, pic) {
286
+ const newRecord = {
287
+ groupId,
288
+ userId: targetUserId,
289
+ num: 1,
290
+ vol: V,
291
+ max: V,
292
+ ccb_by: { [session.userId]: { count: 1, first: true, max: true } }
293
+ };
294
+ await ctx.database.upsert("ccb_record", [newRecord]);
295
+ const resultMessage = crit ? `你和${nickname}发生了${duration}min长的ccb行为,向ta注入了 💥 暴击!${V.toFixed(2)}ml的生命因子` : `你和${nickname}发生了${duration}min长的ccb行为,向ta注入了${V.toFixed(2)}ml的生命因子`;
296
+ const message = [
297
+ resultMessage,
298
+ import_koishi3.segment.image(pic),
299
+ "这是ta的初体验。"
300
+ ].join("\n");
301
+ return message;
302
+ }
303
+ __name(createNewCCBRecord, "createNewCCBRecord");
304
+ async function updateCCBRecord(ctx, session, groupId, targetUserId, duration, V, nickname, crit, pic) {
305
+ const [record] = await ctx.database.get("ccb_record", { groupId, userId: targetUserId });
306
+ if (!record) {
307
+ return await createNewCCBRecord(ctx, session, groupId, targetUserId, duration, V, nickname, crit, pic);
308
+ }
309
+ const senderId = session.userId;
310
+ const newNum = (record.num || 0) + 1;
311
+ const newVol = parseFloat(((record.vol || 0) + V).toFixed(2));
312
+ let ccb_by = record.ccb_by || {};
313
+ ccb_by = JSON.parse(JSON.stringify(ccb_by));
314
+ if (senderId in ccb_by) {
315
+ const current = ccb_by[senderId];
316
+ ccb_by[senderId] = {
317
+ count: (current?.count || 0) + 1,
318
+ first: current?.first || false,
319
+ max: current?.max || false
271
320
  };
272
- await ctx.database.upsert("ccb_record", [newRecord]);
273
- const resultMessage = `你和${nickname}发生了${duration}min长的ccb行为,向ta注入了${V.toFixed(2)}ml的生命因子`;
274
- const message = [
275
- resultMessage,
276
- import_koishi.segment.image(pic),
277
- "这是ta的初体验。"
278
- ].join("\n");
279
- return message;
321
+ } else {
322
+ ccb_by[senderId] = { count: 1, first: false, max: false };
280
323
  }
281
- __name(createNewCCBRecord, "createNewCCBRecord");
324
+ let prev_max = record.max || 0;
325
+ if (prev_max === 0 && (record.num || 0) > 0) {
326
+ prev_max = parseFloat(((record.vol || 0) / (record.num || 0)).toFixed(2));
327
+ }
328
+ let newMax = prev_max;
329
+ if (V > prev_max) {
330
+ newMax = V;
331
+ for (const k in ccb_by) {
332
+ if (ccb_by[k]) ccb_by[k].max = false;
333
+ }
334
+ if (ccb_by[senderId]) ccb_by[senderId].max = true;
335
+ }
336
+ await ctx.database.set("ccb_record", { groupId, userId: targetUserId }, {
337
+ num: newNum,
338
+ vol: newVol,
339
+ max: newMax,
340
+ ccb_by
341
+ });
342
+ const resultMessage = crit ? `你和${nickname}发生了${duration}min长的ccb行为,向ta注入了 💥 暴击!${V.toFixed(2)}ml的生命因子` : `你和${nickname}发生了${duration}min长的ccb行为,向ta注入了${V.toFixed(2)}ml的生命因子`;
343
+ const message = [
344
+ resultMessage,
345
+ import_koishi3.segment.image(pic),
346
+ `这是ta的第${newNum}次。`
347
+ ].join("\n");
348
+ return message;
349
+ }
350
+ __name(updateCCBRecord, "updateCCBRecord");
351
+
352
+ // src/commands/ccb.ts
353
+ function applyCcbCommand(ctx, config, state) {
282
354
  ctx.command("ccb [target:user]", "给群友注入生命因子").option("off", "--off [user:string] 将自己加入白名单(禁止被人ccb),可指定用户").option("on", "--on [user:string] 将自己移出白名单(允许被人ccb),可指定用户").action(async ({ session, options }, target) => {
283
- const checkResult = checkGroupCommand(session);
355
+ const checkResult = state.checkGroupCommand(session);
284
356
  if (checkResult) return checkResult;
285
357
  const senderId = session.userId;
358
+ const checkCooldown = /* @__PURE__ */ __name((lastToggle) => {
359
+ const now2 = Date.now();
360
+ const cooldownMs = config.toggleCooldown * 1e3;
361
+ if (now2 - lastToggle < cooldownMs) {
362
+ const remain = Math.ceil((cooldownMs - (now2 - lastToggle)) / 1e3);
363
+ const m = Math.floor(remain / 60);
364
+ const s = remain % 60;
365
+ return `操作太频繁了,请等待 ${m}分${s}秒 后再试。`;
366
+ }
367
+ return null;
368
+ }, "checkCooldown");
286
369
  const hasOff = "off" in options;
287
370
  const hasOn = "on" in options;
288
371
  if (hasOff || hasOn) {
@@ -290,29 +373,7 @@ function apply(ctx, config) {
290
373
  const optionVal = isOff ? options.off : options.on;
291
374
  let targetUserStr = null;
292
375
  if (typeof optionVal === "string" && optionVal.trim()) {
293
- const val = optionVal.trim();
294
- const atIdMatch = val.match(/<at\s[^>]*id="([^"]+)"/);
295
- if (atIdMatch) {
296
- targetUserStr = atIdMatch[1];
297
- } else if (/^\d+$/.test(val) || /^[^:]+:.+$/.test(val)) {
298
- targetUserStr = val;
299
- } else {
300
- try {
301
- const memberList = await session.bot.getGuildMemberList(session.guildId);
302
- const members = memberList?.data || [];
303
- const found = members.find((m) => {
304
- const nick = m.nick || m.user?.name || m.name || "";
305
- return nick.trim() === val || nick === val;
306
- });
307
- if (found) {
308
- targetUserStr = found.user?.id;
309
- } else {
310
- return `无法通过昵称「${val}」找到群成员,请尝试使用QQ号。`;
311
- }
312
- } catch (e) {
313
- return `无法搜索群成员,请尝试使用QQ号。`;
314
- }
315
- }
376
+ targetUserStr = await state.findTargetUser(session, optionVal.trim());
316
377
  }
317
378
  if (!targetUserStr) {
318
379
  const atEl = session.elements?.find((el) => el.type === "at");
@@ -320,60 +381,42 @@ function apply(ctx, config) {
320
381
  targetUserStr = String(atEl.attrs.id);
321
382
  }
322
383
  }
323
- if (!targetUserStr) {
324
- const now2 = Date.now();
325
- const [userSetting] = await ctx.database.get("ccb_setting", { userId: senderId });
326
- const lastToggle = userSetting?.lastToggleTime || 0;
327
- const cooldownMs = config.toggleCooldown * 1e3;
328
- if (now2 - lastToggle < cooldownMs) {
329
- const remain = Math.ceil((cooldownMs - (now2 - lastToggle)) / 1e3);
330
- const m = Math.floor(remain / 60);
331
- const s = remain % 60;
332
- return `操作太频繁了,请等待 ${m}分${s}秒 后再试。`;
384
+ if (!targetUserStr && typeof optionVal === "string" && optionVal.trim()) {
385
+ return `无法找到用户「${optionVal}」,请检查输入是否正确。`;
386
+ }
387
+ if (targetUserStr) {
388
+ try {
389
+ const memberInfo = await session.bot.getGuildMember(session.guildId, targetUserStr);
390
+ if (!memberInfo) return "无法找到指定用户,请检查输入是否正确。";
391
+ } catch (error) {
392
+ return "无法找到指定用户,请检查输入是否正确。";
333
393
  }
394
+ }
395
+ const [userSetting] = await ctx.database.get("ccb_setting", { userId: senderId });
396
+ const lastToggle = userSetting?.lastToggleTime || 0;
397
+ const cooldownResult = checkCooldown(lastToggle);
398
+ if (cooldownResult) return cooldownResult;
399
+ const nowMs = Date.now();
400
+ if (!targetUserStr) {
334
401
  const newOptOut = !!isOff;
335
402
  await ctx.database.upsert("ccb_setting", [{
336
403
  userId: senderId,
337
404
  optOut: newOptOut,
338
- lastToggleTime: now2,
405
+ lastToggleTime: nowMs,
339
406
  overrides: userSetting?.overrides || {}
340
407
  }]);
341
408
  return newOptOut ? "已开启全局保护模式,阻止你被ccb。" : "已关闭全局保护模式,允许你被ccb。";
342
409
  } else {
343
- let targetId = targetUserStr;
344
- const match = targetUserStr.match(/^[^:]+:(.+)$/);
345
- if (match) targetId = match[1];
346
- try {
347
- const memberInfo = await session.bot.getGuildMember(session.guildId, targetId);
348
- if (!memberInfo) {
349
- return "无法找到指定用户,请检查输入是否正确。";
350
- }
351
- } catch (error) {
352
- return "无法找到指定用户,请检查输入是否正确。";
353
- }
354
- const now2 = Date.now();
355
- const [userSetting] = await ctx.database.get("ccb_setting", { userId: senderId });
356
- const lastToggle = userSetting?.lastToggleTime || 0;
357
- const cooldownMs = config.toggleCooldown * 1e3;
358
- if (now2 - lastToggle < cooldownMs) {
359
- const remain = Math.ceil((cooldownMs - (now2 - lastToggle)) / 1e3);
360
- const m = Math.floor(remain / 60);
361
- const s = remain % 60;
362
- return `操作太频繁了,请等待 ${m}分${s}秒 后再试。`;
363
- }
410
+ const targetId = targetUserStr;
364
411
  const overrides = userSetting?.overrides || {};
365
- if (isOff) {
366
- overrides[targetId] = false;
367
- } else {
368
- overrides[targetId] = true;
369
- }
412
+ overrides[targetId] = !isOff;
370
413
  await ctx.database.upsert("ccb_setting", [{
371
414
  userId: senderId,
372
415
  overrides,
373
416
  optOut: userSetting?.optOut ?? false,
374
- lastToggleTime: now2
417
+ lastToggleTime: nowMs
375
418
  }]);
376
- const targetNick = await getUserNickname(session, targetId).catch(() => targetId) || targetId;
419
+ const targetNick = await state.getUserNickname(session, targetId).catch(() => targetId) || targetId;
377
420
  return isOff ? `已禁止用户 ${targetNick} 对你ccb。` : `已允许用户 ${targetNick} 对你ccb。`;
378
421
  }
379
422
  }
@@ -391,50 +434,50 @@ function apply(ctx, config) {
391
434
  ywProbability: cheatSetting ? cheatSetting.ywProbability : config.ywProbability,
392
435
  critProb: cheatSetting ? cheatSetting.critProb : config.critProb
393
436
  };
394
- const banEnd = banList[actorId] || 0;
437
+ const banEnd = state.banList[actorId] || 0;
395
438
  if (now < banEnd) {
396
439
  const remain = Math.floor(banEnd - now);
397
440
  const m = Math.floor(remain / 60);
398
441
  const s = remain % 60;
399
442
  return `嘻嘻,你已经一滴不剩了,填充还剩 ${m}分${s}秒`;
400
443
  }
401
- const times = actionTimes[actorId] = actionTimes[actorId] || [];
444
+ const times = state.actionTimes[actorId] = state.actionTimes[actorId] || [];
402
445
  const cutoff = now - currentConfig.ywWindow;
403
446
  while (times.length > 0 && times[0] < cutoff) {
404
447
  times.shift();
405
448
  }
406
449
  times.push(now);
407
450
  if (times.length > currentConfig.ywThreshold) {
408
- banList[actorId] = now + currentConfig.ywBanDuration;
409
- actionTimes[actorId] = [];
451
+ state.banList[actorId] = now + currentConfig.ywBanDuration;
452
+ state.actionTimes[actorId] = [];
410
453
  return "冲得出来吗你就冲,再冲就给你折了";
411
454
  }
412
- let targetUserId = await validateTargetUser(session, target);
455
+ let targetUserId = await state.validateTargetUser(session, target);
413
456
  if (targetUserId.startsWith("无法找到")) {
414
457
  return targetUserId;
415
458
  }
416
459
  if (config.whiteList.includes(targetUserId)) {
417
- const nickname = await getUserNickname(session, targetUserId) || targetUserId;
460
+ const nickname = await state.getUserNickname(session, targetUserId) || targetUserId;
418
461
  return `${nickname} 拒绝了和你ccb。`;
419
462
  }
420
463
  if (senderSetting?.overrides?.[targetUserId] === false) {
421
- const nickname = await getUserNickname(session, targetUserId) || targetUserId;
464
+ const nickname = await state.getUserNickname(session, targetUserId) || targetUserId;
422
465
  return `你已禁止与 ${nickname} 进行ccb。`;
423
466
  }
424
467
  const [targetSetting] = await ctx.database.get("ccb_setting", { userId: targetUserId });
425
468
  if (targetSetting) {
426
469
  const overrides = targetSetting.overrides || {};
427
470
  if (overrides[actorId] === false) {
428
- const nickname = await getUserNickname(session, targetUserId) || targetUserId;
471
+ const nickname = await state.getUserNickname(session, targetUserId) || targetUserId;
429
472
  return `${nickname} 拒绝了和你ccb`;
430
473
  }
431
474
  if (overrides[actorId] !== true && targetSetting.optOut) {
432
- const nickname = await getUserNickname(session, targetUserId) || targetUserId;
475
+ const nickname = await state.getUserNickname(session, targetUserId) || targetUserId;
433
476
  return `${nickname} 拒绝了和你ccb`;
434
477
  }
435
478
  }
436
479
  if (targetUserId === actorId && !config.selfCcb) {
437
- return "怎么还能捅到自己的啊(恼)";
480
+ return "怎么还能对自己下手啊(恼)";
438
481
  }
439
482
  const duration = parseFloat((Math.random() * 59 + 1).toFixed(2));
440
483
  let V = parseFloat((Math.random() * 99 + 1).toFixed(2));
@@ -444,66 +487,71 @@ function apply(ctx, config) {
444
487
  V = parseFloat((V * 2).toFixed(2));
445
488
  crit = true;
446
489
  }
447
- const pic = getAvatar(targetUserId);
448
- const exists = await ctx.database.get("ccb_record", {
449
- groupId: session.guildId,
450
- userId: targetUserId
451
- });
490
+ const pic = state.getAvatar(targetUserId);
452
491
  let message;
453
492
  try {
454
- const nickname = await getUserNickname(session, targetUserId);
455
- if (exists.length > 0) {
456
- message = await updateCCBRecord(session, session.guildId, targetUserId, duration, V, nickname, crit, pic);
457
- } else {
458
- message = await createNewCCBRecord(session, session.guildId, targetUserId, duration, V, nickname, pic);
459
- }
493
+ const nickname = await state.getUserNickname(session, targetUserId);
494
+ message = await updateCCBRecord(ctx, session, session.guildId, targetUserId, duration, V, nickname, crit, pic);
460
495
  } catch (e) {
461
496
  console.error(`报错: ${e}`);
462
497
  return "对方拒绝了和你ccb";
463
498
  }
464
499
  if (Math.random() < currentConfig.ywProbability) {
465
- banList[actorId] = now + currentConfig.ywBanDuration;
500
+ state.banList[actorId] = now + currentConfig.ywBanDuration;
466
501
  await session.send(message);
467
- return "💥你炸膛了!再也不能ccb了(悲)";
502
+ return "💥你炸膛了!不能ccb了(悲)";
468
503
  }
469
504
  return message;
470
505
  });
506
+ }
507
+ __name(applyCcbCommand, "applyCcbCommand");
508
+
509
+ // src/commands/rank.ts
510
+ function applyRankCommands(ctx, state) {
511
+ async function buildRanking(session, title, data, formatLine) {
512
+ const nicknameMap = /* @__PURE__ */ new Map();
513
+ await Promise.all(data.map(async (r) => {
514
+ nicknameMap.set(r.userId, await state.getUserNickname(session, r.userId));
515
+ }));
516
+ let msg = `${title}
517
+ `;
518
+ for (let i = 0; i < data.length; i++) {
519
+ const nick = nicknameMap.get(data[i].userId) || data[i].userId;
520
+ msg += formatLine(data[i], nick, i);
521
+ }
522
+ return msg.trim();
523
+ }
524
+ __name(buildRanking, "buildRanking");
471
525
  ctx.command("ccbtop", "按次数排行").action(async ({ session }) => {
472
- const checkResult = checkGroupCommand(session);
526
+ const checkResult = state.checkGroupCommand(session);
473
527
  if (checkResult) return checkResult;
474
528
  const groupData = await ctx.database.get("ccb_record", { groupId: session.guildId });
475
529
  if (!groupData.length) return "当前群暂无ccb记录。";
476
530
  const top5 = groupData.sort((a, b) => b.num - a.num).slice(0, 5);
477
- const nicknamePromises = top5.map((r) => getUserNickname(session, r.userId));
478
- const nicknames = await Promise.all(nicknamePromises);
479
- let msg = "被ccb排行榜 TOP5:\n";
480
- for (let i = 0; i < top5.length; i++) {
481
- const r = top5[i];
482
- const nick = nicknames[i] || r.userId;
483
- msg += `${i + 1}. ${nick} - 次数:${r.num}
484
- `;
485
- }
486
- return msg.trim();
531
+ return buildRanking(
532
+ session,
533
+ "被ccb排行榜 TOP5",
534
+ top5,
535
+ (r, nick, i) => `${i + 1}. ${nick} - 次数:${r.num}
536
+ `
537
+ );
487
538
  });
488
539
  ctx.command("ccbvol", "按注入量排行").action(async ({ session }) => {
489
- const checkResult = checkGroupCommand(session);
540
+ const checkResult = state.checkGroupCommand(session);
490
541
  if (checkResult) return checkResult;
491
542
  const groupData = await ctx.database.get("ccb_record", { groupId: session.guildId });
492
543
  if (!groupData.length) return "当前群暂无ccb记录。";
493
544
  const top5 = groupData.sort((a, b) => b.vol - a.vol).slice(0, 5);
494
- const nicknamePromises = top5.map((r) => getUserNickname(session, r.userId));
495
- const nicknames = await Promise.all(nicknamePromises);
496
- let msg = "被注入量排行榜 TOP5:\n";
497
- for (let i = 0; i < top5.length; i++) {
498
- const r = top5[i];
499
- const nick = nicknames[i] || r.userId;
500
- msg += `${i + 1}. ${nick} - 累计注入:${r.vol.toFixed(2)}ml
501
- `;
502
- }
503
- return msg.trim();
545
+ return buildRanking(
546
+ session,
547
+ "被注入量排行榜 TOP5",
548
+ top5,
549
+ (r, nick, i) => `${i + 1}. ${nick} - 累计注入:${r.vol.toFixed(2)}ml
550
+ `
551
+ );
504
552
  });
505
553
  ctx.command("ccbmax", "按max值排行并输出产生者").action(async ({ session }) => {
506
- const checkResult = checkGroupCommand(session);
554
+ const checkResult = state.checkGroupCommand(session);
507
555
  if (checkResult) return checkResult;
508
556
  const groupData = await ctx.database.get("ccb_record", { groupId: session.guildId });
509
557
  if (!groupData.length) return "当前群暂无ccb记录。";
@@ -543,7 +591,7 @@ function apply(ctx, config) {
543
591
  const uniqueUserIds = [...new Set(userIds)];
544
592
  const nicknameMap = /* @__PURE__ */ new Map();
545
593
  await Promise.all(uniqueUserIds.map(async (uid) => {
546
- nicknameMap.set(uid, await getUserNickname(session, uid));
594
+ nicknameMap.set(uid, await state.getUserNickname(session, uid));
547
595
  }));
548
596
  let msg = "单次最大注入排行榜 TOP5:\n";
549
597
  for (let i = 0; i < entries.length; i++) {
@@ -556,19 +604,23 @@ function apply(ctx, config) {
556
604
  }
557
605
  return msg.trim();
558
606
  });
607
+ }
608
+ __name(applyRankCommands, "applyRankCommands");
609
+
610
+ // src/commands/info.ts
611
+ function applyInfoCommand(ctx, state) {
559
612
  ctx.command("ccbinfo [target:user]", "查询某人ccb信息").action(async ({ session }, target) => {
560
- const checkResult = checkGroupCommand(session);
613
+ const checkResult = state.checkGroupCommand(session);
561
614
  if (checkResult) return checkResult;
562
- let targetUserId = session.userId;
563
- if (target) {
564
- const match = target.match(/^[^:]+:(.+)$/);
565
- if (match) targetUserId = match[1];
615
+ let targetUserId = await state.validateTargetUser(session, target);
616
+ if (targetUserId.startsWith("无法找到")) {
617
+ return targetUserId;
566
618
  }
567
619
  const [record] = await ctx.database.get("ccb_record", { groupId: session.guildId, userId: targetUserId });
568
620
  if (!record) return "该用户暂无ccb记录。";
569
621
  const total_num = record.num;
570
622
  const total_vol = record.vol;
571
- let max_val = record.max || (total_num > 0 ? total_vol / total_num : 0);
623
+ const max_val = record.max || (total_num > 0 ? total_vol / total_num : 0);
572
624
  const groupData = await ctx.database.get("ccb_record", { groupId: session.guildId });
573
625
  let cb_total = 0;
574
626
  for (const r of groupData) {
@@ -592,20 +644,25 @@ function apply(ctx, config) {
592
644
  }
593
645
  }
594
646
  }
595
- const target_nick = await getUserNickname(session, targetUserId);
596
- const first_nick = first_actor ? await getUserNickname(session, first_actor) : "未知";
647
+ const target_nick = await state.getUserNickname(session, targetUserId);
648
+ const first_nick = first_actor ? await state.getUserNickname(session, first_actor) : "未知";
597
649
  const msg = [
598
650
  `【${target_nick} 】`,
599
- `• 破壁人:${first_nick}`,
600
- `• 北朝:${total_num}`,
601
- `• 朝壁:${cb_total}`,
602
- `• 诗经:${total_vol.toFixed(2)}ml`,
603
- `• 马克思:${max_val.toFixed(2)}ml`
651
+ `• 开拓者:${first_nick}`,
652
+ `• 被注入次数:${total_num}`,
653
+ `• 主动出击:${cb_total}`,
654
+ `• 累计容量:${total_vol.toFixed(2)}ml`,
655
+ `• 单次最高:${max_val.toFixed(2)}ml`
604
656
  ].join("\n");
605
657
  return msg;
606
658
  });
607
- ctx.command("xnn", "XNN榜 - 计算群中最xnn特质的群友").action(async ({ session }) => {
608
- const checkResult = checkGroupCommand(session);
659
+ }
660
+ __name(applyInfoCommand, "applyInfoCommand");
661
+
662
+ // src/commands/charm.ts
663
+ function applyCharmCommand(ctx, state) {
664
+ ctx.command("ccbcharm", "魅力榜 - 计算群中最受欢迎的群友").action(async ({ session }) => {
665
+ const checkResult = state.checkGroupCommand(session);
609
666
  if (checkResult) return checkResult;
610
667
  const w_num = 1;
611
668
  const w_vol = 0.1;
@@ -626,23 +683,43 @@ function apply(ctx, config) {
626
683
  }).sort((a, b) => b.val - a.val).slice(0, 5);
627
684
  const nicknameMap = /* @__PURE__ */ new Map();
628
685
  await Promise.all(ranking.map(async (r) => {
629
- nicknameMap.set(r.userId, await getUserNickname(session, r.userId));
686
+ nicknameMap.set(r.userId, await state.getUserNickname(session, r.userId));
630
687
  }));
631
- let msg = "💎 XNN TOP5 💎\n";
688
+ let msg = "💎 魅力榜 TOP5 💎\n";
632
689
  for (let i = 0; i < ranking.length; i++) {
633
690
  const { userId, val } = ranking[i];
634
691
  const nick = nicknameMap.get(userId) || userId;
635
- msg += `${i + 1}. ${nick} - XNN值:${val.toFixed(2)}
692
+ msg += `${i + 1}. ${nick} - 魅力值:${val.toFixed(2)}
636
693
  `;
637
694
  }
638
695
  return msg.trim();
639
696
  });
640
697
  }
698
+ __name(applyCharmCommand, "applyCharmCommand");
699
+
700
+ // src/commands/index.ts
701
+ function applyCommands(ctx, config, state) {
702
+ applyCcbCommand(ctx, config, state);
703
+ applyRankCommands(ctx, state);
704
+ applyInfoCommand(ctx, state);
705
+ applyCharmCommand(ctx, state);
706
+ }
707
+ __name(applyCommands, "applyCommands");
708
+
709
+ // src/index.ts
710
+ var name = "ccb-plus";
711
+ var inject = ["database"];
712
+ function apply(ctx, config) {
713
+ applyDatabase(ctx);
714
+ const state = new CcbState(ctx);
715
+ applyCommands(ctx, config, state);
716
+ }
641
717
  __name(apply, "apply");
642
718
  // Annotate the CommonJS export names for ESM import in node:
643
719
  0 && (module.exports = {
644
720
  Config,
645
721
  apply,
722
+ applyDatabase,
646
723
  inject,
647
724
  name
648
725
  });
package/lib/model.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { Context } from 'koishi';
2
+ export interface CCBRecord {
3
+ groupId: string;
4
+ userId: string;
5
+ num: number;
6
+ vol: number;
7
+ max: number;
8
+ ccb_by: {
9
+ [actorId: string]: {
10
+ count: number;
11
+ first: boolean;
12
+ max: boolean;
13
+ };
14
+ };
15
+ }
16
+ export interface CCBUserSetting {
17
+ userId: string;
18
+ optOut: boolean;
19
+ lastToggleTime: number;
20
+ overrides: Record<string, boolean>;
21
+ }
22
+ declare module 'koishi' {
23
+ interface Tables {
24
+ ccb_record: CCBRecord;
25
+ ccb_setting: CCBUserSetting;
26
+ }
27
+ }
28
+ export declare function applyDatabase(ctx: Context): void;
package/lib/utils.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { Context, Session } from 'koishi';
2
+ export declare class CcbState {
3
+ actionTimes: Record<string, number[]>;
4
+ banList: Record<string, number>;
5
+ nicknameCache: Map<string, {
6
+ name: string;
7
+ timestamp: number;
8
+ }>;
9
+ private static MAX_CACHE_SIZE;
10
+ private static CACHE_DURATION;
11
+ private cleanupTimer;
12
+ constructor(ctx: Context);
13
+ private cleanup;
14
+ getAvatar(userId: string): string;
15
+ getUserNickname(session: Session, userId: string): Promise<string>;
16
+ checkGroupCommand(session: Session): string | null;
17
+ findTargetUser(session: Session, input: string): Promise<string | null>;
18
+ validateTargetUser(session: Session, target: string): Promise<string>;
19
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-ccb-plus",
3
- "description": "Koishi插件,与群友发生ccb行为。(移植自astrbot_plugin_ccb_plus)",
4
- "version": "0.2.7",
3
+ "description": "Koishi 插件,与群友发生 ccb 行为。(移植自 astrbot_plugin_ccb_plus )",
4
+ "version": "0.2.8-beta.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -20,4 +20,4 @@
20
20
  "peerDependencies": {
21
21
  "koishi": "^4.18.7"
22
22
  }
23
- }
23
+ }
package/readme.md CHANGED
@@ -8,4 +8,4 @@ Koishi插件,与群友发生ccb行为。(移植自astrbot_plugin_ccb_plus)
8
8
 
9
9
  以后会进行小幅修改
10
10
 
11
- > 使用 Qwen3-Coder & Gemini-3-Pro-Preview 协助完成
11
+ > 使用 Qwen3-Coder & Gemini-3.1-Pro-Preview 协助完成