koishi-plugin-ggcevo 1.1.1 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -17,6 +17,8 @@ export interface Config {
17
17
  tencentDocsAdminWelfareFileId: string;
18
18
  tencentDocsAdminWelfareSheetId: string;
19
19
  banNotifyGroups: string[];
20
+ handleInactiveUnbindEnabled: boolean;
21
+ handleInactiveUnbindDays: number;
20
22
  }
21
23
  export declare const Config: Schema<Config>;
22
24
  export declare const inject: {
@@ -48,6 +50,7 @@ declare module 'koishi' {
48
50
  ggcevo_docs_token: GgcEvoDocsToken;
49
51
  ggcevo_ban_record: GgcEvoBanRecord;
50
52
  ggcevo_admin_welfare: GgcEvoAdminWelfare;
53
+ ggcevo_mining: GgcEvoMiningState;
51
54
  }
52
55
  }
53
56
  export interface GgcEvoPlayer {
@@ -58,6 +61,8 @@ export interface GgcEvoPlayer {
58
61
  profileId: number;
59
62
  createdAt: Date;
60
63
  isActive: boolean;
64
+ /** 最后使用时间(绑定时记录, 切换时更新切换前后两个句柄), 功能上线前的旧记录可能为 null */
65
+ lastUsedAt: Date;
61
66
  }
62
67
  export interface GgcEvoMapMonitorState {
63
68
  mapId: number;
@@ -102,6 +107,18 @@ export interface GgcEvoAdminWelfare {
102
107
  /** 最近一次同步时间 */
103
108
  update_time: Date;
104
109
  }
110
+ export interface GgcEvoMiningState {
111
+ /** 挖矿身份标识, 复用游戏句柄字符串 */
112
+ user_id: string;
113
+ /** 本轮挖矿开始时间(领取后重置为领取时刻, 立即进入下一轮) */
114
+ start_time: Date;
115
+ /** 累计挖矿总时长(分钟) */
116
+ total_minutes: number;
117
+ /** 累计挖矿收益金币总数 */
118
+ total_coins: number;
119
+ /** 最近一次更新/领取时间 */
120
+ update_time: Date;
121
+ }
105
122
  export interface Backpack {
106
123
  id: number;
107
124
  user_id: string;
package/lib/index.js CHANGED
@@ -40,7 +40,7 @@ var Config = import_koishi.Schema.intersect([
40
40
  mapMonitorRecommendMapName: import_koishi.Schema.string().description("当所有配置地图均离线时, 推荐玩家游玩的地图名称").default("")
41
41
  }).description("地图检测"),
42
42
  import_koishi.Schema.object({
43
- curfewEnabled: import_koishi.Schema.boolean().description("是否启用宵禁(启用后0点-6点禁止签到和抽奖, 17点-24点禁止抽奖)").default(false)
43
+ curfewEnabled: import_koishi.Schema.boolean().description("是否启用宵禁(启用后每晚17点-24点禁止签到、抽奖和挖矿)").default(false)
44
44
  }).description("宵禁设置"),
45
45
  import_koishi.Schema.object({
46
46
  tencentDocsEnabled: import_koishi.Schema.boolean().description("是否启用腾讯文档功能").default(false),
@@ -53,7 +53,11 @@ var Config = import_koishi.Schema.intersect([
53
53
  tencentDocsAdminWelfareFileId: import_koishi.Schema.string().description("管理员福利在线表格的文件ID(短ID或完整ID, A列QQ号/B列句柄)").default(""),
54
54
  tencentDocsAdminWelfareSheetId: import_koishi.Schema.string().description("管理员福利工作表ID(表格URL中tab参数)").default(""),
55
55
  banNotifyGroups: import_koishi.Schema.array(import_koishi.Schema.string()).description("封禁提醒广播的群组ID列表(同步检测到新增封禁记录时在此提醒)").default([])
56
- }).description("腾讯文档")
56
+ }).description("腾讯文档"),
57
+ import_koishi.Schema.object({
58
+ handleInactiveUnbindEnabled: import_koishi.Schema.boolean().description("是否启用句柄长时间未使用自动解绑").default(false),
59
+ handleInactiveUnbindDays: import_koishi.Schema.number().description("句柄超过该天数未使用将自动解绑(仅对非当前使用的句柄生效)").default(90).min(1)
60
+ }).description("句柄自动解绑")
57
61
  ]);
58
62
  var inject = {
59
63
  required: ["database"]
@@ -104,7 +108,8 @@ function apply(ctx, config) {
104
108
  realmId: "unsigned",
105
109
  profileId: "unsigned",
106
110
  createdAt: "timestamp",
107
- isActive: "boolean"
111
+ isActive: "boolean",
112
+ lastUsedAt: { type: "timestamp", nullable: true }
108
113
  }, {
109
114
  autoInc: true,
110
115
  primary: "id"
@@ -233,6 +238,15 @@ function apply(ctx, config) {
233
238
  primary: "id",
234
239
  autoInc: true
235
240
  });
241
+ ctx.model.extend("ggcevo_mining", {
242
+ user_id: "string",
243
+ start_time: "timestamp",
244
+ total_minutes: "unsigned",
245
+ total_coins: "unsigned",
246
+ update_time: "timestamp"
247
+ }, {
248
+ primary: "user_id"
249
+ });
236
250
  if (config.mapMonitorEnabled && config.mapMonitorGroups.length > 0 && config.mapMonitorMapIds.length > 0) {
237
251
  ctx.setInterval(async () => {
238
252
  try {
@@ -306,16 +320,13 @@ function apply(ctx, config) {
306
320
  const checkCurfew = /* @__PURE__ */ __name((type) => {
307
321
  if (!config.curfewEnabled) return null;
308
322
  const hour = (/* @__PURE__ */ new Date()).getHours();
309
- if (type === "signin") {
310
- if (hour < 6) {
311
- return `🌙 宵禁时段(0:00-6:00),暂不能签到。`;
312
- }
313
- } else {
314
- if (hour < 6) {
315
- return `🌙 宵禁时段(0:00-6:00),暂不能抽奖。`;
316
- }
317
- if (hour >= 17) {
323
+ if (hour >= 17) {
324
+ if (type === "signin") {
325
+ return `🌙 宵禁时段(17:00-24:00),暂不能签到。`;
326
+ } else if (type === "lottery") {
318
327
  return `🌙 宵禁时段(17:00-24:00),暂不能抽奖。`;
328
+ } else {
329
+ return `🌙 宵禁时段(17:00-24:00),暂不能挖矿。`;
319
330
  }
320
331
  }
321
332
  return null;
@@ -359,7 +370,8 @@ function apply(ctx, config) {
359
370
  realmId,
360
371
  profileId,
361
372
  isActive: isFirstHandle,
362
- createdAt: /* @__PURE__ */ new Date()
373
+ createdAt: /* @__PURE__ */ new Date(),
374
+ lastUsedAt: /* @__PURE__ */ new Date()
363
375
  });
364
376
  return `<quote id="${session.messageId}"/>✅ 您已成功绑定游戏句柄${isFirstHandle ? "并设为当前使用" : ""}。`;
365
377
  });
@@ -423,9 +435,15 @@ ${message}
423
435
  return `<quote id="${session.messageId}"/>❌ 序号无效,请输入1-${handles.length}之间的数字。`;
424
436
  }
425
437
  const selectedHandle = handles[index - 1];
438
+ const prevActiveHandle = handles.find((h2) => h2.isActive && h2.id !== selectedHandle.id);
426
439
  await Promise.all(handles.map(
427
440
  (handle) => ctx.database.set("sc2arcade_player", { id: handle.id }, { isActive: handle.id === selectedHandle.id })
428
441
  ));
442
+ const now = /* @__PURE__ */ new Date();
443
+ const touchedIds = prevActiveHandle ? [selectedHandle.id, prevActiveHandle.id] : [selectedHandle.id];
444
+ await Promise.all(touchedIds.map(
445
+ (id) => ctx.database.set("sc2arcade_player", { id }, { lastUsedAt: now })
446
+ ));
429
447
  return `<quote id="${session.messageId}"/>✅ 已切换到句柄:${formatHandle(selectedHandle)}`;
430
448
  } catch (error) {
431
449
  console.error("切换句柄时发生错误:", error);
@@ -533,6 +551,32 @@ ${message}`;
533
551
  return "⚠️ 服务器繁忙, 请稍后尝试。";
534
552
  }
535
553
  });
554
+ if (config.handleInactiveUnbindEnabled) {
555
+ const checkInactiveHandles = /* @__PURE__ */ __name(async () => {
556
+ try {
557
+ const handles = await ctx.database.get("sc2arcade_player", {});
558
+ const now = /* @__PURE__ */ new Date();
559
+ const legacyIds = handles.filter((h2) => !h2.lastUsedAt).map((h2) => h2.id);
560
+ if (legacyIds.length > 0) {
561
+ await ctx.database.set("sc2arcade_player", { id: { $in: legacyIds } }, { lastUsedAt: now });
562
+ }
563
+ const handleCountByUser = /* @__PURE__ */ new Map();
564
+ for (const h2 of handles) {
565
+ handleCountByUser.set(h2.userId, (handleCountByUser.get(h2.userId) || 0) + 1);
566
+ }
567
+ const thresholdMs = config.handleInactiveUnbindDays * 24 * 60 * 60 * 1e3;
568
+ const expiredIds = handles.filter((h2) => h2.lastUsedAt && !h2.isActive && (handleCountByUser.get(h2.userId) || 0) > 1 && now.getTime() - new Date(h2.lastUsedAt).getTime() > thresholdMs).map((h2) => h2.id);
569
+ if (expiredIds.length > 0) {
570
+ await ctx.database.remove("sc2arcade_player", { id: { $in: expiredIds } });
571
+ ctx.logger("ggcevo").info("已自动解绑 %d 个长时间未使用的句柄", expiredIds.length);
572
+ }
573
+ } catch (e) {
574
+ ctx.logger("ggcevo").warn("句柄长时间未使用自动解绑任务执行失败: %o", e);
575
+ }
576
+ }, "checkInactiveHandles");
577
+ ctx.setTimeout(checkInactiveHandles, 5e3);
578
+ ctx.setInterval(checkInactiveHandles, 24 * 60 * 60 * 1e3);
579
+ }
536
580
  ctx.command("sc2arcade/地图检测", "查询已配置的地图详细信息").action(async (argv) => {
537
581
  if (!config.mapMonitorEnabled || config.mapMonitorMapIds.length === 0) {
538
582
  return `<quote id="${argv.session.messageId}"/>⚠️ 地图检测功能未开启或未配置地图ID。`;
@@ -709,6 +753,66 @@ ${message}`;
709
753
  }
710
754
  return `<quote id="${session.messageId}"/>${message}`;
711
755
  });
756
+ const MINING_HALF_HOUR_MIN = 30;
757
+ const MINING_COINS_PER_HALF_HOUR = 4;
758
+ const MINING_MAX_MINUTES = 24 * 60;
759
+ ctx.command("ggcevo/挖矿", "领取挂机挖矿收益(每半小时4金币, 上限24小时)").action(async (argv) => {
760
+ const session = argv.session;
761
+ const curfewMsg = checkCurfew("mining");
762
+ if (curfewMsg) return `<quote id="${session.messageId}"/>${curfewMsg}`;
763
+ const handle = await getHandle(session);
764
+ if (!handle) {
765
+ return `<quote id="${session.messageId}"/>🔒 需要先绑定游戏句柄。
766
+ 💡 使用 \`绑定句柄\` 命令进行绑定。`;
767
+ }
768
+ const now = /* @__PURE__ */ new Date();
769
+ const [mining] = await ctx.database.get("ggcevo_mining", { user_id: handle });
770
+ if (!mining) {
771
+ await ctx.database.create("ggcevo_mining", {
772
+ user_id: handle,
773
+ start_time: now,
774
+ total_minutes: 0,
775
+ total_coins: 0,
776
+ update_time: now
777
+ });
778
+ return `<quote id="${session.messageId}"/>⛏️ 开始挂机挖矿!每半小时收益 4 金币,单次存储上限 24 小时,使用 挖矿 命令领取。`;
779
+ }
780
+ const elapsedMinutes = Math.floor((now.getTime() - new Date(mining.start_time).getTime()) / 6e4);
781
+ const cappedMinutes = Math.max(0, Math.min(elapsedMinutes, MINING_MAX_MINUTES));
782
+ const earned = Math.floor(cappedMinutes / MINING_HALF_HOUR_MIN) * MINING_COINS_PER_HALF_HOUR;
783
+ if (earned <= 0) {
784
+ const restMinutes = MINING_HALF_HOUR_MIN - elapsedMinutes;
785
+ return `<quote id="${session.messageId}"/>⛏️ 挖矿进行中,已累计挖矿 ${cappedMinutes} 分钟,还需 ${restMinutes} 分钟即可获得 ${MINING_COINS_PER_HALF_HOUR} 金币。`;
786
+ }
787
+ const [existingGold] = await ctx.database.get("ggcevo_backpack", { user_id: handle, item_id: 1 });
788
+ const newGoldCount = (existingGold?.count || 0) + earned;
789
+ if (existingGold) {
790
+ await ctx.database.upsert("ggcevo_backpack", [{
791
+ user_id: handle,
792
+ item_id: 1,
793
+ count: newGoldCount
794
+ }]);
795
+ } else {
796
+ await ctx.database.create("ggcevo_backpack", {
797
+ user_id: handle,
798
+ item_id: 1,
799
+ count: newGoldCount
800
+ });
801
+ }
802
+ const newTotalMinutes = (mining.total_minutes || 0) + cappedMinutes;
803
+ const newTotalCoins = (mining.total_coins || 0) + earned;
804
+ await ctx.database.set("ggcevo_mining", { user_id: handle }, {
805
+ start_time: now,
806
+ total_minutes: newTotalMinutes,
807
+ total_coins: newTotalCoins,
808
+ update_time: now
809
+ });
810
+ return `<quote id="${session.messageId}"/>⛏️ 挖矿结算!
811
+ 💰 获得 ${earned} 金币
812
+ ⏱️ 本轮挖矿 ${formatMiningDuration(cappedMinutes)}
813
+ 📊 累计挖矿 ${formatMiningDuration(newTotalMinutes)},累计收益 ${newTotalCoins} 金币
814
+ 🌱 已自动进入下一轮挖矿。`;
815
+ });
712
816
  ctx.command("ggcevo/兑换 <name:string>").action(async (argv, name2) => {
713
817
  const session = argv.session;
714
818
  const handle = await getHandle(session);
@@ -2189,17 +2293,10 @@ ${message}`;
2189
2293
  const notifyNewBanRecords = /* @__PURE__ */ __name(async (records) => {
2190
2294
  const groups = config.banNotifyGroups || [];
2191
2295
  if (groups.length === 0 || records.length === 0) return;
2192
- const players = await ctx.database.get("sc2arcade_player", {});
2193
- const handleUserMap = /* @__PURE__ */ new Map();
2194
- for (const p of players) {
2195
- const key = normalizeHandle(`${p.regionId}-S2-${p.realmId}-${p.profileId}`);
2196
- if (p.isActive || !handleUserMap.has(key)) handleUserMap.set(key, p.userId);
2197
- }
2198
2296
  for (const r of records) {
2199
- const userId = handleUserMap.get(normalizeHandle(r.handle));
2200
2297
  const lines = [
2201
2298
  "🚫 封禁提醒",
2202
- userId ? `${(0, import_koishi.h)("at", { id: userId })} 你的句柄 ${r.handle} 有新的封禁记录` : `句柄 ${r.handle} 有新的封禁记录`,
2299
+ `句柄 ${r.handle} 有新的封禁记录`,
2203
2300
  `封禁等级: ${r.ban_level || "-"}`,
2204
2301
  `审核员: ${r.auditor || "-"}`,
2205
2302
  `审核时间: ${dateOnly(r.audit_time) || "-"}`,
@@ -2485,14 +2582,20 @@ ${message}`;
2485
2582
  `审核员: ${r.auditor || "-"}`,
2486
2583
  `审核时间: ${dateOnly(r.audit_time) || "-"}`,
2487
2584
  "",
2488
- '💡 回复 "下一页"/"上一页"/页码数字 翻页, 回复其他任意内容退出'
2585
+ '💡 每次最多展示3条, 回复 "下一页"/"上一页"/页码数字 翻页, 回复其他任意内容退出'
2489
2586
  ].join("\n");
2490
2587
  }, "formatRecord");
2588
+ const MAX_VIEW = 3;
2491
2589
  let page = 0;
2492
- await session.send([
2493
- formatRecord(page),
2494
- `📊 数据最近同步: ${toBeijingTime(syncTime.toISOString())}`
2495
- ].join("\n"));
2590
+ let viewCount = 0;
2591
+ const sendPage = /* @__PURE__ */ __name(async (idx, withSync) => {
2592
+ const lines = [formatRecord(idx)];
2593
+ if (withSync) lines.push(`📊 数据最近同步: ${toBeijingTime(syncTime.toISOString())}`);
2594
+ await session.send(lines.join("\n"));
2595
+ viewCount++;
2596
+ }, "sendPage");
2597
+ await sendPage(page, true);
2598
+ if (viewCount >= MAX_VIEW) return "已退出封禁记录查询。";
2496
2599
  while (true) {
2497
2600
  const input = await session.prompt(6e4);
2498
2601
  if (!input) break;
@@ -2500,18 +2603,21 @@ ${message}`;
2500
2603
  if (/^(下一页|下页|next|n)$/i.test(cmd)) {
2501
2604
  if (page < records.length - 1) {
2502
2605
  page++;
2503
- await session.send(formatRecord(page));
2606
+ await sendPage(page, false);
2607
+ if (viewCount >= MAX_VIEW) break;
2504
2608
  } else await session.send('已是最后一页。回复 "上一页" 或其他任意内容退出。');
2505
2609
  } else if (/^(上一页|上页|prev|p|上一个)$/i.test(cmd)) {
2506
2610
  if (page > 0) {
2507
2611
  page--;
2508
- await session.send(formatRecord(page));
2612
+ await sendPage(page, false);
2613
+ if (viewCount >= MAX_VIEW) break;
2509
2614
  } else await session.send('已是第一页。回复 "下一页" 或其他任意内容退出。');
2510
2615
  } else {
2511
2616
  const n = parseInt(cmd, 10);
2512
2617
  if (!isNaN(n) && n >= 1 && n <= records.length) {
2513
2618
  page = n - 1;
2514
- await session.send(formatRecord(page));
2619
+ await sendPage(page, false);
2620
+ if (viewCount >= MAX_VIEW) break;
2515
2621
  } else {
2516
2622
  break;
2517
2623
  }
@@ -2534,6 +2640,15 @@ function toBeijingTime(isoString) {
2534
2640
  return `${y}/${M}/${d} ${h2}:${m}:${s}`;
2535
2641
  }
2536
2642
  __name(toBeijingTime, "toBeijingTime");
2643
+ function formatMiningDuration(minutes) {
2644
+ const totalMinutes = Math.floor(minutes);
2645
+ const hours = Math.floor(totalMinutes / 60);
2646
+ const mins = totalMinutes % 60;
2647
+ if (hours <= 0) return `${mins}分钟`;
2648
+ if (mins <= 0) return `${hours}小时`;
2649
+ return `${hours}小时${mins}分`;
2650
+ }
2651
+ __name(formatMiningDuration, "formatMiningDuration");
2537
2652
  function translateEventType(eventType) {
2538
2653
  const translations = {
2539
2654
  cameBackOnline: "恢复在线",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-ggcevo",
3
3
  "description": "GGCEVO 综合插件:星际争霸2游戏句柄绑定 + 签到/抽奖/兑换/背包/活动系统等虚拟物品玩法",
4
- "version": "1.1.1",
4
+ "version": "1.1.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -33,15 +33,17 @@ npm install koishi-plugin-ggcevo
33
33
 
34
34
  | 配置项 | 类型 | 默认值 | 说明 |
35
35
  | --- | --- | --- | --- |
36
- | `curfewEnabled` | `boolean` | `false` | 是否启用宵禁。启用后每日 0:00-6:00 禁止签到和抽奖,17:00-24:00 禁止抽奖 |
36
+ | `curfewEnabled` | `boolean` | `false` | 是否启用宵禁。启用后每晚 17:00-24:00 禁止签到、抽奖和挖矿 |
37
+
38
+ > 宵禁影响「咕咕之战」游戏模式下的玩法指令(签到、抽奖、挖矿等)。后续新增的咕咕之战玩法指令均默认受宵禁影响。
37
39
 
38
40
  **宵禁时段说明:**
39
41
 
40
- | 时段 | 签到 | 抽奖 |
41
- | --- | --- | --- |
42
- | 0:00-6:00 | ❌ 禁止 | ❌ 禁止 |
43
- | 6:00-17:00 | ✅ 允许 | ✅ 允许 |
44
- | 17:00-24:00 | ✅ 允许 | ❌ 禁止 |
42
+ | 时段 | 签到 | 抽奖 | 挖矿 |
43
+ | --- | --- | --- | --- |
44
+ | 0:00-6:00 | ✅ 允许 | ✅ 允许 | ✅ 允许 |
45
+ | 6:00-17:00 | ✅ 允许 | ✅ 允许 | ✅ 允许 |
46
+ | 17:00-24:00 | ❌ 禁止 | ❌ 禁止 | ❌ 禁止 |
45
47
 
46
48
  ### 腾讯文档(封禁记录查询)
47
49
 
@@ -58,12 +60,24 @@ npm install koishi-plugin-ggcevo
58
60
  | `tencentDocsBanListSheetId` | `string` | `''` | 封禁记录工作表ID(表格URL中 `tab` 参数) |
59
61
  | `tencentDocsAdminWelfareFileId` | `string` | `''` | 管理员福利在线表格文件ID(短ID或完整ID,A列QQ号/B列句柄) |
60
62
  | `tencentDocsAdminWelfareSheetId` | `string` | `''` | 管理员福利工作表ID(表格URL中 `tab` 参数) |
61
- | `banNotifyGroups` | `string[]` | `[]` | 封禁提醒广播的群组ID列表。同步检测到新增封禁记录时在此提醒:句柄绑定了QQ则 @该用户,未绑定则只提醒句柄(含封禁等级、审核员、审核时间,处罚原因详情见封禁文档) |
63
+ | `banNotifyGroups` | `string[]` | `[]` | 封禁提醒广播的群组ID列表。同步检测到新增封禁记录时在此提醒(含句柄、封禁等级、审核员、审核时间,处罚原因详情见封禁文档) |
62
64
 
63
65
  **授权模式说明:**
64
66
  - **用户级模式**(推荐用于无 `Client Secret` 的场景):填写 `tencentDocsClientId` + `tencentDocsAccessToken` + `tencentDocsOpenId`,三者均通过腾讯文档开放平台扫码授权流程获取。Token 过期后需重新获取并在控制台更新。
65
67
  - **应用级模式**:填写 `tencentDocsClientId` + `tencentDocsClientSecret`,插件自动获取并刷新 Token,无需手动维护。需应用已开通 `scope.auth.account` 权限。
66
68
 
69
+ ### 句柄自动解绑
70
+
71
+ | 配置项 | 类型 | 默认值 | 说明 |
72
+ | --- | --- | --- | --- |
73
+ | `handleInactiveUnbindEnabled` | `boolean` | `false` | 是否启用句柄长时间未使用自动解绑 |
74
+ | `handleInactiveUnbindDays` | `number` | `90` | 句柄超过该天数未使用将自动解绑(**仅对拥有多个句柄的用户的非当前使用句柄生效**,当前活跃句柄不会触发) |
75
+
76
+ **说明:**
77
+ - 绑定句柄时记录最后使用时间(`lastUsedAt`),切换句柄时切换前后两个句柄均更新为当前时间(视为已使用)。
78
+ - 功能上线前已绑定的旧句柄无时间记录,首次检查时以当前时间作为绑定时间补齐。
79
+ - 定时任务每天检查一次(启动后延迟 5 秒首次检查),删除超过阈值天数未使用且非当前使用的句柄。
80
+
67
81
  ## 指令列表
68
82
 
69
83
  ### 星际争霸2 句柄(sc2arcade)
@@ -75,13 +89,15 @@ npm install koishi-plugin-ggcevo
75
89
  | `切换 [index]` | — | 切换正在使用的游戏句柄 |
76
90
  | `查询 [handle]` | — | 查询某游戏句柄是否已被绑定 |
77
91
  | `解绑 [index]` | 解绑句柄 | 解除绑定某个游戏句柄 |
78
- | `地图检测` | — | 查询已配置地图(`mapMonitorMapIds`)的检测状态:在线状态、最后状态变更时间、24h/30d离线次数、近期事件。当所有配置地图均离线时,底部额外提示推荐游玩的地图(需配置 `mapMonitorRecommendMapName`)。需开启 `mapMonitorEnabled` 且配置地图ID |
92
+ | `迁移 <qq>` | — | 将指定QQ号绑定的全部游戏句柄迁移到当前账号(用于从 onebot 机器人切换至 QQ 官方机器人后,将旧 QQ 号下绑定的句柄转移到新的 openid 身份下) |
93
+ | `地图检测` | — | 查询已配置地图(`mapMonitorMapIds`)的检测状态:在线状态、最后状态变更时间、24h/30d离线次数、近期事件。当所有配置地图均离线时,底部额外提示推荐游玩的地图名称(需配置 `mapMonitorRecommendMapName`)。需开启 `mapMonitorEnabled` 且配置地图ID |
79
94
 
80
95
  ### 签到与虚拟物品(ggcevo)
81
96
 
82
97
  | 指令 | 别名 | 权限 | 说明 |
83
98
  | --- | --- | --- | --- |
84
99
  | `签到` | — | — | 每日签到,获取奖励并发放签到券/补签券等。每月首次签到时,若签到者QQ号与句柄同时匹配管理员福利文档记录,额外发放 50 咕咕币津贴 |
100
+ | `挖矿` | — | — | 领取挂机挖矿收益(咕咕之战玩法)。每半小时收益 4 金币,单次存储上限 24 小时,领取后立即进入下一轮挖矿。累计挖矿总时长与累计收益金币数会被记录统计。受宵禁影响:17:00-24:00 禁止领取 |
85
101
  | `兑换 <name>` | — | — | 使用兑换券兑换指定物品。消耗兑换券数量随物品品质而定(T3皮肤=3、T2=4、T1=5、T3宠物=3、T2=4、T1=5、T0宠物=6、入场特效=5、角色冠名权=10、赎罪券=2) |
86
102
  | `抽奖` | — | — | 抽奖。选项:`-p <poolId>` 奖池ID,`-c <count>` 抽奖次数。各池消耗:金币池=100金币/次、普通池=1咕咕币/次、皮肤池/宠物池=3兑换券/次 |
87
103
  | `背包` | — | — | 查看自己的物品背包 |
@@ -95,7 +111,7 @@ npm install koishi-plugin-ggcevo
95
111
  | `活动列表 [showAll]` | — | — | 查看活动列表。默认只显示进行中与未开始的活动,携带参数(如"全部")显示含已过期在内的全部活动 |
96
112
  | `补签` | — | — | 使用补签券补签漏签的日期 |
97
113
  | `使用 <name>` | — | — | 使用指定物品(如赎罪券等) |
98
- | `封禁记录 [user]` | — | — | 查询封禁记录。不带参数查询自己,携带 @用户 查询对方句柄的封禁记录。每页显示 1 条,支持翻页(回复"下一页/上一页/页码"翻页,回复其他任意内容退出)。审核时间仅显示年月日 |
114
+ | `封禁记录 [user]` | — | — | 查询封禁记录。不带参数查询自己,携带 @用户 查询对方句柄的封禁记录。每页显示 1 条,支持翻页,每次指令最多展示 3 条,第 3 条输出后自动退出查询(回复"下一页/上一页/页码"翻页,回复其他任意内容退出)。审核时间仅显示年月日 |
99
115
  | `同步封禁记录` | — | 3 | 立即从腾讯文档拉取全量数据并写入数据库(管理员指令) |
100
116
  | `同步管理员福利` | — | 3 | 立即从腾讯文档拉取管理员福利表(A列QQ号/B列句柄)全量数据并写入数据库(管理员指令) |
101
117
 
@@ -130,7 +146,7 @@ npm install koishi-plugin-ggcevo
130
146
 
131
147
  插件使用前缀 `sc2arcade_`(句柄部分)与 `ggcevo_`(签到/物品部分):
132
148
 
133
- - `sc2arcade_player` — 用户句柄绑定记录(主键 `id`)
149
+ - `sc2arcade_player` — 用户句柄绑定记录(主键 `id`;含 `lastUsedAt` 最后使用时间,用于自动解绑判断)
134
150
  - `sc2arcade_map_monitor` — 地图检测状态
135
151
  - `ggcevo_backpack` — 背包(主键 `user_id` + `item_id`)
136
152
  - `ggcevo_signin_summary` — 签到汇总(主键 `user_id`)
@@ -143,9 +159,38 @@ npm install koishi-plugin-ggcevo
143
159
  - `ggcevo_docs_token` — 腾讯文档令牌缓存(应用级模式使用,主键 `user_id`,应用级账号固定为 `app_account`)
144
160
  - `ggcevo_ban_record` — 封禁记录(主键 `id` 自增,对应文档行号 `id+1`,每小时全量同步)
145
161
  - `ggcevo_admin_welfare` — 管理员福利(主键 `id` 自增,对应文档行号 `id+1`,每小时全量同步;A列QQ号/B列句柄)
162
+ - `ggcevo_mining` — 挖矿状态与累计统计(主键 `user_id`,复用句柄标识;含本轮开始时间 `start_time`、累计挖矿时长 `total_minutes`、累计收益金币 `total_coins`)
146
163
 
147
164
  ## 更新日志
148
165
 
166
+ ### v1.1.4
167
+
168
+ 宵禁时段统一,取消凌晨时段限制。
169
+
170
+ - **宵禁统一为每晚 17:00-24:00**:取消原有 0:00-6:00 的宵禁时段,签到、抽奖、挖矿等咕咕之战玩法指令均只受 17:00-24:00 限制,其余时段(含凌晨 0-6 点)均可正常使用。
171
+
172
+ ### v1.1.3
173
+
174
+ 新增挖矿挂机玩法与句柄长时间未使用自动解绑功能,并扩展宵禁对咕咕之战玩法的覆盖。
175
+
176
+ - **新增挖矿功能**:`挖矿` 指令领取挂机收益——每半小时收益 4 金币(金币直接入背包),单次存储领取上限 24 小时,领取后立即进入下一轮挖矿。累计挖矿总时长与累计收益金币数会记录入库(`ggcevo_mining` 表)。
177
+ - **宵禁扩展(咕咕之战)**:宵禁覆盖「咕咕之战」玩法指令。挖矿受宵禁影响:17:00-24:00 禁止领取;0:00-6:00 允许挖矿。宵禁时段说明表新增挖矿列。后续新增的咕咕之战玩法指令均默认受宵禁影响。
178
+ - **新增句柄自动解绑**:新增配置项 `handleInactiveUnbindEnabled`(默认关闭)与 `handleInactiveUnbindDays`(默认 90 天)。绑定句柄时记录 `lastUsedAt`,切换句柄时切换前后两个句柄均更新为当前时间;功能上线前的旧句柄首次检查时补记当前时间为绑定时间。仅针对拥有多个句柄的用户的非当前使用句柄,超过阈值天数未使用则自动解绑;当前活跃句柄不触发。每天定时检查一次。
179
+
180
+ ### v1.1.2
181
+
182
+ 封禁提醒不再@用户,封禁记录查询增加展示上限。
183
+
184
+ - **封禁提醒去@化**:定时同步检测到新增封禁记录时,群提醒不再 @绑定句柄的用户,仅广播句柄及封禁详情
185
+ - **封禁记录查询上限**:每次指令最多展示 3 条封禁记录,第 3 条输出后自动退出查询,避免无限制翻页刷屏
186
+
187
+ ### v1.1.1
188
+
189
+ 新增句柄绑定迁移指令,适配 QQ 官方机器人。
190
+
191
+ - **新增指令 `迁移 <qq>`**:从 onebot 机器人更换为 QQ 官方机器人后,用户 `userId` 由 QQ 号变更为 openid,使用本指令可将指定 QQ 号下绑定的全部游戏句柄迁移至当前账号(openid)名下
192
+ - **迁移逻辑**:仅更新 `sc2arcade_player` 表中对应记录的 `userId`;若当前账号名下已有句柄,迁移过来的句柄一律不设为当前使用,避免出现多个当前使用句柄
193
+
149
194
  ### v1.1.0
150
195
 
151
196
  优化封禁记录查询的翻页交互。