@zhin.js/plugin-group-suite 1.0.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +42 -58
  2. package/commands/checkin/index.js +11 -0
  3. package/commands/checkin/index.ts +12 -0
  4. package/commands/forget/index.js +10 -0
  5. package/commands/forget/index.ts +12 -0
  6. package/commands/keyword-add/[keyword]/index.js +16 -0
  7. package/commands/keyword-add/[keyword]/index.ts +16 -0
  8. package/commands/keyword-list/index.js +14 -0
  9. package/commands/keyword-list/index.ts +14 -0
  10. package/commands/keyword-remove/[keyword]/index.js +16 -0
  11. package/commands/keyword-remove/[keyword]/index.ts +15 -0
  12. package/commands/mypoints/index.js +10 -0
  13. package/commands/mypoints/index.ts +12 -0
  14. package/commands/mystats/index.js +10 -0
  15. package/commands/mystats/index.ts +12 -0
  16. package/commands/rank/index.js +11 -0
  17. package/commands/rank/index.ts +12 -0
  18. package/commands/stats/index.js +14 -0
  19. package/commands/stats/index.ts +21 -0
  20. package/commands/stats-rank/index.js +13 -0
  21. package/commands/stats-rank/index.ts +20 -0
  22. package/commands/stats-week/index.js +13 -0
  23. package/commands/stats-week/index.ts +20 -0
  24. package/commands/teach/index.js +11 -0
  25. package/commands/teach/index.ts +12 -0
  26. package/commands/teach-list/[[page]]/index.js +13 -0
  27. package/commands/teach-list/[[page]]/index.ts +14 -0
  28. package/commands/teach-regex/index.js +11 -0
  29. package/commands/teach-regex/index.ts +12 -0
  30. package/lib/checkin-lib.d.ts +27 -0
  31. package/lib/checkin-lib.d.ts.map +1 -0
  32. package/lib/checkin-lib.js +133 -0
  33. package/lib/checkin-lib.js.map +1 -0
  34. package/lib/config.d.ts +4 -57
  35. package/lib/config.d.ts.map +1 -1
  36. package/lib/config.js +18 -40
  37. package/lib/config.js.map +1 -1
  38. package/lib/db-store.d.ts +5 -0
  39. package/lib/db-store.d.ts.map +1 -0
  40. package/lib/db-store.js +11 -0
  41. package/lib/db-store.js.map +1 -0
  42. package/lib/index.d.ts +9 -1
  43. package/lib/index.d.ts.map +1 -1
  44. package/lib/index.js +7 -29
  45. package/lib/index.js.map +1 -1
  46. package/lib/keyword-store.d.ts +5 -0
  47. package/lib/keyword-store.d.ts.map +1 -0
  48. package/lib/keyword-store.js +17 -0
  49. package/lib/keyword-store.js.map +1 -0
  50. package/lib/memory-store.d.ts +31 -0
  51. package/lib/memory-store.d.ts.map +1 -0
  52. package/lib/memory-store.js +63 -0
  53. package/lib/memory-store.js.map +1 -0
  54. package/lib/runtime-state.d.ts +24 -0
  55. package/lib/runtime-state.d.ts.map +1 -0
  56. package/lib/runtime-state.js +15 -0
  57. package/lib/runtime-state.js.map +1 -0
  58. package/lib/shared-runtime.d.ts +26 -0
  59. package/lib/shared-runtime.d.ts.map +1 -0
  60. package/lib/shared-runtime.js +34 -0
  61. package/lib/shared-runtime.js.map +1 -0
  62. package/lib/stats-lib.d.ts +39 -0
  63. package/lib/stats-lib.d.ts.map +1 -0
  64. package/lib/stats-lib.js +172 -0
  65. package/lib/stats-lib.js.map +1 -0
  66. package/lib/tables.d.ts +4 -0
  67. package/lib/tables.d.ts.map +1 -0
  68. package/lib/tables.js +40 -0
  69. package/lib/tables.js.map +1 -0
  70. package/lib/teach-lib.d.ts +23 -0
  71. package/lib/teach-lib.d.ts.map +1 -0
  72. package/lib/teach-lib.js +228 -0
  73. package/lib/teach-lib.js.map +1 -0
  74. package/middlewares/keyword-reply/index.js +36 -0
  75. package/middlewares/keyword-reply/index.ts +37 -0
  76. package/middlewares/stats-count/index.js +20 -0
  77. package/middlewares/stats-count/index.ts +22 -0
  78. package/middlewares/teach-reply/index.js +21 -0
  79. package/middlewares/teach-reply/index.ts +22 -0
  80. package/package.json +57 -14
  81. package/plugin.js +38 -0
  82. package/schema.json +17 -0
  83. package/skills/checkin/tools/checkin_query/index.js +42 -0
  84. package/skills/checkin/tools/checkin_query/index.ts +41 -0
  85. package/skills/checkin/tools/checkin_rank/index.js +34 -0
  86. package/skills/checkin/tools/checkin_rank/index.ts +34 -0
  87. package/skills/group-daily-analysis/SKILL.md +11 -4
  88. package/skills/group-daily-analysis/tools/group_announce/index.js +14 -0
  89. package/skills/group-daily-analysis/tools/group_announce/index.ts +14 -0
  90. package/skills/group-daily-analysis/tools/stats_query/index.js +35 -0
  91. package/skills/group-daily-analysis/tools/stats_query/index.ts +36 -0
  92. package/skills/group-daily-analysis/tools/stats_user/index.js +50 -0
  93. package/skills/group-daily-analysis/tools/stats_user/index.ts +46 -0
  94. package/src/checkin-lib.ts +167 -0
  95. package/src/config.ts +23 -57
  96. package/src/db-store.ts +19 -0
  97. package/src/index.ts +39 -33
  98. package/src/keyword-store.ts +18 -0
  99. package/src/memory-store.ts +90 -0
  100. package/src/runtime-state.ts +41 -0
  101. package/src/shared-runtime.ts +44 -0
  102. package/src/stats-lib.ts +198 -0
  103. package/src/tables.ts +46 -0
  104. package/src/teach-lib.ts +294 -0
  105. package/lib/admin.d.ts +0 -4
  106. package/lib/admin.d.ts.map +0 -1
  107. package/lib/admin.js +0 -123
  108. package/lib/admin.js.map +0 -1
  109. package/lib/checkin.d.ts +0 -5
  110. package/lib/checkin.d.ts.map +0 -1
  111. package/lib/checkin.js +0 -168
  112. package/lib/checkin.js.map +0 -1
  113. package/lib/daily-analysis.d.ts +0 -7
  114. package/lib/daily-analysis.d.ts.map +0 -1
  115. package/lib/daily-analysis.js +0 -334
  116. package/lib/daily-analysis.js.map +0 -1
  117. package/lib/shared.d.ts +0 -25
  118. package/lib/shared.d.ts.map +0 -1
  119. package/lib/shared.js +0 -40
  120. package/lib/shared.js.map +0 -1
  121. package/lib/stats.d.ts +0 -12
  122. package/lib/stats.d.ts.map +0 -1
  123. package/lib/stats.js +0 -256
  124. package/lib/stats.js.map +0 -1
  125. package/lib/teach.d.ts +0 -4
  126. package/lib/teach.d.ts.map +0 -1
  127. package/lib/teach.js +0 -294
  128. package/lib/teach.js.map +0 -1
  129. package/plugin.yml +0 -3
  130. package/src/admin.ts +0 -145
  131. package/src/checkin.ts +0 -196
  132. package/src/daily-analysis.ts +0 -458
  133. package/src/shared.ts +0 -51
  134. package/src/stats.ts +0 -297
  135. package/src/teach.ts +0 -307
  136. package/tools/checkin-query/checkin-query.tool.md +0 -11
  137. package/tools/checkin-query/handler.ts +0 -22
  138. package/tools/checkin-rank/checkin-rank.tool.md +0 -11
  139. package/tools/checkin-rank/handler.ts +0 -16
  140. package/tools/stats-query/handler.ts +0 -31
  141. package/tools/stats-query/stats-query.tool.md +0 -16
  142. package/tools/stats-user/handler.ts +0 -28
  143. package/tools/stats-user/stats-user.tool.md +0 -12
@@ -0,0 +1,198 @@
1
+ import type { GroupSuiteConfig } from './config.js';
2
+ import { getStatsModel } from './db-store.js';
3
+ import {
4
+ resolveSender,
5
+ todayStr,
6
+ ts,
7
+ } from './shared-runtime.js';
8
+ import {
9
+ buildStatsRankReportData,
10
+ formatMyStatsText,
11
+ formatRankText,
12
+ type MyStatsReportData,
13
+ } from './stats-data.js';
14
+ import type { GroupSuiteRuntime, PendingStatsIncrement } from './runtime-state.js';
15
+
16
+ export function weekStartStr(): string {
17
+ const d = new Date();
18
+ const day = d.getDay() || 7;
19
+ d.setDate(d.getDate() - day + 1);
20
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
21
+ }
22
+
23
+ export function monthStartStr(): string {
24
+ const d = new Date();
25
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-01`;
26
+ }
27
+
28
+ function bufferKey(userId: string, groupId: string, date: string): string {
29
+ return `${userId}:${groupId}:${date}`;
30
+ }
31
+
32
+ export function resolveGroupId(input: {
33
+ conversation?: { readonly kind?: string; readonly id?: string };
34
+ metadata?: Readonly<Record<string, unknown>>;
35
+ }): string {
36
+ const meta = input.metadata ?? {};
37
+ const channelType = String(meta.type ?? meta.channelType ?? '');
38
+ if (channelType === 'group' || channelType === 'guild' || channelType === 'channel') {
39
+ return String(meta.channelId ?? input.conversation?.id ?? '');
40
+ }
41
+ if (input.conversation && channelType !== 'private') {
42
+ return String(input.conversation.id ?? '');
43
+ }
44
+ return '';
45
+ }
46
+
47
+ export function recordMessage(input: {
48
+ sender?: { readonly id: string; readonly name?: string };
49
+ conversation?: { readonly kind?: string; readonly id?: string };
50
+ metadata?: Readonly<Record<string, unknown>>;
51
+ }, runtime: GroupSuiteRuntime): void {
52
+ const targetBuffer = runtime.statsBuffer;
53
+ const { id: userId, name: userName } = resolveSender(input);
54
+ if (!userId) return;
55
+ const groupId = resolveGroupId(input);
56
+ const key = bufferKey(userId, groupId, todayStr());
57
+ const pending = targetBuffer.get(key);
58
+ if (pending) {
59
+ pending.count++;
60
+ pending.user_name = userName;
61
+ } else {
62
+ targetBuffer.set(key, {
63
+ user_id: userId,
64
+ user_name: userName,
65
+ group_id: groupId,
66
+ date: todayStr(),
67
+ count: 1,
68
+ });
69
+ }
70
+ }
71
+
72
+ export async function flushStatsBuffer(runtime: GroupSuiteRuntime): Promise<void> {
73
+ const targetBuffer = runtime.statsBuffer;
74
+ const M = getStatsModel(runtime.db);
75
+ if (!M || targetBuffer.size === 0) return;
76
+ for (const [key, entry] of [...targetBuffer.entries()]) {
77
+ // 快照本次要落库的计数;flush 期间新累计的部分留在缓冲
78
+ const countSnapshot = entry.count;
79
+ try {
80
+ const existing = (await M.select().where({
81
+ user_id: entry.user_id,
82
+ group_id: entry.group_id,
83
+ date: entry.date,
84
+ })) as Record<string, unknown>[];
85
+ if (existing.length > 0) {
86
+ await M.update({
87
+ count: Number(existing[0]!.count || 0) + countSnapshot,
88
+ user_name: entry.user_name,
89
+ updated_at: ts(),
90
+ }).where({
91
+ user_id: entry.user_id,
92
+ group_id: entry.group_id,
93
+ date: entry.date,
94
+ });
95
+ } else {
96
+ await M.insert({
97
+ user_id: entry.user_id,
98
+ user_name: entry.user_name,
99
+ group_id: entry.group_id,
100
+ date: entry.date,
101
+ count: countSnapshot,
102
+ updated_at: ts(),
103
+ });
104
+ }
105
+ // 只移除写入成功的部分;失败的 key 留在缓冲等下一轮重试
106
+ entry.count -= countSnapshot;
107
+ if (entry.count <= 0 && targetBuffer.get(key) === entry) targetBuffer.delete(key);
108
+ } catch {
109
+ /* single row failure: keep entry in buffer for next flush */
110
+ }
111
+ }
112
+ }
113
+
114
+ export async function queryStats(
115
+ groupId: string,
116
+ fromDate: string,
117
+ runtime: GroupSuiteRuntime,
118
+ ): Promise<Map<string, { name: string; count: number }>> {
119
+ const M = getStatsModel(runtime.db);
120
+ if (!M) return new Map();
121
+ const all = (groupId
122
+ ? await M.select().where({ group_id: groupId })
123
+ : await M.select()) as Record<string, unknown>[];
124
+ const result = new Map<string, { name: string; count: number }>();
125
+ for (const row of all) {
126
+ if (String(row.date) < fromDate) continue;
127
+ const userId = String(row.user_id);
128
+ const existing = result.get(userId);
129
+ if (existing) {
130
+ existing.count += Number(row.count || 0);
131
+ if (row.user_name) existing.name = String(row.user_name);
132
+ } else {
133
+ result.set(userId, {
134
+ name: String(row.user_name || row.user_id),
135
+ count: Number(row.count || 0),
136
+ });
137
+ }
138
+ }
139
+ return result;
140
+ }
141
+
142
+ type MessageInput = {
143
+ sender?: { readonly id: string; readonly name?: string };
144
+ target?: string;
145
+ metadata?: Readonly<Record<string, unknown>>;
146
+ };
147
+
148
+ export async function statsRankText(
149
+ input: MessageInput,
150
+ cfg: GroupSuiteConfig,
151
+ fromDate: string,
152
+ title: string,
153
+ runtime: GroupSuiteRuntime,
154
+ ): Promise<string> {
155
+ await flushStatsBuffer(runtime);
156
+ const groupId = resolveGroupId(input);
157
+ const stats = await queryStats(groupId, fromDate, runtime);
158
+ const { id: userId } = resolveSender(input);
159
+ const data = buildStatsRankReportData(stats, title, cfg.rankSize, userId);
160
+ return formatRankText(data);
161
+ }
162
+
163
+ export async function myStatsText(input: MessageInput, runtime: GroupSuiteRuntime): Promise<string> {
164
+ await flushStatsBuffer(runtime);
165
+ const M = getStatsModel(runtime.db);
166
+ if (!M) return '统计数据库尚未就绪';
167
+ const { id: userId, name: userName } = resolveSender(input);
168
+ if (!userId) return '无法获取用户信息';
169
+ const groupId = resolveGroupId(input);
170
+ const rows = (groupId
171
+ ? await M.select().where({ user_id: userId, group_id: groupId })
172
+ : await M.select().where({ user_id: userId })) as Record<string, unknown>[];
173
+ if (rows.length === 0) return '暂无你的消息记录';
174
+ const today = todayStr();
175
+ const weekStart = weekStartStr();
176
+ const monthStart = monthStartStr();
177
+ let todayCount = 0;
178
+ let weekCount = 0;
179
+ let monthCount = 0;
180
+ let totalCount = 0;
181
+ for (const row of rows) {
182
+ const c = Number(row.count || 0);
183
+ totalCount += c;
184
+ if (String(row.date) >= monthStart) monthCount += c;
185
+ if (String(row.date) >= weekStart) weekCount += c;
186
+ if (row.date === today) todayCount += c;
187
+ }
188
+ const data: MyStatsReportData = {
189
+ userName: userName || '你',
190
+ scope: groupId ? '本群' : '全局',
191
+ todayCount,
192
+ weekCount,
193
+ monthCount,
194
+ totalCount,
195
+ activeDays: rows.length,
196
+ };
197
+ return formatMyStatsText(data);
198
+ }
package/src/tables.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Group-suite table schemas for DatabaseHost.
3
+ */
4
+ import {
5
+ CHECKIN_TABLE,
6
+ TEACH_TABLE,
7
+ STATS_TABLE,
8
+ } from './memory-store.js';
9
+
10
+ export function defineGroupSuiteTables(
11
+ db: { define: (name: string, schema: Record<string, unknown>) => void },
12
+ ): void {
13
+ db.define(CHECKIN_TABLE, {
14
+ user_id: { type: 'text', nullable: false },
15
+ user_name: { type: 'text', default: '' },
16
+ points: { type: 'integer', default: 0 },
17
+ total_checkins: { type: 'integer', default: 0 },
18
+ streak: { type: 'integer', default: 0 },
19
+ max_streak: { type: 'integer', default: 0 },
20
+ last_checkin: { type: 'text', default: '' },
21
+ context_type: { type: 'text', default: 'global' },
22
+ context_id: { type: 'text', default: '' },
23
+ created_at: { type: 'text', default: '' },
24
+ updated_at: { type: 'text', default: '' },
25
+ });
26
+ db.define(TEACH_TABLE, {
27
+ question: { type: 'text', nullable: false },
28
+ answer: { type: 'text', nullable: false },
29
+ is_regex: { type: 'integer', default: 0 },
30
+ context_type: { type: 'text', default: 'global' },
31
+ context_id: { type: 'text', default: '' },
32
+ creator_id: { type: 'text', default: '' },
33
+ creator_name: { type: 'text', default: '' },
34
+ hit_count: { type: 'integer', default: 0 },
35
+ created_at: { type: 'text', default: '' },
36
+ updated_at: { type: 'text', default: '' },
37
+ });
38
+ db.define(STATS_TABLE, {
39
+ user_id: { type: 'text', nullable: false },
40
+ user_name: { type: 'text', default: '' },
41
+ group_id: { type: 'text', default: '' },
42
+ date: { type: 'text', nullable: false },
43
+ count: { type: 'integer', default: 0 },
44
+ updated_at: { type: 'text', default: '' },
45
+ });
46
+ }
@@ -0,0 +1,294 @@
1
+ import type { GroupSuiteConfig } from './config.js';
2
+ import { getTeachModel } from './db-store.js';
3
+ import {
4
+ resolveContextKey,
5
+ resolveSender,
6
+ ts,
7
+ } from './shared-runtime.js';
8
+ import type { GroupSuiteRuntime } from './runtime-state.js';
9
+
10
+ export const TEACH_USAGE_HINT =
11
+ '格式:teach 关键词 回答(回答可含空格),或 teach 问题|答案';
12
+
13
+ function isSafeRegex(pattern: string): boolean {
14
+ if (pattern.length > 200) return false;
15
+ if (/(\+|\*|\{)\)?(\+|\*|\{)/.test(pattern)) return false;
16
+ if (/\([^)]*(\+|\*)[^)]*(\+|\*)[^)]*\)[+*]/.test(pattern)) return false;
17
+ const altGroups = pattern.match(/\([^)]*\|[^)]*\)/g) || [];
18
+ for (const g of altGroups) {
19
+ if ((g.match(/\|/g) || []).length > 10) return false;
20
+ }
21
+ return true;
22
+ }
23
+
24
+ export function parseTeachPair(raw: string): { question: string; answer: string } | null {
25
+ if (!raw) return null;
26
+ const pipe = raw.indexOf('|');
27
+ if (pipe >= 0) {
28
+ const question = raw.slice(0, pipe).trim();
29
+ const answer = raw.slice(pipe + 1).trim();
30
+ if (question && answer) return { question, answer };
31
+ return null;
32
+ }
33
+ const lastSpace = raw.lastIndexOf(' ');
34
+ if (lastSpace < 0) return null;
35
+ const question = raw.slice(0, lastSpace).trim();
36
+ const answer = raw.slice(lastSpace + 1).trim();
37
+ if (!question || !answer) return null;
38
+ return { question, answer };
39
+ }
40
+
41
+ function processAnswer(
42
+ answer: string,
43
+ sender: { id: string; name: string },
44
+ match?: RegExpMatchArray,
45
+ ): string {
46
+ let result = answer;
47
+ result = result.replace(/\{sender\}/g, sender.name || '你');
48
+ result = result.replace(/\{sender\.id\}/g, sender.id || '');
49
+ result = result.replace(/\{time\}/g, new Date().toLocaleTimeString('zh-CN'));
50
+ result = result.replace(/\{date\}/g, new Date().toLocaleDateString('zh-CN'));
51
+ if (match) {
52
+ for (let i = 1; i < match.length; i++) {
53
+ result = result.replace(new RegExp(`\\$${i}`, 'g'), match[i] || '');
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+
59
+ function qaWhereKey(row: {
60
+ question: string;
61
+ context_type: string;
62
+ context_id: string;
63
+ is_regex?: number | boolean;
64
+ }) {
65
+ return {
66
+ question: row.question,
67
+ context_type: row.context_type,
68
+ context_id: row.context_id,
69
+ is_regex: row.is_regex ? 1 : 0,
70
+ };
71
+ }
72
+
73
+ type MessageInput = {
74
+ sender?: { readonly id: string; readonly name?: string };
75
+ target?: string;
76
+ content?: unknown;
77
+ metadata?: Readonly<Record<string, unknown>>;
78
+ };
79
+
80
+ async function findMatch(
81
+ content: string,
82
+ ctxType: string,
83
+ ctxId: string,
84
+ cfg: GroupSuiteConfig,
85
+ runtime: GroupSuiteRuntime,
86
+ ): Promise<Record<string, unknown> | null> {
87
+ const QA = getTeachModel(runtime.db);
88
+ if (!QA) return null;
89
+ const allItems = (await QA.select()) as Record<string, unknown>[];
90
+ const candidates = allItems.filter(
91
+ (item) =>
92
+ item.context_type === 'global' ||
93
+ (item.context_type === ctxType && item.context_id === ctxId),
94
+ );
95
+ for (const item of candidates) {
96
+ if (!item.is_regex && item.question === content) return item;
97
+ }
98
+ if (cfg.teachAllowRegex) {
99
+ for (const item of candidates) {
100
+ if (!item.is_regex) continue;
101
+ try {
102
+ if (new RegExp(String(item.question), 'i').test(content)) return item;
103
+ } catch {
104
+ continue;
105
+ }
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+
111
+ export async function teachAdd(
112
+ input: MessageInput,
113
+ cfg: GroupSuiteConfig,
114
+ payload: string,
115
+ regex = false,
116
+ runtime: GroupSuiteRuntime,
117
+ ): Promise<string> {
118
+ const QA = getTeachModel(runtime.db);
119
+ if (!QA) return '问答数据库尚未就绪,请稍后重试';
120
+ if (regex && !cfg.teachAllowRegex) return '管理员已禁用正则问答';
121
+
122
+ const parsed = parseTeachPair(payload.trim());
123
+ if (!parsed) {
124
+ return regex
125
+ ? '格式:teach-regex 正则 回答,或 teach-regex 正则|回答'
126
+ : TEACH_USAGE_HINT;
127
+ }
128
+ const { question, answer } = parsed;
129
+
130
+ if (regex) {
131
+ try {
132
+ new RegExp(question, 'i');
133
+ } catch (e) {
134
+ return `正则表达式不合法: ${(e as Error).message}`;
135
+ }
136
+ if (!isSafeRegex(question)) return '该正则可能导致性能问题,请简化';
137
+ }
138
+
139
+ const { type: ctxType, id: ctxId } = resolveContextKey(input);
140
+ const sender = resolveSender(input);
141
+
142
+ if (ctxType === 'group') {
143
+ const existing = (await QA.select().where({
144
+ context_type: ctxType,
145
+ context_id: ctxId,
146
+ })) as Record<string, unknown>[];
147
+ if (existing.length >= cfg.teachMaxPerGroup) {
148
+ return `该群问答数已达上限 (${cfg.teachMaxPerGroup})`;
149
+ }
150
+ }
151
+
152
+ const duplicate = (await QA.select().where({
153
+ question,
154
+ context_type: ctxType,
155
+ context_id: ctxId,
156
+ })) as Record<string, unknown>[];
157
+
158
+ if (duplicate.length > 0 && !regex) {
159
+ await QA.update({ answer, updated_at: ts() }).where(
160
+ qaWhereKey(duplicate[0] as {
161
+ question: string;
162
+ context_type: string;
163
+ context_id: string;
164
+ is_regex?: number | boolean;
165
+ }),
166
+ );
167
+ return `已更新问答:「${question}」→「${answer}」`;
168
+ }
169
+
170
+ await QA.insert({
171
+ question,
172
+ answer,
173
+ is_regex: regex ? 1 : 0,
174
+ context_type: ctxType,
175
+ context_id: ctxId,
176
+ creator_id: sender.id,
177
+ creator_name: sender.name,
178
+ hit_count: 0,
179
+ created_at: ts(),
180
+ updated_at: ts(),
181
+ });
182
+
183
+ return regex
184
+ ? `学会了!匹配 /${question}/ 的消息我会回复「${answer}」`
185
+ : `学会了!发送「${question}」我会回复「${answer}」`;
186
+ }
187
+
188
+ export async function teachList(
189
+ input: MessageInput,
190
+ cfg: GroupSuiteConfig,
191
+ page = 1,
192
+ runtime: GroupSuiteRuntime,
193
+ ): Promise<string> {
194
+ const QA = getTeachModel(runtime.db);
195
+ if (!QA) return '问答数据库尚未就绪';
196
+ const safePage = Math.max(1, page);
197
+ const { type: ctxType, id: ctxId } = resolveContextKey(input);
198
+ const allItems = (await QA.select().where(
199
+ ctxType === 'global'
200
+ ? { context_type: 'global' }
201
+ : { context_type: ctxType, context_id: ctxId },
202
+ )) as Record<string, unknown>[];
203
+ if (allItems.length === 0) return '还没有教过任何问答哦~';
204
+ const totalPages = Math.ceil(allItems.length / cfg.teachPageSize);
205
+ const pageNum = Math.min(safePage, totalPages);
206
+ const start = (pageNum - 1) * cfg.teachPageSize;
207
+ const pageItems = allItems.slice(start, start + cfg.teachPageSize);
208
+ const lines = pageItems.map((item, i) => {
209
+ const prefix = item.is_regex ? '[正则]' : '[精确]';
210
+ const q = item.is_regex ? `/${item.question}/` : item.question;
211
+ return `${start + i + 1}. ${prefix} ${q} → ${item.answer}`;
212
+ });
213
+ const header = ctxType === 'global' ? '全局问答列表' : '本群问答列表';
214
+ return `${header}\n${lines.join('\n')}\n第 ${pageNum}/${totalPages} 页 · 共 ${allItems.length} 条`;
215
+ }
216
+
217
+ export async function teachForget(
218
+ input: MessageInput,
219
+ questionRaw: string,
220
+ runtime: GroupSuiteRuntime,
221
+ ): Promise<string> {
222
+ const QA = getTeachModel(runtime.db);
223
+ if (!QA) return '问答数据库尚未就绪';
224
+ const question = questionRaw.trim();
225
+ if (!question) return '请提供要删除的问题';
226
+ const { type: ctxType, id: ctxId } = resolveContextKey(input);
227
+ const items = (await QA.select().where({
228
+ question,
229
+ context_type: ctxType,
230
+ context_id: ctxId,
231
+ })) as Record<string, unknown>[];
232
+ if (items.length === 0) return `没有找到问题「${question}」`;
233
+ await QA.delete().where(
234
+ qaWhereKey(items[0] as {
235
+ question: string;
236
+ context_type: string;
237
+ context_id: string;
238
+ is_regex?: number | boolean;
239
+ }),
240
+ );
241
+ return `已忘记「${question}」的回答`;
242
+ }
243
+
244
+ /** Try teach auto-reply; returns reply text or null. */
245
+ export async function tryTeachReply(
246
+ input: MessageInput,
247
+ cfg: GroupSuiteConfig,
248
+ runtime: GroupSuiteRuntime,
249
+ ): Promise<string | null> {
250
+ const content = typeof input.content === 'string' ? input.content.trim() : '';
251
+ if (!content) return null;
252
+ const { type: ctxType, id: ctxId } = resolveContextKey(input);
253
+ const matched = await findMatch(content, ctxType, ctxId, cfg, runtime);
254
+ if (!matched) return null;
255
+
256
+ const cooldownKey = `${matched.question}:${ctxType}:${ctxId}:${matched.is_regex ?? 0}`;
257
+ const cooldowns = runtime.teachCooldowns;
258
+ const last = cooldowns.get(cooldownKey);
259
+ if (last && Date.now() - last < cfg.teachCooldownMs) return null;
260
+ cooldowns.set(cooldownKey, Date.now());
261
+
262
+ const QA = getTeachModel(runtime.db);
263
+ if (QA) {
264
+ try {
265
+ await QA.update({
266
+ hit_count: Number(matched.hit_count || 0) + 1,
267
+ updated_at: ts(),
268
+ }).where(
269
+ qaWhereKey(matched as {
270
+ question: string;
271
+ context_type: string;
272
+ context_id: string;
273
+ is_regex?: number | boolean;
274
+ }),
275
+ );
276
+ } catch {
277
+ /* ignore */
278
+ }
279
+ }
280
+
281
+ const sender = resolveSender(input);
282
+ let answer = String(matched.answer ?? '');
283
+ if (matched.is_regex) {
284
+ try {
285
+ const m = content.match(new RegExp(String(matched.question), 'i'));
286
+ answer = processAnswer(answer, sender, m || undefined);
287
+ } catch {
288
+ /* ignore */
289
+ }
290
+ } else {
291
+ answer = processAnswer(answer, sender);
292
+ }
293
+ return answer;
294
+ }
package/lib/admin.d.ts DELETED
@@ -1,4 +0,0 @@
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
@@ -1 +0,0 @@
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 DELETED
@@ -1,123 +0,0 @@
1
- import { formatCompact, MessageCommand, ZhinTool, } from "zhin.js";
2
- import { adapterAllowed } from "./shared.js";
3
- export function registerAdmin(plugin, cfg) {
4
- const { addCommand, 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
- endpoint: notice.$endpoint,
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
- endpoint: notice.$endpoint,
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
- plugin.root.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
package/lib/admin.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"admin.js","sourceRoot":"","sources":["../src/admin.ts"],"names":[],"mappings":"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"}
package/lib/checkin.d.ts DELETED
@@ -1,5 +0,0 @@
1
- import { type Plugin } from "zhin.js";
2
- import type { GroupSuiteConfig } from "./config.js";
3
- export declare function getCheckinModel(): any;
4
- export declare function registerCheckin(plugin: Plugin, cfg: GroupSuiteConfig): void;
5
- //# sourceMappingURL=checkin.d.ts.map
@@ -1 +0,0 @@
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;AAepD,wBAAgB,eAAe,IAAI,GAAG,CAErC;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,gBAAgB,GAAG,IAAI,CA+K3E"}