@zhin.js/plugin-group-suite 3.0.1 → 3.0.3

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 (114) hide show
  1. package/README.md +42 -58
  2. package/agent/tools/group_announce.ts +12 -0
  3. package/commands/checkin.ts +12 -0
  4. package/commands/forget.ts +12 -0
  5. package/commands/keyword-add/[keyword:string].ts +15 -0
  6. package/commands/keyword-list.ts +14 -0
  7. package/commands/keyword-remove/[keyword:string].ts +14 -0
  8. package/commands/mypoints.ts +12 -0
  9. package/commands/mystats.ts +12 -0
  10. package/commands/rank.ts +12 -0
  11. package/commands/stats-rank.ts +20 -0
  12. package/commands/stats-week.ts +20 -0
  13. package/commands/stats.ts +21 -0
  14. package/commands/teach-list/[page:number=1].ts +13 -0
  15. package/commands/teach-regex.ts +12 -0
  16. package/commands/teach.ts +12 -0
  17. package/lib/checkin-lib.d.ts +18 -0
  18. package/lib/checkin-lib.d.ts.map +1 -0
  19. package/lib/checkin-lib.js +118 -0
  20. package/lib/checkin-lib.js.map +1 -0
  21. package/lib/config.d.ts +4 -57
  22. package/lib/config.d.ts.map +1 -1
  23. package/lib/config.js +18 -40
  24. package/lib/config.js.map +1 -1
  25. package/lib/db-store.d.ts +12 -0
  26. package/lib/db-store.d.ts.map +1 -0
  27. package/lib/db-store.js +38 -0
  28. package/lib/db-store.js.map +1 -0
  29. package/lib/index.d.ts +9 -1
  30. package/lib/index.d.ts.map +1 -1
  31. package/lib/index.js +7 -29
  32. package/lib/index.js.map +1 -1
  33. package/lib/keyword-store.d.ts +6 -0
  34. package/lib/keyword-store.d.ts.map +1 -0
  35. package/lib/keyword-store.js +21 -0
  36. package/lib/keyword-store.js.map +1 -0
  37. package/lib/memory-store.d.ts +29 -0
  38. package/lib/memory-store.d.ts.map +1 -0
  39. package/lib/memory-store.js +59 -0
  40. package/lib/memory-store.js.map +1 -0
  41. package/lib/runtime-state.d.ts +24 -0
  42. package/lib/runtime-state.d.ts.map +1 -0
  43. package/lib/runtime-state.js +21 -0
  44. package/lib/runtime-state.js.map +1 -0
  45. package/lib/shared-runtime.d.ts +20 -0
  46. package/lib/shared-runtime.d.ts.map +1 -0
  47. package/lib/shared-runtime.js +34 -0
  48. package/lib/shared-runtime.js.map +1 -0
  49. package/lib/stats-lib.d.ts +29 -0
  50. package/lib/stats-lib.d.ts.map +1 -0
  51. package/lib/stats-lib.js +173 -0
  52. package/lib/stats-lib.js.map +1 -0
  53. package/lib/tables.d.ts +4 -0
  54. package/lib/tables.d.ts.map +1 -0
  55. package/lib/tables.js +40 -0
  56. package/lib/tables.js.map +1 -0
  57. package/lib/teach-lib.d.ts +22 -0
  58. package/lib/teach-lib.d.ts.map +1 -0
  59. package/lib/teach-lib.js +233 -0
  60. package/lib/teach-lib.js.map +1 -0
  61. package/middlewares/keyword-reply.ts +37 -0
  62. package/middlewares/stats-count.ts +22 -0
  63. package/middlewares/teach-reply.ts +22 -0
  64. package/package.json +46 -13
  65. package/plugin.ts +39 -0
  66. package/schema.json +17 -0
  67. package/src/checkin-lib.ts +148 -0
  68. package/src/config.ts +23 -57
  69. package/src/db-store.ts +51 -0
  70. package/src/index.ts +47 -33
  71. package/src/keyword-store.ts +24 -0
  72. package/src/memory-store.ts +88 -0
  73. package/src/runtime-state.ts +44 -0
  74. package/src/shared-runtime.ts +44 -0
  75. package/src/stats-lib.ts +202 -0
  76. package/src/tables.ts +46 -0
  77. package/src/teach-lib.ts +301 -0
  78. package/tools/checkin-query/handler.ts +2 -2
  79. package/tools/checkin-rank/handler.ts +1 -1
  80. package/tools/stats-query/handler.ts +2 -2
  81. package/tools/stats-user/handler.ts +3 -2
  82. package/lib/admin.d.ts +0 -4
  83. package/lib/admin.d.ts.map +0 -1
  84. package/lib/admin.js +0 -123
  85. package/lib/admin.js.map +0 -1
  86. package/lib/checkin.d.ts +0 -5
  87. package/lib/checkin.d.ts.map +0 -1
  88. package/lib/checkin.js +0 -168
  89. package/lib/checkin.js.map +0 -1
  90. package/lib/daily-analysis.d.ts +0 -7
  91. package/lib/daily-analysis.d.ts.map +0 -1
  92. package/lib/daily-analysis.js +0 -333
  93. package/lib/daily-analysis.js.map +0 -1
  94. package/lib/shared.d.ts +0 -25
  95. package/lib/shared.d.ts.map +0 -1
  96. package/lib/shared.js +0 -40
  97. package/lib/shared.js.map +0 -1
  98. package/lib/stats.d.ts +0 -12
  99. package/lib/stats.d.ts.map +0 -1
  100. package/lib/stats.js +0 -258
  101. package/lib/stats.js.map +0 -1
  102. package/lib/teach.d.ts +0 -4
  103. package/lib/teach.d.ts.map +0 -1
  104. package/lib/teach.js +0 -294
  105. package/lib/teach.js.map +0 -1
  106. package/plugin.yml +0 -3
  107. package/src/admin.ts +0 -145
  108. package/src/checkin.ts +0 -196
  109. package/src/daily-analysis.ts +0 -458
  110. package/src/shared.ts +0 -51
  111. package/src/stats.ts +0 -298
  112. package/src/teach.ts +0 -307
  113. /package/{skills/checkin/SKILL.md → agent/skills/checkin.md} +0 -0
  114. /package/{skills/group-daily-analysis/SKILL.md → agent/skills/group-daily-analysis.md} +0 -0
package/src/stats.ts DELETED
@@ -1,298 +0,0 @@
1
- import { formatCompact, isActionMessage, MessageCommand, segment, type Plugin } from "zhin.js";
2
- import type { GroupSuiteConfig } from "./config.js";
3
- import { todayStr, ts } from "./shared.js";
4
- import { buildStatsRankReportData, type MyStatsReportData } from "./stats-data.js";
5
- import { buildMyStatsHtml, buildStatsRankHtml, STATS_REPORT_CANVAS } from "./stats-card.js";
6
-
7
- export function weekStartStr(): string {
8
- const d = new Date();
9
- const day = d.getDay() || 7;
10
- d.setDate(d.getDate() - day + 1);
11
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
12
- }
13
-
14
- export function monthStartStr(): string {
15
- const d = new Date();
16
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-01`;
17
- }
18
-
19
- let _db: any = null;
20
-
21
- export function getStatsModel(): any {
22
- return _db?.models?.get("message_stats") ?? null;
23
- }
24
-
25
- interface PendingIncrement {
26
- user_id: string;
27
- user_name: string;
28
- group_id: string;
29
- date: string;
30
- count: number;
31
- }
32
-
33
- const buffer = new Map<string, PendingIncrement>();
34
-
35
- function bufferKey(userId: string, groupId: string, date: string): string {
36
- return `${userId}:${groupId}:${date}`;
37
- }
38
-
39
- function getGroupId(message: { $channel?: { type?: string; id?: string } }): string {
40
- if (message.$channel?.type === "group") {
41
- return String(message.$channel.id ?? "");
42
- }
43
- return "";
44
- }
45
-
46
- export async function flushStatsBuffer(): Promise<void> {
47
- const M = getStatsModel();
48
- if (!M || buffer.size === 0) return;
49
- const entries = [...buffer.values()];
50
- buffer.clear();
51
- for (const entry of entries) {
52
- try {
53
- const existing: any[] = await M.select().where({
54
- user_id: entry.user_id,
55
- group_id: entry.group_id,
56
- date: entry.date,
57
- });
58
- if (existing.length > 0) {
59
- await M.update({
60
- count: (existing[0].count || 0) + entry.count,
61
- user_name: entry.user_name,
62
- updated_at: ts(),
63
- }).where({
64
- user_id: entry.user_id,
65
- group_id: entry.group_id,
66
- date: entry.date,
67
- });
68
- } else {
69
- await M.insert({
70
- user_id: entry.user_id,
71
- user_name: entry.user_name,
72
- group_id: entry.group_id,
73
- date: entry.date,
74
- count: entry.count,
75
- updated_at: ts(),
76
- });
77
- }
78
- } catch {
79
- /* single row failure */
80
- }
81
- }
82
- }
83
-
84
- export async function queryStats(
85
- groupId: string,
86
- fromDate: string,
87
- ): Promise<Map<string, { name: string; count: number }>> {
88
- const M = getStatsModel();
89
- if (!M) return new Map();
90
- const all: any[] = groupId
91
- ? await M.select().where({ group_id: groupId })
92
- : await M.select();
93
- const result = new Map<string, { name: string; count: number }>();
94
- for (const row of all) {
95
- if (row.date < fromDate) continue;
96
- const existing = result.get(row.user_id);
97
- if (existing) {
98
- existing.count += row.count || 0;
99
- if (row.user_name) existing.name = row.user_name;
100
- } else {
101
- result.set(row.user_id, {
102
- name: row.user_name || row.user_id,
103
- count: row.count || 0,
104
- });
105
- }
106
- }
107
- return result;
108
- }
109
-
110
- function statsRankReply(data: ReturnType<typeof buildStatsRankReportData>) {
111
- return segment.html({
112
- html: buildStatsRankHtml(data),
113
- width: 540,
114
- backgroundColor: STATS_REPORT_CANVAS,
115
- fileName: "message-stats-rank.png",
116
- });
117
- }
118
-
119
- function myStatsReply(data: MyStatsReportData) {
120
- return segment.html({
121
- html: buildMyStatsHtml(data),
122
- width: 540,
123
- backgroundColor: STATS_REPORT_CANVAS,
124
- fileName: "my-message-stats.png",
125
- });
126
- }
127
-
128
- export function registerStats(plugin: Plugin, cfg: GroupSuiteConfig): void {
129
- const { logger, addCommand, useContext, onDispose } = plugin;
130
-
131
- useContext("database", (db: any) => {
132
- _db = db;
133
- db.define("message_stats", {
134
- user_id: { type: "text", nullable: false },
135
- user_name: { type: "text", default: "" },
136
- group_id: { type: "text", default: "" },
137
- date: { type: "text", nullable: false },
138
- count: { type: "integer", default: 0 },
139
- updated_at: { type: "text", default: "" },
140
- });
141
- logger.info(formatCompact({ 模块: "消息统计", 数据模型: "已就绪" }));
142
- });
143
-
144
- const flushTimer = setInterval(flushStatsBuffer, 10_000);
145
- onDispose(async () => {
146
- clearInterval(flushTimer);
147
- await flushStatsBuffer();
148
- buffer.clear();
149
- });
150
-
151
- plugin.root.addMiddleware(async (message, next) => {
152
- if (isActionMessage(message)) return next();
153
- const userId = String(message.$sender?.id || "");
154
- if (!userId) return next();
155
- const key = bufferKey(userId, getGroupId(message), todayStr());
156
- const pending = buffer.get(key);
157
- if (pending) {
158
- pending.count++;
159
- pending.user_name = String(message.$sender?.name || "");
160
- } else {
161
- buffer.set(key, {
162
- user_id: userId,
163
- user_name: String(message.$sender?.name || ""),
164
- group_id: getGroupId(message),
165
- date: todayStr(),
166
- count: 1,
167
- });
168
- }
169
- return next();
170
- });
171
-
172
- addCommand(
173
- new MessageCommand("stats")
174
- .desc("消息统计", "查看今日消息统计")
175
- .action(async (message) => {
176
- await flushStatsBuffer();
177
- const groupId = getGroupId(message);
178
- const stats = await queryStats(groupId, todayStr());
179
- const title = groupId ? "今日本群消息统计" : "今日全局消息统计";
180
- const data = buildStatsRankReportData(
181
- stats,
182
- title,
183
- cfg.rankSize,
184
- String(message.$sender?.id || ""),
185
- );
186
- return statsRankReply(data);
187
- }),
188
- );
189
-
190
- addCommand(
191
- new MessageCommand("stats-week")
192
- .desc("周消息统计", "查看本周消息统计")
193
- .action(async (message) => {
194
- await flushStatsBuffer();
195
- const groupId = getGroupId(message);
196
- const stats = await queryStats(groupId, weekStartStr());
197
- const title = groupId ? "本周本群消息统计" : "本周全局消息统计";
198
- const data = buildStatsRankReportData(
199
- stats,
200
- title,
201
- cfg.rankSize,
202
- String(message.$sender?.id || ""),
203
- );
204
- return statsRankReply(data);
205
- }),
206
- );
207
-
208
- addCommand(
209
- new MessageCommand("stats-rank")
210
- .desc("话唠排行", "查看活跃度排行榜")
211
- .action(async (message) => {
212
- await flushStatsBuffer();
213
- const groupId = getGroupId(message);
214
- const stats = await queryStats(groupId, monthStartStr());
215
- const title = groupId ? "本月话唠排行" : "全局话唠排行";
216
- const data = buildStatsRankReportData(
217
- stats,
218
- title,
219
- cfg.rankSize,
220
- String(message.$sender?.id || ""),
221
- );
222
- return statsRankReply(data);
223
- }),
224
- );
225
-
226
- addCommand(
227
- new MessageCommand("mystats")
228
- .desc("我的统计", "查看个人消息统计")
229
- .action(async (message) => {
230
- await flushStatsBuffer();
231
- const M = getStatsModel();
232
- if (!M) return "统计数据库尚未就绪";
233
- const userId = String(message.$sender?.id || "");
234
- if (!userId) return "无法获取用户信息";
235
- const groupId = getGroupId(message);
236
- const rows: any[] = groupId
237
- ? await M.select().where({ user_id: userId, group_id: groupId })
238
- : await M.select().where({ user_id: userId });
239
- if (rows.length === 0) return "暂无你的消息记录";
240
- const today = todayStr();
241
- const weekStart = weekStartStr();
242
- const monthStart = monthStartStr();
243
- let todayCount = 0;
244
- let weekCount = 0;
245
- let monthCount = 0;
246
- let totalCount = 0;
247
- for (const row of rows) {
248
- const c = row.count || 0;
249
- totalCount += c;
250
- if (row.date >= monthStart) monthCount += c;
251
- if (row.date >= weekStart) weekCount += c;
252
- if (row.date === today) todayCount += c;
253
- }
254
- const scope = groupId ? "本群" : "全局";
255
- const data: MyStatsReportData = {
256
- userName: String(message.$sender?.name || "你"),
257
- scope,
258
- todayCount,
259
- weekCount,
260
- monthCount,
261
- totalCount,
262
- activeDays: rows.length,
263
- };
264
- return myStatsReply(data);
265
- }),
266
- );
267
-
268
- const cleanupTimer = setInterval(async () => {
269
- const M = getStatsModel();
270
- if (!M) return;
271
- try {
272
- const cutoff = new Date();
273
- cutoff.setDate(cutoff.getDate() - cfg.statsRetentionDays);
274
- const cutoffStr = `${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, "0")}-${String(cutoff.getDate()).padStart(2, "0")}`;
275
- const all: any[] = await M.select();
276
- for (const row of all.filter((r) => r.date < cutoffStr)) {
277
- await M.delete().where({
278
- user_id: row.user_id,
279
- group_id: row.group_id,
280
- date: row.date,
281
- });
282
- }
283
- } catch {
284
- /* ignore */
285
- }
286
- }, 24 * 60 * 60_000);
287
-
288
- onDispose(() => clearInterval(cleanupTimer));
289
-
290
- logger.info(
291
- formatCompact({
292
- 模块: "消息统计",
293
- 状态: "已加载",
294
- 排行榜大小: cfg.rankSize,
295
- 数据保留天数: cfg.statsRetentionDays,
296
- }),
297
- );
298
- }
package/src/teach.ts DELETED
@@ -1,307 +0,0 @@
1
- import { MessageCommand, type Plugin } from "zhin.js";
2
- import type { GroupSuiteConfig } from "./config.js";
3
- import { extractInboundText, getMessageContextKey, ts } from "./shared.js";
4
-
5
- const cooldownMap = new Map<string, number>();
6
-
7
- function isSafeRegex(pattern: string): boolean {
8
- if (pattern.length > 200) return false;
9
- if (/(\+|\*|\{)\)?(\+|\*|\{)/.test(pattern)) return false;
10
- if (/\([^)]*(\+|\*)[^)]*(\+|\*)[^)]*\)[+*]/.test(pattern)) return false;
11
- const altGroups = pattern.match(/\([^)]*\|[^)]*\)/g) || [];
12
- for (const g of altGroups) {
13
- if ((g.match(/\|/g) || []).length > 10) return false;
14
- }
15
- return true;
16
- }
17
-
18
- /** 从 teach / teach-regex 命令的剩余文本解析「问题|答案」或「问题 答案」 */
19
- function extractRestParam(payload: unknown): string {
20
- if (Array.isArray(payload)) {
21
- return payload.map((v) => String(v)).join(" ").trim();
22
- }
23
- return String(payload ?? "").trim();
24
- }
25
-
26
- function parseTeachPair(raw: string): { question: string; answer: string } | null {
27
- if (!raw) return null;
28
- const pipe = raw.indexOf("|");
29
- if (pipe >= 0) {
30
- const question = raw.slice(0, pipe).trim();
31
- const answer = raw.slice(pipe + 1).trim();
32
- if (question && answer) return { question, answer };
33
- return null;
34
- }
35
- const lastSpace = raw.lastIndexOf(" ");
36
- if (lastSpace < 0) return null;
37
- const question = raw.slice(0, lastSpace).trim();
38
- const answer = raw.slice(lastSpace + 1).trim();
39
- if (!question || !answer) return null;
40
- return { question, answer };
41
- }
42
-
43
- const TEACH_USAGE_HINT =
44
- "格式:teach 关键词 回答(回答可含空格),或 teach 问题|答案";
45
-
46
- function processAnswer(
47
- answer: string,
48
- message: { $sender?: { name?: string; id?: string } },
49
- match?: RegExpMatchArray,
50
- ): string {
51
- let result = answer;
52
- result = result.replace(/\{sender\}/g, message.$sender?.name || "你");
53
- result = result.replace(/\{sender\.id\}/g, message.$sender?.id || "");
54
- result = result.replace(/\{time\}/g, new Date().toLocaleTimeString("zh-CN"));
55
- result = result.replace(/\{date\}/g, new Date().toLocaleDateString("zh-CN"));
56
- if (match) {
57
- for (let i = 1; i < match.length; i++) {
58
- result = result.replace(new RegExp(`\\$${i}`, "g"), match[i] || "");
59
- }
60
- }
61
- return result;
62
- }
63
-
64
- let _db: any = null;
65
-
66
- function getQA(): any {
67
- return _db?.models?.get("teach_qa") ?? null;
68
- }
69
-
70
- /** teach_qa 无自增 id 列,更新/删除用业务主键 */
71
- function qaWhereKey(row: {
72
- question: string;
73
- context_type: string;
74
- context_id: string;
75
- is_regex?: number | boolean;
76
- }) {
77
- return {
78
- question: row.question,
79
- context_type: row.context_type,
80
- context_id: row.context_id,
81
- is_regex: row.is_regex ? 1 : 0,
82
- };
83
- }
84
-
85
- export function registerTeach(plugin: Plugin, cfg: GroupSuiteConfig): void {
86
- const { addCommand, useContext, onDispose } = plugin;
87
- const root = plugin.root;
88
-
89
- useContext("database", (db: any) => {
90
- _db = db;
91
- db.define("teach_qa", {
92
- question: { type: "text", nullable: false },
93
- answer: { type: "text", nullable: false },
94
- is_regex: { type: "integer", default: 0 },
95
- context_type: { type: "text", default: "global" },
96
- context_id: { type: "text", default: "" },
97
- creator_id: { type: "text", default: "" },
98
- creator_name: { type: "text", default: "" },
99
- hit_count: { type: "integer", default: 0 },
100
- created_at: { type: "text", default: "" },
101
- updated_at: { type: "text", default: "" },
102
- });
103
- });
104
-
105
- const cooldownCleanup = setInterval(() => {
106
- const now = Date.now();
107
- for (const [key, time] of cooldownMap) {
108
- if (now - time > cfg.teachCooldownMs * 2) cooldownMap.delete(key);
109
- }
110
- }, 60_000);
111
- onDispose(() => clearInterval(cooldownCleanup));
112
-
113
- async function findMatch(
114
- content: string,
115
- ctxType: string,
116
- ctxId: string,
117
- ): Promise<any | null> {
118
- const QA = getQA();
119
- if (!QA) return null;
120
- const allItems: any[] = await QA.select();
121
- const candidates = allItems.filter(
122
- (item) =>
123
- item.context_type === "global" ||
124
- (item.context_type === ctxType && item.context_id === ctxId),
125
- );
126
- for (const item of candidates) {
127
- if (!item.is_regex && item.question === content) return item;
128
- }
129
- if (cfg.teachAllowRegex) {
130
- for (const item of candidates) {
131
- if (!item.is_regex) continue;
132
- try {
133
- if (new RegExp(item.question, "i").test(content)) return item;
134
- } catch {
135
- continue;
136
- }
137
- }
138
- }
139
- return null;
140
- }
141
-
142
- // 入站链只执行 root.middleware(见 packages/im/core inbound-runner)
143
- root.addMiddleware(async (message, next) => {
144
- const content = extractInboundText(message);
145
- if (!content) return next();
146
- const { type: ctxType, id: ctxId } = getMessageContextKey(message);
147
- const matched = await findMatch(content, ctxType, ctxId);
148
- if (!matched) return next();
149
- const cooldownKey = `${matched.question}:${ctxType}:${ctxId}:${matched.is_regex ?? 0}`;
150
- const last = cooldownMap.get(cooldownKey);
151
- if (last && Date.now() - last < cfg.teachCooldownMs) return next();
152
- cooldownMap.set(cooldownKey, Date.now());
153
- const QA = getQA();
154
- if (QA) {
155
- try {
156
- await QA.update({
157
- hit_count: (matched.hit_count || 0) + 1,
158
- updated_at: ts(),
159
- }).where(qaWhereKey(matched));
160
- } catch {
161
- /* ignore */
162
- }
163
- }
164
- let answer = matched.answer;
165
- if (matched.is_regex) {
166
- try {
167
- const m = content.match(new RegExp(matched.question, "i"));
168
- answer = processAnswer(answer, message, m || undefined);
169
- } catch {
170
- /* ignore */
171
- }
172
- } else {
173
- answer = processAnswer(answer, message);
174
- }
175
- await message.$reply?.(answer);
176
- return;
177
- });
178
-
179
- // 先注册较长前缀,避免 `teach [...]` 误匹配 teach-list / teach-regex
180
- addCommand(
181
- new MessageCommand("teach-list [page:number]")
182
- .desc("问答列表", "查看问答对")
183
- .action(async (message, result) => {
184
- const QA = getQA();
185
- if (!QA) return "问答数据库尚未就绪";
186
- const page = Math.max(1, result.params.page || 1);
187
- const { type: ctxType, id: ctxId } = getMessageContextKey(message);
188
- const allItems: any[] = await QA.select().where(
189
- ctxType === "global"
190
- ? { context_type: "global" }
191
- : { context_type: ctxType, context_id: ctxId },
192
- );
193
- if (allItems.length === 0) return "还没有教过任何问答哦~";
194
- const totalPages = Math.ceil(allItems.length / cfg.teachPageSize);
195
- const safePage = Math.min(page, totalPages);
196
- const start = (safePage - 1) * cfg.teachPageSize;
197
- const pageItems = allItems.slice(start, start + cfg.teachPageSize);
198
- const lines = pageItems.map((item, i) => {
199
- const prefix = item.is_regex ? "[正则]" : "[精确]";
200
- const q = item.is_regex ? `/${item.question}/` : item.question;
201
- return `${start + i + 1}. ${prefix} ${q} → ${item.answer}`;
202
- });
203
- const header = ctxType === "global" ? "全局问答列表" : "本群问答列表";
204
- return `${header}\n${lines.join("\n")}\n第 ${safePage}/${totalPages} 页 · 共 ${allItems.length} 条`;
205
- }),
206
- );
207
-
208
- addCommand(
209
- new MessageCommand("teach-regex [...payload:text]")
210
- .desc("教我正则问答", "用正则匹配")
211
- .usage("teach-regex 正则 回答", "teach-regex 正则|回答")
212
- .action(async (message, result) => {
213
- const QA = getQA();
214
- if (!QA) return "问答数据库尚未就绪";
215
- if (!cfg.teachAllowRegex) return "管理员已禁用正则问答";
216
- const parsed = parseTeachPair(extractRestParam(result.params.payload));
217
- if (!parsed) return "格式:teach-regex 正则 回答,或 teach-regex 正则|回答";
218
- const pattern = parsed.question;
219
- const answer = parsed.answer;
220
- try {
221
- new RegExp(pattern, "i");
222
- } catch (e) {
223
- return `正则表达式不合法: ${(e as Error).message}`;
224
- }
225
- if (!isSafeRegex(pattern)) return "该正则可能导致性能问题,请简化";
226
- const { type: ctxType, id: ctxId } = getMessageContextKey(message);
227
- await QA.insert({
228
- question: pattern,
229
- answer,
230
- is_regex: 1,
231
- context_type: ctxType,
232
- context_id: ctxId,
233
- creator_id: message.$sender?.id || "",
234
- creator_name: message.$sender?.name || "",
235
- hit_count: 0,
236
- created_at: ts(),
237
- updated_at: ts(),
238
- });
239
- return `学会了!匹配 /${pattern}/ 的消息我会回复「${answer}」`;
240
- }),
241
- );
242
-
243
- addCommand(
244
- new MessageCommand("forget <question:text>")
245
- .desc("忘记问答", "删除问答对")
246
- .action(async (message, result) => {
247
- const QA = getQA();
248
- if (!QA) return "问答数据库尚未就绪";
249
- const question = result.params.question?.trim();
250
- if (!question) return "请提供要删除的问题";
251
- const { type: ctxType, id: ctxId } = getMessageContextKey(message);
252
- const items: any[] = await QA.select().where({
253
- question,
254
- context_type: ctxType,
255
- context_id: ctxId,
256
- });
257
- if (items.length === 0) return `没有找到问题「${question}」`;
258
- await QA.delete().where(qaWhereKey(items[0]));
259
- return `已忘记「${question}」的回答`;
260
- }),
261
- );
262
-
263
- addCommand(
264
- new MessageCommand("teach [...payload:text]")
265
- .desc("教我问答", "教会 Endpoint 一个新的问答对")
266
- .usage("teach 关键词 回答", "teach 问题|答案")
267
- .action(async (message, result) => {
268
- const QA = getQA();
269
- if (!QA) return "问答数据库尚未就绪,请稍后重试";
270
- const parsed = parseTeachPair(extractRestParam(result.params.payload));
271
- if (!parsed) return TEACH_USAGE_HINT;
272
- const { question, answer } = parsed;
273
- const { type: ctxType, id: ctxId } = getMessageContextKey(message);
274
- if (ctxType === "group") {
275
- const existing: any[] = await QA.select().where({
276
- context_type: ctxType,
277
- context_id: ctxId,
278
- });
279
- if (existing.length >= cfg.teachMaxPerGroup) {
280
- return `该群问答数已达上限 (${cfg.teachMaxPerGroup})`;
281
- }
282
- }
283
- const duplicate: any[] = await QA.select().where({
284
- question,
285
- context_type: ctxType,
286
- context_id: ctxId,
287
- });
288
- if (duplicate.length > 0) {
289
- await QA.update({ answer, updated_at: ts() }).where(qaWhereKey(duplicate[0]));
290
- return `已更新问答:「${question}」→「${answer}」`;
291
- }
292
- await QA.insert({
293
- question,
294
- answer,
295
- is_regex: 0,
296
- context_type: ctxType,
297
- context_id: ctxId,
298
- creator_id: message.$sender?.id || "",
299
- creator_name: message.$sender?.name || "",
300
- hit_count: 0,
301
- created_at: ts(),
302
- updated_at: ts(),
303
- });
304
- return `学会了!发送「${question}」我会回复「${answer}」`;
305
- }),
306
- );
307
- }