koishi-plugin-ban-qrcode 0.0.0 → 1.0.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/LICENSE +21 -0
- package/lib/detect.d.ts +31 -0
- package/lib/detect.js +106 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +71 -0
- package/lib/qrcode.d.ts +3 -0
- package/lib/qrcode.js +27 -0
- package/package.json +54 -3
- package/readme.md +262 -0
- package/index.js +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 koishi-plugin-ban-qrcode contributors
|
|
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/detect.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface ImageNode {
|
|
2
|
+
type: string;
|
|
3
|
+
attrs?: Record<string, unknown>;
|
|
4
|
+
children?: ImageNode[];
|
|
5
|
+
}
|
|
6
|
+
export type RoleLike = string | {
|
|
7
|
+
id?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
};
|
|
10
|
+
export interface ModerateInput {
|
|
11
|
+
guildId?: string;
|
|
12
|
+
isDirect?: boolean;
|
|
13
|
+
userId?: string;
|
|
14
|
+
selfId?: string;
|
|
15
|
+
ignoreUsers: string[];
|
|
16
|
+
guilds: string[];
|
|
17
|
+
roles?: RoleLike[];
|
|
18
|
+
skipAdmins: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface QrHit {
|
|
21
|
+
src: string;
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function roleLabels(roles?: RoleLike[]): string[];
|
|
25
|
+
export declare function isAdminRole(roles?: RoleLike[]): boolean;
|
|
26
|
+
export declare function collectImageSrcs(nodes: readonly ImageNode[]): string[];
|
|
27
|
+
export declare function shouldModerate(input: ModerateInput): boolean;
|
|
28
|
+
export declare function muteDurationMs(seconds: number): number;
|
|
29
|
+
export declare function parseDataUrl(src: string): Buffer | null;
|
|
30
|
+
export declare function defaultNotify(muteSeconds: number): string;
|
|
31
|
+
export declare function findQrInImages(srcs: readonly string[], download: (src: string) => Promise<Buffer>, decode: (buffer: Buffer) => Promise<string | null>): Promise<QrHit | null>;
|
package/lib/detect.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.roleLabels = roleLabels;
|
|
4
|
+
exports.isAdminRole = isAdminRole;
|
|
5
|
+
exports.collectImageSrcs = collectImageSrcs;
|
|
6
|
+
exports.shouldModerate = shouldModerate;
|
|
7
|
+
exports.muteDurationMs = muteDurationMs;
|
|
8
|
+
exports.parseDataUrl = parseDataUrl;
|
|
9
|
+
exports.defaultNotify = defaultNotify;
|
|
10
|
+
exports.findQrInImages = findQrInImages;
|
|
11
|
+
const ADMIN_ROLES = new Set([
|
|
12
|
+
'owner',
|
|
13
|
+
'admin',
|
|
14
|
+
'administrator',
|
|
15
|
+
'ADMINISTRATOR',
|
|
16
|
+
]);
|
|
17
|
+
function roleLabels(roles) {
|
|
18
|
+
if (!roles?.length)
|
|
19
|
+
return [];
|
|
20
|
+
const labels = [];
|
|
21
|
+
for (const role of roles) {
|
|
22
|
+
if (typeof role === 'string') {
|
|
23
|
+
if (role)
|
|
24
|
+
labels.push(role);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (role.name)
|
|
28
|
+
labels.push(role.name);
|
|
29
|
+
if (role.id && role.id !== role.name)
|
|
30
|
+
labels.push(role.id);
|
|
31
|
+
}
|
|
32
|
+
return labels;
|
|
33
|
+
}
|
|
34
|
+
function isAdminRole(roles) {
|
|
35
|
+
return roleLabels(roles).some(role => ADMIN_ROLES.has(role) || role.toLowerCase() === 'owner');
|
|
36
|
+
}
|
|
37
|
+
function collectImageSrcs(nodes) {
|
|
38
|
+
const srcs = [];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const visit = (items) => {
|
|
41
|
+
for (const node of items) {
|
|
42
|
+
if (node.type === 'img' || node.type === 'image') {
|
|
43
|
+
const src = pickSrc(node.attrs);
|
|
44
|
+
if (src && !seen.has(src)) {
|
|
45
|
+
seen.add(src);
|
|
46
|
+
srcs.push(src);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (node.children?.length)
|
|
50
|
+
visit(node.children);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
visit(nodes);
|
|
54
|
+
return srcs;
|
|
55
|
+
}
|
|
56
|
+
function pickSrc(attrs) {
|
|
57
|
+
const src = attrs?.src ?? attrs?.url;
|
|
58
|
+
return typeof src === 'string' && src.trim() ? src.trim() : undefined;
|
|
59
|
+
}
|
|
60
|
+
function shouldModerate(input) {
|
|
61
|
+
if (input.isDirect)
|
|
62
|
+
return false;
|
|
63
|
+
if (!input.guildId || !input.userId)
|
|
64
|
+
return false;
|
|
65
|
+
if (input.guilds.length && !input.guilds.includes(input.guildId))
|
|
66
|
+
return false;
|
|
67
|
+
if (input.userId === input.selfId)
|
|
68
|
+
return false;
|
|
69
|
+
if (input.ignoreUsers.includes(input.userId))
|
|
70
|
+
return false;
|
|
71
|
+
if (input.skipAdmins && isAdminRole(input.roles))
|
|
72
|
+
return false;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
function muteDurationMs(seconds) {
|
|
76
|
+
return Math.max(0, Math.floor(seconds)) * 1000;
|
|
77
|
+
}
|
|
78
|
+
function parseDataUrl(src) {
|
|
79
|
+
const match = /^data:[^;,]+;base64,([\s\S]+)$/i.exec(src);
|
|
80
|
+
if (!match)
|
|
81
|
+
return null;
|
|
82
|
+
try {
|
|
83
|
+
return Buffer.from(match[1], 'base64');
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function defaultNotify(muteSeconds) {
|
|
90
|
+
if (muteSeconds <= 0)
|
|
91
|
+
return '检测到二维码,已撤回。';
|
|
92
|
+
return `检测到二维码,已撤回并禁言 ${muteSeconds} 秒。`;
|
|
93
|
+
}
|
|
94
|
+
async function findQrInImages(srcs, download, decode) {
|
|
95
|
+
for (const src of srcs) {
|
|
96
|
+
try {
|
|
97
|
+
const text = await decode(await download(src));
|
|
98
|
+
if (text !== null)
|
|
99
|
+
return { src, text };
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// skip a single failed image
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
export declare const name = "ban-qrcode";
|
|
3
|
+
export declare const inject: string[];
|
|
4
|
+
export declare const usage = "\n\u7FA4\u5185\u68C0\u6D4B\u5230\u56FE\u7247\u4E8C\u7EF4\u7801\u65F6\u81EA\u52A8\u64A4\u56DE\uFF0C\u5E76\u7981\u8A00\u53D1\u9001\u8005\uFF08\u9ED8\u8BA4 60 \u79D2\uFF09\u3002\u7528\u4E8E\u62E6\u622A\u5E7F\u544A\u7801\u3002\n\n\u673A\u5668\u4EBA\u9700\u8981\u64A4\u56DE\u6D88\u606F\u548C\u7981\u8A00\u6210\u5458\u7684\u6743\u9650\u3002\u7FA4\u4E3B / \u7BA1\u7406\u5458\u9ED8\u8BA4\u8DF3\u8FC7\u3002\n";
|
|
5
|
+
export interface Config {
|
|
6
|
+
muteSeconds: number;
|
|
7
|
+
recall: boolean;
|
|
8
|
+
notify: boolean;
|
|
9
|
+
notifyText: string;
|
|
10
|
+
skipAdmins: boolean;
|
|
11
|
+
ignoreUsers: string[];
|
|
12
|
+
guilds: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare const Config: Schema<Config>;
|
|
15
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Config = exports.usage = exports.inject = exports.name = void 0;
|
|
4
|
+
exports.apply = apply;
|
|
5
|
+
const koishi_1 = require("koishi");
|
|
6
|
+
const detect_1 = require("./detect");
|
|
7
|
+
const qrcode_1 = require("./qrcode");
|
|
8
|
+
exports.name = 'ban-qrcode';
|
|
9
|
+
exports.inject = ['http'];
|
|
10
|
+
exports.usage = `
|
|
11
|
+
群内检测到图片二维码时自动撤回,并禁言发送者(默认 60 秒)。用于拦截广告码。
|
|
12
|
+
|
|
13
|
+
机器人需要撤回消息和禁言成员的权限。群主 / 管理员默认跳过。
|
|
14
|
+
`;
|
|
15
|
+
exports.Config = koishi_1.Schema.object({
|
|
16
|
+
muteSeconds: koishi_1.Schema.number().min(0).default(60).description('禁言秒数。0 表示只撤回不禁言。'),
|
|
17
|
+
recall: koishi_1.Schema.boolean().default(true).description('撤回含二维码的消息。'),
|
|
18
|
+
notify: koishi_1.Schema.boolean().default(true).description('处理后在群内发送提示。'),
|
|
19
|
+
notifyText: koishi_1.Schema.string().default('').description('自定义提示。留空则按禁言秒数生成。'),
|
|
20
|
+
skipAdmins: koishi_1.Schema.boolean().default(true).description('跳过群主和管理员。'),
|
|
21
|
+
ignoreUsers: koishi_1.Schema.array(koishi_1.Schema.string()).role('table').default([]).description('忽略的用户 ID。'),
|
|
22
|
+
guilds: koishi_1.Schema.array(koishi_1.Schema.string()).role('table').default([]).description('只在这些群生效。留空表示全部群。'),
|
|
23
|
+
});
|
|
24
|
+
function apply(ctx, config) {
|
|
25
|
+
const logger = ctx.logger('ban-qrcode');
|
|
26
|
+
ctx.on('message', async (session) => {
|
|
27
|
+
if (!(0, detect_1.shouldModerate)({
|
|
28
|
+
guildId: session.guildId,
|
|
29
|
+
isDirect: session.isDirect,
|
|
30
|
+
userId: session.userId,
|
|
31
|
+
selfId: session.selfId,
|
|
32
|
+
ignoreUsers: config.ignoreUsers,
|
|
33
|
+
guilds: config.guilds,
|
|
34
|
+
roles: session.event.member?.roles,
|
|
35
|
+
skipAdmins: config.skipAdmins,
|
|
36
|
+
}))
|
|
37
|
+
return;
|
|
38
|
+
const srcs = (0, detect_1.collectImageSrcs)(session.elements ?? []);
|
|
39
|
+
if (!srcs.length)
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
const hit = await (0, detect_1.findQrInImages)(srcs, src => (0, qrcode_1.downloadImage)(ctx.http, src), qrcode_1.decodeQr);
|
|
43
|
+
if (!hit)
|
|
44
|
+
return;
|
|
45
|
+
logger.info('qrcode from %s in %s', session.userId, session.guildId);
|
|
46
|
+
if (config.recall && session.messageId && session.channelId) {
|
|
47
|
+
try {
|
|
48
|
+
await session.bot.deleteMessage(session.channelId, session.messageId);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
logger.warn(error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const duration = (0, detect_1.muteDurationMs)(config.muteSeconds);
|
|
55
|
+
if (duration > 0 && session.guildId && session.userId) {
|
|
56
|
+
try {
|
|
57
|
+
await session.bot.muteGuildMember(session.guildId, session.userId, duration);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
logger.warn(error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (config.notify) {
|
|
64
|
+
await session.send(config.notifyText || (0, detect_1.defaultNotify)(config.muteSeconds));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
logger.warn(error);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
package/lib/qrcode.d.ts
ADDED
package/lib/qrcode.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.downloadImage = downloadImage;
|
|
7
|
+
exports.decodeQr = decodeQr;
|
|
8
|
+
const jimp_1 = __importDefault(require("jimp"));
|
|
9
|
+
const jsqr_1 = __importDefault(require("jsqr"));
|
|
10
|
+
const detect_1 = require("./detect");
|
|
11
|
+
async function downloadImage(http, src) {
|
|
12
|
+
if (src.startsWith('data:')) {
|
|
13
|
+
const data = (0, detect_1.parseDataUrl)(src);
|
|
14
|
+
if (!data)
|
|
15
|
+
throw new Error('invalid data url');
|
|
16
|
+
return data;
|
|
17
|
+
}
|
|
18
|
+
return Buffer.from(await http.get(src, { responseType: 'arraybuffer' }));
|
|
19
|
+
}
|
|
20
|
+
async function decodeQr(buffer) {
|
|
21
|
+
const image = await jimp_1.default.read(buffer);
|
|
22
|
+
const { data, width, height } = image.bitmap;
|
|
23
|
+
const result = (0, jsqr_1.default)(new Uint8ClampedArray(data), width, height, {
|
|
24
|
+
inversionAttempts: 'attemptBoth',
|
|
25
|
+
});
|
|
26
|
+
return result?.data ?? null;
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-ban-qrcode",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"description": "检测到群内二维码后自动撤回并禁言,用于防广告",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib"
|
|
9
|
+
],
|
|
5
10
|
"license": "MIT",
|
|
6
11
|
"author": "K4F7",
|
|
7
12
|
"repository": {
|
|
@@ -9,7 +14,53 @@
|
|
|
9
14
|
"url": "git+https://github.com/K4F7/koishi-plugin-ban-qrcode.git"
|
|
10
15
|
},
|
|
11
16
|
"homepage": "https://github.com/K4F7/koishi-plugin-ban-qrcode",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/K4F7/koishi-plugin-ban-qrcode/issues"
|
|
19
|
+
},
|
|
12
20
|
"publishConfig": {
|
|
13
21
|
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -b",
|
|
25
|
+
"test": "tsx --test tests/*.spec.ts"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"chatbot",
|
|
29
|
+
"koishi",
|
|
30
|
+
"plugin",
|
|
31
|
+
"qrcode",
|
|
32
|
+
"ban",
|
|
33
|
+
"mute",
|
|
34
|
+
"recall",
|
|
35
|
+
"二维码",
|
|
36
|
+
"禁言",
|
|
37
|
+
"撤回",
|
|
38
|
+
"广告"
|
|
39
|
+
],
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"koishi": "^4.18.7"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"jimp": "^0.22.12",
|
|
45
|
+
"jsqr": "^1.4.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.7.5",
|
|
49
|
+
"@types/qrcode": "^1.5.5",
|
|
50
|
+
"koishi": "^4.18.7",
|
|
51
|
+
"qrcode": "^1.5.4",
|
|
52
|
+
"tsx": "^4.16.2",
|
|
53
|
+
"typescript": "^5.6.2"
|
|
54
|
+
},
|
|
55
|
+
"koishi": {
|
|
56
|
+
"description": {
|
|
57
|
+
"zh": "检测到群内二维码后自动撤回并禁言,用于防广告",
|
|
58
|
+
"en": "Recall group QR-code images and mute the sender to stop ads"
|
|
59
|
+
},
|
|
60
|
+
"service": {
|
|
61
|
+
"required": [
|
|
62
|
+
"http"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
14
65
|
}
|
|
15
|
-
}
|
|
66
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# koishi-plugin-ban-qrcode
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/koishi-plugin-ban-qrcode)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://koishi.chat/)
|
|
7
|
+
[](https://github.com/K4F7/koishi-plugin-ban-qrcode/pulls)
|
|
8
|
+
|
|
9
|
+
群内检测到图片二维码时自动撤回,并禁言发送者(默认 1 分钟)。用于群管理、拦截广告码。
|
|
10
|
+
|
|
11
|
+
Recall group QR-code images and mute the sender for one minute by default, to stop advertising codes.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- 🔍 **扫图识码**:下载消息里的图片,解码是否含二维码
|
|
16
|
+
- 🗑️ **自动撤回**:命中后立刻撤回原消息
|
|
17
|
+
- 🔇 **自动禁言**:默认禁言 60 秒,秒数可配
|
|
18
|
+
- 🛡️ **白名单**:默认跳过群主 / 管理员,也可按用户 ID、群 ID 过滤
|
|
19
|
+
- 💬 **群内提示**:处理后可发一句说明,不回显码内内容
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
在 [Koishi](https://koishi.chat/) 控制台搜索 `ban-qrcode` 安装,或:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install koishi-plugin-ban-qrcode
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
启用后,群成员发带二维码的图片会被撤回并禁言 1 分钟。机器人需要**撤回消息**和**禁言成员**的权限。
|
|
30
|
+
|
|
31
|
+
依赖 Koishi 的 `http` 服务(`inject: ['http']`)。
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
### Prerequisites
|
|
36
|
+
|
|
37
|
+
- [Koishi](https://koishi.chat/) `^4.18.7`
|
|
38
|
+
- Node.js 18 或更高(发布流水线使用 Node 24)
|
|
39
|
+
- 适配器实现 `deleteMessage` 和 `muteGuildMember`(QQ / OneBot 等群聊平台)
|
|
40
|
+
|
|
41
|
+
### Package Manager
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# npm
|
|
45
|
+
npm install koishi-plugin-ban-qrcode
|
|
46
|
+
|
|
47
|
+
# pnpm
|
|
48
|
+
pnpm add koishi-plugin-ban-qrcode
|
|
49
|
+
|
|
50
|
+
# yarn
|
|
51
|
+
yarn add koishi-plugin-ban-qrcode
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
安装后在控制台启用插件 `ban-qrcode`。
|
|
55
|
+
|
|
56
|
+
### From Source
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git clone https://github.com/K4F7/koishi-plugin-ban-qrcode.git
|
|
60
|
+
cd koishi-plugin-ban-qrcode
|
|
61
|
+
npm install
|
|
62
|
+
npm test
|
|
63
|
+
npm run build
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`tsc -b` 输出到 `lib/`。GitHub Actions 发布也走这一套。
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
插件没有指令。启用后监听群消息:
|
|
71
|
+
|
|
72
|
+
1. 只处理群聊,忽略私聊和机器人自己的消息
|
|
73
|
+
2. 收集消息及引用里的 `img` / `image`
|
|
74
|
+
3. 下载图片并解码二维码
|
|
75
|
+
4. 命中则撤回、禁言,可选发送提示
|
|
76
|
+
|
|
77
|
+
不会把解码出的文本发回群里,避免扩散广告链接。
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
| 项 | 类型 | 默认 | 说明 |
|
|
82
|
+
| --- | --- | --- | --- |
|
|
83
|
+
| `muteSeconds` | `number` | `60` | 禁言秒数。`0` 表示只撤回不禁言 |
|
|
84
|
+
| `recall` | `boolean` | `true` | 撤回含二维码的消息 |
|
|
85
|
+
| `notify` | `boolean` | `true` | 处理后在群内提示 |
|
|
86
|
+
| `notifyText` | `string` | `''` | 自定义提示。留空则按秒数生成 |
|
|
87
|
+
| `skipAdmins` | `boolean` | `true` | 跳过群主 / 管理员 |
|
|
88
|
+
| `ignoreUsers` | `string[]` | `[]` | 忽略的用户 ID |
|
|
89
|
+
| `guilds` | `string[]` | `[]` | 只在这些群生效。空数组表示全部群 |
|
|
90
|
+
|
|
91
|
+
默认提示:`检测到二维码,已撤回并禁言 60 秒。`
|
|
92
|
+
|
|
93
|
+
## API Reference
|
|
94
|
+
|
|
95
|
+
插件入口导出 `name`、`inject`、`Config`、`apply`。判定与扫码编排在 `src/detect.ts`,下载和解码在 `src/qrcode.ts`。
|
|
96
|
+
|
|
97
|
+
### `collectImageSrcs(nodes)`
|
|
98
|
+
|
|
99
|
+
收集 `img` / `image` 的 `src` 或 `url`,去重并保留顺序,会走进嵌套节点(如引用)。
|
|
100
|
+
|
|
101
|
+
### `shouldModerate(input)`
|
|
102
|
+
|
|
103
|
+
群聊、非自己、未忽略、在生效群内,且(可选)不是管理员时返回 `true`。
|
|
104
|
+
|
|
105
|
+
### `findQrInImages(srcs, download, decode)`
|
|
106
|
+
|
|
107
|
+
按顺序下载并解码,返回第一张命中的 `{ src, text }`。单张失败会跳过。
|
|
108
|
+
|
|
109
|
+
### `decodeQr(buffer)`
|
|
110
|
+
|
|
111
|
+
用 Jimp 读图、jsQR 解码。没有码时返回 `null`。
|
|
112
|
+
|
|
113
|
+
## Architecture
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
src/
|
|
117
|
+
├── index.ts # 插件入口:监听群消息,撤回 / 禁言 / 提示
|
|
118
|
+
├── detect.ts # 收集图片、是否处理、编排扫码
|
|
119
|
+
└── qrcode.ts # 下载图片、解码二维码
|
|
120
|
+
tests/
|
|
121
|
+
└── detect.spec.ts
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
数据流:群消息 → 收集图片 URL → 下载 → jsQR → `deleteMessage` + `muteGuildMember`。
|
|
125
|
+
|
|
126
|
+
禁言时长按 Satori 约定传**毫秒**(60 秒 = `60000`)。
|
|
127
|
+
|
|
128
|
+
## 工作区开发
|
|
129
|
+
|
|
130
|
+
按 [Koishi 工作区开发](https://koishi.chat/zh-CN/guide/develop/workspace.html):本仓库是**独立插件仓**,官方 Yakumo 工作区只用于本地调试,不要把整个工作区提交进来。
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm init koishi@latest
|
|
134
|
+
cd <app>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
把本仓接到工作区的 `external/ban-qrcode`(Windows 可用目录联接):
|
|
138
|
+
|
|
139
|
+
```bat
|
|
140
|
+
mklink /J external\ban-qrcode D:\path\to\koishi-plugin-ban-qrcode
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
或:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm run clone K4F7/koishi-plugin-ban-qrcode
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
与 `jandan` 等其它插件一起放在同一个 Yakumo 工作区即可联合调试:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
koishi-workspace/
|
|
153
|
+
├── external/
|
|
154
|
+
│ ├── jandan # junction → 独立仓
|
|
155
|
+
│ └── ban-qrcode # junction → 独立仓
|
|
156
|
+
├── koishi.yml
|
|
157
|
+
└── package.json
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
然后在工作区根目录:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
yarn dev
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
插件会以 `ban-qrcode` 的名字热重载。本仓自己构建、测试:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npm install
|
|
170
|
+
npm test
|
|
171
|
+
npm run build
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## FAQ
|
|
175
|
+
|
|
176
|
+
<details>
|
|
177
|
+
<summary><strong>为什么没有撤回或禁言?</strong></summary>
|
|
178
|
+
|
|
179
|
+
检查机器人是否有管理员权限,以及适配器是否实现 `deleteMessage` / `muteGuildMember`。权限不足时插件会打日志并继续后续步骤。
|
|
180
|
+
</details>
|
|
181
|
+
|
|
182
|
+
<details>
|
|
183
|
+
<summary><strong>管理员发群码会被禁言吗?</strong></summary>
|
|
184
|
+
|
|
185
|
+
默认不会。`skipAdmins` 为 `true` 时跳过角色为 `owner` / `admin` 的成员。
|
|
186
|
+
</details>
|
|
187
|
+
|
|
188
|
+
<details>
|
|
189
|
+
<summary><strong>为什么有的二维码没扫到?</strong></summary>
|
|
190
|
+
|
|
191
|
+
图太糊、码只占画面很小一块、或格式 Jimp 读不了(少见 webp)时可能漏检。把更清晰的整图发来更容易命中。
|
|
192
|
+
</details>
|
|
193
|
+
|
|
194
|
+
<details>
|
|
195
|
+
<summary><strong>会公布码里的链接吗?</strong></summary>
|
|
196
|
+
|
|
197
|
+
不会。提示语只说明「检测到二维码」,日志也不写码内文本。
|
|
198
|
+
</details>
|
|
199
|
+
|
|
200
|
+
## Troubleshooting
|
|
201
|
+
|
|
202
|
+
**no bot permission / mute failed**
|
|
203
|
+
|
|
204
|
+
机器人不是管理员,或目标是群主 / 更高权限成员。看 `ban-qrcode` logger。
|
|
205
|
+
|
|
206
|
+
**图片下载失败**
|
|
207
|
+
|
|
208
|
+
图床拒绝、过期或需要登录。插件会跳过这张图,继续扫同一条消息里的其它图。
|
|
209
|
+
|
|
210
|
+
## 发布(OIDC Trusted Publishing)
|
|
211
|
+
|
|
212
|
+
仓库:[`K4F7/koishi-plugin-ban-qrcode`](https://github.com/K4F7/koishi-plugin-ban-qrcode)
|
|
213
|
+
|
|
214
|
+
工作流:[`.github/workflows/publish.yml`](.github/workflows/publish.yml)。推送 `v*` tag 后由 GitHub Actions 用 OIDC 发布,**不需要 `NPM_TOKEN`**。
|
|
215
|
+
|
|
216
|
+
### 你需要在 npm 上完成(无法代做)
|
|
217
|
+
|
|
218
|
+
1. 打开 [npmjs.com](https://www.npmjs.com/) 登录,进入 `koishi-plugin-ban-qrcode` 的包设置(新包可先配 **pending trusted publisher**)
|
|
219
|
+
2. 配置 Trusted Publisher:
|
|
220
|
+
- Provider:GitHub Actions
|
|
221
|
+
- Organization or user:`K4F7`
|
|
222
|
+
- Repository:`koishi-plugin-ban-qrcode`
|
|
223
|
+
- Workflow filename:必须是 `publish.yml`(不要带路径)
|
|
224
|
+
- 允许 `npm publish`
|
|
225
|
+
3. 新包可先配 pending trusted publisher,再打 `v1.0.0`;若 npm 要求先有包,用一次性 granular token 发首版再切 OIDC
|
|
226
|
+
4. 首发成功后建议在包设置里开启「Require 2FA and disallow tokens」
|
|
227
|
+
|
|
228
|
+
### 打正式版
|
|
229
|
+
|
|
230
|
+
确认 Trusted Publisher 已配好、`package.json` 的 `version` 与 tag 一致后:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
git tag v1.0.0
|
|
234
|
+
git push origin v1.0.0
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Actions 会 `npm ci` → `npm test` → `npm run build` → `npm publish --access public`,并自动带 provenance。
|
|
238
|
+
|
|
239
|
+
## Contributing
|
|
240
|
+
|
|
241
|
+
欢迎提 issue 和 PR。
|
|
242
|
+
|
|
243
|
+
1. Fork 仓库,建分支:`git checkout -b feat/my-change`
|
|
244
|
+
2. 改代码,补测试(`tests/detect.spec.ts`)
|
|
245
|
+
3. `npm test` 和 `npm run build` 通过
|
|
246
|
+
4. 开 Pull Request
|
|
247
|
+
|
|
248
|
+
提交信息建议用 [Conventional Commits](https://www.conventionalcommits.org/):`feat:` / `fix:` / `docs:` / `test:` / `chore:`。
|
|
249
|
+
|
|
250
|
+
## Changelog
|
|
251
|
+
|
|
252
|
+
当前版本 **1.0.0**:群内扫二维码、自动撤回、默认禁言 60 秒、管理员白名单。
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
[MIT](./LICENSE) © 2026 koishi-plugin-ban-qrcode contributors
|
|
257
|
+
|
|
258
|
+
## Support
|
|
259
|
+
|
|
260
|
+
- 🐛 Issues:[github.com/K4F7/koishi-plugin-ban-qrcode/issues](https://github.com/K4F7/koishi-plugin-ban-qrcode/issues)
|
|
261
|
+
- 📦 npm:[koishi-plugin-ban-qrcode](https://www.npmjs.com/package/koishi-plugin-ban-qrcode)
|
|
262
|
+
- 📖 Koishi 文档:[koishi.chat](https://koishi.chat/)
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = {}
|