koishi-plugin-lili-hub 0.3.2 → 0.3.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.
Files changed (3) hide show
  1. package/lib/index.d.ts +152 -0
  2. package/lib/index.js +749 -73
  3. package/package.json +3 -2
package/lib/index.d.ts ADDED
@@ -0,0 +1,152 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "lili-hub";
3
+ export declare const inject: string[];
4
+ declare module 'koishi' {
5
+ interface Tables {
6
+ lili_message: LiliMessage;
7
+ lili_roulette: LiliRoulette;
8
+ lili_vote: LiliVote;
9
+ lili_duel: LiliDuel;
10
+ lili_chain_record: LiliChainRecord;
11
+ }
12
+ }
13
+ interface LiliMessage {
14
+ id: number;
15
+ gid: string;
16
+ userId: string;
17
+ userName: string;
18
+ content: string;
19
+ timestamp: number;
20
+ messageId: string;
21
+ }
22
+ interface LiliRoulette {
23
+ id: number;
24
+ gid: string;
25
+ hitUserId: string;
26
+ timestamp: number;
27
+ }
28
+ interface LiliVote {
29
+ id: number;
30
+ gid: string;
31
+ targetUserId: string;
32
+ voterUserId: string;
33
+ voteType: 'mute' | 'unmute';
34
+ timestamp: number;
35
+ }
36
+ interface LiliDuel {
37
+ id: number;
38
+ gid: string;
39
+ winnerId: string;
40
+ winnerName: string;
41
+ loserId: string;
42
+ loserName: string;
43
+ winnerTime: number;
44
+ loserTime: number;
45
+ timestamp: number;
46
+ }
47
+ interface LiliChainRecord {
48
+ id: number;
49
+ gid: string;
50
+ userId: string;
51
+ userName: string;
52
+ chainContent: string;
53
+ timestamp: number;
54
+ }
55
+ interface EventLine {
56
+ voice: string;
57
+ text: string;
58
+ }
59
+ interface RouletteConfig {
60
+ chamberSize: number;
61
+ muteDurationMin: number;
62
+ muteDurationMax: number;
63
+ misfireRate: number;
64
+ drinksToDrunkMin: number;
65
+ drinksToDrunkMax: number;
66
+ drunkRateIncrement: number;
67
+ drunkDurationMin: number;
68
+ drunkDurationMax: number;
69
+ drunkMisfireRate: number;
70
+ drunkRampageRate: number;
71
+ drunkRevengeRate: number;
72
+ maxHistoryRounds: number;
73
+ groups: string;
74
+ groupMode: 'whitelist' | 'blacklist';
75
+ }
76
+ interface VoiceConfig {
77
+ voiceEnabled: boolean;
78
+ spin: EventLine;
79
+ shoot: EventLine;
80
+ hit: EventLine;
81
+ misfire: EventLine;
82
+ drunkMisfire: EventLine;
83
+ firstBlood: EventLine;
84
+ doubleKill: EventLine;
85
+ tripleKill: EventLine;
86
+ quadraKill: EventLine;
87
+ pentaKill: EventLine;
88
+ rampage: EventLine;
89
+ revenge: EventLine;
90
+ noTarget: EventLine;
91
+ drink: EventLine;
92
+ drunk: EventLine;
93
+ stillDrunk: EventLine;
94
+ sober: EventLine;
95
+ }
96
+ interface VoteMuteConfig {
97
+ enabled: boolean;
98
+ groups: string;
99
+ groupMode: 'whitelist' | 'blacklist';
100
+ voteWindowDays: number;
101
+ muteThreshold: number;
102
+ unmuteThreshold: number;
103
+ muteSeconds: number;
104
+ }
105
+ interface FoldConfig {
106
+ enabled: boolean;
107
+ groups: string;
108
+ groupMode: 'whitelist' | 'blacklist';
109
+ minutes: number;
110
+ }
111
+ interface ImitationConfig {
112
+ enabled: boolean;
113
+ groups: string;
114
+ groupMode: 'whitelist' | 'blacklist';
115
+ interval: number;
116
+ rate: number;
117
+ }
118
+ interface DedupConfig {
119
+ enabled: boolean;
120
+ groups: string;
121
+ groupMode: 'whitelist' | 'blacklist';
122
+ }
123
+ interface DuelConfig {
124
+ enabled: boolean;
125
+ groups: string;
126
+ groupMode: 'whitelist' | 'blacklist';
127
+ countdownSeconds: number;
128
+ acceptTimeoutSeconds: number;
129
+ drawTimeoutSeconds: number;
130
+ earlyDrawMuteSeconds: number;
131
+ lateMuteSeconds: number;
132
+ }
133
+ interface ChainConfig {
134
+ enabled: boolean;
135
+ groups: string;
136
+ groupMode: 'whitelist' | 'blacklist';
137
+ defaultDeadlineHour: number;
138
+ }
139
+ export interface Config {
140
+ debug: boolean;
141
+ roulette: RouletteConfig;
142
+ voice: VoiceConfig;
143
+ voteMute: VoteMuteConfig;
144
+ fold: FoldConfig;
145
+ imitation: ImitationConfig;
146
+ dedup: DedupConfig;
147
+ duel: DuelConfig;
148
+ chain: ChainConfig;
149
+ }
150
+ export declare const Config: Schema<Config>;
151
+ export declare function apply(ctx: Context, config: Config): void;
152
+ export {};
package/lib/index.js CHANGED
@@ -16,18 +16,20 @@ var __copyProps = (to, from, except, desc) => {
16
16
  };
17
17
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
18
 
19
- // external/lili-hub/packages/lili-hub/src/index.ts
19
+ // src/index.ts
20
20
  var src_exports = {};
21
21
  __export(src_exports, {
22
22
  Config: () => Config,
23
23
  apply: () => apply,
24
24
  inject: () => inject,
25
- name: () => name
25
+ name: () => name,
26
+ optional: () => optional
26
27
  });
27
28
  module.exports = __toCommonJS(src_exports);
28
29
  var import_koishi = require("koishi");
29
30
  var name = "lili-hub";
30
31
  var inject = ["database"];
32
+ var optional = ["schedule", "dialogue"];
31
33
  var SPIN_TEXT = [
32
34
  "\u4E3D\u4E3D\u638F\u51FA\u4E86\u5DE6\u8F6E\u624B\u67AA\uFF0C\u5728\u624B\u91CC\u8F6C\u4E86\u8F6C\u2026\u2026",
33
35
  "\u4E3D\u4E3D\u5439\u4E86\u5439\u67AA\u53E3\uFF0C\u719F\u7EC3\u5730\u88C5\u4E0A\u4E86\u5B50\u5F39\u2026\u2026",
@@ -291,7 +293,8 @@ var Config = import_koishi.Schema.object({
291
293
  chain: import_koishi.Schema.object({
292
294
  enabled: import_koishi.Schema.boolean().description("\u542F\u7528\u7FA4\u63A5\u9F99\u529F\u80FD").default(false),
293
295
  groups: import_koishi.Schema.string().description("\u7FA4\u53F7\u5217\u8868\uFF08\u9017\u53F7\u5206\u9694\uFF09").default(""),
294
- groupMode: import_koishi.Schema.union(["whitelist", "blacklist"]).description("\u7FA4\u7EC4\u8FC7\u6EE4\u6A21\u5F0F\uFF1A\u767D\u540D\u5355 / \u9ED1\u540D\u5355").default("whitelist")
296
+ groupMode: import_koishi.Schema.union(["whitelist", "blacklist"]).description("\u7FA4\u7EC4\u8FC7\u6EE4\u6A21\u5F0F\uFF1A\u767D\u540D\u5355 / \u9ED1\u540D\u5355").default("whitelist"),
297
+ defaultDeadlineHour: import_koishi.Schema.number().description("\u9ED8\u8BA4\u622A\u6B62\u5C0F\u65F6\uFF080-23\uFF09\uFF0C\u521B\u5EFA\u63A5\u9F99\u4E0D\u586B\u65F6\u95F4\u65F6\u9ED8\u8BA4\u5F53\u5929\u6B64\u65F6\u95F4\u622A\u6B62").default(21).min(0).max(23)
295
298
  }).description("\u{1F4DD} \u7FA4\u63A5\u9F99")
296
299
  });
297
300
  function getGroupId(session) {
@@ -307,6 +310,49 @@ function parseGroupList(raw) {
307
310
  if (!raw || !raw.trim()) return [];
308
311
  return raw.split(",").map((s) => s.trim()).filter(Boolean);
309
312
  }
313
+ function elementsToCQCode(elements) {
314
+ if (!elements || !Array.isArray(elements)) return "";
315
+ return elements.map((el) => {
316
+ switch (el.type) {
317
+ case "text":
318
+ return el.attrs?.content || "";
319
+ case "image": {
320
+ const file = el.attrs?.file || el.attrs?.url || "";
321
+ const url = el.attrs?.url || "";
322
+ if (url && file && url !== file) return `[CQ:image,file=${file},url=${url}]`;
323
+ return `[CQ:image,file=${file}]`;
324
+ }
325
+ case "at":
326
+ return `[CQ:at,qq=${el.attrs?.id || ""}]`;
327
+ case "face": {
328
+ const id = el.attrs?.id;
329
+ return id != null ? `[CQ:face,id=${id}]` : "";
330
+ }
331
+ case "mface": {
332
+ const id = el.attrs?.emoji_id || el.attrs?.emojiId;
333
+ return id ? `[CQ:mface,id=${id}]` : "";
334
+ }
335
+ case "record": {
336
+ const file = el.attrs?.file;
337
+ return file ? `[CQ:record,file=${file}]` : "";
338
+ }
339
+ case "video": {
340
+ const file = el.attrs?.file;
341
+ return file ? `[CQ:video,file=${file}]` : "";
342
+ }
343
+ case "reply":
344
+ return "";
345
+ // 回复元素不参与
346
+ case "forward":
347
+ return "";
348
+ // 嵌套转发跳过
349
+ default: {
350
+ if (el.attrs?.content) return String(el.attrs.content);
351
+ return "";
352
+ }
353
+ }
354
+ }).join("");
355
+ }
310
356
  function normalizeText(text) {
311
357
  return text.replace(/\[CQ:[^\]]*\]/g, "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
312
358
  }
@@ -389,19 +435,252 @@ function apply(ctx, config) {
389
435
  loserTime: "unsigned",
390
436
  timestamp: "unsigned"
391
437
  }, { autoInc: true });
438
+ ctx.model.extend("lili_chain_record", {
439
+ id: "unsigned",
440
+ gid: "string",
441
+ userId: "string",
442
+ userName: "string",
443
+ chainContent: "string",
444
+ timestamp: "unsigned"
445
+ }, { autoInc: true });
446
+ ctx.model.extend("lili_chain", {
447
+ id: "unsigned",
448
+ gid: "string",
449
+ chainId: "unsigned",
450
+ trueId: "string",
451
+ content: "string",
452
+ creatorId: "string",
453
+ creatorName: "string",
454
+ participants: "text",
455
+ createdAt: "unsigned",
456
+ deadline: "unsigned",
457
+ active: "boolean"
458
+ }, { autoInc: true });
392
459
  const drinkStates = /* @__PURE__ */ new Map();
393
460
  const rouletteStates = /* @__PURE__ */ new Map();
394
461
  const imitationCounters = /* @__PURE__ */ new Map();
395
462
  const duelStates = /* @__PURE__ */ new Map();
396
463
  const chainStates = /* @__PURE__ */ new Map();
464
+ let chainTrueIdCounter = 0;
465
+ async function initTrueIdCounter() {
466
+ try {
467
+ const rows = await ctx.database.get("lili_chain", {}, { fields: ["trueId"] });
468
+ let maxNum = 0;
469
+ for (const row of rows) {
470
+ if (row.trueId && /^JL(\d+)$/.test(row.trueId)) {
471
+ const n = parseInt(RegExp.$1);
472
+ if (n > maxNum) maxNum = n;
473
+ }
474
+ }
475
+ chainTrueIdCounter = maxNum;
476
+ } catch {
477
+ chainTrueIdCounter = 0;
478
+ }
479
+ }
480
+ function nextTrueId() {
481
+ chainTrueIdCounter++;
482
+ return `JL${String(chainTrueIdCounter).padStart(4, "0")}`;
483
+ }
484
+ async function loadChains() {
485
+ try {
486
+ await initTrueIdCounter();
487
+ const rows = await ctx.database.get("lili_chain", { active: true });
488
+ for (const row of rows) {
489
+ let trueId = row.trueId;
490
+ if (!trueId) {
491
+ trueId = nextTrueId();
492
+ await ctx.database.set("lili_chain", { gid: row.gid, chainId: row.chainId }, { trueId });
493
+ }
494
+ const chain = {
495
+ id: row.chainId,
496
+ trueId,
497
+ content: row.content,
498
+ creatorId: row.creatorId,
499
+ creatorName: row.creatorName,
500
+ participants: JSON.parse(row.participants || "[]"),
501
+ createdAt: row.createdAt,
502
+ deadline: row.deadline
503
+ };
504
+ const arr = chainStates.get(row.gid) || [];
505
+ arr.push(chain);
506
+ chainStates.set(row.gid, arr);
507
+ if (chain.deadline > Date.now()) {
508
+ scheduleChainDeadline(row.gid, chain);
509
+ }
510
+ }
511
+ dbg("\u63A5\u9F99-\u4ECE\u6570\u636E\u5E93\u52A0\u8F7D", { count: rows.length });
512
+ } catch (e) {
513
+ dbg("\u63A5\u9F99-\u52A0\u8F7D\u5931\u8D25", { error: String(e) });
514
+ }
515
+ }
516
+ async function saveChain(gid, chain) {
517
+ try {
518
+ const existing = await ctx.database.get("lili_chain", { gid, chainId: chain.id });
519
+ const data = {
520
+ gid,
521
+ chainId: chain.id,
522
+ trueId: chain.trueId,
523
+ content: chain.content,
524
+ creatorId: chain.creatorId,
525
+ creatorName: chain.creatorName,
526
+ participants: JSON.stringify(chain.participants),
527
+ createdAt: chain.createdAt,
528
+ deadline: chain.deadline,
529
+ active: true
530
+ };
531
+ if (existing.length > 0) {
532
+ await ctx.database.set("lili_chain", { gid, chainId: chain.id }, data);
533
+ } else {
534
+ await ctx.database.create("lili_chain", data);
535
+ }
536
+ } catch (e) {
537
+ dbg("\u63A5\u9F99-\u4FDD\u5B58\u5931\u8D25", { gid, chainId: chain.id, error: String(e) });
538
+ }
539
+ }
540
+ async function deactivateChain(gid, chainId) {
541
+ try {
542
+ await ctx.database.set("lili_chain", { gid, chainId }, { active: false });
543
+ } catch (e) {
544
+ dbg("\u63A5\u9F99-\u6807\u8BB0\u5220\u9664\u5931\u8D25", { gid, chainId, error: String(e) });
545
+ }
546
+ }
547
+ loadChains();
548
+ async function scheduleChainDeadline(gid, chain, session) {
549
+ try {
550
+ const cmd = `\u67E5\u770B\u63A5\u9F99 ${chain.trueId}`;
551
+ const existing = await ctx.database.get("schedule", { command: cmd });
552
+ for (const e of existing) {
553
+ await ctx.database.remove("schedule", { id: e.id });
554
+ }
555
+ const bots = ctx.bots ? [...ctx.bots] : [];
556
+ const botSid = bots.length > 0 ? bots[0].sid : ctx.bot?.sid || "";
557
+ if (!botSid) return;
558
+ let event;
559
+ if (session?.event) {
560
+ event = JSON.parse(JSON.stringify(session.event));
561
+ event.timestamp = Date.now();
562
+ event.message = { ...event.message || {}, content: cmd };
563
+ } else {
564
+ event = {
565
+ selfId: botSid,
566
+ platform: bots[0]?.platform || "onebot",
567
+ timestamp: Date.now(),
568
+ type: "message-created",
569
+ subtype: "group",
570
+ channel: { id: gid, type: 0 },
571
+ guild: { id: gid },
572
+ user: { id: chain.creatorId, name: chain.creatorName },
573
+ message: { content: cmd },
574
+ _type: bots[0]?.platform || "onebot"
575
+ };
576
+ }
577
+ await ctx.database.create("schedule", {
578
+ assignee: botSid,
579
+ time: new Date(chain.deadline),
580
+ interval: 0,
581
+ command: cmd,
582
+ event
583
+ });
584
+ dbg("\u63A5\u9F99-schedule\u5DF2\u5199\u5165", { gid, trueId: chain.trueId, deadline: new Date(chain.deadline).toLocaleString("zh-CN") });
585
+ } catch (e) {
586
+ dbg("\u63A5\u9F99-schedule\u5199\u5165\u5931\u8D25", { error: String(e) });
587
+ }
588
+ }
589
+ function getNextChainId(gid) {
590
+ const chains = chainStates.get(gid);
591
+ if (!chains || chains.length === 0) return 1;
592
+ const maxId = Math.max(...chains.map((c) => c.id));
593
+ return maxId + 1;
594
+ }
397
595
  function formatChain(chain) {
398
- const lines = [chain.content];
596
+ const deadlineStr = new Date(chain.deadline).toLocaleString("zh-CN", {
597
+ month: "2-digit",
598
+ day: "2-digit",
599
+ hour: "2-digit",
600
+ minute: "2-digit"
601
+ });
602
+ const header = `\u{1F4DD} \u63A5\u9F99${chain.id}\uFF1A${chain.content}\uFF08\u622A\u6B62 ${deadlineStr}\uFF09`;
603
+ const lines = [header];
399
604
  for (let i = 0; i < chain.participants.length; i++) {
400
605
  const p = chain.participants[i];
401
- lines.push(`${i + 1}.${p.userName}\uFF08${p.userId}\uFF09`);
606
+ const remark = p.remark ? `\uFF08${p.remark}\uFF09` : "";
607
+ lines.push(`${i + 1}.${p.userName}\uFF08${p.userId}\uFF09${remark}`);
402
608
  }
403
609
  return lines.join("\n");
404
610
  }
611
+ function parseDeadline(raw, defaultHour) {
612
+ if (!raw || !raw.trim()) return null;
613
+ const s = raw.trim();
614
+ const now = /* @__PURE__ */ new Date();
615
+ const fullMatch = s.match(/^(\d{1,2})-(\d{1,2})\s+(\d{1,2})[::](\d{2})$/);
616
+ if (fullMatch) {
617
+ const [_, m, d, h2, min] = fullMatch;
618
+ const dt = new Date(now.getFullYear(), parseInt(m) - 1, parseInt(d), parseInt(h2), parseInt(min), 0, 0);
619
+ if (isNaN(dt.getTime())) return null;
620
+ if (dt.getTime() < Date.now() - 864e5) dt.setFullYear(dt.getFullYear() + 1);
621
+ return dt.getTime();
622
+ }
623
+ const timeMatch = s.match(/^(\d{1,2})[::](\d{2})$/);
624
+ if (timeMatch) {
625
+ const [_, h2, min] = timeMatch;
626
+ const dt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parseInt(h2), parseInt(min), 0, 0);
627
+ if (isNaN(dt.getTime())) return null;
628
+ if (dt.getTime() < Date.now()) dt.setDate(dt.getDate() + 1);
629
+ return dt.getTime();
630
+ }
631
+ return null;
632
+ }
633
+ function getDefaultDeadline(defaultHour) {
634
+ const now = /* @__PURE__ */ new Date();
635
+ const dt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), defaultHour, 0, 0, 0);
636
+ if (dt.getTime() < Date.now()) dt.setDate(dt.getDate() + 1);
637
+ return dt.getTime();
638
+ }
639
+ async function cleanupExpiredChains(gid) {
640
+ const chains = chainStates.get(gid);
641
+ if (!chains) return;
642
+ const now = Date.now();
643
+ const expired = chains.filter((c) => c.deadline <= now);
644
+ const remaining = chains.filter((c) => c.deadline > now);
645
+ for (const chain of expired) {
646
+ await deactivateChain(gid, chain.id);
647
+ }
648
+ if (remaining.length === 0) {
649
+ chainStates.delete(gid);
650
+ } else if (remaining.length !== chains.length) {
651
+ chainStates.set(gid, remaining);
652
+ }
653
+ }
654
+ async function recordChainParticipation(gid, userId, userName, chainContent) {
655
+ try {
656
+ await ctx.database.create("lili_chain_record", {
657
+ gid,
658
+ userId,
659
+ userName,
660
+ chainContent,
661
+ timestamp: Date.now()
662
+ });
663
+ dbg("\u63A5\u9F99-\u8BB0\u5F55\u53C2\u4E0E", { gid, userId, userName });
664
+ } catch (e) {
665
+ dbg("\u63A5\u9F99-\u8BB0\u5F55\u53C2\u4E0E\u5931\u8D25", { gid, error: String(e) });
666
+ }
667
+ }
668
+ async function resolveDialogueChainContent(session, gid, index) {
669
+ try {
670
+ const allDialogues = await ctx.database.get("dialogue", {}, { sort: { id: "asc" }, limit: index });
671
+ if (!allDialogues || allDialogues.length < index) return null;
672
+ const d = allDialogues[index - 1];
673
+ if (!d) return null;
674
+ const question = d.question || d.original || "";
675
+ const answer = d.answer || "";
676
+ const content = question ? `${question} ${answer}` : answer;
677
+ dbg("\u63A5\u9F99-dialogue\u5F15\u7528", { gid, dialogueIndex: index, dialogueId: d.id, question, answer: answer.substring(0, 50) });
678
+ return content || null;
679
+ } catch (e) {
680
+ ctx.logger("lili-hub").warn(`[\u63A5\u9F99] dialogue\u5F15\u7528\u5931\u8D25 gid=${gid} index=${index} error=${String(e)}`);
681
+ return null;
682
+ }
683
+ }
405
684
  function isDrunk(gid) {
406
685
  const s = drinkStates.get(gid);
407
686
  if (!s || s.drunkUntil === 0) return false;
@@ -716,26 +995,43 @@ ${import_koishi.segment.at(targetId)} \u5DF2\u88AB\u89E3\u9664\u7981\u8A00\u3002
716
995
  return `\u{1F50A} \u5DF2\u64A4\u56DE\u4F60\u5BF9 ${import_koishi.segment.at(targetId)} \u7684\u7981\u8A00\u7968\u3002
717
996
  \u{1F4CA} \u5F53\u524D\u7981\u8A00\u7968\uFF1A${muteCount}\uFF08\u9700 \u2264 ${vm.unmuteThreshold} \u624D\u4F1A\u89E3\u7981\uFF09`;
718
997
  });
719
- ctx.command("\u4E3D\u4E3D\u6298\u53E0 <user:user> [minutes:number]", "\u5408\u5E76\u8F6C\u53D1\u5E76\u64A4\u56DE\u76EE\u6807\u7528\u6237\u6700\u8FD1 X \u5206\u949F\u5185\u7684\u6D88\u606F").action(async ({ session }, targetUser, minutesArg) => {
998
+ ctx.command("\u4E3D\u4E3D\u6298\u53E0 [target:text] [minutes:number]", "\u5408\u5E76\u8F6C\u53D1\u5E76\u64A4\u56DE\u76EE\u6807\u7528\u6237\u6700\u8FD1 X \u5206\u949F\u5185\u7684\u6D88\u606F").action(async ({ session }, targetArg, minutesArg) => {
720
999
  if (!session?.userId) return;
721
1000
  const gid = getGroupId(session);
722
1001
  const fc = config.fold;
723
1002
  if (!fc.enabled) return;
724
1003
  if (!isGroupAllowed(gid, fc.groups, fc.groupMode)) return;
725
- const targetId = extractTarget(session, targetUser);
1004
+ let targetId = null;
1005
+ let explicitMins = void 0;
1006
+ if (session.elements) {
1007
+ const atEl = session.elements.find((el) => el.type === "at" && el.attrs?.id);
1008
+ if (atEl) targetId = String(atEl.attrs.id);
1009
+ }
1010
+ if (!targetId && targetArg) {
1011
+ const t = targetArg.trim();
1012
+ if (/^\d{5,12}$/.test(t)) {
1013
+ targetId = t;
1014
+ } else if (/^\d{1,4}$/.test(t)) {
1015
+ explicitMins = parseInt(t);
1016
+ }
1017
+ }
1018
+ if (!targetId) targetId = String(session.userId);
1019
+ const foldedMinutes = typeof minutesArg === "number" && minutesArg >= 1 && minutesArg <= 1440 ? minutesArg : explicitMins && explicitMins >= 1 && explicitMins <= 1440 ? explicitMins : fc.minutes;
726
1020
  if (!targetId) return "\u{1F4E6} \u65E0\u6CD5\u89E3\u6790\u76EE\u6807\u7528\u6237\u3002\u8BF7\u4F7F\u7528 @ \u529F\u80FD\u6216\u76F4\u63A5\u8F93\u5165 QQ \u53F7\u3002";
727
- const foldedMinutes = typeof minutesArg === "number" && minutesArg >= 1 && minutesArg <= 1440 ? minutesArg : fc.minutes;
728
1021
  const since = Date.now() - foldedMinutes * 60 * 1e3;
729
- dbg("\u6298\u53E0-\u67E5\u8BE2", { gid, targetId, since: new Date(since).toLocaleTimeString("zh-CN"), foldedMinutes });
1022
+ ctx.logger("lili-hub").debug(`[\u6298\u53E0] \u67E5\u8BE2 gid=${gid} targetId=${targetId} since=${new Date(since).toLocaleTimeString("zh-CN")} foldedMinutes=${foldedMinutes}`);
730
1023
  let messages;
731
1024
  try {
732
- messages = await ctx.database.get("lili_message", { gid, userId: targetId, timestamp: { $gte: since } }, { sort: { timestamp: "asc" }, limit: 100 });
1025
+ messages = await ctx.database.get("lili_message", { gid, userId: String(targetId), timestamp: { $gte: since } }, { sort: { timestamp: "asc" }, limit: 100 });
733
1026
  } catch (e) {
734
- dbg("\u6298\u53E0-\u67E5\u8BE2\u5931\u8D25", { gid, error: String(e) });
1027
+ ctx.logger("lili-hub").warn(`[\u6298\u53E0] \u6570\u636E\u5E93\u67E5\u8BE2\u5931\u8D25 gid=${gid} targetId=${targetId} error=${String(e)}`);
735
1028
  return "\u{1F4E6} \u67E5\u8BE2\u6D88\u606F\u5931\u8D25\u3002";
736
1029
  }
737
- dbg("\u6298\u53E0-\u67E5\u8BE2\u7ED3\u679C", { gid, targetId, count: messages?.length || 0 });
738
- if (!messages || messages.length === 0) return `\u{1F4E6} ${import_koishi.segment.at(targetId)} \u6700\u8FD1 ${foldedMinutes} \u5206\u949F\u5185\u6CA1\u6709\u53D1\u8A00\u8BB0\u5F55\u3002`;
1030
+ ctx.logger("lili-hub").debug(`[\u6298\u53E0] \u67E5\u8BE2\u7ED3\u679C gid=${gid} targetId=${targetId} count=${messages?.length || 0}`);
1031
+ if (!messages || messages.length === 0) {
1032
+ ctx.logger("lili-hub").warn(`[\u6298\u53E0] \u65E0\u53D1\u8A00\u8BB0\u5F55 gid=${gid} targetId=${targetId} since=${new Date(since).toLocaleTimeString("zh-CN")} minutes=${foldedMinutes}`);
1033
+ return `\u{1F4E6} ${import_koishi.segment.at(targetId)} \u6700\u8FD1 ${foldedMinutes} \u5206\u949F\u5185\u6CA1\u6709\u53D1\u8A00\u8BB0\u5F55\u3002`;
1034
+ }
739
1035
  const nodes = messages.map((m) => ({
740
1036
  type: "node",
741
1037
  data: { name: m.userName, uin: m.userId, content: m.content, time: String(Math.floor(m.timestamp / 1e3)) }
@@ -758,14 +1054,18 @@ ${import_koishi.segment.at(targetId)} \u5DF2\u88AB\u89E3\u9664\u7981\u8A00\u3002
758
1054
  await session.send(`\u{1F4E6} \u4E3D\u4E3D\u6298\u53E0\u4E86 ${import_koishi.segment.at(targetId)} \u6700\u8FD1 ${foldedMinutes} \u5206\u949F\u7684\u6D88\u606F\uFF1A
759
1055
  ` + lines.join("\n"));
760
1056
  }
1057
+ let deletedCount = 0;
761
1058
  for (const m of messages) {
762
1059
  if (m.messageId) {
763
1060
  try {
764
- await session.bot.deleteMessage(session.channelId, m.messageId);
765
- } catch {
1061
+ await session.bot.deleteMessage(gid, String(m.messageId));
1062
+ deletedCount++;
1063
+ } catch (e) {
1064
+ dbg("\u6298\u53E0-\u64A4\u56DE\u5931\u8D25", { messageId: m.messageId, error: String(e) });
766
1065
  }
767
1066
  }
768
1067
  }
1068
+ dbg("\u6298\u53E0-\u64A4\u56DE\u5B8C\u6210", { gid, total: messages.length, deleted: deletedCount });
769
1069
  });
770
1070
  ctx.command("\u4E3D\u4E3D\u5BF9\u51B3 <user:user>", "\u53D1\u8D77\u5BF9\u51B3").action(async ({ session }, targetUser) => {
771
1071
  if (!session?.userId) return;
@@ -1037,76 +1337,396 @@ ${import_koishi.segment.at(targetId)} \u5DF2\u88AB\u89E3\u9664\u7981\u8A00\u3002
1037
1337
  }
1038
1338
  return next();
1039
1339
  });
1040
- ctx.command("\u521B\u5EFA\u63A5\u9F99 <content:string>", "\u521B\u5EFA\u7FA4\u63A5\u9F99\uFF0C\u521B\u5EFA\u8005\u81EA\u52A8\u4E3A\u7B2C\u4E00\u4F4D").action(async ({ session }, content) => {
1340
+ ctx.command("\u521B\u5EFA\u63A5\u9F99 <content:text>", "\u521B\u5EFA\u7FA4\u63A5\u9F99\uFF0C\u652F\u6301\u622A\u6B62\u65F6\u95F4\uFF08\u5982 21:00 \u6216 06-30 21:00\uFF09\uFF0C\u4E0D\u586B\u9ED8\u8BA4\u5F53\u5929\u914D\u7F6E\u7684\u5C0F\u65F6\u6574\u70B9").action(async ({ session }, content) => {
1041
1341
  if (!session?.userId) return;
1042
1342
  const gid = getGroupId(session);
1043
1343
  const cc = config.chain;
1044
1344
  if (!cc.enabled) return;
1045
1345
  if (!isGroupAllowed(gid, cc.groups, cc.groupMode)) return;
1046
1346
  if (!content || !content.trim()) {
1047
- return "\u{1F4DD} \u8BF7\u63D0\u4F9B\u63A5\u9F99\u5185\u5BB9\uFF0C\u4F8B\u5982\uFF1A\u521B\u5EFA\u63A5\u9F99 \u4ECA\u5929\u4E2D\u5348\u5403\u4EC0\u4E48";
1347
+ return "\u{1F4DD} \u683C\u5F0F\uFF1A\u521B\u5EFA\u63A5\u9F99 \u5185\u5BB9 [\u622A\u6B62\u65F6\u95F4]\n\u622A\u6B62\u65F6\u95F4\u793A\u4F8B\uFF1A21:00 \u6216 06-30 21:00\uFF0824\u5C0F\u65F6\u5236\uFF09";
1048
1348
  }
1049
- const chainContent = content.trim();
1349
+ const raw = content.trim();
1350
+ const timePattern = /(\d{1,2}[::]\d{2}|\d{1,2}-\d{1,2}\s+\d{1,2}[::]\d{2})$/;
1351
+ const timeMatch = raw.match(timePattern);
1352
+ let chainContent;
1353
+ let deadline;
1354
+ if (timeMatch) {
1355
+ chainContent = raw.slice(0, timeMatch.index).trim();
1356
+ const parsed = parseDeadline(timeMatch[1], cc.defaultDeadlineHour);
1357
+ deadline = parsed ?? getDefaultDeadline(cc.defaultDeadlineHour);
1358
+ } else {
1359
+ chainContent = raw;
1360
+ deadline = getDefaultDeadline(cc.defaultDeadlineHour);
1361
+ }
1362
+ if (!chainContent) {
1363
+ return "\u{1F4DD} \u8BF7\u63D0\u4F9B\u63A5\u9F99\u5185\u5BB9\u3002\u683C\u5F0F\uFF1A\u521B\u5EFA\u63A5\u9F99 \u5185\u5BB9 [\u622A\u6B62\u65F6\u95F4]";
1364
+ }
1365
+ const dialogueMatch = chainContent.match(/^#(\d+)\s*/);
1366
+ if (dialogueMatch) {
1367
+ const dialogueIndex = parseInt(dialogueMatch[1]);
1368
+ chainContent = await resolveDialogueChainContent(session, gid, dialogueIndex);
1369
+ if (chainContent === null) {
1370
+ return `\u{1F4DD} \u672A\u627E\u5230\u7B2C ${dialogueIndex} \u4E2A\u95EE\u7B54\u5BF9\uFF0C\u6216 dialogue \u63D2\u4EF6\u672A\u52A0\u8F7D\u3002`;
1371
+ }
1372
+ }
1373
+ await cleanupExpiredChains(gid);
1050
1374
  const userId = String(session.userId);
1051
1375
  const userName = session.username || userId;
1052
- chainStates.set(gid, {
1376
+ const chainId = getNextChainId(gid);
1377
+ const trueId = nextTrueId();
1378
+ const chain = {
1379
+ id: chainId,
1380
+ trueId,
1053
1381
  content: chainContent,
1054
1382
  creatorId: userId,
1055
1383
  creatorName: userName,
1056
1384
  participants: [{ userId, userName }],
1057
- createdAt: Date.now()
1385
+ createdAt: Date.now(),
1386
+ deadline
1387
+ };
1388
+ const chains = chainStates.get(gid) || [];
1389
+ chains.push(chain);
1390
+ chainStates.set(gid, chains);
1391
+ await saveChain(gid, chain);
1392
+ await scheduleChainDeadline(gid, chain, session);
1393
+ await recordChainParticipation(gid, userId, userName, chainContent);
1394
+ dbg("\u63A5\u9F99-\u521B\u5EFA", { gid, chainId, creatorId: userId, content: chainContent, deadline: new Date(deadline).toLocaleString("zh-CN") });
1395
+ const deadlineStr = new Date(deadline).toLocaleString("zh-CN", {
1396
+ month: "2-digit",
1397
+ day: "2-digit",
1398
+ hour: "2-digit",
1399
+ minute: "2-digit"
1058
1400
  });
1059
- dbg("\u63A5\u9F99-\u521B\u5EFA", { gid, creatorId: userId, content: chainContent });
1060
- return formatChain(chainStates.get(gid));
1401
+ return `\u{1F4DD} \u63A5\u9F99${chainId} \u5DF2\u521B\u5EFA\uFF01\u622A\u6B62 ${deadlineStr}
1402
+
1403
+ ${formatChain(chain)}`;
1061
1404
  });
1062
- ctx.command("\u67E5\u770B\u63A5\u9F99", "\u67E5\u770B\u5F53\u524D\u7FA4\u63A5\u9F99").action(async ({ session }) => {
1405
+ ctx.command("\u67E5\u770B\u63A5\u9F99 [query:text]", "\u67E5\u770B\u5F53\u524D\u7FA4\u63A5\u9F99\uFF08\u53EF\u6307\u5B9A\u63A5\u9F99\u7F16\u53F7\uFF09").action(async ({ session }, query) => {
1063
1406
  const gid = getGroupId(session);
1064
1407
  const cc = config.chain;
1065
1408
  if (!cc.enabled) return;
1409
+ if (query && /^JL\d{4}$/.test(query)) {
1410
+ return await handleScheduleDeadline(query);
1411
+ }
1066
1412
  if (!isGroupAllowed(gid, cc.groups, cc.groupMode)) return;
1067
- const chain = chainStates.get(gid);
1068
- if (!chain) return '\u{1F4DD} \u672C\u7FA4\u6682\u65E0\u63A5\u9F99\uFF0C\u4F7F\u7528"\u521B\u5EFA\u63A5\u9F99 <\u5185\u5BB9>"\u5F00\u59CB\u4E00\u4E2A\u5427\uFF01';
1069
- return formatChain(chain);
1070
- });
1071
- ctx.command("\u9000\u51FA\u63A5\u9F99", "\u9000\u51FA\u5F53\u524D\u7FA4\u63A5\u9F99").alias("\u53D6\u6D88\u63A5\u9F99").action(async ({ session }) => {
1413
+ await cleanupExpiredChains(gid);
1414
+ const chains = chainStates.get(gid);
1415
+ if (!chains || chains.length === 0) return "\u{1F4DD} \u672C\u7FA4\u6682\u65E0\u8FDB\u884C\u4E2D\u7684\u63A5\u9F99\u3002";
1416
+ if (query && /^\d+$/.test(query)) {
1417
+ const index = parseInt(query);
1418
+ const chain = chains.find((c) => c.id === index);
1419
+ if (!chain) return `\u{1F4DD} \u63A5\u9F99${index} \u4E0D\u5B58\u5728\u3002\u5F53\u524D\u63A5\u9F99\u7F16\u53F7\uFF1A${chains.map((c) => c.id).join("\u3001")}`;
1420
+ return formatChain(chain);
1421
+ }
1422
+ if (chains.length === 1) return formatChain(chains[0]);
1423
+ const lines = [`\u{1F4DD} \u672C\u7FA4\u6709 ${chains.length} \u4E2A\u8FDB\u884C\u4E2D\u7684\u63A5\u9F99\uFF1A`];
1424
+ for (const c of chains) {
1425
+ const deadlineStr = new Date(c.deadline).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
1426
+ lines.push(`${c.id}. ${c.content}\uFF08${c.participants.length}\u4EBA\u53C2\u4E0E\uFF0C\u622A\u6B62 ${deadlineStr}\uFF09`);
1427
+ }
1428
+ lines.push('\u56DE\u590D"\u63A5\u9F99N"\u53C2\u4E0E\u5BF9\u5E94\u7F16\u53F7\u7684\u63A5\u9F99\uFF0C\u5982"\u63A5\u9F991"');
1429
+ return lines.join("\n");
1430
+ }).usage("\u67E5\u770B\u63A5\u9F99 / \u67E5\u770B\u63A5\u9F99 1 \u2014 \u67E5\u770B\u672C\u7FA4\u63A5\u9F99\n\uFF08JL\u5F00\u5934\u7684 trueID \u4EC5\u4F9B schedule \u5185\u90E8\u4F7F\u7528\uFF0C\u4E0D\u5916\u663E\uFF09");
1431
+ async function handleScheduleDeadline(trueId) {
1432
+ for (const [chainGid, chains] of chainStates) {
1433
+ const chain = chains.find((c) => c.trueId === trueId);
1434
+ if (chain) {
1435
+ await deactivateChain(chainGid, chain.id);
1436
+ const newChains = chains.filter((c) => c.trueId !== trueId);
1437
+ if (newChains.length === 0) chainStates.delete(chainGid);
1438
+ else chainStates.set(chainGid, newChains);
1439
+ try {
1440
+ const finalList = formatChain(chain);
1441
+ const bots = ctx.bots ? [...ctx.bots] : [ctx.bot].filter(Boolean);
1442
+ for (const bot of bots) {
1443
+ try {
1444
+ await bot.sendMessage(chainGid, `\u23F0 \u65F6\u95F4\u5230\uFF01\u63A5\u9F99${chain.id} \u5DF2\u622A\u6B62\u3002
1445
+
1446
+ ${finalList}`);
1447
+ break;
1448
+ } catch {
1449
+ }
1450
+ }
1451
+ } catch (e) {
1452
+ dbg("\u63A5\u9F99-\u622A\u6B62\u901A\u77E5\u5931\u8D25", { gid: chainGid, trueId, chainId: chain.id, error: String(e) });
1453
+ }
1454
+ return;
1455
+ }
1456
+ }
1457
+ try {
1458
+ const rows = await ctx.database.get("lili_chain", { trueId, active: true });
1459
+ if (rows.length > 0) {
1460
+ const row = rows[0];
1461
+ await deactivateChain(row.gid, row.chainId);
1462
+ const chain = {
1463
+ id: row.chainId,
1464
+ trueId: row.trueId,
1465
+ content: row.content,
1466
+ creatorId: row.creatorId,
1467
+ creatorName: row.creatorName,
1468
+ participants: JSON.parse(row.participants || "[]"),
1469
+ createdAt: row.createdAt,
1470
+ deadline: row.deadline
1471
+ };
1472
+ try {
1473
+ const finalList = formatChain(chain);
1474
+ const bots = ctx.bots ? [...ctx.bots] : [ctx.bot].filter(Boolean);
1475
+ for (const bot of bots) {
1476
+ try {
1477
+ await bot.sendMessage(row.gid, `\u23F0 \u65F6\u95F4\u5230\uFF01\u63A5\u9F99${chain.id} \u5DF2\u622A\u6B62\u3002
1478
+
1479
+ ${finalList}`);
1480
+ break;
1481
+ } catch {
1482
+ }
1483
+ }
1484
+ } catch (e) {
1485
+ dbg("\u63A5\u9F99-\u622A\u6B62\u901A\u77E5\u5931\u8D25(DB)", { gid: row.gid, trueId, error: String(e) });
1486
+ }
1487
+ }
1488
+ } catch {
1489
+ }
1490
+ }
1491
+ function parseChainCancelArgs(session) {
1492
+ let targetUserId;
1493
+ if (session.elements) {
1494
+ const atEl = session.elements.find((el) => el.type === "at" && el.attrs?.id);
1495
+ if (atEl) targetUserId = atEl.attrs.id;
1496
+ }
1497
+ const content = session.content || "";
1498
+ const text = content.replace(/\[CQ:at[^\]]*\]/g, "").replace(/^取消接龙\s*/, "").trim();
1499
+ if (!text) return { targetUserId };
1500
+ const parts = text.split(/\s+/);
1501
+ if (!targetUserId) {
1502
+ const lastPart = parts[parts.length - 1];
1503
+ if (/^\d{5,12}$/.test(lastPart)) {
1504
+ targetUserId = lastPart;
1505
+ parts.pop();
1506
+ }
1507
+ }
1508
+ if (parts.length > 0 && /^\d+$/.test(parts[0])) {
1509
+ const chainIndex = parseInt(parts[0]);
1510
+ if (chainIndex >= 1 && chainIndex <= 999) return { chainIndex, targetUserId };
1511
+ }
1512
+ return { targetUserId };
1513
+ }
1514
+ ctx.command("\u53D6\u6D88\u63A5\u9F99", "\u9000\u51FA\uFF08\u6216\u5E2E\u4EBA\u9000\u51FA\uFF09\u7FA4\u63A5\u9F99\u3002\u683C\u5F0F\uFF1A\u53D6\u6D88\u63A5\u9F99 / \u53D6\u6D88\u63A5\u9F99 1 / \u53D6\u6D88\u63A5\u9F99 @\u7528\u6237 / \u53D6\u6D88\u63A5\u9F99 1 123456").alias("\u9000\u51FA\u63A5\u9F99").action(async ({ session }) => {
1072
1515
  const gid = getGroupId(session);
1073
1516
  const cc = config.chain;
1074
1517
  if (!cc.enabled) return;
1075
1518
  if (!isGroupAllowed(gid, cc.groups, cc.groupMode)) return;
1076
- const chain = chainStates.get(gid);
1077
- if (!chain) return "\u{1F4DD} \u672C\u7FA4\u6682\u65E0\u63A5\u9F99\u3002";
1078
- const userId = String(session.userId);
1079
- const idx = chain.participants.findIndex((p) => p.userId === userId);
1080
- if (idx === -1) return "\u{1F4DD} \u4F60\u6CA1\u6709\u53C2\u4E0E\u8FD9\u4E2A\u63A5\u9F99\u3002";
1081
- const userName = chain.participants[idx].userName;
1519
+ await cleanupExpiredChains(gid);
1520
+ const chains = chainStates.get(gid);
1521
+ if (!chains || chains.length === 0) return "\u{1F4DD} \u672C\u7FA4\u6682\u65E0\u8FDB\u884C\u4E2D\u7684\u63A5\u9F99\u3002";
1522
+ const { chainIndex, targetUserId } = parseChainCancelArgs(session);
1523
+ const selfId = String(session.userId);
1524
+ const opTarget = targetUserId || selfId;
1525
+ const isSelf = opTarget === selfId;
1526
+ let chain;
1527
+ if (chainIndex !== void 0) {
1528
+ chain = chains.find((c) => c.id === chainIndex);
1529
+ if (!chain) return `\u{1F4DD} \u63A5\u9F99${chainIndex} \u4E0D\u5B58\u5728\u3002\u5F53\u524D\u63A5\u9F99\u7F16\u53F7\uFF1A${chains.map((c) => c.id).join("\u3001")}`;
1530
+ } else if (chains.length === 1) {
1531
+ chain = chains[0];
1532
+ } else {
1533
+ const targetChains = chains.filter((c) => c.participants.some((p2) => p2.userId === opTarget));
1534
+ if (targetChains.length === 0) {
1535
+ return isSelf ? "\u{1F4DD} \u4F60\u6CA1\u6709\u53C2\u4E0E\u4EFB\u4F55\u63A5\u9F99\u3002" : "\u{1F4DD} \u8BE5\u7528\u6237\u672A\u53C2\u4E0E\u4EFB\u4F55\u63A5\u9F99\u3002";
1536
+ }
1537
+ if (targetChains.length === 1) {
1538
+ chain = targetChains[0];
1539
+ } else {
1540
+ const cmd = isSelf ? "\u53D6\u6D88\u63A5\u9F99" : "\u53D6\u6D88\u63A5\u9F99";
1541
+ const suffix = isSelf ? "" : ` @\u7528\u6237`;
1542
+ return `\u{1F4DD} ${isSelf ? "\u4F60" : "\u8BE5\u7528\u6237"}\u53C2\u4E0E\u4E86\u591A\u4E2A\u63A5\u9F99\uFF0C\u8BF7\u6307\u5B9A\u7F16\u53F7\uFF1A${targetChains.map((c) => `${cmd} ${c.id}${suffix}`).join(" \u6216 ")}`;
1543
+ }
1544
+ }
1545
+ const idx = chain.participants.findIndex((p2) => p2.userId === opTarget);
1546
+ if (idx === -1) {
1547
+ return isSelf ? `\u{1F4DD} \u4F60\u6CA1\u6709\u53C2\u4E0E\u63A5\u9F99${chain.id}\u3002` : `\u{1F4DD} \u8BE5\u7528\u6237\u6CA1\u6709\u53C2\u4E0E\u63A5\u9F99${chain.id}\u3002`;
1548
+ }
1549
+ const p = chain.participants[idx];
1082
1550
  chain.participants.splice(idx, 1);
1083
- dbg("\u63A5\u9F99-\u9000\u51FA", { gid, userId, userName, remaining: chain.participants.length });
1551
+ dbg("\u63A5\u9F99-\u53D6\u6D88", { gid, chainId: chain.id, opIsSelf: isSelf, userId: p.userId, userName: p.userName, remaining: chain.participants.length });
1084
1552
  if (chain.participants.length === 0) {
1085
- chainStates.delete(gid);
1086
- return "\u{1F4DD} \u4F60\u9000\u51FA\u4E86\u63A5\u9F99\uFF0C\u63A5\u9F99\u5DF2\u56E0\u65E0\u4EBA\u53C2\u4E0E\u800C\u7ED3\u675F\u3002";
1553
+ const newChains = chains.filter((c) => c.id !== chain.id);
1554
+ if (newChains.length === 0) chainStates.delete(gid);
1555
+ else chainStates.set(gid, newChains);
1556
+ await deactivateChain(gid, chain.id);
1557
+ const who = isSelf ? `\u4F60` : `${p.userName}`;
1558
+ return `\u{1F4DD} ${who}${isSelf ? "" : " \u88AB"}\u9000\u51FA\u4E86\u63A5\u9F99${chain.id}\uFF0C\u8BE5\u63A5\u9F99\u5DF2\u56E0\u65E0\u4EBA\u53C2\u4E0E\u800C\u7ED3\u675F\u3002`;
1087
1559
  }
1088
- await session.send(`\u{1F4DD} ${userName} \u5DF2\u9000\u51FA\u63A5\u9F99\u3002
1560
+ await saveChain(gid, chain);
1561
+ if (isSelf) return `\u{1F4DD} \u4F60\u5DF2\u9000\u51FA\u63A5\u9F99${chain.id}\u3002
1089
1562
 
1090
- ${formatChain(chain)}`);
1563
+ ${formatChain(chain)}`;
1564
+ return `\u{1F4DD} ${p.userName} \u5DF2\u88AB\u79FB\u51FA\u63A5\u9F99${chain.id}\u3002
1565
+
1566
+ ${formatChain(chain)}`;
1567
+ });
1568
+ ctx.command("\u63A5\u9F99\u699C", "\u67E5\u770B\u672C\u7FA4\u63A5\u9F99\u6392\u884C\u699C\uFF08\u7EDF\u8BA1\u6BCF\u4EBA\u53C2\u4E0E\u6B21\u6570\uFF09").action(async ({ session }) => {
1569
+ const gid = getGroupId(session);
1570
+ const cc = config.chain;
1571
+ if (!cc.enabled) return;
1572
+ if (!isGroupAllowed(gid, cc.groups, cc.groupMode)) return;
1573
+ try {
1574
+ const records = await ctx.database.get("lili_chain_record", { gid });
1575
+ if (!records || records.length === 0) return "\u{1F4DD} \u672C\u7FA4\u6682\u65E0\u63A5\u9F99\u8BB0\u5F55\u3002";
1576
+ const userStats = /* @__PURE__ */ new Map();
1577
+ for (const r of records) {
1578
+ const existing = userStats.get(r.userId);
1579
+ if (existing) {
1580
+ existing.count++;
1581
+ if (r.timestamp > existing.lastTime) existing.lastTime = r.timestamp;
1582
+ } else {
1583
+ userStats.set(r.userId, { userName: r.userName, count: 1, lastTime: r.timestamp });
1584
+ }
1585
+ }
1586
+ const sorted = [...userStats.entries()].sort((a, b) => b[1].count - a[1].count);
1587
+ const lines = ["\u{1F3C6} \u7FA4\u63A5\u9F99\u6392\u884C\u699C"];
1588
+ for (let i = 0; i < sorted.length; i++) {
1589
+ const [userId, stat] = sorted[i];
1590
+ const lastStr = new Date(stat.lastTime).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
1591
+ const medal = i === 0 ? "\u{1F947}" : i === 1 ? "\u{1F948}" : i === 2 ? "\u{1F949}" : `${i + 1}.`;
1592
+ lines.push(`${medal} ${stat.userName} \u2014 ${stat.count} \u6B21\uFF08\u6700\u8FD1\uFF1A${lastStr}\uFF09`);
1593
+ }
1594
+ return lines.join("\n");
1595
+ } catch (e) {
1596
+ dbg("\u63A5\u9F99\u699C\u67E5\u8BE2\u5931\u8D25", { gid, error: String(e) });
1597
+ return "\u{1F4DD} \u67E5\u8BE2\u6392\u884C\u699C\u5931\u8D25\u3002";
1598
+ }
1599
+ });
1600
+ ctx.command("\u6E05\u9664\u6D88\u606F\u8BB0\u5F55", "\u6E05\u9664 lili_message \u6570\u636E\u5E93\uFF08\u6C34\u8FC7\u4E86/\u6298\u53E0\u7684\u6D88\u606F\u8BB0\u5F55\uFF09").action(async () => {
1601
+ try {
1602
+ await ctx.database.remove("lili_message", {});
1603
+ return "\u{1F9F9} \u5DF2\u6E05\u9664\u5168\u90E8\u6D88\u606F\u8BB0\u5F55\u3002";
1604
+ } catch (e) {
1605
+ dbg("\u6E05\u9664\u6D88\u606F\u8BB0\u5F55\u5931\u8D25", { error: String(e) });
1606
+ return "\u{1F9F9} \u6E05\u9664\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6570\u636E\u5E93\u8FDE\u63A5\u3002";
1607
+ }
1091
1608
  });
1092
1609
  ctx.middleware(async (session, next) => {
1093
1610
  if (!config.chain.enabled) return next();
1094
1611
  const gid = getGroupId(session);
1095
1612
  if (!gid) return next();
1096
1613
  if (!isGroupAllowed(gid, config.chain.groups, config.chain.groupMode)) return next();
1097
- const text = (session.content || "").trim();
1098
- if (text !== "\u63A5\u9F99") return next();
1099
- const chain = chainStates.get(gid);
1100
- if (!chain) return next();
1101
- const userId = String(session.userId);
1102
- if (chain.participants.some((p) => p.userId === userId)) {
1103
- await session.send("\u{1F4DD} \u4F60\u5DF2\u7ECF\u53C2\u4E0E\u8FC7\u8FD9\u4E2A\u63A5\u9F99\u4E86\uFF01");
1614
+ const rawText = (session.content || "").trim();
1615
+ const modifyPattern = /^接龙(\d+)\s*~\s*(.+)$/;
1616
+ const modifyMatch = rawText.match(modifyPattern);
1617
+ if (modifyMatch) {
1618
+ await cleanupExpiredChains(gid);
1619
+ const chains2 = chainStates.get(gid);
1620
+ if (!chains2 || chains2.length === 0) {
1621
+ await session.send("\u{1F4DD} \u672C\u7FA4\u6682\u65E0\u8FDB\u884C\u4E2D\u7684\u63A5\u9F99\u3002");
1622
+ return;
1623
+ }
1624
+ const chainIdx = parseInt(modifyMatch[1]);
1625
+ const newContent = modifyMatch[2].trim();
1626
+ const chain2 = chains2.find((c) => c.id === chainIdx);
1627
+ if (!chain2) {
1628
+ await session.send(`\u{1F4DD} \u63A5\u9F99${chainIdx} \u4E0D\u5B58\u5728\u3002\u5F53\u524D\u63A5\u9F99\u7F16\u53F7\uFF1A${chains2.map((c) => c.id).join("\u3001")}`);
1629
+ return;
1630
+ }
1631
+ const userId2 = String(session.userId);
1632
+ if (chain2.creatorId !== userId2) {
1633
+ await session.send("\u{1F4DD} \u53EA\u6709\u63A5\u9F99\u521B\u5EFA\u8005\u624D\u80FD\u4FEE\u6539\u5185\u5BB9\u3002");
1634
+ return;
1635
+ }
1636
+ if (Date.now() > chain2.deadline) {
1637
+ await session.send(`\u{1F4DD} \u63A5\u9F99${chain2.id} \u5DF2\u622A\u6B62\uFF0C\u65E0\u6CD5\u4FEE\u6539\u3002`);
1638
+ return;
1639
+ }
1640
+ const oldContent = chain2.content;
1641
+ chain2.content = newContent;
1642
+ await saveChain(gid, chain2);
1643
+ dbg("\u63A5\u9F99-\u4FEE\u6539", { gid, chainId: chain2.id, userId: userId2, oldContent, newContent });
1644
+ await session.send(`\u{1F4DD} \u63A5\u9F99${chain2.id} \u5185\u5BB9\u5DF2\u66F4\u65B0\uFF1A${oldContent} \u2192 ${newContent}
1645
+
1646
+ ${formatChain(chain2)}`);
1104
1647
  return;
1105
1648
  }
1106
- const userName = session.username || userId;
1107
- chain.participants.push({ userId, userName });
1108
- dbg("\u63A5\u9F99-\u53C2\u4E0E", { gid, userId, participantCount: chain.participants.length });
1109
- await session.send(formatChain(chain));
1649
+ const chainPattern = /^接龙(\d*)\s*(.*)$/;
1650
+ const match = rawText.match(chainPattern);
1651
+ if (!match) return next();
1652
+ await cleanupExpiredChains(gid);
1653
+ const chains = chainStates.get(gid);
1654
+ if (!chains || chains.length === 0) return next();
1655
+ const chainIndexStrPart = match[1] || "";
1656
+ const restRaw = match[2] || "";
1657
+ let helpTargetId;
1658
+ let restText = rawText.replace(/\[CQ:at[^\]]*\]/g, "").replace(/^接龙\d*\s*/, "").trim();
1659
+ if (session.elements) {
1660
+ const atEl = session.elements.find((el) => el.type === "at" && el.attrs?.id);
1661
+ if (atEl) {
1662
+ helpTargetId = atEl.attrs.id;
1663
+ restText = restText.replace(/@\S+/g, "").trim();
1664
+ }
1665
+ }
1666
+ if (!helpTargetId && restText) {
1667
+ const parts = restText.split(/\s+/);
1668
+ const lastPart = parts[parts.length - 1];
1669
+ if (/^\d{5,12}$/.test(lastPart)) {
1670
+ helpTargetId = lastPart;
1671
+ parts.pop();
1672
+ restText = parts.join(" ").trim();
1673
+ }
1674
+ }
1675
+ restText = restText.replace(/@\S+/g, "").replace(/\[CQ:at[^\]]*\]/g, "").replace(/<at[^>]*\/?>/g, "").trim();
1676
+ let remark;
1677
+ if (restText) {
1678
+ const remarkMatch = restText.match(/^[((](.+)[))]$/);
1679
+ if (remarkMatch) {
1680
+ remark = remarkMatch[1].trim();
1681
+ } else {
1682
+ remark = restText || void 0;
1683
+ }
1684
+ }
1685
+ const chainIndexStr = chainIndexStrPart;
1686
+ const userId = helpTargetId || String(session.userId);
1687
+ let userName;
1688
+ if (helpTargetId && helpTargetId !== String(session.userId)) {
1689
+ const atEl = session.elements?.find((el) => el.type === "at" && String(el.attrs?.id) === String(helpTargetId));
1690
+ userName = atEl?.attrs?.name || helpTargetId;
1691
+ } else {
1692
+ userName = session.username || String(session.userId);
1693
+ }
1694
+ let chain;
1695
+ if (chainIndexStr) {
1696
+ const idx = parseInt(chainIndexStr);
1697
+ chain = chains.find((c) => c.id === idx);
1698
+ if (!chain) {
1699
+ await session.send(`\u{1F4DD} \u63A5\u9F99${idx} \u4E0D\u5B58\u5728\u3002\u5F53\u524D\u63A5\u9F99\u7F16\u53F7\uFF1A${chains.map((c) => c.id).join("\u3001")}`);
1700
+ return;
1701
+ }
1702
+ } else if (chains.length === 1) {
1703
+ chain = chains[0];
1704
+ } else {
1705
+ const lines = [helpTargetId ? `\u{1F4DD} \u5E2E ${userName} \u63A5\u9F99\uFF1F\u672C\u7FA4\u6709 ${chains.length} \u4E2A\u8FDB\u884C\u4E2D\u7684\u63A5\u9F99\uFF0C\u8BF7\u6307\u5B9A\u7F16\u53F7\uFF1A` : `\u{1F4DD} \u672C\u7FA4\u6709 ${chains.length} \u4E2A\u8FDB\u884C\u4E2D\u7684\u63A5\u9F99\uFF0C\u8BF7\u6307\u5B9A\u7F16\u53F7\uFF1A`];
1706
+ for (const c of chains) {
1707
+ const deadlineStr = new Date(c.deadline).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
1708
+ lines.push(`\u63A5\u9F99${c.id}\uFF1A${c.content}\uFF08${c.participants.length}\u4EBA\u53C2\u4E0E\uFF0C\u622A\u6B62 ${deadlineStr}\uFF09`);
1709
+ }
1710
+ const prefix = helpTargetId ? "\u63A5\u9F99" : "";
1711
+ lines.push(`\u56DE\u590D"${prefix}\u63A5\u9F991"\u3001"${prefix}\u63A5\u9F992"\u7B49\u53C2\u4E0E\u5BF9\u5E94\u63A5\u9F99`);
1712
+ await session.send(lines.join("\n"));
1713
+ return;
1714
+ }
1715
+ if (Date.now() > chain.deadline) {
1716
+ await session.send(`\u{1F4DD} \u63A5\u9F99${chain.id} \u5DF2\u622A\u6B62\uFF01`);
1717
+ return;
1718
+ }
1719
+ if (chain.participants.some((p) => String(p.userId) === String(userId))) {
1720
+ await session.send(helpTargetId ? `\u{1F4DD} ${userName} \u5DF2\u7ECF\u53C2\u4E0E\u8FC7\u63A5\u9F99${chain.id}\u4E86\uFF01` : `\u{1F4DD} \u4F60\u5DF2\u7ECF\u53C2\u4E0E\u8FC7\u63A5\u9F99${chain.id}\u4E86\uFF01`);
1721
+ return;
1722
+ }
1723
+ const finalUserId = String(userId);
1724
+ chain.participants.push({ userId: finalUserId, userName, remark });
1725
+ dbg("\u63A5\u9F99-\u53C2\u4E0E", { gid, chainId: chain.id, userId: finalUserId, remark, helpTarget: !!helpTargetId, participantCount: chain.participants.length });
1726
+ await saveChain(gid, chain);
1727
+ await recordChainParticipation(gid, finalUserId, userName, chain.content);
1728
+ const whoStr = helpTargetId ? `\u5DF2\u5E2E ${userName} \u52A0\u5165\u63A5\u9F99${chain.id}` : "";
1729
+ await session.send(`${whoStr ? "\u{1F4DD} " + whoStr + "\n\n" : ""}${formatChain(chain)}`);
1110
1730
  return;
1111
1731
  });
1112
1732
  ctx.middleware(async (session, next) => {
@@ -1207,29 +1827,67 @@ ${formatChain(chain)}`);
1207
1827
  });
1208
1828
  if (isFwd && forwardTexts.length > 0) {
1209
1829
  dbg("\u67E5\u91CD\u68C0\u6D4B", { gid, forwardTextsCount: forwardTexts.length });
1830
+ const textContents = [];
1831
+ const imageContents = [];
1210
1832
  for (const fwdText of forwardTexts) {
1211
1833
  if (!fwdText) continue;
1212
- try {
1213
- const sevenDaysAgoMs = Date.now() - 7 * 24 * 60 * 60 * 1e3;
1214
- const recentMsgs = await ctx.database.get("lili_message", { gid, timestamp: { $gte: sevenDaysAgoMs } }, { sort: { timestamp: "asc" }, limit: 1e3 });
1215
- const match = recentMsgs.find((m) => {
1216
- if (m.content === fwdText) return true;
1217
- return normalizeText(m.content) === fwdText;
1218
- });
1219
- dbg("\u67E5\u91CD\u67E5\u8BE2", { gid, fwdText: fwdText.substring(0, 50), recentMsgCount: recentMsgs?.length || 0, matched: !!match });
1220
- if (match) {
1221
- const original = match;
1222
- const dateStr = new Date(original.timestamp).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
1223
- await session.send([
1224
- (0, import_koishi.h)("quote", { id: original.messageId }),
1225
- (0, import_koishi.h)("at", { id: session.userId }),
1226
- import_koishi.h.text(` \u{1F4A7} \u6C34\u8FC7\u4E86\uFF01\u8FD9\u6761\u6D88\u606F\u5728 ${dateStr} \u5C31\u53D1\u8FC7\u4E86\uFF01`)
1227
- ]);
1228
- return next();
1834
+ if (fwdText.startsWith("[img:")) {
1835
+ imageContents.push(fwdText);
1836
+ } else if (fwdText.startsWith("[face:") || fwdText.startsWith("[mface:")) {
1837
+ } else {
1838
+ textContents.push(fwdText);
1839
+ }
1840
+ }
1841
+ const sevenDaysAgoMs = Date.now() - 7 * 24 * 60 * 60 * 1e3;
1842
+ let recentMsgs = [];
1843
+ try {
1844
+ recentMsgs = await ctx.database.get("lili_message", { gid, timestamp: { $gte: sevenDaysAgoMs } }, { sort: { timestamp: "asc" }, limit: 1e3 });
1845
+ } catch (e) {
1846
+ dbg("\u67E5\u91CD\u67E5\u8BE2\u5F02\u5E38", { gid, error: String(e) });
1847
+ }
1848
+ let matched = null;
1849
+ for (const fwdText of textContents) {
1850
+ if (fwdText.length <= 30) continue;
1851
+ const match = recentMsgs.find((m) => {
1852
+ if (m.content === fwdText) return true;
1853
+ return normalizeText(m.content) === fwdText;
1854
+ });
1855
+ if (match) {
1856
+ matched = match;
1857
+ dbg("\u67E5\u91CD\u547D\u4E2D-\u6587\u5B57", { gid, fwdText: fwdText.substring(0, 50), length: fwdText.length });
1858
+ break;
1859
+ }
1860
+ }
1861
+ if (!matched && imageContents.length >= 2) {
1862
+ let imageMatchCount = 0;
1863
+ let firstImgMatch = null;
1864
+ for (const imgContent of imageContents) {
1865
+ const found = recentMsgs.find((m) => m.content === imgContent || normalizeText(m.content) === imgContent);
1866
+ if (found) {
1867
+ imageMatchCount++;
1868
+ if (!firstImgMatch) firstImgMatch = found;
1229
1869
  }
1230
- } catch (e) {
1231
- dbg("\u67E5\u91CD\u67E5\u8BE2\u5F02\u5E38", { gid, error: String(e) });
1232
1870
  }
1871
+ if (imageMatchCount >= 2) {
1872
+ matched = firstImgMatch;
1873
+ dbg("\u67E5\u91CD\u547D\u4E2D-\u56FE\u7247", { gid, imageMatchCount, totalImages: imageContents.length });
1874
+ }
1875
+ }
1876
+ dbg("\u67E5\u91CD\u7ED3\u679C", {
1877
+ gid,
1878
+ textCount: textContents.length,
1879
+ imageCount: imageContents.length,
1880
+ matched: !!matched
1881
+ });
1882
+ if (matched) {
1883
+ const original = matched;
1884
+ const dateStr = new Date(original.timestamp).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
1885
+ await session.send([
1886
+ (0, import_koishi.h)("quote", { id: original.messageId }),
1887
+ (0, import_koishi.h)("at", { id: session.userId }),
1888
+ import_koishi.h.text(` \u{1F4A7} \u6C34\u8FC7\u4E86\uFF01\u8FD9\u6761\u6D88\u606F\u5728 ${dateStr} \u5C31\u53D1\u8FC7\u4E86\uFF01`)
1889
+ ]);
1890
+ return next();
1233
1891
  }
1234
1892
  }
1235
1893
  }
@@ -1276,7 +1934,7 @@ ${formatChain(chain)}`);
1276
1934
  if (shouldRecord && !isFwd) {
1277
1935
  const shouldStore = shouldRecordAll || normalizedText && textLen >= 15 && textLen <= 60;
1278
1936
  if (shouldStore) {
1279
- const storeContent = shouldRecordAll ? session.content || normalizedText || "" : normalizedText;
1937
+ const storeContent = shouldRecordAll ? elementsToCQCode(session.elements) || normalizedText || "" : normalizedText;
1280
1938
  if (storeContent) {
1281
1939
  try {
1282
1940
  await ctx.database.create("lili_message", {
@@ -1287,9 +1945,9 @@ ${formatChain(chain)}`);
1287
1945
  timestamp: Date.now(),
1288
1946
  messageId: session.messageId || ""
1289
1947
  });
1290
- dbg("\u6D88\u606F\u5DF2\u5165\u5E93", { gid, userId: String(session.userId), contentLen: storeContent.length, shouldRecordAll, messageId: session.messageId });
1948
+ dbg("\u6D88\u606F\u5DF2\u5165\u5E93", { gid, userId: String(session.userId), contentLen: storeContent.length, shouldRecordAll, messageId: session.messageId, contentPreview: storeContent.substring(0, 40) });
1291
1949
  } catch (e) {
1292
- dbg("\u6D88\u606F\u5165\u5E93\u5931\u8D25", { gid, error: String(e) });
1950
+ ctx.logger("lili-hub").warn(`[\u6D88\u606F\u5165\u5E93\u5931\u8D25] gid=${gid} userId=${session.userId} error=${String(e)}`);
1293
1951
  }
1294
1952
  }
1295
1953
  }
@@ -1326,11 +1984,29 @@ ${formatChain(chain)}`);
1326
1984
  } catch {
1327
1985
  }
1328
1986
  }, 36e5 * 6);
1987
+ ctx.setInterval(async () => {
1988
+ const now = Date.now();
1989
+ for (const [gid, chains] of chainStates) {
1990
+ const remaining = chains.filter((c) => c.deadline > now);
1991
+ if (remaining.length === 0) {
1992
+ chainStates.delete(gid);
1993
+ } else if (remaining.length !== chains.length) {
1994
+ chainStates.set(gid, remaining);
1995
+ }
1996
+ }
1997
+ }, 3e5);
1998
+ ctx.setInterval(async () => {
1999
+ try {
2000
+ await ctx.database.remove("lili_chain_record", { timestamp: { $lt: Date.now() - 90 * 24 * 60 * 60 * 1e3 } });
2001
+ } catch {
2002
+ }
2003
+ }, 36e5 * 12);
1329
2004
  }
1330
2005
  // Annotate the CommonJS export names for ESM import in node:
1331
2006
  0 && (module.exports = {
1332
2007
  Config,
1333
2008
  apply,
1334
2009
  inject,
1335
- name
2010
+ name,
2011
+ optional
1336
2012
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-lili-hub",
3
3
  "description": "丽丽Hub — 自用丽丽主题QQ群娱乐插件:俄罗斯轮盘赌、喝酒发酒疯、模仿群友说话、合并消息查重(水过了)",
4
- "version": "0.3.2",
4
+ "version": "0.3.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -28,7 +28,8 @@
28
28
  "en": "Lili Hub — Russian roulette, drinking games, speech imitation, and message dedup"
29
29
  },
30
30
  "service": {
31
- "required": ["database"]
31
+ "required": ["database"],
32
+ "optional": ["schedule", "dialogue"]
32
33
  }
33
34
  }
34
35
  }