@zhin.js/plugin-group-suite 1.1.7 → 1.1.9

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.
Files changed (83) hide show
  1. package/README.md +55 -24
  2. package/commands/forget/index.js +1 -0
  3. package/commands/forget/index.ts +1 -0
  4. package/commands/keyword-add/[keyword]/index.js +10 -7
  5. package/commands/keyword-add/[keyword]/index.ts +11 -8
  6. package/commands/keyword-list/index.js +8 -5
  7. package/commands/keyword-list/index.ts +9 -6
  8. package/commands/keyword-remove/[keyword]/index.js +10 -6
  9. package/commands/keyword-remove/[keyword]/index.ts +11 -7
  10. package/commands/stats-card/index.js +23 -0
  11. package/commands/stats-card/index.ts +24 -0
  12. package/commands/teach/index.js +1 -0
  13. package/commands/teach/index.ts +1 -0
  14. package/commands/teach-regex/index.js +1 -0
  15. package/commands/teach-regex/index.ts +1 -0
  16. package/handlers/welcome/index.js +25 -0
  17. package/handlers/welcome/index.ts +24 -0
  18. package/lib/checkin-lib.d.ts +4 -24
  19. package/lib/checkin-lib.d.ts.map +1 -1
  20. package/lib/checkin-lib.js +3 -4
  21. package/lib/checkin-lib.js.map +1 -1
  22. package/lib/config.d.ts +4 -0
  23. package/lib/config.d.ts.map +1 -1
  24. package/lib/config.js +2 -0
  25. package/lib/config.js.map +1 -1
  26. package/lib/conversation-scope.d.ts +21 -0
  27. package/lib/conversation-scope.d.ts.map +1 -0
  28. package/lib/conversation-scope.js +33 -0
  29. package/lib/conversation-scope.js.map +1 -0
  30. package/lib/index.d.ts +1 -1
  31. package/lib/index.d.ts.map +1 -1
  32. package/lib/index.js +1 -1
  33. package/lib/index.js.map +1 -1
  34. package/lib/keyword-store.d.ts +4 -0
  35. package/lib/keyword-store.d.ts.map +1 -1
  36. package/lib/keyword-store.js +15 -0
  37. package/lib/keyword-store.js.map +1 -1
  38. package/lib/keywords.d.ts +21 -0
  39. package/lib/keywords.d.ts.map +1 -0
  40. package/lib/keywords.js +72 -0
  41. package/lib/keywords.js.map +1 -0
  42. package/lib/runtime-state.d.ts +5 -1
  43. package/lib/runtime-state.d.ts.map +1 -1
  44. package/lib/runtime-state.js.map +1 -1
  45. package/lib/shared-runtime.d.ts +3 -8
  46. package/lib/shared-runtime.d.ts.map +1 -1
  47. package/lib/shared-runtime.js +4 -10
  48. package/lib/shared-runtime.js.map +1 -1
  49. package/lib/stats-lib.d.ts +5 -26
  50. package/lib/stats-lib.d.ts.map +1 -1
  51. package/lib/stats-lib.js +29 -15
  52. package/lib/stats-lib.js.map +1 -1
  53. package/lib/teach-lib.d.ts +2 -9
  54. package/lib/teach-lib.d.ts.map +1 -1
  55. package/lib/teach-lib.js +6 -8
  56. package/lib/teach-lib.js.map +1 -1
  57. package/middlewares/keyword-reply/index.js +3 -5
  58. package/middlewares/keyword-reply/index.ts +3 -5
  59. package/middlewares/stats-count/index.js +3 -7
  60. package/middlewares/stats-count/index.ts +3 -7
  61. package/package.json +30 -13
  62. package/plugin.js +9 -9
  63. package/schedules/flush-stats/index.js +11 -0
  64. package/schedules/flush-stats/index.ts +11 -0
  65. package/schema.json +51 -10
  66. package/skills/checkin/tools/checkin_query/index.js +5 -8
  67. package/skills/checkin/tools/checkin_query/index.ts +5 -9
  68. package/skills/checkin/tools/checkin_rank/index.js +4 -7
  69. package/skills/checkin/tools/checkin_rank/index.ts +4 -7
  70. package/skills/group-daily-analysis/tools/stats_query/index.js +3 -4
  71. package/skills/group-daily-analysis/tools/stats_query/index.ts +3 -4
  72. package/skills/group-daily-analysis/tools/stats_user/index.js +4 -5
  73. package/skills/group-daily-analysis/tools/stats_user/index.ts +4 -5
  74. package/src/checkin-lib.ts +7 -7
  75. package/src/config.ts +6 -0
  76. package/src/conversation-scope.ts +45 -0
  77. package/src/index.ts +1 -0
  78. package/src/keyword-store.ts +22 -0
  79. package/src/keywords.ts +72 -0
  80. package/src/runtime-state.ts +5 -1
  81. package/src/shared-runtime.ts +5 -14
  82. package/src/stats-lib.ts +36 -29
  83. package/src/teach-lib.ts +8 -17
@@ -2,7 +2,7 @@
2
2
  import { defineMiddleware } from 'zhin.js/middleware';
3
3
  import { resolveGroupSuiteConfig } from "../../lib/config.js";
4
4
  import { matchKeyword } from "../../lib/keyword-store.js";
5
- import { resolveGroupSuiteRuntime } from "../../lib/runtime-state.js";
5
+ import { keywords } from "../../lib/keywords.js";
6
6
  /**
7
7
  * Keyword auto-reply (enabled via config.keywordReply).
8
8
  * Uses Runtime Message.content / metadata (legacy used $raw / $channel).
@@ -15,9 +15,7 @@ export default defineMiddleware({
15
15
  await next();
16
16
  return;
17
17
  }
18
- const meta = context.input.metadata ?? {};
19
- const channelType = String(meta.type ?? meta.channelType ?? '');
20
- if (channelType === 'private') {
18
+ if (context.input.conversation.kind !== 'group') {
21
19
  await next();
22
20
  return;
23
21
  }
@@ -26,7 +24,7 @@ export default defineMiddleware({
26
24
  await next();
27
25
  return;
28
26
  }
29
- const reply = matchKeyword(text, resolveGroupSuiteRuntime(context).keywords);
27
+ const reply = matchKeyword(text, new Map(await keywords(context.input, context).read()));
30
28
  if (reply) {
31
29
  await context.input.$reply(reply);
32
30
  return;
@@ -2,7 +2,7 @@ import { defineMiddleware } from 'zhin.js/middleware';
2
2
  import type { Message } from '@zhin.js/core/runtime';
3
3
  import { resolveGroupSuiteConfig, type GroupSuiteConfig } from '../../src/config.js';
4
4
  import { matchKeyword } from '../../src/keyword-store.js';
5
- import { resolveGroupSuiteRuntime } from '../../src/runtime-state.js';
5
+ import { keywords } from '../../src/keywords.js';
6
6
 
7
7
  /**
8
8
  * Keyword auto-reply (enabled via config.keywordReply).
@@ -16,9 +16,7 @@ export default defineMiddleware<Message, GroupSuiteConfig>({
16
16
  await next();
17
17
  return;
18
18
  }
19
- const meta = context.input.metadata ?? {};
20
- const channelType = String(meta.type ?? meta.channelType ?? '');
21
- if (channelType === 'private') {
19
+ if (context.input.conversation.kind !== 'group') {
22
20
  await next();
23
21
  return;
24
22
  }
@@ -27,7 +25,7 @@ export default defineMiddleware<Message, GroupSuiteConfig>({
27
25
  await next();
28
26
  return;
29
27
  }
30
- const reply = matchKeyword(text, resolveGroupSuiteRuntime(context).keywords);
28
+ const reply = matchKeyword(text, new Map(await keywords(context.input, context).read()));
31
29
  if (reply) {
32
30
  await context.input.$reply(reply);
33
31
  return;
@@ -2,18 +2,14 @@
2
2
  import { defineMiddleware } from 'zhin.js/middleware';
3
3
  import { recordMessage } from "../../lib/stats-lib.js";
4
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
- */
5
+ /** Count in memory; the admitted Schedule Host flushes batches without slowing every message. */
11
6
  export default defineMiddleware({
12
7
  target: 'inbound',
13
8
  async handle(context, next) {
14
9
  const content = typeof context.input.content === 'string' ? context.input.content : '';
15
10
  if (!content.startsWith('/')) {
16
- recordMessage(context.input, resolveGroupSuiteRuntime(context));
11
+ const runtime = resolveGroupSuiteRuntime(context);
12
+ recordMessage(context.input, runtime);
17
13
  }
18
14
  await next();
19
15
  },
@@ -4,18 +4,14 @@ import type { GroupSuiteConfig } from '../../src/config.js';
4
4
  import { recordMessage } from '../../src/stats-lib.js';
5
5
  import { resolveGroupSuiteRuntime } from '../../src/runtime-state.js';
6
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
- */
7
+ /** Count in memory; the admitted Schedule Host flushes batches without slowing every message. */
13
8
  export default defineMiddleware<Message, GroupSuiteConfig>({
14
9
  target: 'inbound',
15
10
  async handle(context, next) {
16
11
  const content = typeof context.input.content === 'string' ? context.input.content : '';
17
12
  if (!content.startsWith('/')) {
18
- recordMessage(context.input, resolveGroupSuiteRuntime(context));
13
+ const runtime = resolveGroupSuiteRuntime(context);
14
+ recordMessage(context.input, runtime);
19
15
  }
20
16
  await next();
21
17
  },
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-group-suite",
3
- "version": "1.1.7",
4
- "description": "Zhin.js 群运营套件 — slice-2 checkin/teach/stats + keywords (Plugin Runtime, in-memory)",
3
+ "version": "1.1.9",
4
+ "description": "Zhin.js 群运营套件:签到、教学、统计、持久词库与群欢迎",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./lib/index.d.ts",
11
- "development": "./src/index.ts",
12
11
  "import": "./lib/index.js"
13
12
  },
14
13
  "./package.json": "./package.json"
@@ -18,6 +17,8 @@
18
17
  "schema.json",
19
18
  "commands",
20
19
  "middlewares",
20
+ "handlers",
21
+ "schedules",
21
22
  "src",
22
23
  "lib",
23
24
  "skills",
@@ -40,23 +41,25 @@
40
41
  },
41
42
  "license": "MIT",
42
43
  "dependencies": {
43
- "@zhin.js/components": "1.1.2",
44
- "@zhin.js/core": "1.1.42",
45
- "@zhin.js/jsx": "1.1.2",
46
- "@zhin.js/skill": "1.1.4",
47
- "@zhin.js/tool": "1.1.4"
44
+ "@zhin.js/components": "1.1.3",
45
+ "@zhin.js/core": "1.1.44",
46
+ "@zhin.js/jsx": "1.1.3",
47
+ "@zhin.js/skill": "1.1.6",
48
+ "@zhin.js/tool": "1.1.6"
48
49
  },
49
50
  "devDependencies": {
50
51
  "typescript": "^6.0.3",
51
52
  "vitest": "^4.1.10",
52
53
  "zod": "^4.4.3",
53
- "zhin.js": "1.1.7"
54
+ "zhin.js": "1.1.9"
54
55
  },
55
56
  "peerDependencies": {
56
57
  "zod": "^4.0.0",
57
- "@zhin.js/command": "^1.1.5",
58
- "@zhin.js/middleware": "^1.1.4",
59
- "zhin.js": "^1.1.7"
58
+ "@zhin.js/command": "^1.1.7",
59
+ "@zhin.js/middleware": "^1.1.6",
60
+ "zhin.js": "^1.1.9",
61
+ "@zhin.js/handler": "^1.1.5",
62
+ "@zhin.js/schedule-feature": "^1.1.5"
60
63
  },
61
64
  "peerDependenciesMeta": {
62
65
  "@zhin.js/command": {
@@ -67,6 +70,12 @@
67
70
  },
68
71
  "zod": {
69
72
  "optional": true
73
+ },
74
+ "@zhin.js/handler": {
75
+ "optional": true
76
+ },
77
+ "@zhin.js/schedule-feature": {
78
+ "optional": true
70
79
  }
71
80
  },
72
81
  "repository": {
@@ -79,7 +88,7 @@
79
88
  "registry": "https://registry.npmjs.org"
80
89
  },
81
90
  "engines": {
82
- "node": "^20.19.0 || >=22.12.0"
91
+ "node": ">=22.19.0"
83
92
  },
84
93
  "zhin": {
85
94
  "protocol": 1,
@@ -103,6 +112,14 @@
103
112
  {
104
113
  "package": "@zhin.js/skill",
105
114
  "api": "^1.0.0"
115
+ },
116
+ {
117
+ "package": "@zhin.js/handler",
118
+ "api": "^1.0.0"
119
+ },
120
+ {
121
+ "package": "@zhin.js/schedule-feature",
122
+ "api": "^1.0.0"
106
123
  }
107
124
  ],
108
125
  "plugins": []
package/plugin.js CHANGED
@@ -1,14 +1,14 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { definePlugin, databaseHostToken } from 'zhin.js';
3
+ import { resolveGroupSuiteConfig } from "./lib/config.js";
3
4
  import { createInMemoryGroupSuiteDb } from "./lib/memory-store.js";
4
5
  import { createGroupSuiteRuntime, groupSuiteRuntimeToken } from "./lib/runtime-state.js";
5
- import { flushStatsBuffer } from "./lib/stats-lib.js";
6
6
  import { defineGroupSuiteTables } from "./lib/tables.js";
7
7
  /**
8
8
  * Plugin Runtime:
9
9
  * - checkin / teach / stats commands + keyword middleware
10
10
  * - DB: prefer `databaseHostToken`; else in-memory fallback
11
- * - Stats buffer: periodic flush every 10s + final flush on dispose
11
+ * - Stats writes run inside admitted schedule/query operations
12
12
  */
13
13
  export default definePlugin({
14
14
  name: 'group-suite',
@@ -25,14 +25,14 @@ export default definePlugin({
25
25
  }
26
26
  return createInMemoryGroupSuiteDb();
27
27
  })();
28
+ const config = resolveGroupSuiteConfig(context.config.get());
29
+ if (config.keywordReply && !config.ephemeralKeywords) {
30
+ // Fail before activation rather than claiming durable keywords in a memory fallback.
31
+ if (!context.resources.has(databaseHostToken))
32
+ throw new Error('Persistent keywords require a Database Host; demos may explicitly set ephemeralKeywords: true');
33
+ void context.resources.use(databaseHostToken).state;
34
+ }
28
35
  const runtime = createGroupSuiteRuntime(db);
29
36
  context.resources.provide(groupSuiteRuntimeToken, runtime);
30
- const flushTimer = setInterval(() => {
31
- void flushStatsBuffer(runtime);
32
- }, 10_000);
33
- context.lifecycle.add(async () => {
34
- clearInterval(flushTimer);
35
- await flushStatsBuffer(runtime);
36
- });
37
37
  },
38
38
  });
@@ -0,0 +1,11 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineSchedule } from '@zhin.js/schedule-feature';
3
+ import { groupSuiteRuntimeToken } from "../../lib/runtime-state.js";
4
+ import { flushStatsBuffer } from "../../lib/stats-lib.js";
5
+ export default defineSchedule({
6
+ cron: '*/10 * * * * *',
7
+ description: 'Flush owner-scoped message count batches',
8
+ execute(context) {
9
+ return flushStatsBuffer(context.use(groupSuiteRuntimeToken));
10
+ },
11
+ });
@@ -0,0 +1,11 @@
1
+ import { defineSchedule } from '@zhin.js/schedule-feature';
2
+ import { groupSuiteRuntimeToken } from '../../src/runtime-state.js';
3
+ import { flushStatsBuffer } from '../../src/stats-lib.js';
4
+
5
+ export default defineSchedule({
6
+ cron: '*/10 * * * * *',
7
+ description: 'Flush owner-scoped message count batches',
8
+ execute(context) {
9
+ return flushStatsBuffer(context.use(groupSuiteRuntimeToken));
10
+ },
11
+ });
package/schema.json CHANGED
@@ -3,15 +3,56 @@
3
3
  "type": "object",
4
4
  "additionalProperties": false,
5
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 }
6
+ "keywordReply": {
7
+ "type": "boolean",
8
+ "default": false
9
+ },
10
+ "basePointsMin": {
11
+ "type": "number",
12
+ "default": 10
13
+ },
14
+ "basePointsMax": {
15
+ "type": "number",
16
+ "default": 30
17
+ },
18
+ "streakBonus": {
19
+ "type": "number",
20
+ "default": 5
21
+ },
22
+ "streakCap": {
23
+ "type": "number",
24
+ "default": 50
25
+ },
26
+ "rankSize": {
27
+ "type": "number",
28
+ "default": 10
29
+ },
30
+ "teachMaxPerGroup": {
31
+ "type": "number",
32
+ "default": 200
33
+ },
34
+ "teachCooldownMs": {
35
+ "type": "number",
36
+ "default": 3000
37
+ },
38
+ "teachAllowRegex": {
39
+ "type": "boolean",
40
+ "default": true
41
+ },
42
+ "teachPageSize": {
43
+ "type": "number",
44
+ "default": 10
45
+ },
46
+ "welcomeMessage": {
47
+ "type": "string",
48
+ "maxLength": 2000,
49
+ "default": "",
50
+ "description": "Group welcome text. Empty disables; {name}/{id} refer to the joined member."
51
+ },
52
+ "ephemeralKeywords": {
53
+ "type": "boolean",
54
+ "default": false,
55
+ "description": "Explicit volatile keywords. With false, automatic replies require Database Host at startup and keyword management requires its persistent state service when invoked."
56
+ }
16
57
  }
17
58
  }
@@ -1,4 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { conversationKey, toolGroupConversation } from "../../../../lib/conversation-scope.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { getCheckinModel } from "../../../../lib/db-store.js";
4
5
  import { groupSuiteRuntimeToken } from "../../../../lib/runtime-state.js";
@@ -13,19 +14,18 @@ export default defineAgentTool({
13
14
  },
14
15
  },
15
16
  requiresApproval: 'never',
17
+ scopes: ['group'],
16
18
  async execute({ user_id: userId, group_id: requestedGroupId }, context) {
17
19
  const runtime = context.use(groupSuiteRuntimeToken);
18
20
  const model = getCheckinModel(runtime.db);
19
21
  if (!model)
20
22
  return '签到数据库尚未就绪';
21
- const groupId = requestedGroupId ?? currentGroupId(context.origin);
23
+ const groupId = conversationKey(toolGroupConversation(context, requestedGroupId));
22
24
  const query = {
23
25
  ...(userId ? { user_id: userId } : {}),
24
- ...(groupId ? { context_type: 'group', context_id: groupId } : {}),
26
+ context_type: 'group', context_id: groupId,
25
27
  };
26
- const rows = Object.keys(query).length > 0
27
- ? await model.select().where(query)
28
- : await model.select();
28
+ const rows = await model.select().where(query);
29
29
  if (userId) {
30
30
  if (rows.length === 0)
31
31
  return `用户 ${userId} 没有签到记录`;
@@ -37,6 +37,3 @@ export default defineAgentTool({
37
37
  return `签到系统统计\n总用户: ${rows.length}\n总积分: ${totalPoints}\n今日签到: ${todayCount}人`;
38
38
  },
39
39
  });
40
- function currentGroupId(origin) {
41
- return origin.kind === 'im' && origin.scope !== 'private' ? origin.sceneId : '';
42
- }
@@ -1,3 +1,4 @@
1
+ import { conversationKey, toolGroupConversation } from '../../../../src/conversation-scope.js';
1
2
  import { defineAgentTool } from '@zhin.js/tool';
2
3
  import { getCheckinModel } from '../../../../src/db-store.js';
3
4
  import { groupSuiteRuntimeToken } from '../../../../src/runtime-state.js';
@@ -13,18 +14,17 @@ export default defineAgentTool<{ user_id?: string; group_id?: string }>({
13
14
  },
14
15
  },
15
16
  requiresApproval: 'never',
17
+ scopes: ['group'],
16
18
  async execute({ user_id: userId, group_id: requestedGroupId }, context) {
17
19
  const runtime = context.use(groupSuiteRuntimeToken);
18
20
  const model = getCheckinModel(runtime.db);
19
21
  if (!model) return '签到数据库尚未就绪';
20
- const groupId = requestedGroupId ?? currentGroupId(context.origin);
22
+ const groupId = conversationKey(toolGroupConversation(context, requestedGroupId));
21
23
  const query = {
22
24
  ...(userId ? { user_id: userId } : {}),
23
- ...(groupId ? { context_type: 'group', context_id: groupId } : {}),
25
+ context_type: 'group', context_id: groupId,
24
26
  };
25
- const rows = Object.keys(query).length > 0
26
- ? await model.select().where(query)
27
- : await model.select();
27
+ const rows = await model.select().where(query);
28
28
  if (userId) {
29
29
  if (rows.length === 0) return `用户 ${userId} 没有签到记录`;
30
30
  const user = rows[0]!;
@@ -35,7 +35,3 @@ export default defineAgentTool<{ user_id?: string; group_id?: string }>({
35
35
  return `签到系统统计\n总用户: ${rows.length}\n总积分: ${totalPoints}\n今日签到: ${todayCount}人`;
36
36
  },
37
37
  });
38
-
39
- function currentGroupId(origin: import('@zhin.js/tool').ToolInvocationOrigin): string {
40
- return origin.kind === 'im' && origin.scope !== 'private' ? origin.sceneId : '';
41
- }
@@ -1,4 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { conversationKey, toolGroupConversation } from "../../../../lib/conversation-scope.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { getCheckinModel } from "../../../../lib/db-store.js";
4
5
  import { groupSuiteRuntimeToken } from "../../../../lib/runtime-state.js";
@@ -12,17 +13,13 @@ export default defineAgentTool({
12
13
  },
13
14
  },
14
15
  requiresApproval: 'never',
16
+ scopes: ['group'],
15
17
  async execute({ limit, group_id: requestedGroupId }, context) {
16
18
  const model = getCheckinModel(context.use(groupSuiteRuntimeToken).db);
17
19
  if (!model)
18
20
  return '签到数据库尚未就绪';
19
- const groupId = requestedGroupId
20
- ?? (context.origin.kind === 'im' && context.origin.scope !== 'private'
21
- ? context.origin.sceneId
22
- : '');
23
- const rows = groupId
24
- ? await model.select().where({ context_type: 'group', context_id: groupId })
25
- : await model.select();
21
+ const groupId = conversationKey(toolGroupConversation(context, requestedGroupId));
22
+ const rows = await model.select().where({ context_type: 'group', context_id: groupId });
26
23
  const count = Math.min(Math.max(Number(limit) || 10, 1), 50);
27
24
  const ranked = [...rows]
28
25
  .sort((left, right) => Number(right.points ?? 0) - Number(left.points ?? 0))
@@ -1,3 +1,4 @@
1
+ import { conversationKey, toolGroupConversation } from '../../../../src/conversation-scope.js';
1
2
  import { defineAgentTool } from '@zhin.js/tool';
2
3
  import { getCheckinModel } from '../../../../src/db-store.js';
3
4
  import { groupSuiteRuntimeToken } from '../../../../src/runtime-state.js';
@@ -12,16 +13,12 @@ export default defineAgentTool<{ limit?: number; group_id?: string }>({
12
13
  },
13
14
  },
14
15
  requiresApproval: 'never',
16
+ scopes: ['group'],
15
17
  async execute({ limit, group_id: requestedGroupId }, context) {
16
18
  const model = getCheckinModel(context.use(groupSuiteRuntimeToken).db);
17
19
  if (!model) return '签到数据库尚未就绪';
18
- const groupId = requestedGroupId
19
- ?? (context.origin.kind === 'im' && context.origin.scope !== 'private'
20
- ? context.origin.sceneId
21
- : '');
22
- const rows = groupId
23
- ? await model.select().where({ context_type: 'group', context_id: groupId })
24
- : await model.select();
20
+ const groupId = conversationKey(toolGroupConversation(context, requestedGroupId));
21
+ const rows = await model.select().where({ context_type: 'group', context_id: groupId });
25
22
  const count = Math.min(Math.max(Number(limit) || 10, 1), 50);
26
23
  const ranked = [...rows]
27
24
  .sort((left, right) => Number(right.points ?? 0) - Number(left.points ?? 0))
@@ -1,4 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { conversationKey, toolGroupConversation } from "../../../../lib/conversation-scope.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { groupSuiteRuntimeToken } from "../../../../lib/runtime-state.js";
4
5
  import { flushStatsBuffer, monthStartStr, queryStats, weekStartStr } from "../../../../lib/stats-lib.js";
@@ -13,13 +14,11 @@ export default defineAgentTool({
13
14
  },
14
15
  },
15
16
  requiresApproval: 'never',
17
+ scopes: ['group'],
16
18
  async execute({ group_id: requestedGroupId, period = 'today' }, context) {
17
19
  const runtime = context.use(groupSuiteRuntimeToken);
18
20
  await flushStatsBuffer(runtime);
19
- const groupId = requestedGroupId
20
- ?? (context.origin.kind === 'im' && context.origin.scope !== 'private'
21
- ? context.origin.sceneId
22
- : '');
21
+ const groupId = conversationKey(toolGroupConversation(context, requestedGroupId));
23
22
  const fromDate = period === 'week'
24
23
  ? weekStartStr()
25
24
  : period === 'month' ? monthStartStr() : todayStr();
@@ -1,3 +1,4 @@
1
+ import { conversationKey, toolGroupConversation } from '../../../../src/conversation-scope.js';
1
2
  import { defineAgentTool } from '@zhin.js/tool';
2
3
  import { groupSuiteRuntimeToken } from '../../../../src/runtime-state.js';
3
4
  import { flushStatsBuffer, monthStartStr, queryStats, weekStartStr } from '../../../../src/stats-lib.js';
@@ -15,13 +16,11 @@ export default defineAgentTool<{ group_id?: string; period?: Period }>({
15
16
  },
16
17
  },
17
18
  requiresApproval: 'never',
19
+ scopes: ['group'],
18
20
  async execute({ group_id: requestedGroupId, period = 'today' }, context) {
19
21
  const runtime = context.use(groupSuiteRuntimeToken);
20
22
  await flushStatsBuffer(runtime);
21
- const groupId = requestedGroupId
22
- ?? (context.origin.kind === 'im' && context.origin.scope !== 'private'
23
- ? context.origin.sceneId
24
- : '');
23
+ const groupId = conversationKey(toolGroupConversation(context, requestedGroupId));
25
24
  const fromDate = period === 'week'
26
25
  ? weekStartStr()
27
26
  : period === 'month' ? monthStartStr() : todayStr();
@@ -1,4 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { conversationKey, toolGroupConversation } from "../../../../lib/conversation-scope.js";
2
3
  import { defineAgentTool } from '@zhin.js/tool';
3
4
  import { getStatsModel } from "../../../../lib/db-store.js";
4
5
  import { groupSuiteRuntimeToken } from "../../../../lib/runtime-state.js";
@@ -15,19 +16,17 @@ export default defineAgentTool({
15
16
  required: ['user_id'],
16
17
  },
17
18
  requiresApproval: 'never',
19
+ scopes: ['group'],
18
20
  async execute({ user_id: userId, group_id: requestedGroupId }, context) {
19
21
  const runtime = context.use(groupSuiteRuntimeToken);
20
22
  await flushStatsBuffer(runtime);
21
23
  const model = getStatsModel(runtime.db);
22
24
  if (!model)
23
25
  return '统计数据库尚未就绪';
24
- const groupId = requestedGroupId
25
- ?? (context.origin.kind === 'im' && context.origin.scope !== 'private'
26
- ? context.origin.sceneId
27
- : '');
26
+ const groupId = conversationKey(toolGroupConversation(context, requestedGroupId));
28
27
  const rows = await model.select().where({
29
28
  user_id: userId,
30
- ...(groupId ? { group_id: groupId } : {}),
29
+ group_id: groupId,
31
30
  });
32
31
  if (rows.length === 0)
33
32
  return `用户 ${userId} 暂无消息记录`;
@@ -1,3 +1,4 @@
1
+ import { conversationKey, toolGroupConversation } from '../../../../src/conversation-scope.js';
1
2
  import { defineAgentTool } from '@zhin.js/tool';
2
3
  import { getStatsModel } from '../../../../src/db-store.js';
3
4
  import { groupSuiteRuntimeToken } from '../../../../src/runtime-state.js';
@@ -15,18 +16,16 @@ export default defineAgentTool<{ user_id: string; group_id?: string }>({
15
16
  required: ['user_id'],
16
17
  },
17
18
  requiresApproval: 'never',
19
+ scopes: ['group'],
18
20
  async execute({ user_id: userId, group_id: requestedGroupId }, context) {
19
21
  const runtime = context.use(groupSuiteRuntimeToken);
20
22
  await flushStatsBuffer(runtime);
21
23
  const model = getStatsModel(runtime.db);
22
24
  if (!model) return '统计数据库尚未就绪';
23
- const groupId = requestedGroupId
24
- ?? (context.origin.kind === 'im' && context.origin.scope !== 'private'
25
- ? context.origin.sceneId
26
- : '');
25
+ const groupId = conversationKey(toolGroupConversation(context, requestedGroupId));
27
26
  const rows = await model.select().where({
28
27
  user_id: userId,
29
- ...(groupId ? { group_id: groupId } : {}),
28
+ group_id: groupId,
30
29
  });
31
30
  if (rows.length === 0) return `用户 ${userId} 暂无消息记录`;
32
31
  const today = todayStr();
@@ -1,3 +1,4 @@
1
+ import type { ConversationInput } from './conversation-scope.js';
1
2
  import type { GroupSuiteConfig } from './config.js';
2
3
  import { getCheckinModel } from './db-store.js';
3
4
  import {
@@ -64,7 +65,7 @@ function serializedCheckin<T>(
64
65
  }
65
66
 
66
67
  export async function doCheckin(
67
- input: { sender?: { readonly id: string; readonly name?: string }; target?: string; metadata?: Readonly<Record<string, unknown>> },
68
+ input: ConversationInput,
68
69
  cfg: GroupSuiteConfig,
69
70
  runtime: GroupSuiteRuntime,
70
71
  ): Promise<string> {
@@ -113,7 +114,7 @@ export async function doCheckin(
113
114
  }
114
115
 
115
116
  export async function myPoints(
116
- input: { sender?: { readonly id: string; readonly name?: string }; target?: string; metadata?: Readonly<Record<string, unknown>> },
117
+ input: ConversationInput,
117
118
  runtime: GroupSuiteRuntime,
118
119
  ): Promise<string> {
119
120
  const M = getCheckinModel(runtime.db);
@@ -139,7 +140,7 @@ export async function myPoints(
139
140
  }
140
141
 
141
142
  export async function pointsRank(
142
- input: { sender?: { readonly id: string; readonly name?: string }; target?: string; metadata?: Readonly<Record<string, unknown>> },
143
+ input: ConversationInput,
143
144
  cfg: GroupSuiteConfig,
144
145
  runtime: GroupSuiteRuntime,
145
146
  ): Promise<string> {
@@ -147,9 +148,7 @@ export async function pointsRank(
147
148
  if (!M) return '签到数据库尚未就绪';
148
149
  const { type: ctxType, id: ctxId } = resolveContextKey(input);
149
150
  const all = (await M.select().where(
150
- ctxType === 'global'
151
- ? { context_type: 'global' }
152
- : { context_type: ctxType, context_id: ctxId },
151
+ { context_type: ctxType, context_id: ctxId },
153
152
  )) as Record<string, unknown>[];
154
153
  if (all.length === 0) return '还没有人签到过哦~';
155
154
  const sorted = all.sort((a, b) => Number(b.points || 0) - Number(a.points || 0));
@@ -159,7 +158,8 @@ export async function pointsRank(
159
158
  const prefix = i < 3 ? medals[i] : `${i + 1}.`;
160
159
  return `${prefix} ${u.user_name || u.user_id} — ${u.points}分 (${u.total_checkins}天)`;
161
160
  });
162
- const title = ctxType === 'group' ? '本群积分排行' : '全局积分排行';
161
+ const title = ctxType === 'group' ? '本群积分排行'
162
+ : ctxType === 'channel' ? '本频道积分排行' : '当前私聊积分排行';
163
163
  const { id: userId } = resolveSender(input);
164
164
  const myRank = sorted.findIndex((u) => u.user_id === userId);
165
165
  const footer = myRank >= 0 ? `\n你的排名: 第${myRank + 1}名` : '';
package/src/config.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export interface GroupSuiteConfig {
2
2
  keywordReply: boolean;
3
+ /** Explicitly volatile keywords for demos without a persistent Database Host. */
4
+ ephemeralKeywords: boolean;
5
+ /** Empty disables group member welcomes. {name} and {id} are literal placeholders. */
6
+ welcomeMessage: string;
3
7
  basePointsMin: number;
4
8
  basePointsMax: number;
5
9
  streakBonus: number;
@@ -13,6 +17,8 @@ export interface GroupSuiteConfig {
13
17
 
14
18
  export const DEFAULT_GROUP_SUITE_CONFIG: GroupSuiteConfig = {
15
19
  keywordReply: false,
20
+ ephemeralKeywords: false,
21
+ welcomeMessage: '',
16
22
  basePointsMin: 10,
17
23
  basePointsMax: 30,
18
24
  streakBonus: 5,