@zhin.js/plugin-group-suite 1.0.0 → 1.1.0
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 +12 -0
- package/commands/checkin.js +11 -0
- package/commands/checkin.ts +12 -0
- package/commands/forget.js +10 -0
- package/commands/forget.ts +12 -0
- package/commands/keyword-add/[keyword].js +16 -0
- package/commands/keyword-add/[keyword].ts +16 -0
- package/commands/keyword-list.js +14 -0
- package/commands/keyword-list.ts +14 -0
- package/commands/keyword-remove/[keyword].js +16 -0
- package/commands/keyword-remove/[keyword].ts +15 -0
- package/commands/mypoints.js +10 -0
- package/commands/mypoints.ts +12 -0
- package/commands/mystats.js +10 -0
- package/commands/mystats.ts +12 -0
- package/commands/rank.js +11 -0
- package/commands/rank.ts +12 -0
- package/commands/stats-rank.js +13 -0
- package/commands/stats-rank.ts +20 -0
- package/commands/stats-week.js +13 -0
- package/commands/stats-week.ts +20 -0
- package/commands/stats.js +14 -0
- package/commands/stats.ts +21 -0
- package/commands/teach-list/[[page]].js +13 -0
- package/commands/teach-list/[[page]].ts +14 -0
- package/commands/teach-regex.js +11 -0
- package/commands/teach-regex.ts +12 -0
- package/commands/teach.js +11 -0
- package/commands/teach.ts +12 -0
- package/lib/analysis.d.ts +1 -1
- package/lib/analysis.d.ts.map +1 -1
- package/lib/checkin-lib.d.ts +27 -0
- package/lib/checkin-lib.d.ts.map +1 -0
- package/lib/checkin-lib.js +134 -0
- package/lib/checkin-lib.js.map +1 -0
- package/lib/config.d.ts +4 -57
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +18 -40
- package/lib/config.js.map +1 -1
- 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 +9 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +7 -29
- package/lib/index.js.map +1 -1
- 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 +31 -0
- package/lib/memory-store.d.ts.map +1 -0
- package/lib/memory-store.js +63 -0
- package/lib/memory-store.js.map +1 -0
- package/lib/runtime-state.d.ts +25 -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 +26 -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-lib.d.ts +41 -0
- package/lib/stats-lib.d.ts.map +1 -0
- package/lib/stats-lib.js +177 -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 +25 -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.js +36 -0
- package/middlewares/keyword-reply.ts +37 -0
- package/middlewares/stats-count.js +20 -0
- package/middlewares/stats-count.ts +22 -0
- package/middlewares/teach-reply.js +21 -0
- package/middlewares/teach-reply.ts +22 -0
- package/package.json +53 -14
- package/plugin.js +40 -0
- package/schema.json +17 -0
- package/src/analysis.ts +1 -1
- package/src/checkin-lib.ts +165 -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 +90 -0
- package/src/runtime-state.ts +44 -0
- package/src/shared-runtime.ts +44 -0
- package/src/stats-lib.ts +205 -0
- package/src/tables.ts +46 -0
- package/src/teach-lib.ts +301 -0
- package/tools/checkin-query/handler.js +22 -0
- package/tools/checkin-query/handler.ts +2 -2
- package/tools/checkin-rank/handler.js +15 -0
- package/tools/checkin-rank/handler.ts +1 -1
- package/tools/stats-query/handler.js +28 -0
- package/tools/stats-query/handler.ts +2 -2
- package/tools/stats-user/handler.js +29 -0
- package/tools/stats-user/handler.ts +3 -2
- package/lib/admin.d.ts +0 -4
- package/lib/admin.d.ts.map +0 -1
- package/lib/admin.js +0 -123
- package/lib/admin.js.map +0 -1
- package/lib/checkin.d.ts +0 -5
- package/lib/checkin.d.ts.map +0 -1
- package/lib/checkin.js +0 -168
- package/lib/checkin.js.map +0 -1
- package/lib/daily-analysis.d.ts +0 -7
- package/lib/daily-analysis.d.ts.map +0 -1
- package/lib/daily-analysis.js +0 -334
- package/lib/daily-analysis.js.map +0 -1
- package/lib/shared.d.ts +0 -25
- package/lib/shared.d.ts.map +0 -1
- package/lib/shared.js +0 -40
- package/lib/shared.js.map +0 -1
- package/lib/stats.d.ts +0 -12
- package/lib/stats.d.ts.map +0 -1
- package/lib/stats.js +0 -256
- package/lib/stats.js.map +0 -1
- package/lib/teach.d.ts +0 -4
- package/lib/teach.d.ts.map +0 -1
- package/lib/teach.js +0 -294
- package/lib/teach.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/admin.ts +0 -145
- package/src/checkin.ts +0 -196
- package/src/daily-analysis.ts +0 -458
- package/src/shared.ts +0 -51
- package/src/stats.ts +0 -297
- package/src/teach.ts +0 -307
- /package/{skills/checkin/SKILL.md → agent/skills/checkin.md} +0 -0
- /package/{skills/group-daily-analysis/SKILL.md → agent/skills/group-daily-analysis.md} +0 -0
package/src/index.ts
CHANGED
|
@@ -1,33 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_GROUP_SUITE_CONFIG,
|
|
3
|
+
resolveGroupSuiteConfig,
|
|
4
|
+
} from './config.js';
|
|
5
|
+
export type { GroupSuiteConfig } from './config.js';
|
|
6
|
+
export { doCheckin, myPoints, pointsRank } from './checkin-lib.js';
|
|
7
|
+
export {
|
|
8
|
+
addKeyword,
|
|
9
|
+
listKeywords,
|
|
10
|
+
matchKeyword,
|
|
11
|
+
removeKeyword,
|
|
12
|
+
resetKeywords,
|
|
13
|
+
} from './keyword-store.js';
|
|
14
|
+
export {
|
|
15
|
+
ensureGroupSuiteMemoryDb,
|
|
16
|
+
getCheckinModel,
|
|
17
|
+
getGroupSuiteDb,
|
|
18
|
+
getStatsModel,
|
|
19
|
+
getTeachModel,
|
|
20
|
+
resetGroupSuiteDb,
|
|
21
|
+
registerGroupSuiteDb,
|
|
22
|
+
setGroupSuiteDb,
|
|
23
|
+
} from './db-store.js';
|
|
24
|
+
export {
|
|
25
|
+
createGroupSuiteRuntime,
|
|
26
|
+
groupSuiteRuntimeToken,
|
|
27
|
+
resolveGroupSuiteRuntime,
|
|
28
|
+
} from './runtime-state.js';
|
|
29
|
+
export type { GroupSuiteRuntime, PendingStatsIncrement } from './runtime-state.js';
|
|
30
|
+
export {
|
|
31
|
+
parseTeachPair,
|
|
32
|
+
resetTeachCooldown,
|
|
33
|
+
teachAdd,
|
|
34
|
+
teachForget,
|
|
35
|
+
teachList,
|
|
36
|
+
tryTeachReply,
|
|
37
|
+
} from './teach-lib.js';
|
|
38
|
+
export {
|
|
39
|
+
flushStatsBuffer,
|
|
40
|
+
monthStartStr,
|
|
41
|
+
myStatsText,
|
|
42
|
+
queryStats,
|
|
43
|
+
recordMessage,
|
|
44
|
+
resetStatsBuffer,
|
|
45
|
+
statsRankText,
|
|
46
|
+
weekStartStr,
|
|
47
|
+
} from './stats-lib.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const keywords = new Map<string, string>();
|
|
2
|
+
|
|
3
|
+
export function addKeyword(keyword: string, reply: string, store = keywords): void {
|
|
4
|
+
store.set(keyword, reply);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function removeKeyword(keyword: string, store = keywords): boolean {
|
|
8
|
+
return store.delete(keyword);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function listKeywords(store = keywords): Array<[string, string]> {
|
|
12
|
+
return Array.from(store.entries());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function matchKeyword(text: string, store = keywords): string | null {
|
|
16
|
+
for (const [keyword, reply] of store) {
|
|
17
|
+
if (text.includes(keyword)) return reply;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resetKeywords(): void {
|
|
23
|
+
keywords.clear();
|
|
24
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory group-suite models (slice-2) until Plugin Runtime DatabaseFeature Resource lands.
|
|
3
|
+
*
|
|
4
|
+
* 模型表面与 `@zhin.js/plugin-runtime` 的 DatabaseHostModel 结构兼容
|
|
5
|
+
* (where 返回 PromiseLike,host 侧是链式 Selection),
|
|
6
|
+
* plugin.ts 可把 PluginDatabaseHost 直接当 GroupSuiteMemoryDb 用。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type GroupSuiteRow = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
export interface GroupSuiteModel {
|
|
12
|
+
select: (...fields: string[]) => {
|
|
13
|
+
where: (query: Record<string, unknown>) => PromiseLike<GroupSuiteRow[]>;
|
|
14
|
+
then: <TResult1 = GroupSuiteRow[], TResult2 = never>(
|
|
15
|
+
onfulfilled?: ((value: GroupSuiteRow[]) => TResult1 | PromiseLike<TResult1>) | null,
|
|
16
|
+
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
|
17
|
+
) => Promise<TResult1 | TResult2>;
|
|
18
|
+
};
|
|
19
|
+
insert: (row: Record<string, unknown>) => Promise<unknown>;
|
|
20
|
+
delete: () => { where: (query: Record<string, unknown>) => Promise<unknown> };
|
|
21
|
+
update: (patch: Record<string, unknown>) => {
|
|
22
|
+
where: (query: Record<string, unknown>) => Promise<unknown>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createMemoryModel(): GroupSuiteModel {
|
|
27
|
+
const rows: GroupSuiteRow[] = [];
|
|
28
|
+
let nextId = 1;
|
|
29
|
+
|
|
30
|
+
function matches(row: Record<string, unknown>, query: Record<string, unknown>): boolean {
|
|
31
|
+
return Object.entries(query).every(([key, value]) => row[key] === value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function all(): GroupSuiteRow[] {
|
|
35
|
+
return rows.map((row) => ({ ...row }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
select: () => {
|
|
40
|
+
const promise = Promise.resolve(all());
|
|
41
|
+
return {
|
|
42
|
+
where: async (query: Record<string, unknown>) =>
|
|
43
|
+
rows.filter((row) => matches(row, query)).map((row) => ({ ...row })),
|
|
44
|
+
then: (onfulfilled, onrejected) => promise.then(onfulfilled, onrejected),
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
insert: async (row: Record<string, unknown>) => {
|
|
48
|
+
const withId: GroupSuiteRow = { id: String(nextId++), ...row };
|
|
49
|
+
rows.push(withId);
|
|
50
|
+
return { ...withId };
|
|
51
|
+
},
|
|
52
|
+
delete: () => ({
|
|
53
|
+
where: async (query: Record<string, unknown>) => {
|
|
54
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
55
|
+
if (matches(rows[i]!, query)) rows.splice(i, 1);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
update: (patch: Record<string, unknown>) => ({
|
|
60
|
+
where: async (query: Record<string, unknown>) => {
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
if (matches(row, query)) Object.assign(row, patch);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface GroupSuiteMemoryDb {
|
|
70
|
+
models: {
|
|
71
|
+
get: (name: string) => GroupSuiteModel | undefined;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const CHECKIN_TABLE = 'checkin_records';
|
|
76
|
+
export const TEACH_TABLE = 'teach_qa';
|
|
77
|
+
export const STATS_TABLE = 'message_stats';
|
|
78
|
+
|
|
79
|
+
export function createInMemoryGroupSuiteDb(): GroupSuiteMemoryDb {
|
|
80
|
+
const models = new Map<string, GroupSuiteModel>([
|
|
81
|
+
[CHECKIN_TABLE, createMemoryModel()],
|
|
82
|
+
[TEACH_TABLE, createMemoryModel()],
|
|
83
|
+
[STATS_TABLE, createMemoryModel()],
|
|
84
|
+
]);
|
|
85
|
+
return {
|
|
86
|
+
models: {
|
|
87
|
+
get: (name: string) => models.get(name),
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createToken, type Token } from 'zhin.js';
|
|
2
|
+
import type { GroupSuiteMemoryDb } from './memory-store.js';
|
|
3
|
+
|
|
4
|
+
export interface PendingStatsIncrement {
|
|
5
|
+
user_id: string;
|
|
6
|
+
user_name: string;
|
|
7
|
+
group_id: string;
|
|
8
|
+
date: string;
|
|
9
|
+
count: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** All mutable state owned by one group-suite plugin instance. */
|
|
13
|
+
export interface GroupSuiteRuntime {
|
|
14
|
+
readonly db: GroupSuiteMemoryDb;
|
|
15
|
+
readonly keywords: Map<string, string>;
|
|
16
|
+
readonly teachCooldowns: Map<string, number>;
|
|
17
|
+
readonly statsBuffer: Map<string, PendingStatsIncrement>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const groupSuiteRuntimeToken = createToken<GroupSuiteRuntime>(
|
|
21
|
+
'zhin.group-suite.runtime',
|
|
22
|
+
'Owner-scoped Group Suite database and ephemeral state',
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export function createGroupSuiteRuntime(db: GroupSuiteMemoryDb): GroupSuiteRuntime {
|
|
26
|
+
return {
|
|
27
|
+
db,
|
|
28
|
+
keywords: new Map(),
|
|
29
|
+
teachCooldowns: new Map(),
|
|
30
|
+
statsBuffer: new Map(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveGroupSuiteRuntime(context: {
|
|
35
|
+
owner?: { id?: unknown };
|
|
36
|
+
use<T>(token: Token<T>): T;
|
|
37
|
+
}): GroupSuiteRuntime | undefined {
|
|
38
|
+
try {
|
|
39
|
+
return context.use(groupSuiteRuntimeToken);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (context.owner?.id) throw error;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -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
|
+
conversation?: { readonly kind?: string; readonly id?: 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' || channelType === 'channel') {
|
|
28
|
+
return { type: 'group', id: String(meta.channelId ?? input.conversation?.id ?? '') };
|
|
29
|
+
}
|
|
30
|
+
if (input.conversation && channelType !== 'private') {
|
|
31
|
+
return { type: 'group', id: String(input.conversation.id ?? '') };
|
|
32
|
+
}
|
|
33
|
+
return { type: 'global', id: '' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveSender(input: {
|
|
37
|
+
sender?: { readonly id: string; readonly name?: string };
|
|
38
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
39
|
+
}): { id: string; name: string } {
|
|
40
|
+
const meta = input.metadata ?? {};
|
|
41
|
+
const id = input.sender?.id ?? String(meta.senderId ?? '');
|
|
42
|
+
const name = input.sender?.name ?? String(meta.senderName ?? meta.name ?? '用户');
|
|
43
|
+
return { id, name };
|
|
44
|
+
}
|
package/src/stats-lib.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
conversation?: { readonly kind?: string; readonly id?: 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' || channelType === 'channel') {
|
|
41
|
+
return String(meta.channelId ?? input.conversation?.id ?? '');
|
|
42
|
+
}
|
|
43
|
+
if (input.conversation && channelType !== 'private') {
|
|
44
|
+
return String(input.conversation.id ?? '');
|
|
45
|
+
}
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function recordMessage(input: {
|
|
50
|
+
sender?: { readonly id: string; readonly name?: string };
|
|
51
|
+
conversation?: { readonly kind?: string; readonly id?: 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
|
+
for (const [key, entry] of [...targetBuffer.entries()]) {
|
|
79
|
+
// 快照本次要落库的计数;flush 期间新累计的部分留在缓冲
|
|
80
|
+
const countSnapshot = entry.count;
|
|
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) + countSnapshot,
|
|
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: countSnapshot,
|
|
104
|
+
updated_at: ts(),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// 只移除写入成功的部分;失败的 key 留在缓冲等下一轮重试
|
|
108
|
+
entry.count -= countSnapshot;
|
|
109
|
+
if (entry.count <= 0 && targetBuffer.get(key) === entry) targetBuffer.delete(key);
|
|
110
|
+
} catch {
|
|
111
|
+
/* single row failure: keep entry in buffer for next flush */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function queryStats(
|
|
117
|
+
groupId: string,
|
|
118
|
+
fromDate: string,
|
|
119
|
+
runtime?: GroupSuiteRuntime,
|
|
120
|
+
): Promise<Map<string, { name: string; count: number }>> {
|
|
121
|
+
const M = getStatsModel(runtime?.db);
|
|
122
|
+
if (!M) return new Map();
|
|
123
|
+
const all = (groupId
|
|
124
|
+
? await M.select().where({ group_id: groupId })
|
|
125
|
+
: await M.select()) as Record<string, unknown>[];
|
|
126
|
+
const result = new Map<string, { name: string; count: number }>();
|
|
127
|
+
for (const row of all) {
|
|
128
|
+
if (String(row.date) < fromDate) continue;
|
|
129
|
+
const userId = String(row.user_id);
|
|
130
|
+
const existing = result.get(userId);
|
|
131
|
+
if (existing) {
|
|
132
|
+
existing.count += Number(row.count || 0);
|
|
133
|
+
if (row.user_name) existing.name = String(row.user_name);
|
|
134
|
+
} else {
|
|
135
|
+
result.set(userId, {
|
|
136
|
+
name: String(row.user_name || row.user_id),
|
|
137
|
+
count: Number(row.count || 0),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
type MessageInput = {
|
|
145
|
+
sender?: { readonly id: string; readonly name?: string };
|
|
146
|
+
target?: string;
|
|
147
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export async function statsRankText(
|
|
151
|
+
input: MessageInput,
|
|
152
|
+
cfg: GroupSuiteConfig,
|
|
153
|
+
fromDate: string,
|
|
154
|
+
title: string,
|
|
155
|
+
runtime?: GroupSuiteRuntime,
|
|
156
|
+
): Promise<string> {
|
|
157
|
+
await flushStatsBuffer(runtime);
|
|
158
|
+
const groupId = resolveGroupId(input);
|
|
159
|
+
const stats = await queryStats(groupId, fromDate, runtime);
|
|
160
|
+
const { id: userId } = resolveSender(input);
|
|
161
|
+
const data = buildStatsRankReportData(stats, title, cfg.rankSize, userId);
|
|
162
|
+
return formatRankText(data);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function myStatsText(input: MessageInput, runtime?: GroupSuiteRuntime): Promise<string> {
|
|
166
|
+
await flushStatsBuffer(runtime);
|
|
167
|
+
const M = getStatsModel(runtime?.db);
|
|
168
|
+
if (!M) return '统计数据库尚未就绪';
|
|
169
|
+
const { id: userId, name: userName } = resolveSender(input);
|
|
170
|
+
if (!userId) return '无法获取用户信息';
|
|
171
|
+
const groupId = resolveGroupId(input);
|
|
172
|
+
const rows = (groupId
|
|
173
|
+
? await M.select().where({ user_id: userId, group_id: groupId })
|
|
174
|
+
: await M.select().where({ user_id: userId })) as Record<string, unknown>[];
|
|
175
|
+
if (rows.length === 0) return '暂无你的消息记录';
|
|
176
|
+
const today = todayStr();
|
|
177
|
+
const weekStart = weekStartStr();
|
|
178
|
+
const monthStart = monthStartStr();
|
|
179
|
+
let todayCount = 0;
|
|
180
|
+
let weekCount = 0;
|
|
181
|
+
let monthCount = 0;
|
|
182
|
+
let totalCount = 0;
|
|
183
|
+
for (const row of rows) {
|
|
184
|
+
const c = Number(row.count || 0);
|
|
185
|
+
totalCount += c;
|
|
186
|
+
if (String(row.date) >= monthStart) monthCount += c;
|
|
187
|
+
if (String(row.date) >= weekStart) weekCount += c;
|
|
188
|
+
if (row.date === today) todayCount += c;
|
|
189
|
+
}
|
|
190
|
+
const data: MyStatsReportData = {
|
|
191
|
+
userName: userName || '你',
|
|
192
|
+
scope: groupId ? '本群' : '全局',
|
|
193
|
+
todayCount,
|
|
194
|
+
weekCount,
|
|
195
|
+
monthCount,
|
|
196
|
+
totalCount,
|
|
197
|
+
activeDays: rows.length,
|
|
198
|
+
};
|
|
199
|
+
return formatMyStatsText(data);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Test helper */
|
|
203
|
+
export function resetStatsBuffer(): void {
|
|
204
|
+
buffer.clear();
|
|
205
|
+
}
|
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
|
+
}
|