@zhin.js/plugin-group-suite 1.1.6 → 1.1.8

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 (79) 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/runtime-state.d.ts +5 -1
  39. package/lib/runtime-state.d.ts.map +1 -1
  40. package/lib/runtime-state.js.map +1 -1
  41. package/lib/shared-runtime.d.ts +3 -8
  42. package/lib/shared-runtime.d.ts.map +1 -1
  43. package/lib/shared-runtime.js +4 -10
  44. package/lib/shared-runtime.js.map +1 -1
  45. package/lib/stats-lib.d.ts +5 -26
  46. package/lib/stats-lib.d.ts.map +1 -1
  47. package/lib/stats-lib.js +29 -15
  48. package/lib/stats-lib.js.map +1 -1
  49. package/lib/teach-lib.d.ts +2 -9
  50. package/lib/teach-lib.d.ts.map +1 -1
  51. package/lib/teach-lib.js +6 -8
  52. package/lib/teach-lib.js.map +1 -1
  53. package/middlewares/keyword-reply/index.js +3 -5
  54. package/middlewares/keyword-reply/index.ts +3 -5
  55. package/middlewares/stats-count/index.js +3 -7
  56. package/middlewares/stats-count/index.ts +3 -7
  57. package/package.json +29 -11
  58. package/plugin.js +9 -9
  59. package/schedules/flush-stats/index.js +11 -0
  60. package/schedules/flush-stats/index.ts +11 -0
  61. package/schema.json +51 -10
  62. package/skills/checkin/tools/checkin_query/index.js +5 -8
  63. package/skills/checkin/tools/checkin_query/index.ts +5 -9
  64. package/skills/checkin/tools/checkin_rank/index.js +4 -7
  65. package/skills/checkin/tools/checkin_rank/index.ts +4 -7
  66. package/skills/group-daily-analysis/tools/stats_query/index.js +3 -4
  67. package/skills/group-daily-analysis/tools/stats_query/index.ts +3 -4
  68. package/skills/group-daily-analysis/tools/stats_user/index.js +4 -5
  69. package/skills/group-daily-analysis/tools/stats_user/index.ts +4 -5
  70. package/src/checkin-lib.ts +7 -7
  71. package/src/config.ts +6 -0
  72. package/src/conversation-scope.ts +45 -0
  73. package/src/index.ts +1 -0
  74. package/src/keyword-store.ts +22 -0
  75. package/src/keywords.ts +72 -0
  76. package/src/runtime-state.ts +5 -1
  77. package/src/shared-runtime.ts +5 -14
  78. package/src/stats-lib.ts +36 -29
  79. package/src/teach-lib.ts +8 -17
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-group-suite",
3
- "version": "1.1.6",
4
- "description": "Zhin.js 群运营套件 — slice-2 checkin/teach/stats + keywords (Plugin Runtime, in-memory)",
3
+ "version": "1.1.8",
4
+ "description": "Zhin.js 群运营套件:签到、教学、统计、持久词库与群欢迎",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -18,6 +18,8 @@
18
18
  "schema.json",
19
19
  "commands",
20
20
  "middlewares",
21
+ "handlers",
22
+ "schedules",
21
23
  "src",
22
24
  "lib",
23
25
  "skills",
@@ -40,23 +42,25 @@
40
42
  },
41
43
  "license": "MIT",
42
44
  "dependencies": {
43
- "@zhin.js/components": "1.1.1",
44
- "@zhin.js/core": "1.1.41",
45
- "@zhin.js/jsx": "1.1.1",
46
- "@zhin.js/skill": "1.1.4",
47
- "@zhin.js/tool": "1.1.4"
45
+ "@zhin.js/components": "1.1.2",
46
+ "@zhin.js/core": "1.1.43",
47
+ "@zhin.js/jsx": "1.1.2",
48
+ "@zhin.js/skill": "1.1.5",
49
+ "@zhin.js/tool": "1.1.5"
48
50
  },
49
51
  "devDependencies": {
50
52
  "typescript": "^6.0.3",
51
53
  "vitest": "^4.1.10",
52
54
  "zod": "^4.4.3",
53
- "zhin.js": "1.1.6"
55
+ "zhin.js": "1.1.8"
54
56
  },
55
57
  "peerDependencies": {
56
58
  "zod": "^4.0.0",
57
- "@zhin.js/command": "^1.1.5",
58
- "@zhin.js/middleware": "^1.1.4",
59
- "zhin.js": "^1.1.6"
59
+ "@zhin.js/command": "^1.1.6",
60
+ "@zhin.js/middleware": "^1.1.5",
61
+ "zhin.js": "^1.1.8",
62
+ "@zhin.js/handler": "^1.1.4",
63
+ "@zhin.js/schedule-feature": "^1.1.4"
60
64
  },
61
65
  "peerDependenciesMeta": {
62
66
  "@zhin.js/command": {
@@ -67,6 +71,12 @@
67
71
  },
68
72
  "zod": {
69
73
  "optional": true
74
+ },
75
+ "@zhin.js/handler": {
76
+ "optional": true
77
+ },
78
+ "@zhin.js/schedule-feature": {
79
+ "optional": true
70
80
  }
71
81
  },
72
82
  "repository": {
@@ -103,6 +113,14 @@
103
113
  {
104
114
  "package": "@zhin.js/skill",
105
115
  "api": "^1.0.0"
116
+ },
117
+ {
118
+ "package": "@zhin.js/handler",
119
+ "api": "^1.0.0"
120
+ },
121
+ {
122
+ "package": "@zhin.js/schedule-feature",
123
+ "api": "^1.0.0"
106
124
  }
107
125
  ],
108
126
  "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,
@@ -0,0 +1,45 @@
1
+ import { createDeliveryTarget, type DeliveryTarget } from 'zhin.js';
2
+ import { adapterFeatureId, type AdapterIndex } from 'zhin.js/adapter';
3
+ import type { ToolInvocationOrigin } from '@zhin.js/tool';
4
+
5
+ export interface ConversationInput {
6
+ readonly conversation?: DeliveryTarget;
7
+ readonly sender?: Readonly<{ id: string; name?: string }>;
8
+ readonly content?: unknown;
9
+ readonly metadata?: Readonly<Record<string, unknown>>;
10
+ }
11
+
12
+ /** Versioned, unambiguous identity; historical bare group ids are deliberately not read. */
13
+ export function conversationKey(target: DeliveryTarget): string {
14
+ const canonical = createDeliveryTarget({ conversation: target });
15
+ return JSON.stringify(['conversation-v1', canonical.endpoint.adapter, canonical.endpoint.id,
16
+ canonical.kind, canonical.id, canonical.parent?.kind ?? null, canonical.parent?.id ?? null,
17
+ canonical.threadId ?? null]);
18
+ }
19
+
20
+ export function requireConversation(input: ConversationInput): DeliveryTarget {
21
+ if (!input.conversation) throw new Error('Group Suite requires a canonical conversation');
22
+ return createDeliveryTarget({ conversation: input.conversation });
23
+ }
24
+
25
+ /** Tool origins contain an endpoint selector, not a canonical endpoint capability id. */
26
+ export function toolGroupConversation(context: {
27
+ readonly origin: ToolInvocationOrigin;
28
+ project<T>(feature: typeof adapterFeatureId): T;
29
+ }, groupId?: string): DeliveryTarget {
30
+ if (context.origin.kind !== 'im' || context.origin.scope !== 'group') {
31
+ throw new Error('Group Suite query tools require a group IM origin');
32
+ }
33
+ const origin = context.origin;
34
+ const index = context.project<AdapterIndex>(adapterFeatureId);
35
+ const matches = index.describe().filter(endpoint =>
36
+ (String(endpoint.id) === origin.endpoint || endpoint.name === origin.endpoint)
37
+ && index.clientAdapter(endpoint.id) === origin.platform);
38
+ if (matches.length !== 1) throw new Error('Group Suite tool endpoint is missing or ambiguous');
39
+ return createDeliveryTarget({ conversation: {
40
+ endpoint: { id: String(matches[0]!.id), adapter: origin.platform },
41
+ kind: 'group', id: groupId ?? origin.sceneId,
42
+ ...(origin.threadId !== undefined && (groupId === undefined || groupId === origin.sceneId)
43
+ ? { threadId: origin.threadId } : {}),
44
+ } });
45
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export type { GroupSuiteConfig } from './config.js';
6
6
  export { doCheckin, myPoints, pointsRank } from './checkin-lib.js';
7
7
  export {
8
8
  addKeyword,
9
+ keywordStore,
9
10
  listKeywords,
10
11
  matchKeyword,
11
12
  removeKeyword,
@@ -1,3 +1,7 @@
1
+ import type { MessageBase } from '@zhin.js/core/runtime';
2
+ import { conversationKey } from './conversation-scope.js';
3
+ import type { GroupSuiteRuntime } from './runtime-state.js';
4
+
1
5
  export function addKeyword(keyword: string, reply: string, store: Map<string, string>): void {
2
6
  store.set(keyword, reply);
3
7
  }
@@ -16,3 +20,21 @@ export function matchKeyword(text: string, store: Map<string, string>): string |
16
20
  }
17
21
  return null;
18
22
  }
23
+
24
+ /** Owner is already isolated by the Resource; canonical endpoint and group form the inner scope. */
25
+ export function keywordStore(
26
+ input: Pick<MessageBase, 'conversation'> | undefined,
27
+ runtime: GroupSuiteRuntime,
28
+ ): Map<string, string> {
29
+ const conversation = input?.conversation;
30
+ if (conversation?.kind !== 'group' || !conversation.id || !conversation.endpoint.id) {
31
+ throw new Error('Keywords require a group conversation with an endpoint');
32
+ }
33
+ const key = conversationKey(conversation);
34
+ let store = runtime.keywords.get(key);
35
+ if (!store) {
36
+ store = new Map();
37
+ runtime.keywords.set(key, store);
38
+ }
39
+ return store;
40
+ }