@zhin.js/plugin-group-suite 0.1.0

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 (58) hide show
  1. package/README.md +69 -0
  2. package/lib/admin.d.ts +4 -0
  3. package/lib/admin.d.ts.map +1 -0
  4. package/lib/admin.js +123 -0
  5. package/lib/admin.js.map +1 -0
  6. package/lib/analysis.d.ts +83 -0
  7. package/lib/analysis.d.ts.map +1 -0
  8. package/lib/analysis.js +167 -0
  9. package/lib/analysis.js.map +1 -0
  10. package/lib/checkin.d.ts +4 -0
  11. package/lib/checkin.d.ts.map +1 -0
  12. package/lib/checkin.js +163 -0
  13. package/lib/checkin.js.map +1 -0
  14. package/lib/config.d.ts +71 -0
  15. package/lib/config.d.ts.map +1 -0
  16. package/lib/config.js +44 -0
  17. package/lib/config.js.map +1 -0
  18. package/lib/daily-analysis.d.ts +7 -0
  19. package/lib/daily-analysis.d.ts.map +1 -0
  20. package/lib/daily-analysis.js +346 -0
  21. package/lib/daily-analysis.js.map +1 -0
  22. package/lib/index.d.ts +2 -0
  23. package/lib/index.d.ts.map +1 -0
  24. package/lib/index.js +30 -0
  25. package/lib/index.js.map +1 -0
  26. package/lib/shared.d.ts +23 -0
  27. package/lib/shared.d.ts.map +1 -0
  28. package/lib/shared.js +30 -0
  29. package/lib/shared.js.map +1 -0
  30. package/lib/stats.d.ts +6 -0
  31. package/lib/stats.d.ts.map +1 -0
  32. package/lib/stats.js +244 -0
  33. package/lib/stats.js.map +1 -0
  34. package/lib/teach.d.ts +4 -0
  35. package/lib/teach.d.ts.map +1 -0
  36. package/lib/teach.js +252 -0
  37. package/lib/teach.js.map +1 -0
  38. package/package.json +69 -0
  39. package/plugin.yml +3 -0
  40. package/skills/checkin/SKILL.md +49 -0
  41. package/skills/group-daily-analysis/SKILL.md +29 -0
  42. package/src/admin.ts +145 -0
  43. package/src/analysis.ts +227 -0
  44. package/src/checkin.ts +191 -0
  45. package/src/config.ts +72 -0
  46. package/src/daily-analysis.ts +467 -0
  47. package/src/index.ts +33 -0
  48. package/src/shared.ts +42 -0
  49. package/src/stats.ts +288 -0
  50. package/src/teach.ts +259 -0
  51. package/tools/checkin-query/checkin-query.tool.md +11 -0
  52. package/tools/checkin-query/handler.ts +21 -0
  53. package/tools/checkin-rank/checkin-rank.tool.md +11 -0
  54. package/tools/checkin-rank/handler.ts +16 -0
  55. package/tools/stats-query/handler.ts +25 -0
  56. package/tools/stats-query/stats-query.tool.md +16 -0
  57. package/tools/stats-user/handler.ts +27 -0
  58. package/tools/stats-user/stats-user.tool.md +12 -0
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @zhin.js/plugin-group-suite
2
+
3
+ 群运营一体化插件(**破坏性合并**,替代以下独立包,不再维护):
4
+
5
+ - `@zhin.js/plugin-group-admin`
6
+ - `@zhin.js/plugin-checkin`
7
+ - `@zhin.js/plugin-stats`
8
+ - `@zhin.js/plugin-group-daily-analysis`
9
+ - `@zhin.js/plugin-teach`
10
+
11
+ ## 安装
12
+
13
+ ```bash
14
+ pnpm add @zhin.js/plugin-group-suite
15
+ ```
16
+
17
+ 旧包已在 npm 标记废弃(维护者执行):
18
+
19
+ ```bash
20
+ npm login # 或 export NODE_AUTH_TOKEN=...
21
+ node scripts/deprecate-group-legacy-packages.mjs
22
+ ```
23
+
24
+ ## 配置(扁平 `groupSuite`,多数项有默认值)
25
+
26
+ ```yaml
27
+ plugins:
28
+ - "@zhin.js/plugin-group-suite"
29
+
30
+ database:
31
+ dialect: sqlite
32
+ filename: ./data/bot.db
33
+
34
+ inbox:
35
+ enabled: true # 群日报需要
36
+
37
+ # 可整段省略,使用默认值
38
+ groupSuite:
39
+ noticeAdapters: [icqq]
40
+ autoAnalysisEnabled: true
41
+ autoAnalysisCron: "0 9 * * *"
42
+ ```
43
+
44
+ 常用可选字段(单层,无需 `admin:` / `checkin:` 等嵌套):
45
+
46
+ | 字段 | 默认 | 说明 |
47
+ |------|------|------|
48
+ | `welcome` | 欢迎新成员… | 入群欢迎语 |
49
+ | `recallNotify` | `true` | 撤回提示 |
50
+ | `keywordReply` | `false` | 内置关键词(建议用 teach) |
51
+ | `noticeAdapters` | `[icqq]` | 处理 notice 的适配器 |
52
+ | `basePointsMin` / `basePointsMax` | 10 / 30 | 签到积分 |
53
+ | `rankSize` | 10 | 签到与统计排行人数 |
54
+ | `statsRetentionDays` | 90 | 统计保留天数 |
55
+ | `analysisDays` | 1 | 群分析天数 |
56
+ | `analysisGroups` | `[]` | 日报白名单(空=不限制) |
57
+ | `analysisGroupsBlock` | `[]` | 日报黑名单 |
58
+ | `teachMaxPerGroup` | 200 | 每群问答上限 |
59
+ | `teachCooldownMs` | 3000 | 问答冷却(毫秒) |
60
+
61
+ ## 命令
62
+
63
+ | 命令 | 模块 |
64
+ |------|------|
65
+ | `签到` / `checkin` / `mypoints` / `rank` | 签到 |
66
+ | `stats` / `stats-week` / `stats-rank` / `mystats` | 统计 |
67
+ | `群分析` / `分析设置` | 日报 |
68
+ | `teach` / `teach-regex` / `forget` / `teach-list` | 问答 |
69
+ | `添加关键词` … | 仅 `keywordReply: true` |
package/lib/admin.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { type Plugin } from "zhin.js";
2
+ import type { GroupSuiteConfig } from "./config.js";
3
+ export declare function registerAdmin(plugin: Plugin, cfg: GroupSuiteConfig): void;
4
+ //# sourceMappingURL=admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../src/admin.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EAEZ,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAuIzE"}
package/lib/admin.js ADDED
@@ -0,0 +1,123 @@
1
+ import { formatCompact, MessageCommand, ZhinTool, } from "zhin.js";
2
+ import { adapterAllowed } from "./shared.js";
3
+ export function registerAdmin(plugin, cfg) {
4
+ const { addCommand, addMiddleware, addTool, logger } = plugin;
5
+ const welcomeTemplate = cfg.welcome;
6
+ const noticeAdapters = cfg.noticeAdapters;
7
+ plugin.on("notice.receive", async (notice) => {
8
+ if (!adapterAllowed(noticeAdapters, String(notice.$adapter)))
9
+ return;
10
+ if (notice.$type === "group_member_increase") {
11
+ if (notice.$channel.type !== "group")
12
+ return;
13
+ const targetName = notice.$target?.name || notice.$target?.id || "新成员";
14
+ const welcomeMsg = `🎉 ${targetName},${welcomeTemplate}`;
15
+ try {
16
+ const adapter = plugin.inject(notice.$adapter);
17
+ if (!adapter?.sendMessage)
18
+ return;
19
+ await adapter.sendMessage({
20
+ context: notice.$adapter,
21
+ bot: notice.$bot,
22
+ content: welcomeMsg,
23
+ id: notice.$channel.id,
24
+ type: notice.$channel.type,
25
+ });
26
+ }
27
+ catch (e) {
28
+ logger.warn(formatCompact({
29
+ op: "welcome",
30
+ ok: false,
31
+ error: e instanceof Error ? e.message : String(e),
32
+ }));
33
+ }
34
+ return;
35
+ }
36
+ if (cfg.recallNotify && notice.$type === "group_recall") {
37
+ if (notice.$channel.type !== "group")
38
+ return;
39
+ const operatorName = notice.$operator?.name || notice.$operator?.id || "某人";
40
+ const msg = `⚠️ ${operatorName} 撤回了一条消息`;
41
+ try {
42
+ const adapter = plugin.inject(notice.$adapter);
43
+ if (!adapter?.sendMessage)
44
+ return;
45
+ await adapter.sendMessage({
46
+ context: notice.$adapter,
47
+ bot: notice.$bot,
48
+ content: msg,
49
+ id: notice.$channel.id,
50
+ type: notice.$channel.type,
51
+ });
52
+ }
53
+ catch (e) {
54
+ logger.warn(formatCompact({
55
+ op: "recall_notice",
56
+ ok: false,
57
+ error: e instanceof Error ? e.message : String(e),
58
+ }));
59
+ }
60
+ }
61
+ });
62
+ if (!cfg.keywordReply)
63
+ return;
64
+ const keywords = new Map();
65
+ addMiddleware(async (message, next) => {
66
+ if (message.$channel.type !== "group") {
67
+ await next();
68
+ return;
69
+ }
70
+ const raw = message.$raw?.trim();
71
+ if (!raw) {
72
+ await next();
73
+ return;
74
+ }
75
+ for (const [keyword, reply] of keywords) {
76
+ if (raw.includes(keyword)) {
77
+ await message.$reply(reply);
78
+ return;
79
+ }
80
+ }
81
+ await next();
82
+ });
83
+ addCommand(new MessageCommand("添加关键词 <keyword:text> <reply:text>")
84
+ .desc("添加一个关键词自动回复对")
85
+ .action(async (_message, result) => {
86
+ const { keyword, reply } = result.params;
87
+ if (!keyword || !reply)
88
+ return "请提供关键词和回复内容";
89
+ keywords.set(keyword, reply);
90
+ return `已添加关键词「${keyword}」→「${reply}」`;
91
+ }));
92
+ addCommand(new MessageCommand("删除关键词 <keyword:text>")
93
+ .desc("删除一个关键词")
94
+ .action(async (_message, result) => {
95
+ const { keyword } = result.params;
96
+ if (!keyword)
97
+ return "请提供要删除的关键词";
98
+ if (!keywords.has(keyword))
99
+ return `关键词「${keyword}」不存在`;
100
+ keywords.delete(keyword);
101
+ return `已删除关键词「${keyword}」`;
102
+ }));
103
+ addCommand(new MessageCommand("关键词列表")
104
+ .desc("查看所有关键词回复对")
105
+ .action(async () => {
106
+ if (keywords.size === 0)
107
+ return "当前没有设置任何关键词";
108
+ const lines = Array.from(keywords.entries()).map(([k, v], i) => `${i + 1}. 「${k}」→「${v}」`);
109
+ return `关键词列表(共 ${keywords.size} 条):\n${lines.join("\n")}`;
110
+ }));
111
+ addTool(new ZhinTool("group_announce")
112
+ .desc("向群聊发送公告/通知消息")
113
+ .keyword("群公告", "通知", "announce")
114
+ .param("message", { type: "string", description: "要发送的公告内容" }, true)
115
+ .execute(async (args) => {
116
+ const content = args.message;
117
+ if (!content?.trim())
118
+ return "公告内容不能为空";
119
+ return `📢 群公告:\n${content}`;
120
+ })
121
+ .toTool());
122
+ }
123
+ //# sourceMappingURL=admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.js","sourceRoot":"","sources":["../src/admin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * 群日常分析:从收件箱查询、基础统计、文本报告。
3
+ * 数据源为 zhin 内置 unified_inbox_message 表(需 inbox.enabled)。
4
+ *
5
+ * 话题/金句/用户画像分析逻辑灵感来自:
6
+ * https://github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis
7
+ */
8
+ export interface InboxMessageRow {
9
+ id?: number;
10
+ adapter: string;
11
+ bot_id: string;
12
+ channel_id: string;
13
+ channel_type: string;
14
+ sender_id: string;
15
+ sender_name: string | null;
16
+ content: string;
17
+ /** 收件箱里可能是字符串,也可能被 ORM/驱动解析成对象 */
18
+ raw: unknown;
19
+ created_at: number;
20
+ }
21
+ export interface BasicStats {
22
+ messageCount: number;
23
+ participantCount: number;
24
+ totalChars: number;
25
+ hourlyDistribution: Record<number, number>;
26
+ mostActiveHour: number;
27
+ }
28
+ /** 话题摘要(灵感来自 astrbot_plugin_qq_group_daily_analysis) */
29
+ export interface SummaryTopic {
30
+ topic: string;
31
+ summary?: string;
32
+ }
33
+ /** 金句(灵感来自 astrbot_plugin_qq_group_daily_analysis) */
34
+ export interface GoldenQuote {
35
+ content: string;
36
+ sender: string;
37
+ reason: string;
38
+ }
39
+ /** 用户称号/画像(灵感来自 astrbot_plugin_qq_group_daily_analysis) */
40
+ export interface UserTitle {
41
+ name: string;
42
+ user_id: string;
43
+ title: string;
44
+ reason?: string;
45
+ }
46
+ export interface LLMAnalysis {
47
+ topics: SummaryTopic[];
48
+ quotes: GoldenQuote[];
49
+ userTitles: UserTitle[];
50
+ }
51
+ export interface AnalysisResult {
52
+ stats: BasicStats;
53
+ textReport: string;
54
+ llm?: LLMAnalysis;
55
+ }
56
+ /**
57
+ * 从 content (JSON) 或 raw 提取纯文本,用于统计与 LLM
58
+ */
59
+ export declare function extractText(row: InboxMessageRow): string;
60
+ /**
61
+ * 计算基础统计:消息数、人数、总字数、按小时分布、最活跃时段
62
+ */
63
+ export declare function computeBasicStats(rows: InboxMessageRow[]): BasicStats;
64
+ /**
65
+ * 生成纯文本报告(不含 LLM 内容)
66
+ */
67
+ export declare function formatTextReport(stats: BasicStats, options: {
68
+ channelName?: string;
69
+ days: number;
70
+ startDate: string;
71
+ endDate: string;
72
+ }): string;
73
+ /**
74
+ * 将 LLM 分析结果追加到文本报告。
75
+ * 话题/金句/用户画像 设计灵感来自 astrbot_plugin_qq_group_daily_analysis
76
+ */
77
+ export declare function appendLLMReport(textReport: string, llm: LLMAnalysis): string;
78
+ /**
79
+ * 从 LLM 返回的文本中解析出 topics / quotes / userTitles。
80
+ * 约定 LLM 返回 JSON 或类似结构,此处做宽松解析。
81
+ */
82
+ export declare function parseLLMResponse(raw: string): LLMAnalysis;
83
+ //# sourceMappingURL=analysis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../src/analysis.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,eAAe;IAC9B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,GAAG,EAAE,OAAO,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,sDAAsD;AACtD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,2DAA2D;AAC3D,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,UAAU,EAAE,SAAS,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,UAAU,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,WAAW,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CA2BxD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,EAAE,GAAG,UAAU,CA+BrE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClF,MAAM,CAqBR;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,MAAM,CAuB5E;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAiCzD"}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * 群日常分析:从收件箱查询、基础统计、文本报告。
3
+ * 数据源为 zhin 内置 unified_inbox_message 表(需 inbox.enabled)。
4
+ *
5
+ * 话题/金句/用户画像分析逻辑灵感来自:
6
+ * https://github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis
7
+ */
8
+ /**
9
+ * 从 content (JSON) 或 raw 提取纯文本,用于统计与 LLM
10
+ */
11
+ export function extractText(row) {
12
+ const raw = row.raw;
13
+ if (typeof raw === "string") {
14
+ const t = raw.trim();
15
+ if (t)
16
+ return t;
17
+ }
18
+ // 非字符串 raw(如 icqq 存的对象)不走 trim,避免报错;改从 content 解析
19
+ try {
20
+ const c = row.content;
21
+ const contentStr = typeof c === "string" ? c : JSON.stringify(c ?? []);
22
+ const content = JSON.parse(contentStr || "[]");
23
+ if (typeof content === "string")
24
+ return content;
25
+ if (Array.isArray(content)) {
26
+ return content
27
+ .map((part) => {
28
+ if (part?.type === "text") {
29
+ return part.data?.text ?? part.text ?? "";
30
+ }
31
+ return part?.text ?? "";
32
+ })
33
+ .filter(Boolean)
34
+ .join("");
35
+ }
36
+ }
37
+ catch {
38
+ // ignore
39
+ }
40
+ return "";
41
+ }
42
+ /**
43
+ * 计算基础统计:消息数、人数、总字数、按小时分布、最活跃时段
44
+ */
45
+ export function computeBasicStats(rows) {
46
+ const participantIds = new Set();
47
+ let totalChars = 0;
48
+ const hourly = {};
49
+ for (let h = 0; h < 24; h++)
50
+ hourly[h] = 0;
51
+ for (const row of rows) {
52
+ participantIds.add(row.sender_id);
53
+ const text = extractText(row);
54
+ totalChars += text.length;
55
+ const date = new Date(row.created_at);
56
+ const h = date.getHours();
57
+ hourly[h] = (hourly[h] || 0) + 1;
58
+ }
59
+ let mostActiveHour = 0;
60
+ let maxCount = 0;
61
+ for (let h = 0; h < 24; h++) {
62
+ if (hourly[h] > maxCount) {
63
+ maxCount = hourly[h];
64
+ mostActiveHour = h;
65
+ }
66
+ }
67
+ return {
68
+ messageCount: rows.length,
69
+ participantCount: participantIds.size,
70
+ totalChars,
71
+ hourlyDistribution: hourly,
72
+ mostActiveHour,
73
+ };
74
+ }
75
+ /**
76
+ * 生成纯文本报告(不含 LLM 内容)
77
+ */
78
+ export function formatTextReport(stats, options) {
79
+ const lines = [];
80
+ const title = options.channelName
81
+ ? `【${options.channelName}】群日常分析`
82
+ : "群日常分析";
83
+ lines.push(`📊 ${title}`);
84
+ lines.push(`📅 统计区间:${options.startDate} 至 ${options.endDate}(最近 ${options.days} 天)`);
85
+ lines.push("");
86
+ lines.push(`💬 消息总数:${stats.messageCount} 条`);
87
+ lines.push(`👥 参与人数:${stats.participantCount} 人`);
88
+ lines.push(`📝 总字数:${stats.totalChars} 字`);
89
+ lines.push(`⏰ 最活跃时段:${stats.mostActiveHour}:00 - ${stats.mostActiveHour + 1}:00`);
90
+ lines.push("");
91
+ lines.push("📈 每小时消息分布:");
92
+ const maxH = Math.max(...Object.values(stats.hourlyDistribution), 1);
93
+ for (let h = 0; h < 24; h++) {
94
+ const c = stats.hourlyDistribution[h] || 0;
95
+ const bar = "█".repeat(Math.round((c / maxH) * 10)) || "░";
96
+ lines.push(` ${String(h).padStart(2, "0")}:00 ${bar} ${c}`);
97
+ }
98
+ return lines.join("\n");
99
+ }
100
+ /**
101
+ * 将 LLM 分析结果追加到文本报告。
102
+ * 话题/金句/用户画像 设计灵感来自 astrbot_plugin_qq_group_daily_analysis
103
+ */
104
+ export function appendLLMReport(textReport, llm) {
105
+ const lines = [textReport, ""];
106
+ if (llm.topics?.length) {
107
+ lines.push("🔥 热门话题(LLM 提取):");
108
+ llm.topics.forEach((t, i) => {
109
+ lines.push(` ${i + 1}. ${t.topic}${t.summary ? ` — ${t.summary}` : ""}`);
110
+ });
111
+ lines.push("");
112
+ }
113
+ if (llm.quotes?.length) {
114
+ lines.push("💬 金句(LLM 筛选):");
115
+ llm.quotes.forEach((q, i) => {
116
+ lines.push(` ${i + 1}. 「${q.content}」 — ${q.sender}(${q.reason})`);
117
+ });
118
+ lines.push("");
119
+ }
120
+ if (llm.userTitles?.length) {
121
+ lines.push("👤 用户画像/称号(LLM):");
122
+ llm.userTitles.forEach((u, i) => {
123
+ lines.push(` ${i + 1}. ${u.name} — ${u.title}${u.reason ? `(${u.reason})` : ""}`);
124
+ });
125
+ }
126
+ return lines.join("\n");
127
+ }
128
+ /**
129
+ * 从 LLM 返回的文本中解析出 topics / quotes / userTitles。
130
+ * 约定 LLM 返回 JSON 或类似结构,此处做宽松解析。
131
+ */
132
+ export function parseLLMResponse(raw) {
133
+ const result = { topics: [], quotes: [], userTitles: [] };
134
+ try {
135
+ const trimmed = raw.trim();
136
+ const jsonMatch = trimmed.match(/\{[\s\S]*\}/);
137
+ if (jsonMatch) {
138
+ const obj = JSON.parse(jsonMatch[0]);
139
+ if (Array.isArray(obj.topics)) {
140
+ result.topics = obj.topics.map((t) => ({
141
+ topic: typeof t.topic === "string" ? t.topic : String(t.topic || ""),
142
+ summary: typeof t.summary === "string" ? t.summary : undefined,
143
+ }));
144
+ }
145
+ if (Array.isArray(obj.quotes)) {
146
+ result.quotes = obj.quotes.map((q) => ({
147
+ content: String(q.content ?? ""),
148
+ sender: String(q.sender ?? ""),
149
+ reason: String(q.reason ?? ""),
150
+ }));
151
+ }
152
+ if (Array.isArray(obj.user_titles)) {
153
+ result.userTitles = obj.user_titles.map((u) => ({
154
+ name: String(u.name ?? ""),
155
+ user_id: String(u.user_id ?? ""),
156
+ title: String(u.title ?? ""),
157
+ reason: typeof u.reason === "string" ? u.reason : undefined,
158
+ }));
159
+ }
160
+ }
161
+ }
162
+ catch {
163
+ // ignore parse errors
164
+ }
165
+ return result;
166
+ }
167
+ //# sourceMappingURL=analysis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis.js","sourceRoot":"","sources":["../src/analysis.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { type Plugin } from "zhin.js";
2
+ import type { GroupSuiteConfig } from "./config.js";
3
+ export declare function registerCheckin(plugin: Plugin, cfg: GroupSuiteConfig): void;
4
+ //# sourceMappingURL=checkin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkin.d.ts","sourceRoot":"","sources":["../src/checkin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAmBpD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,gBAAgB,GAAG,IAAI,CA0K3E"}
package/lib/checkin.js ADDED
@@ -0,0 +1,163 @@
1
+ import { formatCompact, MessageCommand } from "zhin.js";
2
+ import { getDatabase, getMessageContextKey, todayStr, ts } from "./shared.js";
3
+ function yesterdayStr() {
4
+ const d = new Date();
5
+ d.setDate(d.getDate() - 1);
6
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
7
+ }
8
+ function randomInt(min, max) {
9
+ return Math.floor(Math.random() * (max - min + 1)) + min;
10
+ }
11
+ let _db = null;
12
+ function getModel() {
13
+ return _db?.models?.get("checkin_records") ?? null;
14
+ }
15
+ export function registerCheckin(plugin, cfg) {
16
+ const { logger, addCommand, useContext } = plugin;
17
+ useContext("database", (db) => {
18
+ _db = db;
19
+ db.define("checkin_records", {
20
+ user_id: { type: "text", nullable: false },
21
+ user_name: { type: "text", default: "" },
22
+ points: { type: "integer", default: 0 },
23
+ total_checkins: { type: "integer", default: 0 },
24
+ streak: { type: "integer", default: 0 },
25
+ max_streak: { type: "integer", default: 0 },
26
+ last_checkin: { type: "text", default: "" },
27
+ context_type: { type: "text", default: "global" },
28
+ context_id: { type: "text", default: "" },
29
+ created_at: { type: "text", default: "" },
30
+ updated_at: { type: "text", default: "" },
31
+ });
32
+ logger.info(formatCompact({ op: "checkin_model" }));
33
+ });
34
+ async function getOrCreateUser(model, userId, userName, ctxType, ctxId) {
35
+ const rows = (await model.select().where({
36
+ user_id: userId,
37
+ context_type: ctxType,
38
+ context_id: ctxId,
39
+ }));
40
+ if (rows.length > 0)
41
+ return rows[0];
42
+ await model.insert({
43
+ user_id: userId,
44
+ user_name: userName,
45
+ points: 0,
46
+ total_checkins: 0,
47
+ streak: 0,
48
+ max_streak: 0,
49
+ last_checkin: "",
50
+ context_type: ctxType,
51
+ context_id: ctxId,
52
+ created_at: ts(),
53
+ updated_at: ts(),
54
+ });
55
+ const created = (await model.select().where({
56
+ user_id: userId,
57
+ context_type: ctxType,
58
+ context_id: ctxId,
59
+ }));
60
+ return created[0] ?? null;
61
+ }
62
+ addCommand(new MessageCommand("checkin")
63
+ .desc("签到", "每日签到获得积分")
64
+ .action(async (message) => {
65
+ const M = getModel();
66
+ if (!M)
67
+ return "签到数据库尚未就绪,请稍后重试";
68
+ const userId = String(message.$sender?.id || "");
69
+ const userName = String(message.$sender?.name || "用户");
70
+ if (!userId)
71
+ return "无法获取用户信息";
72
+ const { type: ctxType, id: ctxId } = getMessageContextKey(message);
73
+ const user = await getOrCreateUser(M, userId, userName, ctxType, ctxId);
74
+ if (!user)
75
+ return "签到失败,请重试";
76
+ const today = todayStr();
77
+ if (user.last_checkin === today) {
78
+ return `你今天已经签到过了哦~\n当前积分: ${user.points}\n连续签到: ${user.streak} 天`;
79
+ }
80
+ const isConsecutive = user.last_checkin === yesterdayStr();
81
+ const newStreak = isConsecutive ? Number(user.streak) + 1 : 1;
82
+ const base = randomInt(cfg.basePointsMin, cfg.basePointsMax);
83
+ const bonus = Math.min(newStreak * cfg.streakBonus, cfg.streakCap);
84
+ const earned = base + bonus;
85
+ const newPoints = Number(user.points || 0) + earned;
86
+ await M.update({
87
+ points: newPoints,
88
+ total_checkins: Number(user.total_checkins || 0) + 1,
89
+ streak: newStreak,
90
+ max_streak: Math.max(Number(user.max_streak || 0), newStreak),
91
+ last_checkin: today,
92
+ user_name: userName,
93
+ updated_at: ts(),
94
+ }).where({ id: user.id });
95
+ const lines = [`签到成功!`, `基础积分: +${base}`];
96
+ if (bonus > 0)
97
+ lines.push(`连续奖励: +${bonus} (连续${newStreak}天)`);
98
+ lines.push(`本次获得: +${earned}`, `当前积分: ${newPoints}`);
99
+ return lines.join("\n");
100
+ }));
101
+ addCommand(new MessageCommand("mypoints")
102
+ .desc("我的积分", "查看个人积分与签到信息")
103
+ .action(async (message) => {
104
+ const M = getModel();
105
+ if (!M)
106
+ return "签到数据库尚未就绪";
107
+ const userId = String(message.$sender?.id || "");
108
+ const { type: ctxType, id: ctxId } = getMessageContextKey(message);
109
+ const rows = (await M.select().where({
110
+ user_id: userId,
111
+ context_type: ctxType,
112
+ context_id: ctxId,
113
+ }));
114
+ if (rows.length === 0)
115
+ return "你还没有签到记录哦,发送「签到」开始吧!";
116
+ const u = rows[0];
117
+ const checkedToday = u.last_checkin === todayStr();
118
+ return [
119
+ `${u.user_name || "用户"} 的积分信息`,
120
+ `积分: ${u.points}`,
121
+ `累计签到: ${u.total_checkins} 天`,
122
+ `连续签到: ${u.streak} 天`,
123
+ `最长连续: ${u.max_streak} 天`,
124
+ checkedToday ? "今日已签到 ✓" : "今日未签到",
125
+ ].join("\n");
126
+ }));
127
+ addCommand(new MessageCommand("rank")
128
+ .desc("积分排行", "查看积分排行榜")
129
+ .action(async (message) => {
130
+ const M = getModel();
131
+ if (!M)
132
+ return "签到数据库尚未就绪";
133
+ const { type: ctxType, id: ctxId } = getMessageContextKey(message);
134
+ const all = (await M.select().where(ctxType === "global"
135
+ ? { context_type: "global" }
136
+ : { context_type: ctxType, context_id: ctxId }));
137
+ if (all.length === 0)
138
+ return "还没有人签到过哦~";
139
+ const sorted = all.sort((a, b) => Number(b.points || 0) - Number(a.points || 0));
140
+ const top = sorted.slice(0, cfg.rankSize);
141
+ const medals = ["🥇", "🥈", "🥉"];
142
+ const lines = top.map((u, i) => {
143
+ const prefix = i < 3 ? medals[i] : `${i + 1}.`;
144
+ return `${prefix} ${u.user_name || u.user_id} — ${u.points}分 (${u.total_checkins}天)`;
145
+ });
146
+ const title = ctxType === "group" ? "本群积分排行" : "全局积分排行";
147
+ const userId = String(message.$sender?.id || "");
148
+ const myRank = sorted.findIndex((u) => u.user_id === userId);
149
+ const footer = myRank >= 0 ? `\n你的排名: 第${myRank + 1}名` : "";
150
+ return `${title}\n${lines.join("\n")}${footer}`;
151
+ }));
152
+ if (!_db) {
153
+ const db = getDatabase(plugin);
154
+ if (db)
155
+ _db = db;
156
+ }
157
+ logger.info(formatCompact({
158
+ op: "checkin_load",
159
+ base_min: cfg.basePointsMin,
160
+ base_max: cfg.basePointsMax,
161
+ }));
162
+ }
163
+ //# sourceMappingURL=checkin.js.map