mioku-plugin-admin 2.2.2 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.md CHANGED
@@ -39,6 +39,39 @@ fields:
39
39
  type: switch
40
40
  description: Bot被踢出群聊时通知主人
41
41
 
42
+ - key: base.welcome.enabled
43
+ label: 启用新人入群欢迎
44
+ type: switch
45
+ description: 新人(非机器人)入群时发送欢迎消息
46
+
47
+ - key: base.welcome.mode
48
+ label: 欢迎模式
49
+ type: select
50
+ options:
51
+ - value: ai
52
+ label: 使用AI生成
53
+ - value: text
54
+ label: 固定文本
55
+ description: AI 模式由 chat-runtime 模型生成;text 模式直接套用固定欢迎模板
56
+
57
+ - key: base.welcome.text
58
+ label: 固定欢迎文本
59
+ type: textarea
60
+ description: 欢迎模板,支持 {user} 和 {group} 占位符
61
+ placeholder: 欢迎新人~
62
+
63
+ - key: base.welcome.aiPrompt
64
+ label: AI欢迎额外提示词
65
+ type: textarea
66
+ description: 作为额外要求传给模型,默认留空即可
67
+ placeholder: 例如:提醒新成员查看群公告
68
+
69
+ - key: base.welcome.batchWindowMs
70
+ label: AI 欢迎聚合窗口 (毫秒)
71
+ type: number
72
+ description: 短时间内多个新成员入群时,攒齐该时长内的成员后只发一次 AI 欢迎,避免频繁调用模型。设为 0 表示关闭聚合、逐个欢迎。默认 20000。
73
+ placeholder: 20000
74
+
42
75
  ---
43
76
 
44
77
  ```mioku-fields
@@ -50,4 +83,9 @@ keys:
50
83
  - base.notifyGroupBan
51
84
  - base.notifyGroupUnban
52
85
  - base.notifyGroupKick
53
- ```
86
+ - base.welcome.enabled
87
+ - base.welcome.mode
88
+ - base.welcome.text
89
+ - base.welcome.aiPrompt
90
+ - base.welcome.batchWindowMs
91
+ ```
package/config.ts CHANGED
@@ -8,6 +8,13 @@ export interface AdminConfig {
8
8
  notifyGroupBan: boolean;
9
9
  notifyGroupUnban: boolean;
10
10
  notifyGroupKick: boolean;
11
+ welcome: {
12
+ enabled: boolean;
13
+ mode: "ai" | "text";
14
+ text: string;
15
+ aiPrompt: string;
16
+ batchWindowMs: number;
17
+ };
11
18
  }
12
19
 
13
20
  export const DEFAULT_CONFIG: AdminConfig = {
@@ -18,6 +25,13 @@ export const DEFAULT_CONFIG: AdminConfig = {
18
25
  notifyGroupBan: true,
19
26
  notifyGroupUnban: true,
20
27
  notifyGroupKick: true,
28
+ welcome: {
29
+ enabled: true,
30
+ mode: "ai",
31
+ text: "欢迎新人~",
32
+ aiPrompt: "",
33
+ batchWindowMs: 20000,
34
+ },
21
35
  };
22
36
 
23
37
  export function normalizeConfig(raw: any): AdminConfig {
@@ -33,9 +47,31 @@ export function normalizeConfig(raw: any): AdminConfig {
33
47
  notifyGroupBan: raw?.notifyGroupBan ?? DEFAULT_CONFIG.notifyGroupBan,
34
48
  notifyGroupUnban: raw?.notifyGroupUnban ?? DEFAULT_CONFIG.notifyGroupUnban,
35
49
  notifyGroupKick: raw?.notifyGroupKick ?? DEFAULT_CONFIG.notifyGroupKick,
50
+ welcome: {
51
+ enabled:
52
+ raw?.welcome?.enabled ?? DEFAULT_CONFIG.welcome.enabled,
53
+ mode: raw?.welcome?.mode === "text" ? "text" : "ai",
54
+ text:
55
+ typeof raw?.welcome?.text === "string"
56
+ ? raw.welcome.text
57
+ : DEFAULT_CONFIG.welcome.text,
58
+ aiPrompt:
59
+ typeof raw?.welcome?.aiPrompt === "string"
60
+ ? raw.welcome.aiPrompt
61
+ : DEFAULT_CONFIG.welcome.aiPrompt,
62
+ batchWindowMs: normalizeBatchWindowMs(raw?.welcome?.batchWindowMs),
63
+ },
36
64
  };
37
65
  }
38
66
 
67
+ function normalizeBatchWindowMs(value: unknown): number {
68
+ const num = Number(value);
69
+ if (!Number.isFinite(num) || num < 0) {
70
+ return DEFAULT_CONFIG.welcome.batchWindowMs;
71
+ }
72
+ return Math.floor(num);
73
+ }
74
+
39
75
  // 格式化秒数为可读时长
40
76
  export function formatDuration(seconds: number): string {
41
77
  if (seconds <= 0) return "0秒";
@@ -111,4 +147,4 @@ export async function getMemberRole(
111
147
  } catch {
112
148
  return "member";
113
149
  }
114
- }
150
+ }
package/index.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { definePlugin, type MiokiContext } from "mioki";
2
- import type { ConfigService } from "mioku";
2
+ import type { AIService, ConfigService } from "mioku";
3
3
  import { setPluginRuntimeState, resetPluginRuntimeState } from "mioku";
4
4
  import { DEFAULT_CONFIG, normalizeConfig } from "./config";
5
5
  import type { AdminConfig } from "./config";
6
6
  import { registerNotificationHandlers } from "./notify";
7
7
  import { registerPersonalCommands } from "./commands/personal";
8
8
  import { registerGroupAdminCommands } from "./commands/group";
9
+ import { registerWelcomeHandler } from "./notify/welcome";
9
10
 
10
11
  interface RuntimeState {
11
12
  ctx?: MiokiContext;
@@ -15,10 +16,11 @@ interface RuntimeState {
15
16
  export default definePlugin({
16
17
  name: "admin",
17
18
  version: "1.0.0",
18
- description: "管理插件,提供事件通知与管理指令",
19
+ description: "管理插件,提供事件通知与群管/个人管理指令",
19
20
 
20
21
  async setup(ctx: MiokiContext) {
21
22
  const configService = ctx.services?.config as ConfigService | undefined;
23
+ const aiService = ctx.services?.ai as AIService | undefined;
22
24
 
23
25
  let config: AdminConfig = { ...DEFAULT_CONFIG };
24
26
 
@@ -38,6 +40,9 @@ export default definePlugin({
38
40
  // 注册事件通知
39
41
  registerNotificationHandlers(ctx, getConfig);
40
42
 
43
+ // 注册新人入群欢迎
44
+ registerWelcomeHandler(ctx, aiService, getConfig);
45
+
41
46
  // 注册指令
42
47
  registerPersonalCommands(ctx);
43
48
  registerGroupAdminCommands(ctx);
@@ -49,4 +54,4 @@ export default definePlugin({
49
54
  ctx.logger.info("管理插件已卸载");
50
55
  };
51
56
  },
52
- });
57
+ });
@@ -0,0 +1,223 @@
1
+ import { type MiokiContext, wait } from "mioki";
2
+ import { getPluginRuntimeState, type AIService } from "mioku";
3
+ import type { AdminConfig } from "../config";
4
+
5
+ export async function resolveMemberName(
6
+ ctx: MiokiContext,
7
+ groupId: number,
8
+ userId: number,
9
+ selfId: number,
10
+ ): Promise<string> {
11
+ try {
12
+ const member = await ctx
13
+ .pickBot(selfId)
14
+ .getGroupMemberInfo(groupId, userId);
15
+ return (
16
+ String(member?.card || "").trim() ||
17
+ String(member?.nickname || "").trim() ||
18
+ String(userId)
19
+ );
20
+ } catch {
21
+ return String(userId);
22
+ }
23
+ }
24
+
25
+ interface PendingMember {
26
+ userId: number;
27
+ memberName: string;
28
+ }
29
+
30
+ interface BatchState {
31
+ members: PendingMember[];
32
+ timer: ReturnType<typeof setTimeout> | null;
33
+ groupName: string;
34
+ }
35
+
36
+ const RUNTIME_KEY = "welcomeBatch";
37
+
38
+ function getBatchMap(): Map<string, BatchState> {
39
+ const state = getPluginRuntimeState("admin");
40
+ if (!state[RUNTIME_KEY]) {
41
+ state[RUNTIME_KEY] = new Map<string, BatchState>();
42
+ }
43
+ return state[RUNTIME_KEY] as Map<string, BatchState>;
44
+ }
45
+
46
+ function batchKey(selfId: number, groupId: number): string {
47
+ return `${selfId}:${groupId}`;
48
+ }
49
+
50
+ function renderTemplate(
51
+ template: string,
52
+ values: Record<string, string>,
53
+ ): string {
54
+ let output = String(template || "");
55
+ for (const [key, value] of Object.entries(values)) {
56
+ output = output.split(`{${key}}`).join(value);
57
+ }
58
+ return output;
59
+ }
60
+
61
+ function normalizeGeneratedText(value: string): string {
62
+ return String(value || "")
63
+ .replace(/[`"'“”‘’]/g, "")
64
+ .replace(/\s+/g, " ")
65
+ .trim();
66
+ }
67
+
68
+ async function flushBatch(options: {
69
+ ctx: MiokiContext;
70
+ aiService?: AIService;
71
+ config: AdminConfig;
72
+ selfId: number;
73
+ groupId: number;
74
+ groupName: string;
75
+ members: PendingMember[];
76
+ }): Promise<string> {
77
+ const { ctx, aiService, config, selfId, groupId, groupName, members } = options;
78
+ if (!members.length) return "";
79
+
80
+ const names = members.map((m) => m.memberName || String(m.userId));
81
+ const userList = names.join("、");
82
+ const userIdList = members.map((m) => String(m.userId)).join(", ");
83
+
84
+ const fallbackText =
85
+ normalizeGeneratedText(
86
+ renderTemplate(config.welcome.text, {
87
+ user: userList,
88
+ group: groupName,
89
+ }),
90
+ ) || `欢迎新人~`;
91
+
92
+ if (config.welcome.mode !== "ai") {
93
+ return fallbackText;
94
+ }
95
+
96
+ const chatRuntime = aiService?.getChatRuntime();
97
+ if (!chatRuntime) {
98
+ return fallbackText;
99
+ }
100
+
101
+ try {
102
+ await chatRuntime.generateNotice({
103
+ selfId,
104
+ groupId,
105
+ send: true,
106
+ instruction: [
107
+ `当前有 ${members.length} 位新成员同时入群,请一次性发送一段统一的欢迎语(不要逐个 @ 欢迎、不要重复点名)。`,
108
+ `新成员昵称:${userList}`,
109
+ `新成员 QQ:${userIdList}`,
110
+ `所在群:${groupName}`,
111
+ `${config.welcome.aiPrompt || ""}`,
112
+ ].join("\n"),
113
+ });
114
+
115
+ return "";
116
+ } catch (error) {
117
+ ctx.logger.error(`admin welcome chat-runtime 生成失败: ${error}`);
118
+ return fallbackText;
119
+ }
120
+ }
121
+
122
+ export function registerWelcomeHandler(
123
+ ctx: MiokiContext,
124
+ aiService: AIService | undefined,
125
+ getConfig: () => AdminConfig,
126
+ ): () => void {
127
+ const batches = getBatchMap();
128
+
129
+ const dispose = ctx.handle("notice.group.increase" as any, async (event: any) => {
130
+ const cfg = getConfig();
131
+ const selfId = Number(event?.self_id || ctx.self_id);
132
+ const groupId = Number(event?.group_id || 0);
133
+ const userId = Number(event?.user_id || 0);
134
+ if (!groupId || !userId) return;
135
+ if (userId === selfId) return;
136
+
137
+ if (!cfg.welcome.enabled) return;
138
+
139
+ const groupName =
140
+ String(event?.group?.group_name || "").trim() || String(groupId);
141
+
142
+ const batchWindowMs = Math.max(0, Number(cfg.welcome.batchWindowMs) || 0);
143
+
144
+ if (batchWindowMs === 0) {
145
+ const memberName = await resolveMemberName(ctx, groupId, userId, selfId);
146
+ const welcomeMessage = await flushBatch({
147
+ ctx,
148
+ aiService,
149
+ config: cfg,
150
+ selfId,
151
+ groupId,
152
+ groupName,
153
+ members: [{ userId, memberName }],
154
+ });
155
+ if (!welcomeMessage) return;
156
+ const bot = ctx.pickBot(selfId);
157
+ if (!bot) return;
158
+ try {
159
+ await bot.sendGroupMsg(groupId, [ctx.segment.text(welcomeMessage)]);
160
+ } catch (error) {
161
+ ctx.logger.warn(`发送入群欢迎失败: ${error}`);
162
+ }
163
+ return;
164
+ }
165
+
166
+ const key = batchKey(selfId, groupId);
167
+ let state = batches.get(key);
168
+ if (!state) {
169
+ state = { members: [], timer: null, groupName };
170
+ batches.set(key, state);
171
+ }
172
+ if (groupName && groupName !== String(groupId)) {
173
+ state.groupName = groupName;
174
+ }
175
+
176
+ const memberName = await resolveMemberName(ctx, groupId, userId, selfId);
177
+ if (!state.members.some((m) => m.userId === userId)) {
178
+ state.members.push({ userId, memberName });
179
+ }
180
+
181
+ if (state.timer) {
182
+ return;
183
+ }
184
+
185
+ state.timer = setTimeout(async () => {
186
+ try {
187
+ const pending = state;
188
+ batches.delete(key);
189
+ if (!pending || !pending.members.length) return;
190
+
191
+ const currentConfig = getConfig();
192
+ const welcomeMessage = await flushBatch({
193
+ ctx,
194
+ aiService,
195
+ config: currentConfig,
196
+ selfId,
197
+ groupId,
198
+ groupName: pending.groupName,
199
+ members: pending.members,
200
+ });
201
+ if (!welcomeMessage) return;
202
+
203
+ const bot = ctx.pickBot(selfId);
204
+ if (!bot) return;
205
+ try {
206
+ await bot.sendGroupMsg(groupId, [ctx.segment.text(welcomeMessage)]);
207
+ } catch (error) {
208
+ ctx.logger.warn(`发送入群欢迎失败: ${error}`);
209
+ }
210
+ } catch (error) {
211
+ ctx.logger.error(`admin welcome 批次处理失败: ${error}`);
212
+ }
213
+ }, batchWindowMs);
214
+ });
215
+
216
+ return () => {
217
+ for (const state of batches.values()) {
218
+ if (state.timer) clearTimeout(state.timer);
219
+ }
220
+ batches.clear();
221
+ dispose();
222
+ };
223
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-admin",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
4
4
  "description": "管理插件,提供事件通知与群管/个人管理指令",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -112,6 +112,11 @@
112
112
  {
113
113
  "id": "/全部群聊",
114
114
  "match": "/^\\/全部群聊/"
115
+ },
116
+ {
117
+ "id": "welcome",
118
+ "event": "notice.group.increase",
119
+ "description": "新人入群相关功能"
115
120
  }
116
121
  ],
117
122
  "help": {