@zhin.js/plugin-group-suite 1.1.25 → 1.1.26
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/LICENSE +21 -0
- package/README.md +18 -1
- package/commands/checkin/index.js +3 -3
- package/commands/checkin/index.ts +3 -3
- package/commands/forget/index.js +1 -1
- package/commands/forget/index.ts +1 -1
- package/commands/keyword-add/[keyword]/index.js +1 -1
- package/commands/keyword-add/[keyword]/index.ts +1 -1
- package/commands/keyword-remove/[keyword]/index.js +1 -1
- package/commands/keyword-remove/[keyword]/index.ts +1 -1
- package/commands/rank/index.js +3 -3
- package/commands/rank/index.ts +3 -3
- package/commands/stats/index.js +3 -3
- package/commands/stats/index.ts +3 -3
- package/commands/stats-card/index.js +2 -2
- package/commands/stats-card/index.ts +2 -2
- package/commands/stats-rank/index.js +3 -3
- package/commands/stats-rank/index.ts +3 -3
- package/commands/stats-week/index.js +3 -3
- package/commands/stats-week/index.ts +3 -3
- package/commands/teach/index.js +4 -4
- package/commands/teach/index.ts +4 -4
- package/commands/teach-list/[[page]]/index.js +3 -3
- package/commands/teach-list/[[page]]/index.ts +3 -3
- package/commands/teach-regex/index.js +4 -4
- package/commands/teach-regex/index.ts +4 -4
- package/handlers/welcome/index.js +2 -2
- package/handlers/welcome/index.ts +2 -2
- package/lib/config.d.ts +15 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +11 -1
- package/lib/config.js.map +1 -1
- package/lib/teach-lib.d.ts +12 -0
- package/lib/teach-lib.d.ts.map +1 -1
- package/lib/teach-lib.js +104 -29
- package/lib/teach-lib.js.map +1 -1
- package/middlewares/keyword-reply/index.js +2 -2
- package/middlewares/keyword-reply/index.ts +2 -2
- package/middlewares/teach-reply/index.js +2 -2
- package/middlewares/teach-reply/index.ts +2 -2
- package/package.json +16 -21
- package/plugin.js +5 -1
- package/schema.json +97 -14
- package/src/config.ts +22 -2
- package/src/teach-lib.ts +86 -23
package/src/teach-lib.ts
CHANGED
|
@@ -11,15 +11,82 @@ import type { GroupSuiteRuntime } from './runtime-state.js';
|
|
|
11
11
|
|
|
12
12
|
export const TEACH_USAGE_HINT = '格式:关键词 回答,或 问题|答案';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/** Regex rules are only tried against messages up to this length. */
|
|
15
|
+
export const TEACH_REGEX_MAX_CONTENT = 200;
|
|
16
|
+
/** `*`, `+` and `{n,m}` quantifiers allowed in one pattern (bounds polynomial backtracking). */
|
|
17
|
+
const TEACH_REGEX_MAX_QUANTIFIERS = 3;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Conservative structural check for user-taught patterns, which run against every inbound
|
|
21
|
+
* message. It refuses the shapes that backtrack super-linearly:
|
|
22
|
+
* - a repeated group whose body contains a quantifier or an alternation
|
|
23
|
+
* (`(\w+\s?)+`, `(a|aa)+`, `(.*a){10}`);
|
|
24
|
+
* - backreferences and lookarounds;
|
|
25
|
+
* - more than a few repeat operators in one pattern (`.*.*.*.*x`).
|
|
26
|
+
* A character class (`[ab]+`) is the accepted way to repeat a set of alternatives.
|
|
27
|
+
*/
|
|
28
|
+
export function isSafeRegex(pattern: string): boolean {
|
|
15
29
|
if (pattern.length > 200) return false;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
/** One entry per open group: true once its body holds a quantifier or an alternation. */
|
|
31
|
+
const open: boolean[] = [];
|
|
32
|
+
/** Set right after `)`: whether the group that just closed is "complex". */
|
|
33
|
+
let closed: boolean | undefined;
|
|
34
|
+
let repeats = 0;
|
|
35
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
36
|
+
const ch = pattern[i]!;
|
|
37
|
+
const group = closed;
|
|
38
|
+
closed = undefined;
|
|
39
|
+
if (ch === '\\') {
|
|
40
|
+
const next = pattern[i + 1] ?? '';
|
|
41
|
+
if (/[1-9k]/.test(next)) return false;
|
|
42
|
+
i += 1;
|
|
43
|
+
} else if (ch === '[') {
|
|
44
|
+
// Skip the class body; quantifier characters inside it are literals.
|
|
45
|
+
for (i += 1; i < pattern.length && pattern[i] !== ']'; i += 1) if (pattern[i] === '\\') i += 1;
|
|
46
|
+
} else if (ch === '(') {
|
|
47
|
+
if (/^\(\?<?[=!]/.test(pattern.slice(i, i + 4))) return false;
|
|
48
|
+
open.push(false);
|
|
49
|
+
} else if (ch === ')') {
|
|
50
|
+
const complex = open.pop();
|
|
51
|
+
if (complex === undefined) return false;
|
|
52
|
+
// A complex group makes its parent complex too: `((a+))+`.
|
|
53
|
+
if (complex && open.length > 0) open[open.length - 1] = true;
|
|
54
|
+
closed = complex;
|
|
55
|
+
} else if (ch === '|') {
|
|
56
|
+
if (open.length > 0) open[open.length - 1] = true;
|
|
57
|
+
} else if (ch === '*' || ch === '+' || ch === '?' || (ch === '{' && /^\{\d+(?:,\d*)?\}/.test(pattern.slice(i)))) {
|
|
58
|
+
if (ch !== '?') {
|
|
59
|
+
if (group) return false;
|
|
60
|
+
repeats += 1;
|
|
61
|
+
if (repeats > TEACH_REGEX_MAX_QUANTIFIERS) return false;
|
|
62
|
+
}
|
|
63
|
+
if (open.length > 0) open[open.length - 1] = true;
|
|
64
|
+
if (ch === '{') i = pattern.indexOf('}', i);
|
|
65
|
+
// A following `?` only makes the quantifier lazy.
|
|
66
|
+
if (pattern[i + 1] === '?') i += 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return open.length === 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Compiled teach patterns, keyed by source. `null` marks a pattern that must not run. */
|
|
73
|
+
const compiledTeachRegex = new Map<string, RegExp | null>();
|
|
74
|
+
|
|
75
|
+
function teachRegex(source: string): RegExp | null {
|
|
76
|
+
const cached = compiledTeachRegex.get(source);
|
|
77
|
+
if (cached !== undefined) return cached;
|
|
78
|
+
let compiled: RegExp | null = null;
|
|
79
|
+
// Rules stored before the stricter check are re-validated here, not only when they are taught.
|
|
80
|
+
if (isSafeRegex(source)) {
|
|
81
|
+
try {
|
|
82
|
+
compiled = new RegExp(source, 'i');
|
|
83
|
+
} catch {
|
|
84
|
+
compiled = null;
|
|
85
|
+
}
|
|
21
86
|
}
|
|
22
|
-
|
|
87
|
+
if (compiledTeachRegex.size >= 1000) compiledTeachRegex.clear();
|
|
88
|
+
compiledTeachRegex.set(source, compiled);
|
|
89
|
+
return compiled;
|
|
23
90
|
}
|
|
24
91
|
|
|
25
92
|
export function parseTeachPair(raw: string): { question: string; answer: string } | null {
|
|
@@ -83,20 +150,20 @@ async function findMatch(
|
|
|
83
150
|
): Promise<Record<string, unknown> | null> {
|
|
84
151
|
const QA = getTeachModel(runtime.db);
|
|
85
152
|
if (!QA) return null;
|
|
86
|
-
|
|
87
|
-
|
|
153
|
+
// Exact rules are a point lookup; the full rule list is only loaded when regex rules can apply.
|
|
154
|
+
const exact = (await QA.select().where({
|
|
155
|
+
question: content, context_type: ctxType, context_id: ctxId,
|
|
88
156
|
})) as Record<string, unknown>[];
|
|
89
|
-
for (const item of
|
|
90
|
-
if (!item.is_regex
|
|
157
|
+
for (const item of exact) {
|
|
158
|
+
if (!item.is_regex) return item;
|
|
91
159
|
}
|
|
92
|
-
if (cfg.teachAllowRegex) {
|
|
160
|
+
if (cfg.teachAllowRegex && content.length <= TEACH_REGEX_MAX_CONTENT) {
|
|
161
|
+
const candidates = (await QA.select().where({
|
|
162
|
+
context_type: ctxType, context_id: ctxId,
|
|
163
|
+
})) as Record<string, unknown>[];
|
|
93
164
|
for (const item of candidates) {
|
|
94
165
|
if (!item.is_regex) continue;
|
|
95
|
-
|
|
96
|
-
if (new RegExp(String(item.question), 'i').test(content)) return item;
|
|
97
|
-
} catch {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
166
|
+
if (teachRegex(String(item.question))?.test(content)) return item;
|
|
100
167
|
}
|
|
101
168
|
}
|
|
102
169
|
return null;
|
|
@@ -273,12 +340,8 @@ export async function tryTeachReply(
|
|
|
273
340
|
const sender = resolveSender(input);
|
|
274
341
|
let answer = String(matched.answer ?? '');
|
|
275
342
|
if (matched.is_regex) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
answer = processAnswer(answer, sender, m || undefined, input.locale);
|
|
279
|
-
} catch {
|
|
280
|
-
/* ignore */
|
|
281
|
-
}
|
|
343
|
+
const m = teachRegex(String(matched.question))?.exec(content);
|
|
344
|
+
answer = processAnswer(answer, sender, m || undefined, input.locale);
|
|
282
345
|
} else {
|
|
283
346
|
answer = processAnswer(answer, sender, undefined, input.locale);
|
|
284
347
|
}
|