koishi-plugin-blacklist-online 0.1.3 → 0.1.5

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 furryaxw
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/lib/core.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Context, Session } from 'koishi';
2
2
  import { PluginConfig } from './types';
3
3
  export declare const sleep: (ms: number) => Promise<unknown>;
4
4
  export declare function isUserAdmin(session: Session, config: PluginConfig, userId: string): Promise<boolean>;
5
- export declare function syncBlacklist(ctx: Context, config: PluginConfig): Promise<void>;
5
+ export declare function syncBlacklist(ctx: Context, config: PluginConfig): Promise<boolean>;
6
6
  export declare function queueRequest(ctx: Context, type: 'ADD' | 'REMOVE' | 'CANCEL', payload: any): Promise<any>;
7
7
  export declare function processOfflineQueue(ctx: Context, config: PluginConfig): Promise<void>;
8
8
  export declare function checkAndHandleUser(ctx: Context, config: PluginConfig, session: Session, user_id: string): Promise<boolean>;
@@ -13,3 +13,4 @@ guildId: string): Promise<{
13
13
  total: number;
14
14
  error?: string;
15
15
  }>;
16
+ export declare function scanAllGuilds(ctx: Context, config: PluginConfig): Promise<void>;
package/lib/core.js CHANGED
@@ -8,6 +8,7 @@ exports.processOfflineQueue = processOfflineQueue;
8
8
  exports.checkAndHandleUser = checkAndHandleUser;
9
9
  exports.parseUserId = parseUserId;
10
10
  exports.scanGuild = scanGuild;
11
+ exports.scanAllGuilds = scanAllGuilds;
11
12
  const koishi_1 = require("koishi");
12
13
  const node_crypto_1 = require("node:crypto");
13
14
  const logger = new koishi_1.Logger('blacklist-online');
@@ -50,36 +51,44 @@ async function syncBlacklist(ctx, config) {
50
51
  timeout: 10000
51
52
  });
52
53
  const { strategy, newRevision, data } = response;
54
+ let hasNewEntries = false; // 标记是否有新增
53
55
  if (strategy === 'up-to-date') {
54
56
  logger.debug('✅ 黑名单已是最新');
55
- return;
57
+ return false;
56
58
  }
57
59
  if (strategy === 'full_replace') {
58
- logger.info(`📥 全量同步 -> ${newRevision} (条数: ${data.length})`);
60
+ logger.info(`执行全量同步,条数: ${data.length}`);
61
+ // 1. 先清空本地表,确保环境干净
59
62
  await ctx.database.remove('blacklist_users', {});
63
+ // 2. 批量写入。由于我们定义了 primary: 'user_id',这里即便有重复也会直接覆盖
60
64
  if (data.length > 0) {
61
- // 修复:使用 upsert 明确告知数据库如果主键冲突则更新
62
- // 注意:Koishi upsert 在处理大量数据时,建议分批以保证稳定性
63
- const batchSize = 50;
65
+ // 分批处理,防止 SQLite 单次 SQL 语句过长
66
+ const batchSize = 100;
64
67
  for (let i = 0; i < data.length; i += batchSize) {
65
68
  const batch = data.slice(i, i + batchSize);
66
- // 这里的字段名必须与后端返回的 snake_case 保持完全一致
67
69
  await ctx.database.upsert('blacklist_users', batch);
68
70
  }
71
+ hasNewEntries = true; // 全量更新通常视为有变化,触发一次扫描比较安全
69
72
  }
70
73
  }
71
74
  else if (strategy === 'incremental') {
72
75
  logger.info(`📥 增量同步 -> ${newRevision}`);
73
- if (data.upserts?.length)
76
+ if (data.upserts?.length) {
74
77
  await ctx.database.upsert('blacklist_users', data.upserts);
75
- if (data.deletes?.length)
78
+ hasNewEntries = true; // 有新增或更新,标记为 true
79
+ }
80
+ // 删除操作不触发扫描
81
+ if (data.deletes?.length) {
76
82
  await ctx.database.remove('blacklist_users', { user_id: data.deletes });
83
+ }
77
84
  }
78
85
  await ctx.database.upsert('blacklist_meta', [{ key: 'sync_revision', value: newRevision }]);
79
86
  logger.info('✅ 同步完成');
87
+ return hasNewEntries;
80
88
  }
81
89
  catch (error) {
82
90
  logger.warn(`❌ 同步失败: ${error.message || error}`);
91
+ return false;
83
92
  }
84
93
  }
85
94
  // --- 3. 队列入队 ---
@@ -190,8 +199,14 @@ async function checkAndHandleUser(ctx, config, session, user_id) {
190
199
  .replace('{userId}', user_id)
191
200
  .replace('{reason}', reason)
192
201
  .replace('{guild}', session.guildId);
193
- // 引用回复
194
- await session.send(session.messageId ? (0, koishi_1.h)('quote', { id: session.messageId }) + msg : msg);
202
+ // 发送消息
203
+ try {
204
+ // 扫描时 session 是伪造的,没有 messageId,直接 send 即可
205
+ await session.send(session.messageId ? (0, koishi_1.h)('quote', { id: session.messageId }) + msg : msg);
206
+ }
207
+ catch (e) {
208
+ logger.warn(`[群: ${session.guildId}] 发送通知失败: ${e}`);
209
+ }
195
210
  }
196
211
  // 执行踢出
197
212
  let kicked = false;
@@ -208,7 +223,10 @@ async function checkAndHandleUser(ctx, config, session, user_id) {
208
223
  await (0, exports.sleep)(config.retryDelay);
209
224
  else {
210
225
  const failMsg = config.kickFailMessage.replace('{user}', displayName).replace('{reason}', String(e));
211
- await session.send(failMsg);
226
+ try {
227
+ await session.send(failMsg);
228
+ }
229
+ catch { }
212
230
  }
213
231
  }
214
232
  }
@@ -262,7 +280,8 @@ guildId) {
262
280
  const fakeSession = bot.session({
263
281
  type: 'message',
264
282
  guildId,
265
- user: { id: bot.selfId }, // 模拟机器人自己
283
+ channelId: guildId,
284
+ user: { id: bot.selfId },
266
285
  });
267
286
  let handled = 0;
268
287
  const BATCH_SIZE = 5;
@@ -284,3 +303,26 @@ guildId) {
284
303
  return { handled: 0, total: 0, error: String(error) };
285
304
  }
286
305
  }
306
+ async function scanAllGuilds(ctx, config) {
307
+ logger.info('🚀 检测到黑名单更新,触发自动全局扫描...');
308
+ let totalHandled = 0;
309
+ let processedGuilds = 0;
310
+ for (const bot of ctx.bots) {
311
+ try {
312
+ const guilds = await bot.getGuildList();
313
+ for (const guild of guilds.data) {
314
+ // 调用现有的单群扫描逻辑
315
+ const result = await scanGuild(ctx, config, bot, guild.id);
316
+ if (result.handled > 0) {
317
+ logger.info(`[自动扫描] 群 ${guild.id}: 处理 ${result.handled} 人`);
318
+ totalHandled += result.handled;
319
+ }
320
+ processedGuilds++;
321
+ }
322
+ }
323
+ catch (e) {
324
+ logger.warn(`Bot ${bot.selfId} 自动扫描出错: ${e}`);
325
+ }
326
+ }
327
+ logger.info(`✅ 自动扫描完成。扫描群组: ${processedGuilds}, 处理人数: ${totalHandled}`);
328
+ }
package/lib/index.d.ts CHANGED
@@ -2,6 +2,6 @@ import { Context, Schema } from 'koishi';
2
2
  import { PluginConfig } from './types';
3
3
  export declare const name = "blacklist-online";
4
4
  export declare const inject: string[];
5
- export declare const usage = "\n## \u529F\u80FD\u8BF4\u660E\n\u4E00\u4E2A\u5F3A\u5927\u7684\u3001\u57FA\u4E8E\u6570\u636E\u5E93\u7684\u7FA4\u7EC4\u9ED1\u540D\u5355\u7BA1\u7406\u63D2\u4EF6\u3002\n- **\u53D7\u4FDD\u62A4\u540D\u5355**: \u914D\u7F6E\u4E2D\u7684\u7528\u6237\u65E0\u6CD5\u88AB\u62C9\u9ED1\u3002\n- **\u5206\u7FA4\u7BA1\u7406**: \u53EF\u4E3A\u6BCF\u4E2A\u7FA4\u72EC\u7ACB\u8BBE\u7F6E\u9ED1\u540D\u5355\u5904\u7406\u6A21\u5F0F\u3002\n- **\u5165\u7FA4\u626B\u63CF**: \u53EF\u914D\u7F6E\u673A\u5668\u4EBA\u5728\u52A0\u5165\u65B0\u7FA4\u7EC4\u65F6\uFF0C\u81EA\u52A8\u626B\u63CF\u7FA4\u5185\u73B0\u6709\u6210\u5458\u3002\n- **\u81EA\u52A8\u62D2\u7EDD**: \u81EA\u52A8\u62D2\u7EDD\u6570\u636E\u5E93\u9ED1\u540D\u5355\u7528\u6237\u7684\u52A0\u7FA4\u7533\u8BF7\u3002\n- **\u624B\u52A8\u626B\u63CF**: \u63D0\u4F9B\u6307\u4EE4\u624B\u52A8\u626B\u63CF\u5F53\u524D\u6216\u5168\u90E8\u7FA4\u7EC4\u3002\n- **\u6743\u9650\u63A7\u5236**: \u6240\u6709\u6307\u4EE4\u5747\u6709\u6743\u9650\u7B49\u7EA7\u63A7\u5236\u3002\n";
5
+ export declare const usage = "\n## \u529F\u80FD\u8BF4\u660E\n\u4E00\u4E2A\u5F3A\u5927\u7684\u3001\u57FA\u4E8E\u6570\u636E\u5E93\u7684\u7FA4\u7EC4\u9ED1\u540D\u5355\u7BA1\u7406\u63D2\u4EF6\u3002\n\n- **\u4E91\u7AEF\u540C\u6B65 & \u81EA\u52A8\u5DE1\u822A**: \u5B9E\u65F6\u540C\u6B65\u4E91\u7AEF\u9ED1\u540D\u5355\u3002**\u5F53\u68C0\u6D4B\u5230\u65B0\u589E\u9ED1\u540D\u5355\u7528\u6237\u65F6\uFF0C\u4F1A\u81EA\u52A8\u89E6\u53D1\u5168\u5C40\u626B\u63CF**\uFF0C\u53CA\u65F6\u6E05\u7406\u6240\u6709\u7FA4\u5185\u7684\u6F5C\u5728\u5A01\u80C1\u3002\n- **\u516C\u5F00\u5904\u5211**: \u65E0\u8BBA\u662F\u81EA\u52A8\u626B\u63CF\u8FD8\u662F\u624B\u52A8\u626B\u63CF\uFF0C\u53D1\u73B0\u9ED1\u540D\u5355\u7528\u6237\u65F6\u5747\u4F1A\u5728\u7FA4\u5185\u53D1\u9001\u5305\u542B\u539F\u56E0\u7684\u901A\u544A\uFF08\u53D6\u51B3\u4E8E\u7FA4\u6A21\u5F0F\u8BBE\u7F6E\uFF09\u3002\n- **\u53D7\u4FDD\u62A4\u540D\u5355**: \u914D\u7F6E\u4E2D\u7684\u7528\u6237\uFF08\u5982\u7FA4\u4E3B\u3001\u7279\u5B9A\u7BA1\u7406\u5458\uFF09\u62E5\u6709\u8C41\u514D\u6743\uFF0C\u65E0\u6CD5\u88AB\u62C9\u9ED1\u3002\n- **\u5206\u7FA4\u7BA1\u7406**: \u53EF\u4E3A\u6BCF\u4E2A\u7FA4\u72EC\u7ACB\u8BBE\u7F6E\u5904\u7406\u6A21\u5F0F\uFF08\u4EC5\u901A\u77E5 / \u4EC5\u8E22\u51FA / \u901A\u77E5\u5E76\u8E22\u51FA / \u5173\u95ED\uFF09\u3002\n- **\u5165\u7FA4\u68C0\u6D4B**: \u65B0\u6210\u5458\u8FDB\u7FA4\u6216\u7533\u8BF7\u52A0\u7FA4\u65F6\uFF0C\u81EA\u52A8\u68C0\u6D4B\u5E76\u62E6\u622A\u9ED1\u540D\u5355\u7528\u6237\u3002\n- **\u624B\u52A8\u626B\u63CF**: \u63D0\u4F9B\u6307\u4EE4\u624B\u52A8\u626B\u63CF\u5F53\u524D\u6216\u5168\u90E8\u7FA4\u7EC4\u3002\n- **\u6743\u9650\u63A7\u5236**: \u4E25\u683C\u7684\u6307\u4EE4\u6743\u9650\u5206\u7EA7\u7BA1\u7406\u3002\n";
6
6
  export declare const Config: Schema<PluginConfig>;
7
7
  export declare function apply(ctx: Context, config: PluginConfig): void;
package/lib/index.js CHANGED
@@ -3,19 +3,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Config = exports.usage = exports.inject = exports.name = void 0;
4
4
  exports.apply = apply;
5
5
  const koishi_1 = require("koishi");
6
- const node_crypto_1 = require("node:crypto"); // 【修复】使用原生 crypto
6
+ const node_crypto_1 = require("node:crypto");
7
7
  const core_1 = require("./core");
8
8
  exports.name = 'blacklist-online';
9
9
  exports.inject = ['database', 'http'];
10
10
  exports.usage = `
11
11
  ## 功能说明
12
12
  一个强大的、基于数据库的群组黑名单管理插件。
13
- - **受保护名单**: 配置中的用户无法被拉黑。
14
- - **分群管理**: 可为每个群独立设置黑名单处理模式。
15
- - **入群扫描**: 可配置机器人在加入新群组时,自动扫描群内现有成员。
16
- - **自动拒绝**: 自动拒绝数据库黑名单用户的加群申请。
13
+
14
+ - **云端同步 & 自动巡航**: 实时同步云端黑名单。**当检测到新增黑名单用户时,会自动触发全局扫描**,及时清理所有群内的潜在威胁。
15
+ - **公开处刑**: 无论是自动扫描还是手动扫描,发现黑名单用户时均会在群内发送包含原因的通告(取决于群模式设置)。
16
+ - **受保护名单**: 配置中的用户(如群主、特定管理员)拥有豁免权,无法被拉黑。
17
+ - **分群管理**: 可为每个群独立设置处理模式(仅通知 / 仅踢出 / 通知并踢出 / 关闭)。
18
+ - **入群检测**: 新成员进群或申请加群时,自动检测并拦截黑名单用户。
17
19
  - **手动扫描**: 提供指令手动扫描当前或全部群组。
18
- - **权限控制**: 所有指令均有权限等级控制。
20
+ - **权限控制**: 严格的指令权限分级管理。
19
21
  `;
20
22
  // --- Schema 定义 ---
21
23
  exports.Config = koishi_1.Schema.intersect([
@@ -79,13 +81,23 @@ function apply(ctx, config) {
79
81
  logger.info(`📱 当前实例 UUID: ${entries[0].value}`);
80
82
  }
81
83
  // 启动时立即同步一次
82
- (0, core_1.syncBlacklist)(ctx, config);
84
+ const hasUpdates = await (0, core_1.syncBlacklist)(ctx, config);
83
85
  // 启动时处理积压队列
84
86
  (0, core_1.processOfflineQueue)(ctx, config);
87
+ // 如果启动同步有更新,触发全群扫描
88
+ if (hasUpdates) {
89
+ (0, core_1.scanAllGuilds)(ctx, config);
90
+ }
85
91
  });
86
92
  // 3. 定时任务
87
- ctx.setInterval(() => (0, core_1.syncBlacklist)(ctx, config), 5 * koishi_1.Time.minute); // 每5分同步
88
- ctx.setInterval(() => (0, core_1.processOfflineQueue)(ctx, config), koishi_1.Time.minute); // 每1分处理队列
93
+ ctx.setInterval(async () => {
94
+ // 每次定时同步,如果有更新,就触发扫描
95
+ const hasUpdates = await (0, core_1.syncBlacklist)(ctx, config);
96
+ if (hasUpdates) {
97
+ await (0, core_1.scanAllGuilds)(ctx, config);
98
+ }
99
+ }, 5 * koishi_1.Time.minute); // 每5分同步
100
+ ctx.setInterval(() => (0, core_1.processOfflineQueue)(ctx, config), koishi_1.Time.minute); // 每分钟同步
89
101
  // 4. 事件监听
90
102
  // 监听加群申请 (自动拒绝)
91
103
  ctx.on('guild-member-request', async (session) => {
@@ -131,12 +143,12 @@ function apply(ctx, config) {
131
143
  return '❌ 该用户在本地白名单中,无法拉黑。';
132
144
  const requestId = (0, node_crypto_1.randomUUID)();
133
145
  const payload = {
134
- requestId: requestId,
146
+ request_id: requestId,
135
147
  type: 'ADD',
136
- applicantId: session.userId,
137
- targetUserId: userId,
148
+ applicant_id: session.userId,
149
+ target_user_id: userId,
138
150
  reason,
139
- guildId: session.guildId,
151
+ guild_id: session.guildId,
140
152
  timestamp: Date.now()
141
153
  };
142
154
  try {
@@ -161,10 +173,10 @@ function apply(ctx, config) {
161
173
  const userId = (0, core_1.parseUserId)(user);
162
174
  const requestId = (0, node_crypto_1.randomUUID)();
163
175
  const payload = {
164
- requestId: requestId,
176
+ request_id: requestId,
165
177
  type: 'REMOVE',
166
- applicantId: session.userId,
167
- targetUserId: userId,
178
+ applicant_id: session.userId,
179
+ target_user_id: userId,
168
180
  reason,
169
181
  timestamp: Date.now()
170
182
  };
@@ -185,9 +197,9 @@ function apply(ctx, config) {
185
197
  if (!uuid)
186
198
  return '请输入要撤回的申请 UUID。';
187
199
  const payload = {
188
- requestId: (0, node_crypto_1.randomUUID)(),
189
- targetRequestId: uuid,
190
- applicantId: session?.userId,
200
+ request_id: (0, node_crypto_1.randomUUID)(),
201
+ target_request_id: uuid,
202
+ applicant_id: session?.userId,
191
203
  timestamp: Date.now()
192
204
  };
193
205
  try {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-blacklist-online",
3
3
  "description": "自用插件",
4
- "version": "0.1.3",
4
+ "version": "0.1.5",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [