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 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.
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import { HTTP } from 'koishi';
2
+ export declare function downloadImage(http: HTTP, src: string): Promise<Buffer>;
3
+ export declare function decodeQr(buffer: Buffer): Promise<string | null>;
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
- "version": "0.0.0",
4
- "description": "Placeholder for Trusted Publisher setup. Real plugin ships as 1.0.0 via OIDC.",
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
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-ban-qrcode?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-ban-qrcode)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](./LICENSE)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.6-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
6
+ [![Koishi](https://img.shields.io/badge/Koishi-4-026d4d?style=flat-square)](https://koishi.chat/)
7
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](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 = {}