@zhin.js/plugin-group-suite 0.1.0 → 0.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.
package/src/teach.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { MessageCommand, type Plugin } from "zhin.js";
2
2
  import type { GroupSuiteConfig } from "./config.js";
3
- import { getMessageContextKey, ts } from "./shared.js";
3
+ import { extractInboundText, getMessageContextKey, ts } from "./shared.js";
4
4
 
5
5
  const cooldownMap = new Map<string, number>();
6
6
 
@@ -15,6 +15,34 @@ function isSafeRegex(pattern: string): boolean {
15
15
  return true;
16
16
  }
17
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
+
18
46
  function processAnswer(
19
47
  answer: string,
20
48
  message: { $sender?: { name?: string; id?: string } },
@@ -39,8 +67,24 @@ function getQA(): any {
39
67
  return _db?.models?.get("teach_qa") ?? null;
40
68
  }
41
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
+
42
85
  export function registerTeach(plugin: Plugin, cfg: GroupSuiteConfig): void {
43
- const { addCommand, addMiddleware, useContext, onDispose } = plugin;
86
+ const { addCommand, useContext, onDispose } = plugin;
87
+ const root = plugin.root;
44
88
 
45
89
  useContext("database", (db: any) => {
46
90
  _db = db;
@@ -95,13 +139,14 @@ export function registerTeach(plugin: Plugin, cfg: GroupSuiteConfig): void {
95
139
  return null;
96
140
  }
97
141
 
98
- addMiddleware(async (message, next) => {
99
- const content = (message.$raw || "").trim();
142
+ // 入站链只执行 root.middleware(见 packages/im/core inbound-runner)
143
+ root.addMiddleware(async (message, next) => {
144
+ const content = extractInboundText(message);
100
145
  if (!content) return next();
101
146
  const { type: ctxType, id: ctxId } = getMessageContextKey(message);
102
147
  const matched = await findMatch(content, ctxType, ctxId);
103
148
  if (!matched) return next();
104
- const cooldownKey = `${matched.id}:${ctxType}:${ctxId}`;
149
+ const cooldownKey = `${matched.question}:${ctxType}:${ctxId}:${matched.is_regex ?? 0}`;
105
150
  const last = cooldownMap.get(cooldownKey);
106
151
  if (last && Date.now() - last < cfg.teachCooldownMs) return next();
107
152
  cooldownMap.set(cooldownKey, Date.now());
@@ -111,7 +156,7 @@ export function registerTeach(plugin: Plugin, cfg: GroupSuiteConfig): void {
111
156
  await QA.update({
112
157
  hit_count: (matched.hit_count || 0) + 1,
113
158
  updated_at: ts(),
114
- }).where({ id: matched.id });
159
+ }).where(qaWhereKey(matched));
115
160
  } catch {
116
161
  /* ignore */
117
162
  }
@@ -128,64 +173,50 @@ export function registerTeach(plugin: Plugin, cfg: GroupSuiteConfig): void {
128
173
  answer = processAnswer(answer, message);
129
174
  }
130
175
  await message.$reply(answer);
176
+ return;
131
177
  });
132
178
 
179
+ // 先注册较长前缀,避免 `teach [...]` 误匹配 teach-list / teach-regex
133
180
  addCommand(
134
- new MessageCommand("teach <question:text> <answer:text>")
135
- .desc("教我问答", "教会 Bot 一个新的问答对")
181
+ new MessageCommand("teach-list [page:number]")
182
+ .desc("问答列表", "查看问答对")
136
183
  .action(async (message, result) => {
137
184
  const QA = getQA();
138
- if (!QA) return "问答数据库尚未就绪,请稍后重试";
139
- const question = result.params.question?.trim();
140
- const answer = result.params.answer?.trim();
141
- if (!question || !answer) return "请提供问题和回答";
185
+ if (!QA) return "问答数据库尚未就绪";
186
+ const page = Math.max(1, result.params.page || 1);
142
187
  const { type: ctxType, id: ctxId } = getMessageContextKey(message);
143
- if (ctxType === "group") {
144
- const existing: any[] = await QA.select().where({
145
- context_type: ctxType,
146
- context_id: ctxId,
147
- });
148
- if (existing.length >= cfg.teachMaxPerGroup) {
149
- return `该群问答数已达上限 (${cfg.teachMaxPerGroup})`;
150
- }
151
- }
152
- const duplicate: any[] = await QA.select().where({
153
- question,
154
- context_type: ctxType,
155
- context_id: ctxId,
156
- });
157
- if (duplicate.length > 0) {
158
- await QA.update({ answer, updated_at: ts() }).where({
159
- id: duplicate[0].id,
160
- });
161
- return `已更新问答:「${question}」→「${answer}」`;
162
- }
163
- await QA.insert({
164
- question,
165
- answer,
166
- is_regex: 0,
167
- context_type: ctxType,
168
- context_id: ctxId,
169
- creator_id: message.$sender?.id || "",
170
- creator_name: message.$sender?.name || "",
171
- hit_count: 0,
172
- created_at: ts(),
173
- updated_at: ts(),
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}`;
174
202
  });
175
- return `学会了!发送「${question}」我会回复「${answer}」`;
203
+ const header = ctxType === "global" ? "全局问答列表" : "本群问答列表";
204
+ return `${header}\n${lines.join("\n")}\n第 ${safePage}/${totalPages} 页 · 共 ${allItems.length} 条`;
176
205
  }),
177
206
  );
178
207
 
179
208
  addCommand(
180
- new MessageCommand("teach-regex <pattern:text> <answer:text>")
209
+ new MessageCommand("teach-regex [...payload:text]")
181
210
  .desc("教我正则问答", "用正则匹配")
211
+ .usage("teach-regex 正则 回答", "teach-regex 正则|回答")
182
212
  .action(async (message, result) => {
183
213
  const QA = getQA();
184
214
  if (!QA) return "问答数据库尚未就绪";
185
215
  if (!cfg.teachAllowRegex) return "管理员已禁用正则问答";
186
- const pattern = result.params.pattern?.trim();
187
- const answer = result.params.answer?.trim();
188
- if (!pattern || !answer) 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;
189
220
  try {
190
221
  new RegExp(pattern, "i");
191
222
  } catch (e) {
@@ -224,36 +255,53 @@ export function registerTeach(plugin: Plugin, cfg: GroupSuiteConfig): void {
224
255
  context_id: ctxId,
225
256
  });
226
257
  if (items.length === 0) return `没有找到问题「${question}」`;
227
- await QA.delete().where({ id: items[0].id });
258
+ await QA.delete().where(qaWhereKey(items[0]));
228
259
  return `已忘记「${question}」的回答`;
229
260
  }),
230
261
  );
231
262
 
232
263
  addCommand(
233
- new MessageCommand("teach-list [page:number]")
234
- .desc("问答列表", "查看问答对")
264
+ new MessageCommand("teach [...payload:text]")
265
+ .desc("教我问答", "教会 Bot 一个新的问答对")
266
+ .usage("teach 关键词 回答", "teach 问题|答案")
235
267
  .action(async (message, result) => {
236
268
  const QA = getQA();
237
- if (!QA) return "问答数据库尚未就绪";
238
- const page = Math.max(1, result.params.page || 1);
269
+ if (!QA) return "问答数据库尚未就绪,请稍后重试";
270
+ const parsed = parseTeachPair(extractRestParam(result.params.payload));
271
+ if (!parsed) return TEACH_USAGE_HINT;
272
+ const { question, answer } = parsed;
239
273
  const { type: ctxType, id: ctxId } = getMessageContextKey(message);
240
- const allItems: any[] = await QA.select().where(
241
- ctxType === "global"
242
- ? { context_type: "global" }
243
- : { context_type: ctxType, context_id: ctxId },
244
- );
245
- if (allItems.length === 0) return "还没有教过任何问答哦~";
246
- const totalPages = Math.ceil(allItems.length / cfg.teachPageSize);
247
- const safePage = Math.min(page, totalPages);
248
- const start = (safePage - 1) * cfg.teachPageSize;
249
- const pageItems = allItems.slice(start, start + cfg.teachPageSize);
250
- const lines = pageItems.map((item, i) => {
251
- const prefix = item.is_regex ? "[正则]" : "[精确]";
252
- const q = item.is_regex ? `/${item.question}/` : item.question;
253
- return `${start + i + 1}. ${prefix} ${q} → ${item.answer}`;
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,
254
287
  });
255
- const header = ctxType === "global" ? "全局问答列表" : "本群问答列表";
256
- return `${header}\n${lines.join("\n")}\n第 ${safePage}/${totalPages} 页 · 共 ${allItems.length} 条`;
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}」`;
257
305
  }),
258
306
  );
259
307
  }
@@ -1,7 +1,8 @@
1
- import { getModel, todayStr } from '../../src/index.js';
1
+ import { getCheckinModel } from '../../src/checkin.js';
2
+ import { todayStr } from '../../src/shared.js';
2
3
 
3
4
  export default async function (args: { user_id?: string }) {
4
- const M = getModel();
5
+ const M = getCheckinModel();
5
6
  if (!M) return '签到数据库尚未就绪';
6
7
 
7
8
  const userId = args.user_id;
@@ -1,7 +1,7 @@
1
- import { getModel } from '../../src/index.js';
1
+ import { getCheckinModel } from '../../src/checkin.js';
2
2
 
3
3
  export default async function (args: { limit?: number }) {
4
- const M = getModel();
4
+ const M = getCheckinModel();
5
5
  if (!M) return '签到数据库尚未就绪';
6
6
 
7
7
  const limit = Math.min(Math.max(Number(args.limit) || 10, 1), 50);
@@ -1,7 +1,13 @@
1
- import { flushBuffer, queryStats, todayStr, weekStartStr, monthStartStr } from '../../src/index.js';
1
+ import {
2
+ flushStatsBuffer,
3
+ queryStats,
4
+ weekStartStr,
5
+ monthStartStr,
6
+ } from '../../src/stats.js';
7
+ import { todayStr } from '../../src/shared.js';
2
8
 
3
9
  export default async function (args: { group_id?: string; period?: string }) {
4
- await flushBuffer();
10
+ await flushStatsBuffer();
5
11
  const groupId = args.group_id || '';
6
12
  const period = args.period || 'today';
7
13
 
@@ -1,8 +1,9 @@
1
- import { flushBuffer, getModel, todayStr, weekStartStr } from '../../src/index.js';
1
+ import { flushStatsBuffer, getStatsModel, weekStartStr } from '../../src/stats.js';
2
+ import { todayStr } from '../../src/shared.js';
2
3
 
3
4
  export default async function (args: { user_id: string }) {
4
- await flushBuffer();
5
- const M = getModel();
5
+ await flushStatsBuffer();
6
+ const M = getStatsModel();
6
7
  if (!M) return '统计数据库尚未就绪';
7
8
 
8
9
  const userId = args.user_id;