koishi-plugin-ggcevo 1.1.2 → 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);
@@ -2536,6 +2640,15 @@ function toBeijingTime(isoString) {
2536
2640
  return `${y}/${M}/${d} ${h2}:${m}:${s}`;
2537
2641
  }
2538
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");
2539
2652
  function translateEventType(eventType) {
2540
2653
  const translations = {
2541
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.2",
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
 
@@ -64,6 +66,18 @@ npm install koishi-plugin-ggcevo
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)
@@ -83,6 +97,7 @@ npm install koishi-plugin-ggcevo
83
97
  | 指令 | 别名 | 权限 | 说明 |
84
98
  | --- | --- | --- | --- |
85
99
  | `签到` | — | — | 每日签到,获取奖励并发放签到券/补签券等。每月首次签到时,若签到者QQ号与句柄同时匹配管理员福利文档记录,额外发放 50 咕咕币津贴 |
100
+ | `挖矿` | — | — | 领取挂机挖矿收益(咕咕之战玩法)。每半小时收益 4 金币,单次存储上限 24 小时,领取后立即进入下一轮挖矿。累计挖矿总时长与累计收益金币数会被记录统计。受宵禁影响:17:00-24:00 禁止领取 |
86
101
  | `兑换 <name>` | — | — | 使用兑换券兑换指定物品。消耗兑换券数量随物品品质而定(T3皮肤=3、T2=4、T1=5、T3宠物=3、T2=4、T1=5、T0宠物=6、入场特效=5、角色冠名权=10、赎罪券=2) |
87
102
  | `抽奖` | — | — | 抽奖。选项:`-p <poolId>` 奖池ID,`-c <count>` 抽奖次数。各池消耗:金币池=100金币/次、普通池=1咕咕币/次、皮肤池/宠物池=3兑换券/次 |
88
103
  | `背包` | — | — | 查看自己的物品背包 |
@@ -131,7 +146,7 @@ npm install koishi-plugin-ggcevo
131
146
 
132
147
  插件使用前缀 `sc2arcade_`(句柄部分)与 `ggcevo_`(签到/物品部分):
133
148
 
134
- - `sc2arcade_player` — 用户句柄绑定记录(主键 `id`)
149
+ - `sc2arcade_player` — 用户句柄绑定记录(主键 `id`;含 `lastUsedAt` 最后使用时间,用于自动解绑判断)
135
150
  - `sc2arcade_map_monitor` — 地图检测状态
136
151
  - `ggcevo_backpack` — 背包(主键 `user_id` + `item_id`)
137
152
  - `ggcevo_signin_summary` — 签到汇总(主键 `user_id`)
@@ -144,9 +159,24 @@ npm install koishi-plugin-ggcevo
144
159
  - `ggcevo_docs_token` — 腾讯文档令牌缓存(应用级模式使用,主键 `user_id`,应用级账号固定为 `app_account`)
145
160
  - `ggcevo_ban_record` — 封禁记录(主键 `id` 自增,对应文档行号 `id+1`,每小时全量同步)
146
161
  - `ggcevo_admin_welfare` — 管理员福利(主键 `id` 自增,对应文档行号 `id+1`,每小时全量同步;A列QQ号/B列句柄)
162
+ - `ggcevo_mining` — 挖矿状态与累计统计(主键 `user_id`,复用句柄标识;含本轮开始时间 `start_time`、累计挖矿时长 `total_minutes`、累计收益金币 `total_coins`)
147
163
 
148
164
  ## 更新日志
149
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
+
150
180
  ### v1.1.2
151
181
 
152
182
  封禁提醒不再@用户,封禁记录查询增加展示上限。