@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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineMiddleware } from 'zhin.js/middleware';
|
|
3
|
+
import { resolveGroupSuiteConfig } from "../lib/config.js";
|
|
4
|
+
import { matchKeyword } from "../lib/keyword-store.js";
|
|
5
|
+
import { resolveGroupSuiteRuntime } from "../lib/runtime-state.js";
|
|
6
|
+
/**
|
|
7
|
+
* Keyword auto-reply (enabled via config.keywordReply).
|
|
8
|
+
* Uses Runtime Message.content / metadata (legacy used $raw / $channel).
|
|
9
|
+
*/
|
|
10
|
+
export default defineMiddleware({
|
|
11
|
+
target: 'inbound',
|
|
12
|
+
async handle(context, next) {
|
|
13
|
+
const config = resolveGroupSuiteConfig(context.config);
|
|
14
|
+
if (!config.keywordReply) {
|
|
15
|
+
await next();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const meta = context.input.metadata ?? {};
|
|
19
|
+
const channelType = String(meta.type ?? meta.channelType ?? '');
|
|
20
|
+
if (channelType === 'private') {
|
|
21
|
+
await next();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const text = typeof context.input.content === 'string' ? context.input.content.trim() : '';
|
|
25
|
+
if (!text) {
|
|
26
|
+
await next();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const reply = matchKeyword(text, resolveGroupSuiteRuntime(context)?.keywords);
|
|
30
|
+
if (reply) {
|
|
31
|
+
await context.input.$reply(reply);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await next();
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineMiddleware } from 'zhin.js/middleware';
|
|
2
|
+
import type { Message } from '@zhin.js/core/runtime';
|
|
3
|
+
import { resolveGroupSuiteConfig, type GroupSuiteConfig } from '../src/config.js';
|
|
4
|
+
import { matchKeyword } from '../src/keyword-store.js';
|
|
5
|
+
import { resolveGroupSuiteRuntime } from '../src/runtime-state.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Keyword auto-reply (enabled via config.keywordReply).
|
|
9
|
+
* Uses Runtime Message.content / metadata (legacy used $raw / $channel).
|
|
10
|
+
*/
|
|
11
|
+
export default defineMiddleware<Message, GroupSuiteConfig>({
|
|
12
|
+
target: 'inbound',
|
|
13
|
+
async handle(context, next) {
|
|
14
|
+
const config = resolveGroupSuiteConfig(context.config);
|
|
15
|
+
if (!config.keywordReply) {
|
|
16
|
+
await next();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const meta = context.input.metadata ?? {};
|
|
20
|
+
const channelType = String(meta.type ?? meta.channelType ?? '');
|
|
21
|
+
if (channelType === 'private') {
|
|
22
|
+
await next();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const text = typeof context.input.content === 'string' ? context.input.content.trim() : '';
|
|
26
|
+
if (!text) {
|
|
27
|
+
await next();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const reply = matchKeyword(text, resolveGroupSuiteRuntime(context)?.keywords);
|
|
31
|
+
if (reply) {
|
|
32
|
+
await context.input.$reply(reply);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
await next();
|
|
36
|
+
},
|
|
37
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineMiddleware } from 'zhin.js/middleware';
|
|
3
|
+
import { recordMessage } from "../lib/stats-lib.js";
|
|
4
|
+
import { resolveGroupSuiteRuntime } from "../lib/runtime-state.js";
|
|
5
|
+
/**
|
|
6
|
+
* Buffer inbound messages into in-memory message_stats (flushed on query).
|
|
7
|
+
* Skips command messages (legacy used `isActionMessage` to skip interactive
|
|
8
|
+
* action/command messages; the Runtime pipeline represents commands as
|
|
9
|
+
* text starting with the dispatcher prefix '/').
|
|
10
|
+
*/
|
|
11
|
+
export default defineMiddleware({
|
|
12
|
+
target: 'inbound',
|
|
13
|
+
async handle(context, next) {
|
|
14
|
+
const content = typeof context.input.content === 'string' ? context.input.content : '';
|
|
15
|
+
if (!content.startsWith('/')) {
|
|
16
|
+
recordMessage(context.input, resolveGroupSuiteRuntime(context));
|
|
17
|
+
}
|
|
18
|
+
await next();
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineMiddleware } from 'zhin.js/middleware';
|
|
2
|
+
import type { Message } from '@zhin.js/core/runtime';
|
|
3
|
+
import type { GroupSuiteConfig } from '../src/config.js';
|
|
4
|
+
import { recordMessage } from '../src/stats-lib.js';
|
|
5
|
+
import { resolveGroupSuiteRuntime } from '../src/runtime-state.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Buffer inbound messages into in-memory message_stats (flushed on query).
|
|
9
|
+
* Skips command messages (legacy used `isActionMessage` to skip interactive
|
|
10
|
+
* action/command messages; the Runtime pipeline represents commands as
|
|
11
|
+
* text starting with the dispatcher prefix '/').
|
|
12
|
+
*/
|
|
13
|
+
export default defineMiddleware<Message, GroupSuiteConfig>({
|
|
14
|
+
target: 'inbound',
|
|
15
|
+
async handle(context, next) {
|
|
16
|
+
const content = typeof context.input.content === 'string' ? context.input.content : '';
|
|
17
|
+
if (!content.startsWith('/')) {
|
|
18
|
+
recordMessage(context.input, resolveGroupSuiteRuntime(context));
|
|
19
|
+
}
|
|
20
|
+
await next();
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { defineMiddleware } from 'zhin.js/middleware';
|
|
3
|
+
import { resolveGroupSuiteConfig } from "../lib/config.js";
|
|
4
|
+
import { tryTeachReply } from "../lib/teach-lib.js";
|
|
5
|
+
import { resolveGroupSuiteRuntime } from "../lib/runtime-state.js";
|
|
6
|
+
/**
|
|
7
|
+
* Teach Q&A auto-reply (exact / optional regex).
|
|
8
|
+
* Uses Runtime Message.content / metadata.
|
|
9
|
+
*/
|
|
10
|
+
export default defineMiddleware({
|
|
11
|
+
target: 'inbound',
|
|
12
|
+
async handle(context, next) {
|
|
13
|
+
const config = resolveGroupSuiteConfig(context.config);
|
|
14
|
+
const reply = await tryTeachReply(context.input, config, resolveGroupSuiteRuntime(context));
|
|
15
|
+
if (reply) {
|
|
16
|
+
await context.input.$reply(reply);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
await next();
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineMiddleware } from 'zhin.js/middleware';
|
|
2
|
+
import type { Message } from '@zhin.js/core/runtime';
|
|
3
|
+
import { resolveGroupSuiteConfig, type GroupSuiteConfig } from '../src/config.js';
|
|
4
|
+
import { tryTeachReply } from '../src/teach-lib.js';
|
|
5
|
+
import { resolveGroupSuiteRuntime } from '../src/runtime-state.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Teach Q&A auto-reply (exact / optional regex).
|
|
9
|
+
* Uses Runtime Message.content / metadata.
|
|
10
|
+
*/
|
|
11
|
+
export default defineMiddleware<Message, GroupSuiteConfig>({
|
|
12
|
+
target: 'inbound',
|
|
13
|
+
async handle(context, next) {
|
|
14
|
+
const config = resolveGroupSuiteConfig(context.config);
|
|
15
|
+
const reply = await tryTeachReply(context.input, config, resolveGroupSuiteRuntime(context));
|
|
16
|
+
if (reply) {
|
|
17
|
+
await context.input.$reply(reply);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
await next();
|
|
21
|
+
},
|
|
22
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/plugin-group-suite",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Zhin.js 群运营套件 —
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Zhin.js 群运营套件 — slice-2 checkin/teach/stats + keywords (Plugin Runtime, in-memory)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -14,11 +14,14 @@
|
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
+
"plugin.js",
|
|
18
|
+
"schema.json",
|
|
19
|
+
"commands",
|
|
20
|
+
"middlewares",
|
|
17
21
|
"src",
|
|
18
22
|
"lib",
|
|
19
|
-
"
|
|
23
|
+
"agent",
|
|
20
24
|
"tools",
|
|
21
|
-
"plugin.yml",
|
|
22
25
|
"README.md"
|
|
23
26
|
],
|
|
24
27
|
"keywords": [
|
|
@@ -29,8 +32,7 @@
|
|
|
29
32
|
"group",
|
|
30
33
|
"checkin",
|
|
31
34
|
"stats",
|
|
32
|
-
"teach"
|
|
33
|
-
"welcome"
|
|
35
|
+
"teach"
|
|
34
36
|
],
|
|
35
37
|
"author": {
|
|
36
38
|
"name": "lc-cn",
|
|
@@ -39,19 +41,34 @@
|
|
|
39
41
|
},
|
|
40
42
|
"license": "MIT",
|
|
41
43
|
"dependencies": {
|
|
42
|
-
"@zhin.js/
|
|
44
|
+
"@zhin.js/core": "1.1.35",
|
|
45
|
+
"@zhin.js/satori": "1.1.0"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"typescript": "^6.0.3",
|
|
46
|
-
"
|
|
47
|
-
"
|
|
49
|
+
"vitest": "^4.1.10",
|
|
50
|
+
"zod": "^4.4.3",
|
|
51
|
+
"@zhin.js/agent": "1.1.23",
|
|
52
|
+
"zhin.js": "1.1.0"
|
|
48
53
|
},
|
|
49
54
|
"peerDependencies": {
|
|
50
|
-
"
|
|
51
|
-
"@zhin.js/
|
|
55
|
+
"zod": "^4.0.0",
|
|
56
|
+
"@zhin.js/agent": "^1.1.23",
|
|
57
|
+
"@zhin.js/command": "^1.1.0",
|
|
58
|
+
"@zhin.js/middleware": "^1.1.0",
|
|
59
|
+
"zhin.js": "^1.1.0"
|
|
52
60
|
},
|
|
53
61
|
"peerDependenciesMeta": {
|
|
54
|
-
"@zhin.js/
|
|
62
|
+
"@zhin.js/agent": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"@zhin.js/command": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"@zhin.js/middleware": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"zod": {
|
|
55
72
|
"optional": true
|
|
56
73
|
}
|
|
57
74
|
},
|
|
@@ -64,8 +81,30 @@
|
|
|
64
81
|
"access": "public",
|
|
65
82
|
"registry": "https://registry.npmjs.org"
|
|
66
83
|
},
|
|
84
|
+
"engines": {
|
|
85
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
86
|
+
},
|
|
87
|
+
"zhin": {
|
|
88
|
+
"protocol": 1,
|
|
89
|
+
"type": "plugin",
|
|
90
|
+
"entry": "./plugin.js",
|
|
91
|
+
"engine": "^1.0.0",
|
|
92
|
+
"runtime": "trusted",
|
|
93
|
+
"features": [
|
|
94
|
+
{
|
|
95
|
+
"package": "@zhin.js/command",
|
|
96
|
+
"api": "^1.0.0"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"package": "@zhin.js/middleware",
|
|
100
|
+
"api": "^1.0.0"
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"plugins": []
|
|
104
|
+
},
|
|
67
105
|
"scripts": {
|
|
68
|
-
"build": "tsc
|
|
69
|
-
"clean": "rimraf lib"
|
|
106
|
+
"build": "tsc",
|
|
107
|
+
"clean": "rimraf lib",
|
|
108
|
+
"test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/utils/group-suite/tests"
|
|
70
109
|
}
|
|
71
110
|
}
|
package/plugin.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { definePlugin, databaseHostToken } from 'zhin.js';
|
|
3
|
+
import { registerGroupSuiteDb } from "./lib/db-store.js";
|
|
4
|
+
import { createInMemoryGroupSuiteDb } from "./lib/memory-store.js";
|
|
5
|
+
import { createGroupSuiteRuntime, groupSuiteRuntimeToken } from "./lib/runtime-state.js";
|
|
6
|
+
import { flushStatsBuffer } from "./lib/stats-lib.js";
|
|
7
|
+
import { defineGroupSuiteTables } from "./lib/tables.js";
|
|
8
|
+
/**
|
|
9
|
+
* Plugin Runtime:
|
|
10
|
+
* - checkin / teach / stats commands + keyword middleware
|
|
11
|
+
* - DB: prefer `databaseHostToken`; else in-memory fallback
|
|
12
|
+
* - Stats buffer: periodic flush every 10s + final flush on dispose
|
|
13
|
+
*/
|
|
14
|
+
export default definePlugin({
|
|
15
|
+
name: 'group-suite',
|
|
16
|
+
metadata: {
|
|
17
|
+
displayName: 'Group Suite',
|
|
18
|
+
},
|
|
19
|
+
setup(context) {
|
|
20
|
+
// host 与 memory 模型表面结构兼容(where 均返回 PromiseLike),可直接互换。
|
|
21
|
+
const db = (() => {
|
|
22
|
+
if (context.resources.has(databaseHostToken)) {
|
|
23
|
+
const host = context.resources.use(databaseHostToken);
|
|
24
|
+
defineGroupSuiteTables(host);
|
|
25
|
+
return host;
|
|
26
|
+
}
|
|
27
|
+
return createInMemoryGroupSuiteDb();
|
|
28
|
+
})();
|
|
29
|
+
const runtime = createGroupSuiteRuntime(db);
|
|
30
|
+
context.resources.provide(groupSuiteRuntimeToken, runtime);
|
|
31
|
+
context.lifecycle.add(registerGroupSuiteDb(db));
|
|
32
|
+
const flushTimer = setInterval(() => {
|
|
33
|
+
void flushStatsBuffer(runtime);
|
|
34
|
+
}, 10_000);
|
|
35
|
+
context.lifecycle.add(async () => {
|
|
36
|
+
clearInterval(flushTimer);
|
|
37
|
+
await flushStatsBuffer(runtime);
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
});
|
package/schema.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"keywordReply": { "type": "boolean", "default": false },
|
|
7
|
+
"basePointsMin": { "type": "number", "default": 10 },
|
|
8
|
+
"basePointsMax": { "type": "number", "default": 30 },
|
|
9
|
+
"streakBonus": { "type": "number", "default": 5 },
|
|
10
|
+
"streakCap": { "type": "number", "default": 50 },
|
|
11
|
+
"rankSize": { "type": "number", "default": 10 },
|
|
12
|
+
"teachMaxPerGroup": { "type": "number", "default": 200 },
|
|
13
|
+
"teachCooldownMs": { "type": "number", "default": 3000 },
|
|
14
|
+
"teachAllowRegex": { "type": "boolean", "default": true },
|
|
15
|
+
"teachPageSize": { "type": "number", "default": 10 }
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/analysis.ts
CHANGED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { GroupSuiteConfig } from './config.js';
|
|
2
|
+
import { getCheckinModel } from './db-store.js';
|
|
3
|
+
import {
|
|
4
|
+
randomInt,
|
|
5
|
+
resolveContextKey,
|
|
6
|
+
resolveSender,
|
|
7
|
+
todayStr,
|
|
8
|
+
ts,
|
|
9
|
+
yesterdayStr,
|
|
10
|
+
} from './shared-runtime.js';
|
|
11
|
+
import type { GroupSuiteRuntime } from './runtime-state.js';
|
|
12
|
+
|
|
13
|
+
async function getOrCreateUser(
|
|
14
|
+
model: any,
|
|
15
|
+
userId: string,
|
|
16
|
+
userName: string,
|
|
17
|
+
ctxType: string,
|
|
18
|
+
ctxId: string,
|
|
19
|
+
): Promise<Record<string, unknown> | null> {
|
|
20
|
+
const rows = (await model.select().where({
|
|
21
|
+
user_id: userId,
|
|
22
|
+
context_type: ctxType,
|
|
23
|
+
context_id: ctxId,
|
|
24
|
+
})) as Record<string, unknown>[];
|
|
25
|
+
if (rows.length > 0) return rows[0];
|
|
26
|
+
|
|
27
|
+
await model.insert({
|
|
28
|
+
user_id: userId,
|
|
29
|
+
user_name: userName,
|
|
30
|
+
points: 0,
|
|
31
|
+
total_checkins: 0,
|
|
32
|
+
streak: 0,
|
|
33
|
+
max_streak: 0,
|
|
34
|
+
last_checkin: '',
|
|
35
|
+
context_type: ctxType,
|
|
36
|
+
context_id: ctxId,
|
|
37
|
+
created_at: ts(),
|
|
38
|
+
updated_at: ts(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const created = (await model.select().where({
|
|
42
|
+
user_id: userId,
|
|
43
|
+
context_type: ctxType,
|
|
44
|
+
context_id: ctxId,
|
|
45
|
+
})) as Record<string, unknown>[];
|
|
46
|
+
return created[0] ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 同一 user_id+context 的签到串行化(per-user promise 链),
|
|
51
|
+
* 避免 check-then-act 竞态导致连发两条都签到成功。
|
|
52
|
+
*/
|
|
53
|
+
const checkinChains = new Map<string, Promise<unknown>>();
|
|
54
|
+
|
|
55
|
+
function serializedCheckin<T>(key: string, task: () => Promise<T>): Promise<T> {
|
|
56
|
+
const prev = checkinChains.get(key) ?? Promise.resolve();
|
|
57
|
+
const next = prev.then(task, task);
|
|
58
|
+
checkinChains.set(key, next);
|
|
59
|
+
return next.finally(() => {
|
|
60
|
+
if (checkinChains.get(key) === next) checkinChains.delete(key);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function doCheckin(
|
|
65
|
+
input: { sender?: { readonly id: string; readonly name?: string }; target?: string; metadata?: Readonly<Record<string, unknown>> },
|
|
66
|
+
cfg: GroupSuiteConfig,
|
|
67
|
+
runtime?: GroupSuiteRuntime,
|
|
68
|
+
): Promise<string> {
|
|
69
|
+
const M = getCheckinModel(runtime?.db);
|
|
70
|
+
if (!M) return '签到数据库尚未就绪,请稍后重试';
|
|
71
|
+
|
|
72
|
+
const { id: userId, name: userName } = resolveSender(input);
|
|
73
|
+
if (!userId) return '无法获取用户信息';
|
|
74
|
+
|
|
75
|
+
const { type: ctxType, id: ctxId } = resolveContextKey(input);
|
|
76
|
+
return serializedCheckin(`${userId}:${ctxType}:${ctxId}`, async () => {
|
|
77
|
+
const user = await getOrCreateUser(M, userId, userName, ctxType, ctxId);
|
|
78
|
+
if (!user) return '签到失败,请重试';
|
|
79
|
+
|
|
80
|
+
const today = todayStr();
|
|
81
|
+
if (user.last_checkin === today) {
|
|
82
|
+
return `你今天已经签到过了哦~\n当前积分: ${user.points}\n连续签到: ${user.streak} 天`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isConsecutive = user.last_checkin === yesterdayStr();
|
|
86
|
+
const newStreak = isConsecutive ? Number(user.streak) + 1 : 1;
|
|
87
|
+
const base = randomInt(cfg.basePointsMin, cfg.basePointsMax);
|
|
88
|
+
const bonus = Math.min(newStreak * cfg.streakBonus, cfg.streakCap);
|
|
89
|
+
const earned = base + bonus;
|
|
90
|
+
const newPoints = Number(user.points || 0) + earned;
|
|
91
|
+
|
|
92
|
+
await M.update({
|
|
93
|
+
points: newPoints,
|
|
94
|
+
total_checkins: Number(user.total_checkins || 0) + 1,
|
|
95
|
+
streak: newStreak,
|
|
96
|
+
max_streak: Math.max(Number(user.max_streak || 0), newStreak),
|
|
97
|
+
last_checkin: today,
|
|
98
|
+
user_name: userName,
|
|
99
|
+
updated_at: ts(),
|
|
100
|
+
}).where({
|
|
101
|
+
user_id: userId,
|
|
102
|
+
context_type: ctxType,
|
|
103
|
+
context_id: ctxId,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const lines = [`签到成功!`, `基础积分: +${base}`];
|
|
107
|
+
if (bonus > 0) lines.push(`连续奖励: +${bonus} (连续${newStreak}天)`);
|
|
108
|
+
lines.push(`本次获得: +${earned}`, `当前积分: ${newPoints}`);
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function myPoints(
|
|
114
|
+
input: { sender?: { readonly id: string; readonly name?: string }; target?: string; metadata?: Readonly<Record<string, unknown>> },
|
|
115
|
+
runtime?: GroupSuiteRuntime,
|
|
116
|
+
): Promise<string> {
|
|
117
|
+
const M = getCheckinModel(runtime?.db);
|
|
118
|
+
if (!M) return '签到数据库尚未就绪';
|
|
119
|
+
const { id: userId } = resolveSender(input);
|
|
120
|
+
const { type: ctxType, id: ctxId } = resolveContextKey(input);
|
|
121
|
+
const rows = (await M.select().where({
|
|
122
|
+
user_id: userId,
|
|
123
|
+
context_type: ctxType,
|
|
124
|
+
context_id: ctxId,
|
|
125
|
+
})) as Record<string, unknown>[];
|
|
126
|
+
if (rows.length === 0) return '你还没有签到记录哦,发送「checkin」开始吧!';
|
|
127
|
+
const u = rows[0];
|
|
128
|
+
const checkedToday = u.last_checkin === todayStr();
|
|
129
|
+
return [
|
|
130
|
+
`${u.user_name || '用户'} 的积分信息`,
|
|
131
|
+
`积分: ${u.points}`,
|
|
132
|
+
`累计签到: ${u.total_checkins} 天`,
|
|
133
|
+
`连续签到: ${u.streak} 天`,
|
|
134
|
+
`最长连续: ${u.max_streak} 天`,
|
|
135
|
+
checkedToday ? '今日已签到 ✓' : '今日未签到',
|
|
136
|
+
].join('\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function pointsRank(
|
|
140
|
+
input: { sender?: { readonly id: string; readonly name?: string }; target?: string; metadata?: Readonly<Record<string, unknown>> },
|
|
141
|
+
cfg: GroupSuiteConfig,
|
|
142
|
+
runtime?: GroupSuiteRuntime,
|
|
143
|
+
): Promise<string> {
|
|
144
|
+
const M = getCheckinModel(runtime?.db);
|
|
145
|
+
if (!M) return '签到数据库尚未就绪';
|
|
146
|
+
const { type: ctxType, id: ctxId } = resolveContextKey(input);
|
|
147
|
+
const all = (await M.select().where(
|
|
148
|
+
ctxType === 'global'
|
|
149
|
+
? { context_type: 'global' }
|
|
150
|
+
: { context_type: ctxType, context_id: ctxId },
|
|
151
|
+
)) as Record<string, unknown>[];
|
|
152
|
+
if (all.length === 0) return '还没有人签到过哦~';
|
|
153
|
+
const sorted = all.sort((a, b) => Number(b.points || 0) - Number(a.points || 0));
|
|
154
|
+
const top = sorted.slice(0, cfg.rankSize);
|
|
155
|
+
const medals = ['🥇', '🥈', '🥉'];
|
|
156
|
+
const lines = top.map((u, i) => {
|
|
157
|
+
const prefix = i < 3 ? medals[i] : `${i + 1}.`;
|
|
158
|
+
return `${prefix} ${u.user_name || u.user_id} — ${u.points}分 (${u.total_checkins}天)`;
|
|
159
|
+
});
|
|
160
|
+
const title = ctxType === 'group' ? '本群积分排行' : '全局积分排行';
|
|
161
|
+
const { id: userId } = resolveSender(input);
|
|
162
|
+
const myRank = sorted.findIndex((u) => u.user_id === userId);
|
|
163
|
+
const footer = myRank >= 0 ? `\n你的排名: 第${myRank + 1}名` : '';
|
|
164
|
+
return `${title}\n${lines.join('\n')}${footer}`;
|
|
165
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,68 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/** 扁平 groupSuite 配置(单层字段,默认值覆盖多数场景) */
|
|
4
|
-
export const groupSuiteSchema = Schema.object({
|
|
5
|
-
welcome: Schema.string()
|
|
6
|
-
.default("欢迎新成员加入本群!")
|
|
7
|
-
.description("入群欢迎语"),
|
|
8
|
-
recallNotify: Schema.boolean()
|
|
9
|
-
.default(true)
|
|
10
|
-
.description("群消息撤回时是否提示"),
|
|
11
|
-
keywordReply: Schema.boolean()
|
|
12
|
-
.default(false)
|
|
13
|
-
.description("内置关键词回复(默认关闭,用 teach 问答)"),
|
|
14
|
-
noticeAdapters: Schema.list(Schema.string())
|
|
15
|
-
.default(["icqq"])
|
|
16
|
-
.description("处理 notice 的适配器(欢迎/撤回)"),
|
|
17
|
-
|
|
18
|
-
basePointsMin: Schema.number().default(10).min(1).max(100),
|
|
19
|
-
basePointsMax: Schema.number().default(30).min(1).max(200),
|
|
20
|
-
streakBonus: Schema.number().default(5).min(0).max(100),
|
|
21
|
-
streakCap: Schema.number().default(50).min(0).max(500),
|
|
22
|
-
|
|
23
|
-
rankSize: Schema.number()
|
|
24
|
-
.default(10)
|
|
25
|
-
.min(3)
|
|
26
|
-
.max(50)
|
|
27
|
-
.description("签到排行与消息统计排行显示人数"),
|
|
28
|
-
statsRetentionDays: Schema.number().default(90).min(7).max(365),
|
|
29
|
-
|
|
30
|
-
analysisDays: Schema.number().default(1).min(1).max(30),
|
|
31
|
-
autoAnalysisEnabled: Schema.boolean().default(false),
|
|
32
|
-
autoAnalysisCron: Schema.string().default("0 9 * * *"),
|
|
33
|
-
analysisGroups: Schema.list(Schema.string())
|
|
34
|
-
.default([])
|
|
35
|
-
.description("启用群日报的群 ID 白名单(空=不限制)"),
|
|
36
|
-
analysisGroupsBlock: Schema.list(Schema.string())
|
|
37
|
-
.default([])
|
|
38
|
-
.description("禁用群日报的群 ID"),
|
|
39
|
-
analysisMaxMessages: Schema.number().default(500).min(100).max(5000),
|
|
40
|
-
|
|
41
|
-
teachMaxPerGroup: Schema.number().default(200).min(1).max(1000),
|
|
42
|
-
teachCooldownMs: Schema.number().default(3000).min(0).max(60000),
|
|
43
|
-
teachAllowRegex: Schema.boolean().default(true),
|
|
44
|
-
teachPageSize: Schema.number().default(10).min(5).max(50),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
export type GroupSuiteConfig = {
|
|
48
|
-
welcome: string;
|
|
49
|
-
recallNotify: boolean;
|
|
1
|
+
export interface GroupSuiteConfig {
|
|
50
2
|
keywordReply: boolean;
|
|
51
|
-
noticeAdapters: string[];
|
|
52
3
|
basePointsMin: number;
|
|
53
4
|
basePointsMax: number;
|
|
54
5
|
streakBonus: number;
|
|
55
6
|
streakCap: number;
|
|
56
7
|
rankSize: number;
|
|
57
|
-
statsRetentionDays: number;
|
|
58
|
-
analysisDays: number;
|
|
59
|
-
autoAnalysisEnabled: boolean;
|
|
60
|
-
autoAnalysisCron: string;
|
|
61
|
-
analysisGroups: string[];
|
|
62
|
-
analysisGroupsBlock: string[];
|
|
63
|
-
analysisMaxMessages: number;
|
|
64
8
|
teachMaxPerGroup: number;
|
|
65
9
|
teachCooldownMs: number;
|
|
66
10
|
teachAllowRegex: boolean;
|
|
67
11
|
teachPageSize: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_GROUP_SUITE_CONFIG: GroupSuiteConfig = {
|
|
15
|
+
keywordReply: false,
|
|
16
|
+
basePointsMin: 10,
|
|
17
|
+
basePointsMax: 30,
|
|
18
|
+
streakBonus: 5,
|
|
19
|
+
streakCap: 50,
|
|
20
|
+
rankSize: 10,
|
|
21
|
+
teachMaxPerGroup: 200,
|
|
22
|
+
teachCooldownMs: 3000,
|
|
23
|
+
teachAllowRegex: true,
|
|
24
|
+
teachPageSize: 10,
|
|
68
25
|
};
|
|
26
|
+
|
|
27
|
+
export function resolveGroupSuiteConfig(
|
|
28
|
+
raw: Partial<GroupSuiteConfig> | undefined,
|
|
29
|
+
): GroupSuiteConfig {
|
|
30
|
+
return {
|
|
31
|
+
...DEFAULT_GROUP_SUITE_CONFIG,
|
|
32
|
+
...raw,
|
|
33
|
+
};
|
|
34
|
+
}
|
package/src/db-store.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHECKIN_TABLE,
|
|
3
|
+
createInMemoryGroupSuiteDb,
|
|
4
|
+
STATS_TABLE,
|
|
5
|
+
TEACH_TABLE,
|
|
6
|
+
type GroupSuiteMemoryDb,
|
|
7
|
+
type GroupSuiteModel,
|
|
8
|
+
} from './memory-store.js';
|
|
9
|
+
|
|
10
|
+
let _db: GroupSuiteMemoryDb | null = null;
|
|
11
|
+
const registrations: Array<{ readonly value: GroupSuiteMemoryDb }> = [];
|
|
12
|
+
|
|
13
|
+
/** Ensure an in-memory store when Runtime DatabaseFeature Resource is not wired yet. */
|
|
14
|
+
export function ensureGroupSuiteMemoryDb(): GroupSuiteMemoryDb {
|
|
15
|
+
if (!_db) _db = createInMemoryGroupSuiteDb();
|
|
16
|
+
return _db;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getGroupSuiteDb(): GroupSuiteMemoryDb | null {
|
|
20
|
+
return registrations[registrations.length - 1]?.value ?? ensureGroupSuiteMemoryDb();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function setGroupSuiteDb(db: GroupSuiteMemoryDb | null): void {
|
|
24
|
+
_db = db;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function registerGroupSuiteDb(db: GroupSuiteMemoryDb): () => void {
|
|
28
|
+
const registration = Object.freeze({ value: db });
|
|
29
|
+
registrations.push(registration);
|
|
30
|
+
return () => {
|
|
31
|
+
const index = registrations.lastIndexOf(registration);
|
|
32
|
+
if (index >= 0) registrations.splice(index, 1);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Test helper: clear module store. */
|
|
37
|
+
export function resetGroupSuiteDb(): void {
|
|
38
|
+
_db = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getCheckinModel(db = getGroupSuiteDb()): GroupSuiteModel | null {
|
|
42
|
+
return db?.models.get(CHECKIN_TABLE) ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getTeachModel(db = getGroupSuiteDb()): GroupSuiteModel | null {
|
|
46
|
+
return db?.models.get(TEACH_TABLE) ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getStatsModel(db = getGroupSuiteDb()): GroupSuiteModel | null {
|
|
50
|
+
return db?.models.get(STATS_TABLE) ?? null;
|
|
51
|
+
}
|