@zhin.js/plugin-group-suite 3.0.2 → 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.
- package/README.md +42 -58
- package/agent/tools/group_announce.ts +2 -2
- package/commands/checkin.ts +12 -0
- package/commands/forget.ts +12 -0
- package/commands/keyword-add/[keyword:string].ts +15 -0
- package/commands/keyword-list.ts +14 -0
- package/commands/keyword-remove/[keyword:string].ts +14 -0
- package/commands/mypoints.ts +12 -0
- package/commands/mystats.ts +12 -0
- package/commands/rank.ts +12 -0
- package/commands/stats-rank.ts +20 -0
- package/commands/stats-week.ts +20 -0
- package/commands/stats.ts +21 -0
- package/commands/teach-list/[page:number=1].ts +13 -0
- package/commands/teach-regex.ts +12 -0
- package/commands/teach.ts +12 -0
- package/lib/analysis-card.d.ts +4 -0
- package/lib/analysis-card.d.ts.map +1 -0
- package/lib/analysis-card.js.map +1 -0
- package/lib/analysis.d.ts +95 -0
- package/lib/analysis.d.ts.map +1 -0
- package/lib/analysis.js.map +1 -0
- package/lib/card-layout.d.ts +27 -0
- package/lib/card-layout.d.ts.map +1 -0
- package/lib/card-layout.js.map +1 -0
- package/lib/checkin-lib.d.ts +18 -0
- package/lib/checkin-lib.d.ts.map +1 -0
- package/lib/checkin-lib.js +118 -0
- package/lib/checkin-lib.js.map +1 -0
- package/lib/config.d.ts +15 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +19 -0
- package/lib/config.js.map +1 -0
- package/lib/db-store.d.ts +12 -0
- package/lib/db-store.d.ts.map +1 -0
- package/lib/db-store.js +38 -0
- package/lib/db-store.js.map +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +8 -0
- package/lib/index.js.map +1 -0
- package/lib/keyword-store.d.ts +6 -0
- package/lib/keyword-store.d.ts.map +1 -0
- package/lib/keyword-store.js +21 -0
- package/lib/keyword-store.js.map +1 -0
- package/lib/memory-store.d.ts +29 -0
- package/lib/memory-store.d.ts.map +1 -0
- package/lib/memory-store.js +59 -0
- package/lib/memory-store.js.map +1 -0
- package/lib/runtime-state.d.ts +24 -0
- package/lib/runtime-state.d.ts.map +1 -0
- package/lib/runtime-state.js +21 -0
- package/lib/runtime-state.js.map +1 -0
- package/lib/shared-runtime.d.ts +20 -0
- package/lib/shared-runtime.d.ts.map +1 -0
- package/lib/shared-runtime.js +34 -0
- package/lib/shared-runtime.js.map +1 -0
- package/lib/stats-card.d.ts +5 -0
- package/lib/stats-card.d.ts.map +1 -0
- package/lib/stats-card.js.map +1 -0
- package/lib/stats-data.d.ts +32 -0
- package/lib/stats-data.d.ts.map +1 -0
- package/lib/stats-data.js.map +1 -0
- package/lib/stats-lib.d.ts +29 -0
- package/lib/stats-lib.d.ts.map +1 -0
- package/lib/stats-lib.js +173 -0
- package/lib/stats-lib.js.map +1 -0
- package/lib/tables.d.ts +4 -0
- package/lib/tables.d.ts.map +1 -0
- package/lib/tables.js +40 -0
- package/lib/tables.js.map +1 -0
- package/lib/teach-lib.d.ts +22 -0
- package/lib/teach-lib.d.ts.map +1 -0
- package/lib/teach-lib.js +233 -0
- package/lib/teach-lib.js.map +1 -0
- package/middlewares/keyword-reply.ts +37 -0
- package/middlewares/stats-count.ts +22 -0
- package/middlewares/teach-reply.ts +22 -0
- package/package.json +37 -15
- package/plugin.ts +39 -0
- package/schema.json +17 -0
- package/src/checkin-lib.ts +148 -0
- package/src/config.ts +23 -57
- package/src/db-store.ts +51 -0
- package/src/index.ts +47 -33
- package/src/keyword-store.ts +24 -0
- package/src/memory-store.ts +88 -0
- package/src/runtime-state.ts +44 -0
- package/src/shared-runtime.ts +44 -0
- package/src/stats-lib.ts +202 -0
- package/src/tables.ts +46 -0
- package/src/teach-lib.ts +301 -0
- package/tools/checkin-query/handler.ts +2 -2
- package/tools/checkin-rank/handler.ts +1 -1
- package/tools/stats-query/handler.ts +2 -2
- package/tools/stats-user/handler.ts +3 -2
- package/lib/agent/tools/group_announce.js +0 -13
- package/lib/agent/tools/group_announce.js.map +0 -1
- package/lib/src/admin.js +0 -114
- package/lib/src/admin.js.map +0 -1
- package/lib/src/analysis-card.js.map +0 -1
- package/lib/src/analysis.js.map +0 -1
- package/lib/src/card-layout.js.map +0 -1
- package/lib/src/checkin.js +0 -168
- package/lib/src/checkin.js.map +0 -1
- package/lib/src/config.js +0 -41
- package/lib/src/config.js.map +0 -1
- package/lib/src/daily-analysis.js +0 -333
- package/lib/src/daily-analysis.js.map +0 -1
- package/lib/src/index.js +0 -30
- package/lib/src/index.js.map +0 -1
- package/lib/src/shared.js +0 -40
- package/lib/src/shared.js.map +0 -1
- package/lib/src/stats-card.js.map +0 -1
- package/lib/src/stats-data.js.map +0 -1
- package/lib/src/stats.js +0 -258
- package/lib/src/stats.js.map +0 -1
- package/lib/src/teach.js +0 -294
- package/lib/src/teach.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/admin.ts +0 -136
- package/src/checkin.ts +0 -196
- package/src/daily-analysis.ts +0 -458
- package/src/shared.ts +0 -51
- package/src/stats.ts +0 -298
- package/src/teach.ts +0 -307
- /package/lib/{src/analysis-card.js → analysis-card.js} +0 -0
- /package/lib/{src/analysis.js → analysis.js} +0 -0
- /package/lib/{src/card-layout.js → card-layout.js} +0 -0
- /package/lib/{src/stats-card.js → stats-card.js} +0 -0
- /package/lib/{src/stats-data.js → stats-data.js} +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function todayStr(): string {
|
|
2
|
+
const d = new Date();
|
|
3
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function yesterdayStr(): string {
|
|
7
|
+
const d = new Date();
|
|
8
|
+
d.setDate(d.getDate() - 1);
|
|
9
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ts(): string {
|
|
13
|
+
return new Date().toISOString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function randomInt(min: number, max: number): number {
|
|
17
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Runtime Message → group/global context */
|
|
21
|
+
export function resolveContextKey(input: {
|
|
22
|
+
target?: string;
|
|
23
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
24
|
+
}): { type: string; id: string } {
|
|
25
|
+
const meta = input.metadata ?? {};
|
|
26
|
+
const channelType = String(meta.type ?? meta.channelType ?? '');
|
|
27
|
+
if (channelType === 'group' || channelType === 'guild') {
|
|
28
|
+
return { type: 'group', id: String(input.target ?? meta.channelId ?? '') };
|
|
29
|
+
}
|
|
30
|
+
if (input.target && channelType !== 'private') {
|
|
31
|
+
return { type: 'group', id: String(input.target) };
|
|
32
|
+
}
|
|
33
|
+
return { type: 'global', id: '' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveSender(input: {
|
|
37
|
+
sender?: string;
|
|
38
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
39
|
+
}): { id: string; name: string } {
|
|
40
|
+
const meta = input.metadata ?? {};
|
|
41
|
+
const id = String(input.sender ?? meta.senderId ?? '');
|
|
42
|
+
const name = String(meta.senderName ?? meta.name ?? '用户');
|
|
43
|
+
return { id, name };
|
|
44
|
+
}
|
package/src/stats-lib.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
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
|
+
const buffer = new Map<string, PendingStatsIncrement>();
|
|
29
|
+
|
|
30
|
+
function bufferKey(userId: string, groupId: string, date: string): string {
|
|
31
|
+
return `${userId}:${groupId}:${date}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveGroupId(input: {
|
|
35
|
+
target?: string;
|
|
36
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
37
|
+
}): string {
|
|
38
|
+
const meta = input.metadata ?? {};
|
|
39
|
+
const channelType = String(meta.type ?? meta.channelType ?? '');
|
|
40
|
+
if (channelType === 'group' || channelType === 'guild') {
|
|
41
|
+
return String(input.target ?? meta.channelId ?? '');
|
|
42
|
+
}
|
|
43
|
+
if (input.target && channelType !== 'private') {
|
|
44
|
+
return String(input.target);
|
|
45
|
+
}
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function recordMessage(input: {
|
|
50
|
+
sender?: string;
|
|
51
|
+
target?: string;
|
|
52
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
53
|
+
}, runtime?: GroupSuiteRuntime): void {
|
|
54
|
+
const targetBuffer = runtime?.statsBuffer ?? buffer;
|
|
55
|
+
const { id: userId, name: userName } = resolveSender(input);
|
|
56
|
+
if (!userId) return;
|
|
57
|
+
const groupId = resolveGroupId(input);
|
|
58
|
+
const key = bufferKey(userId, groupId, todayStr());
|
|
59
|
+
const pending = targetBuffer.get(key);
|
|
60
|
+
if (pending) {
|
|
61
|
+
pending.count++;
|
|
62
|
+
pending.user_name = userName;
|
|
63
|
+
} else {
|
|
64
|
+
targetBuffer.set(key, {
|
|
65
|
+
user_id: userId,
|
|
66
|
+
user_name: userName,
|
|
67
|
+
group_id: groupId,
|
|
68
|
+
date: todayStr(),
|
|
69
|
+
count: 1,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function flushStatsBuffer(runtime?: GroupSuiteRuntime): Promise<void> {
|
|
75
|
+
const targetBuffer = runtime?.statsBuffer ?? buffer;
|
|
76
|
+
const M = getStatsModel(runtime?.db);
|
|
77
|
+
if (!M || targetBuffer.size === 0) return;
|
|
78
|
+
const entries = [...targetBuffer.values()];
|
|
79
|
+
targetBuffer.clear();
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
try {
|
|
82
|
+
const existing = (await M.select().where({
|
|
83
|
+
user_id: entry.user_id,
|
|
84
|
+
group_id: entry.group_id,
|
|
85
|
+
date: entry.date,
|
|
86
|
+
})) as Record<string, unknown>[];
|
|
87
|
+
if (existing.length > 0) {
|
|
88
|
+
await M.update({
|
|
89
|
+
count: Number(existing[0]!.count || 0) + entry.count,
|
|
90
|
+
user_name: entry.user_name,
|
|
91
|
+
updated_at: ts(),
|
|
92
|
+
}).where({
|
|
93
|
+
user_id: entry.user_id,
|
|
94
|
+
group_id: entry.group_id,
|
|
95
|
+
date: entry.date,
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
await M.insert({
|
|
99
|
+
user_id: entry.user_id,
|
|
100
|
+
user_name: entry.user_name,
|
|
101
|
+
group_id: entry.group_id,
|
|
102
|
+
date: entry.date,
|
|
103
|
+
count: entry.count,
|
|
104
|
+
updated_at: ts(),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
/* single row failure */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function queryStats(
|
|
114
|
+
groupId: string,
|
|
115
|
+
fromDate: string,
|
|
116
|
+
runtime?: GroupSuiteRuntime,
|
|
117
|
+
): Promise<Map<string, { name: string; count: number }>> {
|
|
118
|
+
const M = getStatsModel(runtime?.db);
|
|
119
|
+
if (!M) return new Map();
|
|
120
|
+
const all = (groupId
|
|
121
|
+
? await M.select().where({ group_id: groupId })
|
|
122
|
+
: await M.select()) as Record<string, unknown>[];
|
|
123
|
+
const result = new Map<string, { name: string; count: number }>();
|
|
124
|
+
for (const row of all) {
|
|
125
|
+
if (String(row.date) < fromDate) continue;
|
|
126
|
+
const userId = String(row.user_id);
|
|
127
|
+
const existing = result.get(userId);
|
|
128
|
+
if (existing) {
|
|
129
|
+
existing.count += Number(row.count || 0);
|
|
130
|
+
if (row.user_name) existing.name = String(row.user_name);
|
|
131
|
+
} else {
|
|
132
|
+
result.set(userId, {
|
|
133
|
+
name: String(row.user_name || row.user_id),
|
|
134
|
+
count: Number(row.count || 0),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type MessageInput = {
|
|
142
|
+
sender?: string;
|
|
143
|
+
target?: string;
|
|
144
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export async function statsRankText(
|
|
148
|
+
input: MessageInput,
|
|
149
|
+
cfg: GroupSuiteConfig,
|
|
150
|
+
fromDate: string,
|
|
151
|
+
title: string,
|
|
152
|
+
runtime?: GroupSuiteRuntime,
|
|
153
|
+
): Promise<string> {
|
|
154
|
+
await flushStatsBuffer(runtime);
|
|
155
|
+
const groupId = resolveGroupId(input);
|
|
156
|
+
const stats = await queryStats(groupId, fromDate, runtime);
|
|
157
|
+
const { id: userId } = resolveSender(input);
|
|
158
|
+
const data = buildStatsRankReportData(stats, title, cfg.rankSize, userId);
|
|
159
|
+
return formatRankText(data);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function myStatsText(input: MessageInput, runtime?: GroupSuiteRuntime): Promise<string> {
|
|
163
|
+
await flushStatsBuffer(runtime);
|
|
164
|
+
const M = getStatsModel(runtime?.db);
|
|
165
|
+
if (!M) return '统计数据库尚未就绪';
|
|
166
|
+
const { id: userId, name: userName } = resolveSender(input);
|
|
167
|
+
if (!userId) return '无法获取用户信息';
|
|
168
|
+
const groupId = resolveGroupId(input);
|
|
169
|
+
const rows = (groupId
|
|
170
|
+
? await M.select().where({ user_id: userId, group_id: groupId })
|
|
171
|
+
: await M.select().where({ user_id: userId })) as Record<string, unknown>[];
|
|
172
|
+
if (rows.length === 0) return '暂无你的消息记录';
|
|
173
|
+
const today = todayStr();
|
|
174
|
+
const weekStart = weekStartStr();
|
|
175
|
+
const monthStart = monthStartStr();
|
|
176
|
+
let todayCount = 0;
|
|
177
|
+
let weekCount = 0;
|
|
178
|
+
let monthCount = 0;
|
|
179
|
+
let totalCount = 0;
|
|
180
|
+
for (const row of rows) {
|
|
181
|
+
const c = Number(row.count || 0);
|
|
182
|
+
totalCount += c;
|
|
183
|
+
if (String(row.date) >= monthStart) monthCount += c;
|
|
184
|
+
if (String(row.date) >= weekStart) weekCount += c;
|
|
185
|
+
if (row.date === today) todayCount += c;
|
|
186
|
+
}
|
|
187
|
+
const data: MyStatsReportData = {
|
|
188
|
+
userName: userName || '你',
|
|
189
|
+
scope: groupId ? '本群' : '全局',
|
|
190
|
+
todayCount,
|
|
191
|
+
weekCount,
|
|
192
|
+
monthCount,
|
|
193
|
+
totalCount,
|
|
194
|
+
activeDays: rows.length,
|
|
195
|
+
};
|
|
196
|
+
return formatMyStatsText(data);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Test helper */
|
|
200
|
+
export function resetStatsBuffer(): void {
|
|
201
|
+
buffer.clear();
|
|
202
|
+
}
|
package/src/tables.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group-suite table schemas for DatabaseHost / legacy DatabaseFeature.
|
|
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
|
+
}
|
package/src/teach-lib.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
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
|
+
const cooldownMap = new Map<string, number>();
|
|
11
|
+
|
|
12
|
+
export const TEACH_USAGE_HINT =
|
|
13
|
+
'格式:teach 关键词 回答(回答可含空格),或 teach 问题|答案';
|
|
14
|
+
|
|
15
|
+
function isSafeRegex(pattern: string): boolean {
|
|
16
|
+
if (pattern.length > 200) return false;
|
|
17
|
+
if (/(\+|\*|\{)\)?(\+|\*|\{)/.test(pattern)) return false;
|
|
18
|
+
if (/\([^)]*(\+|\*)[^)]*(\+|\*)[^)]*\)[+*]/.test(pattern)) return false;
|
|
19
|
+
const altGroups = pattern.match(/\([^)]*\|[^)]*\)/g) || [];
|
|
20
|
+
for (const g of altGroups) {
|
|
21
|
+
if ((g.match(/\|/g) || []).length > 10) return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export 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
|
+
function processAnswer(
|
|
44
|
+
answer: string,
|
|
45
|
+
sender: { id: string; name: string },
|
|
46
|
+
match?: RegExpMatchArray,
|
|
47
|
+
): string {
|
|
48
|
+
let result = answer;
|
|
49
|
+
result = result.replace(/\{sender\}/g, sender.name || '你');
|
|
50
|
+
result = result.replace(/\{sender\.id\}/g, sender.id || '');
|
|
51
|
+
result = result.replace(/\{time\}/g, new Date().toLocaleTimeString('zh-CN'));
|
|
52
|
+
result = result.replace(/\{date\}/g, new Date().toLocaleDateString('zh-CN'));
|
|
53
|
+
if (match) {
|
|
54
|
+
for (let i = 1; i < match.length; i++) {
|
|
55
|
+
result = result.replace(new RegExp(`\\$${i}`, 'g'), match[i] || '');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function qaWhereKey(row: {
|
|
62
|
+
question: string;
|
|
63
|
+
context_type: string;
|
|
64
|
+
context_id: string;
|
|
65
|
+
is_regex?: number | boolean;
|
|
66
|
+
}) {
|
|
67
|
+
return {
|
|
68
|
+
question: row.question,
|
|
69
|
+
context_type: row.context_type,
|
|
70
|
+
context_id: row.context_id,
|
|
71
|
+
is_regex: row.is_regex ? 1 : 0,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type MessageInput = {
|
|
76
|
+
sender?: string;
|
|
77
|
+
target?: string;
|
|
78
|
+
content?: unknown;
|
|
79
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
async function findMatch(
|
|
83
|
+
content: string,
|
|
84
|
+
ctxType: string,
|
|
85
|
+
ctxId: string,
|
|
86
|
+
cfg: GroupSuiteConfig,
|
|
87
|
+
runtime?: GroupSuiteRuntime,
|
|
88
|
+
): Promise<Record<string, unknown> | null> {
|
|
89
|
+
const QA = getTeachModel(runtime?.db);
|
|
90
|
+
if (!QA) return null;
|
|
91
|
+
const allItems = (await QA.select()) as Record<string, unknown>[];
|
|
92
|
+
const candidates = allItems.filter(
|
|
93
|
+
(item) =>
|
|
94
|
+
item.context_type === 'global' ||
|
|
95
|
+
(item.context_type === ctxType && item.context_id === ctxId),
|
|
96
|
+
);
|
|
97
|
+
for (const item of candidates) {
|
|
98
|
+
if (!item.is_regex && item.question === content) return item;
|
|
99
|
+
}
|
|
100
|
+
if (cfg.teachAllowRegex) {
|
|
101
|
+
for (const item of candidates) {
|
|
102
|
+
if (!item.is_regex) continue;
|
|
103
|
+
try {
|
|
104
|
+
if (new RegExp(String(item.question), 'i').test(content)) return item;
|
|
105
|
+
} catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function teachAdd(
|
|
114
|
+
input: MessageInput,
|
|
115
|
+
cfg: GroupSuiteConfig,
|
|
116
|
+
payload: string,
|
|
117
|
+
regex = false,
|
|
118
|
+
runtime?: GroupSuiteRuntime,
|
|
119
|
+
): Promise<string> {
|
|
120
|
+
const QA = getTeachModel(runtime?.db);
|
|
121
|
+
if (!QA) return '问答数据库尚未就绪,请稍后重试';
|
|
122
|
+
if (regex && !cfg.teachAllowRegex) return '管理员已禁用正则问答';
|
|
123
|
+
|
|
124
|
+
const parsed = parseTeachPair(payload.trim());
|
|
125
|
+
if (!parsed) {
|
|
126
|
+
return regex
|
|
127
|
+
? '格式:teach-regex 正则 回答,或 teach-regex 正则|回答'
|
|
128
|
+
: TEACH_USAGE_HINT;
|
|
129
|
+
}
|
|
130
|
+
const { question, answer } = parsed;
|
|
131
|
+
|
|
132
|
+
if (regex) {
|
|
133
|
+
try {
|
|
134
|
+
new RegExp(question, 'i');
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return `正则表达式不合法: ${(e as Error).message}`;
|
|
137
|
+
}
|
|
138
|
+
if (!isSafeRegex(question)) return '该正则可能导致性能问题,请简化';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { type: ctxType, id: ctxId } = resolveContextKey(input);
|
|
142
|
+
const sender = resolveSender(input);
|
|
143
|
+
|
|
144
|
+
if (ctxType === 'group') {
|
|
145
|
+
const existing = (await QA.select().where({
|
|
146
|
+
context_type: ctxType,
|
|
147
|
+
context_id: ctxId,
|
|
148
|
+
})) as Record<string, unknown>[];
|
|
149
|
+
if (existing.length >= cfg.teachMaxPerGroup) {
|
|
150
|
+
return `该群问答数已达上限 (${cfg.teachMaxPerGroup})`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const duplicate = (await QA.select().where({
|
|
155
|
+
question,
|
|
156
|
+
context_type: ctxType,
|
|
157
|
+
context_id: ctxId,
|
|
158
|
+
})) as Record<string, unknown>[];
|
|
159
|
+
|
|
160
|
+
if (duplicate.length > 0 && !regex) {
|
|
161
|
+
await QA.update({ answer, updated_at: ts() }).where(
|
|
162
|
+
qaWhereKey(duplicate[0] as {
|
|
163
|
+
question: string;
|
|
164
|
+
context_type: string;
|
|
165
|
+
context_id: string;
|
|
166
|
+
is_regex?: number | boolean;
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
return `已更新问答:「${question}」→「${answer}」`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await QA.insert({
|
|
173
|
+
question,
|
|
174
|
+
answer,
|
|
175
|
+
is_regex: regex ? 1 : 0,
|
|
176
|
+
context_type: ctxType,
|
|
177
|
+
context_id: ctxId,
|
|
178
|
+
creator_id: sender.id,
|
|
179
|
+
creator_name: sender.name,
|
|
180
|
+
hit_count: 0,
|
|
181
|
+
created_at: ts(),
|
|
182
|
+
updated_at: ts(),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return regex
|
|
186
|
+
? `学会了!匹配 /${question}/ 的消息我会回复「${answer}」`
|
|
187
|
+
: `学会了!发送「${question}」我会回复「${answer}」`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function teachList(
|
|
191
|
+
input: MessageInput,
|
|
192
|
+
cfg: GroupSuiteConfig,
|
|
193
|
+
page = 1,
|
|
194
|
+
runtime?: GroupSuiteRuntime,
|
|
195
|
+
): Promise<string> {
|
|
196
|
+
const QA = getTeachModel(runtime?.db);
|
|
197
|
+
if (!QA) return '问答数据库尚未就绪';
|
|
198
|
+
const safePage = Math.max(1, page);
|
|
199
|
+
const { type: ctxType, id: ctxId } = resolveContextKey(input);
|
|
200
|
+
const allItems = (await QA.select().where(
|
|
201
|
+
ctxType === 'global'
|
|
202
|
+
? { context_type: 'global' }
|
|
203
|
+
: { context_type: ctxType, context_id: ctxId },
|
|
204
|
+
)) as Record<string, unknown>[];
|
|
205
|
+
if (allItems.length === 0) return '还没有教过任何问答哦~';
|
|
206
|
+
const totalPages = Math.ceil(allItems.length / cfg.teachPageSize);
|
|
207
|
+
const pageNum = Math.min(safePage, totalPages);
|
|
208
|
+
const start = (pageNum - 1) * cfg.teachPageSize;
|
|
209
|
+
const pageItems = allItems.slice(start, start + cfg.teachPageSize);
|
|
210
|
+
const lines = pageItems.map((item, i) => {
|
|
211
|
+
const prefix = item.is_regex ? '[正则]' : '[精确]';
|
|
212
|
+
const q = item.is_regex ? `/${item.question}/` : item.question;
|
|
213
|
+
return `${start + i + 1}. ${prefix} ${q} → ${item.answer}`;
|
|
214
|
+
});
|
|
215
|
+
const header = ctxType === 'global' ? '全局问答列表' : '本群问答列表';
|
|
216
|
+
return `${header}\n${lines.join('\n')}\n第 ${pageNum}/${totalPages} 页 · 共 ${allItems.length} 条`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function teachForget(
|
|
220
|
+
input: MessageInput,
|
|
221
|
+
questionRaw: string,
|
|
222
|
+
runtime?: GroupSuiteRuntime,
|
|
223
|
+
): Promise<string> {
|
|
224
|
+
const QA = getTeachModel(runtime?.db);
|
|
225
|
+
if (!QA) return '问答数据库尚未就绪';
|
|
226
|
+
const question = questionRaw.trim();
|
|
227
|
+
if (!question) return '请提供要删除的问题';
|
|
228
|
+
const { type: ctxType, id: ctxId } = resolveContextKey(input);
|
|
229
|
+
const items = (await QA.select().where({
|
|
230
|
+
question,
|
|
231
|
+
context_type: ctxType,
|
|
232
|
+
context_id: ctxId,
|
|
233
|
+
})) as Record<string, unknown>[];
|
|
234
|
+
if (items.length === 0) return `没有找到问题「${question}」`;
|
|
235
|
+
await QA.delete().where(
|
|
236
|
+
qaWhereKey(items[0] as {
|
|
237
|
+
question: string;
|
|
238
|
+
context_type: string;
|
|
239
|
+
context_id: string;
|
|
240
|
+
is_regex?: number | boolean;
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
return `已忘记「${question}」的回答`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Try teach auto-reply; returns reply text or null. */
|
|
247
|
+
export async function tryTeachReply(
|
|
248
|
+
input: MessageInput,
|
|
249
|
+
cfg: GroupSuiteConfig,
|
|
250
|
+
runtime?: GroupSuiteRuntime,
|
|
251
|
+
): Promise<string | null> {
|
|
252
|
+
const content = typeof input.content === 'string' ? input.content.trim() : '';
|
|
253
|
+
if (!content) return null;
|
|
254
|
+
const { type: ctxType, id: ctxId } = resolveContextKey(input);
|
|
255
|
+
const matched = await findMatch(content, ctxType, ctxId, cfg, runtime);
|
|
256
|
+
if (!matched) return null;
|
|
257
|
+
|
|
258
|
+
const cooldownKey = `${matched.question}:${ctxType}:${ctxId}:${matched.is_regex ?? 0}`;
|
|
259
|
+
const cooldowns = runtime?.teachCooldowns ?? cooldownMap;
|
|
260
|
+
const last = cooldowns.get(cooldownKey);
|
|
261
|
+
if (last && Date.now() - last < cfg.teachCooldownMs) return null;
|
|
262
|
+
cooldowns.set(cooldownKey, Date.now());
|
|
263
|
+
|
|
264
|
+
const QA = getTeachModel(runtime?.db);
|
|
265
|
+
if (QA) {
|
|
266
|
+
try {
|
|
267
|
+
await QA.update({
|
|
268
|
+
hit_count: Number(matched.hit_count || 0) + 1,
|
|
269
|
+
updated_at: ts(),
|
|
270
|
+
}).where(
|
|
271
|
+
qaWhereKey(matched as {
|
|
272
|
+
question: string;
|
|
273
|
+
context_type: string;
|
|
274
|
+
context_id: string;
|
|
275
|
+
is_regex?: number | boolean;
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
} catch {
|
|
279
|
+
/* ignore */
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const sender = resolveSender(input);
|
|
284
|
+
let answer = String(matched.answer ?? '');
|
|
285
|
+
if (matched.is_regex) {
|
|
286
|
+
try {
|
|
287
|
+
const m = content.match(new RegExp(String(matched.question), 'i'));
|
|
288
|
+
answer = processAnswer(answer, sender, m || undefined);
|
|
289
|
+
} catch {
|
|
290
|
+
/* ignore */
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
answer = processAnswer(answer, sender);
|
|
294
|
+
}
|
|
295
|
+
return answer;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Test helper */
|
|
299
|
+
export function resetTeachCooldown(): void {
|
|
300
|
+
cooldownMap.clear();
|
|
301
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getCheckinModel } from '../../src/
|
|
2
|
-
import { todayStr } from '../../src/shared.js';
|
|
1
|
+
import { getCheckinModel } from '../../src/db-store.js';
|
|
2
|
+
import { todayStr } from '../../src/shared-runtime.js';
|
|
3
3
|
|
|
4
4
|
export default async function (args: { user_id?: string }) {
|
|
5
5
|
const M = getCheckinModel();
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
queryStats,
|
|
4
4
|
weekStartStr,
|
|
5
5
|
monthStartStr,
|
|
6
|
-
} from '../../src/stats.js';
|
|
7
|
-
import { todayStr } from '../../src/shared.js';
|
|
6
|
+
} from '../../src/stats-lib.js';
|
|
7
|
+
import { todayStr } from '../../src/shared-runtime.js';
|
|
8
8
|
|
|
9
9
|
export default async function (args: { group_id?: string; period?: string }) {
|
|
10
10
|
await flushStatsBuffer();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { flushStatsBuffer,
|
|
2
|
-
import {
|
|
1
|
+
import { flushStatsBuffer, weekStartStr } from '../../src/stats-lib.js';
|
|
2
|
+
import { getStatsModel } from '../../src/db-store.js';
|
|
3
|
+
import { todayStr } from '../../src/shared-runtime.js';
|
|
3
4
|
|
|
4
5
|
export default async function (args: { user_id: string }) {
|
|
5
6
|
await flushStatsBuffer();
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { defineTool } from '@zhin.js/agent/tools';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
export default defineTool({
|
|
4
|
-
description: '向群聊发送公告/通知消息',
|
|
5
|
-
inputSchema: z.object({ message: z.string().min(1) }),
|
|
6
|
-
keywords: ['群公告', '通知', 'announce'],
|
|
7
|
-
async execute({ message }) {
|
|
8
|
-
if (!message?.trim())
|
|
9
|
-
return '公告内容不能为空';
|
|
10
|
-
return `📢 群公告:\n${message}`;
|
|
11
|
-
},
|
|
12
|
-
});
|
|
13
|
-
//# sourceMappingURL=group_announce.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"group_announce.js","sourceRoot":"","sources":["../../../agent/tools/group_announce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAe,UAAU,CAAsB;IAC7C,WAAW,EAAE,cAAc;IAC3B,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACrD,QAAQ,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,CAAC;IACnC,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE;QACvB,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE;YAAE,OAAO,UAAU,CAAC;QACxC,OAAO,YAAY,OAAO,EAAE,CAAC;IAC/B,CAAC;CACF,CAAC,CAAC"}
|