@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/README.md +1 -1
- package/lib/admin.js +2 -2
- package/lib/admin.js.map +1 -1
- package/lib/checkin.d.ts +1 -0
- package/lib/checkin.d.ts.map +1 -1
- package/lib/checkin.js +14 -9
- package/lib/checkin.js.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/shared.d.ts +3 -1
- package/lib/shared.d.ts.map +1 -1
- package/lib/shared.js +10 -0
- package/lib/shared.js.map +1 -1
- package/lib/stats.d.ts +6 -0
- package/lib/stats.d.ts.map +1 -1
- package/lib/stats.js +30 -21
- package/lib/stats.js.map +1 -1
- package/lib/teach.d.ts.map +1 -1
- package/lib/teach.js +112 -70
- package/lib/teach.js.map +1 -1
- package/package.json +5 -5
- package/src/admin.ts +2 -2
- package/src/checkin.ts +14 -9
- package/src/index.ts +1 -1
- package/src/shared.ts +10 -1
- package/src/stats.ts +30 -21
- package/src/teach.ts +117 -69
- package/tools/checkin-query/handler.ts +3 -2
- package/tools/checkin-rank/handler.ts +2 -2
- package/tools/stats-query/handler.ts +8 -2
- package/tools/stats-user/handler.ts +4 -3
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,
|
|
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
|
-
|
|
99
|
-
|
|
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.
|
|
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(
|
|
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
|
|
135
|
-
.desc("
|
|
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
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
context_type:
|
|
146
|
-
context_id: ctxId,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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 {
|
|
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 =
|
|
5
|
+
const M = getCheckinModel();
|
|
5
6
|
if (!M) return '签到数据库尚未就绪';
|
|
6
7
|
|
|
7
8
|
const userId = args.user_id;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getCheckinModel } from '../../src/checkin.js';
|
|
2
2
|
|
|
3
3
|
export default async function (args: { limit?: number }) {
|
|
4
|
-
const M =
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
5
|
-
const M =
|
|
5
|
+
await flushStatsBuffer();
|
|
6
|
+
const M = getStatsModel();
|
|
6
7
|
if (!M) return '统计数据库尚未就绪';
|
|
7
8
|
|
|
8
9
|
const userId = args.user_id;
|