koishi-plugin-ban-qrcode 1.2.1 → 1.3.1
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/lib/detect.js +36 -2
- package/lib/document.d.ts +1 -0
- package/lib/document.js +105 -16
- package/lib/index.js +16 -1
- package/lib/share.d.ts +3 -0
- package/lib/share.js +35 -1
- package/package.json +1 -1
- package/readme.md +3 -3
package/lib/detect.js
CHANGED
|
@@ -51,9 +51,17 @@ const SHARE_TYPES = new Set([
|
|
|
51
51
|
'share',
|
|
52
52
|
'ark',
|
|
53
53
|
'lightapp',
|
|
54
|
+
'miniapp',
|
|
54
55
|
'onebot:json',
|
|
55
56
|
'onebot:xml',
|
|
56
57
|
'onebot:share',
|
|
58
|
+
'onebot:ark',
|
|
59
|
+
'onebot:lightapp',
|
|
60
|
+
'onebot:miniapp',
|
|
61
|
+
'weapp',
|
|
62
|
+
'wxapp',
|
|
63
|
+
'miniprogram',
|
|
64
|
+
'onebot:weapp',
|
|
57
65
|
]);
|
|
58
66
|
const CONTACT_TYPES = new Set([
|
|
59
67
|
'contact',
|
|
@@ -98,8 +106,8 @@ function collectMessageParts(nodes, extra = '') {
|
|
|
98
106
|
if (typeof content === 'string' && content)
|
|
99
107
|
texts.push(content);
|
|
100
108
|
}
|
|
101
|
-
if (SHARE_TYPES.has(node.type)) {
|
|
102
|
-
addShare(
|
|
109
|
+
if (SHARE_TYPES.has(node.type) || looksLikeShareAttrs(node.attrs)) {
|
|
110
|
+
addShare(sharePayloadFromAttrs(node.attrs));
|
|
103
111
|
}
|
|
104
112
|
if (CONTACT_TYPES.has(node.type)) {
|
|
105
113
|
addShare(node.attrs);
|
|
@@ -198,6 +206,32 @@ function unwrapShareData(raw) {
|
|
|
198
206
|
}
|
|
199
207
|
return raw;
|
|
200
208
|
}
|
|
209
|
+
function looksLikeShareAttrs(attrs) {
|
|
210
|
+
if (!attrs)
|
|
211
|
+
return false;
|
|
212
|
+
if (typeof attrs.app === 'string')
|
|
213
|
+
return true;
|
|
214
|
+
if (attrs.meta && (attrs.prompt || attrs.view || attrs.bizsrc))
|
|
215
|
+
return true;
|
|
216
|
+
const wrapped = attrs.data ?? attrs.content ?? attrs.value;
|
|
217
|
+
if (typeof wrapped === 'string' && /"app"\s*:/.test(wrapped))
|
|
218
|
+
return true;
|
|
219
|
+
if (wrapped && typeof wrapped === 'object' && !Array.isArray(wrapped)) {
|
|
220
|
+
return typeof wrapped.app === 'string';
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
function sharePayloadFromAttrs(attrs) {
|
|
225
|
+
if (!attrs)
|
|
226
|
+
return undefined;
|
|
227
|
+
const wrapped = attrs.data ?? attrs.content ?? attrs.value;
|
|
228
|
+
if (wrapped !== undefined && wrapped !== null && wrapped !== '') {
|
|
229
|
+
return unwrapShareData(wrapped);
|
|
230
|
+
}
|
|
231
|
+
if (typeof attrs.app === 'string' || attrs.meta)
|
|
232
|
+
return attrs;
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
201
235
|
function isDownloadableSrc(src) {
|
|
202
236
|
return /^(https?:|data:|file:)/i.test(src);
|
|
203
237
|
}
|
package/lib/document.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export declare function parseTencentDocUrl(url: string): {
|
|
|
16
16
|
pageUrl: string;
|
|
17
17
|
} | null;
|
|
18
18
|
export declare function collectTencentDocUrls(text: string): string[];
|
|
19
|
+
export declare function extractTencentDocUrlsFromShare(payload: string): string[];
|
|
19
20
|
export declare function parseOpendocBody(body: string): DocContent | null;
|
|
20
21
|
export declare function fetchTencentDoc(http: DocHttp, url: string): Promise<DocContent | null>;
|
|
21
22
|
export declare function extractOfficeText(buffer: Buffer, name: string, maxBytes?: number): DocContent | null;
|
package/lib/document.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DEFAULT_MAX_OFFICE_BYTES = void 0;
|
|
4
4
|
exports.parseTencentDocUrl = parseTencentDocUrl;
|
|
5
5
|
exports.collectTencentDocUrls = collectTencentDocUrls;
|
|
6
|
+
exports.extractTencentDocUrlsFromShare = extractTencentDocUrlsFromShare;
|
|
6
7
|
exports.parseOpendocBody = parseOpendocBody;
|
|
7
8
|
exports.fetchTencentDoc = fetchTencentDoc;
|
|
8
9
|
exports.extractOfficeText = extractOfficeText;
|
|
@@ -10,33 +11,81 @@ exports.extractReadableText = extractReadableText;
|
|
|
10
11
|
const node_zlib_1 = require("node:zlib");
|
|
11
12
|
const detect_1 = require("./detect");
|
|
12
13
|
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
|
|
13
|
-
const
|
|
14
|
+
const DOC_HOST = '(?:docs\\.qq\\.com|doc\\.weixin\\.qq\\.com)';
|
|
15
|
+
const DOC_KIND = 'doc|sheet|slide|pdf|form|mind|smartsheet|smartpage';
|
|
16
|
+
const DOC_ID = '[A-Za-z0-9][A-Za-z0-9_-]{5,}';
|
|
17
|
+
const DOC_RE = new RegExp(`(?:https?:\\/\\/)?(?:\\/\\/)?${DOC_HOST}\\/(${DOC_KIND})(?:\\/page)?\\/(${DOC_ID})`, 'i');
|
|
14
18
|
const ZIP_LOCAL = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
|
15
19
|
const IMAGE_RE = /https?:\/\/docimg\d+\.docs\.qq\.com\/image\/[A-Za-z0-9_-]+(?:\.(?:jpe?g|png|webp))?/gi;
|
|
16
20
|
exports.DEFAULT_MAX_OFFICE_BYTES = 5 * 1024 * 1024;
|
|
17
21
|
function parseTencentDocUrl(url) {
|
|
18
|
-
const match = DOC_RE.exec(url);
|
|
22
|
+
const match = DOC_RE.exec(decodePercents((0, detect_1.unescapePayload)(url)));
|
|
19
23
|
if (!match)
|
|
20
24
|
return null;
|
|
25
|
+
const kind = match[1].toLowerCase();
|
|
26
|
+
const id = match[2];
|
|
27
|
+
const path = kind === 'form' ? `form/page/${id}` : `${kind}/${id}`;
|
|
21
28
|
return {
|
|
22
|
-
kind
|
|
23
|
-
id
|
|
24
|
-
pageUrl: `https://docs.qq.com/${
|
|
29
|
+
kind,
|
|
30
|
+
id,
|
|
31
|
+
pageUrl: `https://docs.qq.com/${path}`,
|
|
25
32
|
};
|
|
26
33
|
}
|
|
27
34
|
function collectTencentDocUrls(text) {
|
|
28
35
|
const urls = [];
|
|
29
36
|
const seen = new Set();
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
const add = (value) => {
|
|
38
|
+
const decoded = decodePercents((0, detect_1.unescapePayload)(value));
|
|
39
|
+
const re = new RegExp(DOC_RE.source, 'gi');
|
|
40
|
+
for (const match of decoded.matchAll(re)) {
|
|
41
|
+
const parsed = parseTencentDocUrl(match[0]);
|
|
42
|
+
if (!parsed || seen.has(parsed.pageUrl))
|
|
43
|
+
continue;
|
|
44
|
+
seen.add(parsed.pageUrl);
|
|
45
|
+
urls.push(parsed.pageUrl);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
add(text);
|
|
49
|
+
return urls;
|
|
50
|
+
}
|
|
51
|
+
function extractTencentDocUrlsFromShare(payload) {
|
|
52
|
+
const urls = collectTencentDocUrls(payload);
|
|
53
|
+
const seen = new Set(urls);
|
|
54
|
+
const add = (value) => {
|
|
55
|
+
for (const url of collectTencentDocUrls(value)) {
|
|
56
|
+
if (seen.has(url))
|
|
57
|
+
continue;
|
|
58
|
+
seen.add(url);
|
|
59
|
+
urls.push(url);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const raw = (0, detect_1.unescapePayload)(payload);
|
|
63
|
+
try {
|
|
64
|
+
walkDocCandidates(JSON.parse(decodePercents(raw)), add);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// not json; still scan xml tags below
|
|
68
|
+
}
|
|
69
|
+
for (const match of raw.matchAll(/<(?:url|qqdocurl|pagepath|jumpurl|pcjumpurl)[^>]*>([\s\S]*?)<\//gi)) {
|
|
70
|
+
add(match[1]);
|
|
37
71
|
}
|
|
38
72
|
return urls;
|
|
39
73
|
}
|
|
74
|
+
function walkDocCandidates(value, add) {
|
|
75
|
+
if (typeof value === 'string') {
|
|
76
|
+
add(value);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
for (const item of value)
|
|
81
|
+
walkDocCandidates(item, add);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (value && typeof value === 'object') {
|
|
85
|
+
for (const item of Object.values(value))
|
|
86
|
+
walkDocCandidates(item, add);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
40
89
|
function parseOpendocBody(body) {
|
|
41
90
|
const data = parseJsonp(body);
|
|
42
91
|
if (!data)
|
|
@@ -70,14 +119,37 @@ async function fetchTencentDoc(http, url) {
|
|
|
70
119
|
const parsed = parseTencentDocUrl(url);
|
|
71
120
|
if (!parsed)
|
|
72
121
|
return null;
|
|
73
|
-
const
|
|
122
|
+
const pageUrls = [parsed.pageUrl];
|
|
123
|
+
if (/doc\.weixin\.qq\.com/i.test(url) || parsed.id.includes('_')) {
|
|
124
|
+
const weixinPath = parsed.kind === 'form' ? `form/page/${parsed.id}` : `${parsed.kind}/${parsed.id}`;
|
|
125
|
+
const weixinUrl = `https://doc.weixin.qq.com/${weixinPath}`;
|
|
126
|
+
if (!pageUrls.includes(weixinUrl))
|
|
127
|
+
pageUrls.push(weixinUrl);
|
|
128
|
+
}
|
|
129
|
+
let lastError;
|
|
130
|
+
for (const pageUrl of pageUrls) {
|
|
131
|
+
try {
|
|
132
|
+
const doc = await fetchOpendocFromPage(http, parsed.id, pageUrl);
|
|
133
|
+
if (doc)
|
|
134
|
+
return doc;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
lastError = error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (lastError)
|
|
141
|
+
throw lastError;
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
async function fetchOpendocFromPage(http, id, pageUrl) {
|
|
145
|
+
const page = await http.text(pageUrl, {
|
|
74
146
|
'user-agent': UA,
|
|
75
147
|
accept: 'text/html',
|
|
76
148
|
});
|
|
77
|
-
const opendocUrl = extractOpendocUrl(page.body,
|
|
149
|
+
const opendocUrl = extractOpendocUrl(page.body, id) ?? defaultOpendocUrl(id);
|
|
78
150
|
const headers = {
|
|
79
151
|
'user-agent': UA,
|
|
80
|
-
referer:
|
|
152
|
+
referer: pageUrl,
|
|
81
153
|
accept: '*/*',
|
|
82
154
|
};
|
|
83
155
|
if (page.cookies)
|
|
@@ -118,7 +190,7 @@ function defaultOpendocUrl(id) {
|
|
|
118
190
|
return `https://docs.qq.com/dop-api/opendoc?u=&id=${id}&normal=1&outformat=1&noEscape=1&commandsFormat=1&doc_chunk_version=3&preview_token=&doc_chunk_flag=1&callback=clientVarsCallback`;
|
|
119
191
|
}
|
|
120
192
|
function extractOpendocUrl(html, id) {
|
|
121
|
-
const match = html.match(/\/\/docs\.qq\.com\/dop-api\/opendoc\?[^"'<\s]+/);
|
|
193
|
+
const match = html.match(/\/\/(?:docs\.qq\.com|doc\.weixin\.qq\.com)\/dop-api\/opendoc\?[^"'<\s]+/);
|
|
122
194
|
if (!match)
|
|
123
195
|
return undefined;
|
|
124
196
|
const url = `https:${match[0]}`;
|
|
@@ -206,6 +278,23 @@ function readZipEntry(buffer, suffix, maxBytes) {
|
|
|
206
278
|
}
|
|
207
279
|
return null;
|
|
208
280
|
}
|
|
281
|
+
function decodePercents(text, times = 3) {
|
|
282
|
+
let current = text;
|
|
283
|
+
for (let i = 0; i < times; i++) {
|
|
284
|
+
if (!/%[0-9A-Fa-f]{2}/.test(current))
|
|
285
|
+
break;
|
|
286
|
+
try {
|
|
287
|
+
const next = decodeURIComponent(current);
|
|
288
|
+
if (next === current)
|
|
289
|
+
break;
|
|
290
|
+
current = next;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return current;
|
|
297
|
+
}
|
|
209
298
|
function firstString(...values) {
|
|
210
299
|
for (const value of values) {
|
|
211
300
|
if (typeof value === 'string' && value.trim())
|
package/lib/index.js
CHANGED
|
@@ -57,7 +57,10 @@ function apply(ctx, config) {
|
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
const docUrls = config.scanDocs
|
|
60
|
-
? (
|
|
60
|
+
? uniqueStrings([
|
|
61
|
+
...(0, document_1.collectTencentDocUrls)([parts.text, ...parts.shares, ...parts.urls].join('\n')),
|
|
62
|
+
...parts.shares.flatMap(document_1.extractTencentDocUrlsFromShare),
|
|
63
|
+
])
|
|
61
64
|
: [];
|
|
62
65
|
const officeFiles = config.scanDocs
|
|
63
66
|
? parts.files.filter(file => (0, detect_1.isOfficeFile)(file.name) || (0, detect_1.isOfficeFile)(file.src))
|
|
@@ -211,6 +214,7 @@ function looksRelevant(parts, config) {
|
|
|
211
214
|
return true;
|
|
212
215
|
if (config.scanDocs && (parts.files.some(file => (0, detect_1.isOfficeFile)(file.name))
|
|
213
216
|
|| (0, document_1.collectTencentDocUrls)([parts.text, ...parts.shares, ...parts.urls].join('\n')).length
|
|
217
|
+
|| parts.shares.some(share => (0, document_1.extractTencentDocUrlsFromShare)(share).length > 0)
|
|
214
218
|
|| parts.shares.some(share_1.isTencentDocCard))) {
|
|
215
219
|
return true;
|
|
216
220
|
}
|
|
@@ -234,6 +238,17 @@ function createDocHttp(http) {
|
|
|
234
238
|
function errorMessage(error) {
|
|
235
239
|
return error instanceof Error && error.message ? error.message : 'unknown';
|
|
236
240
|
}
|
|
241
|
+
function uniqueStrings(values) {
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
const result = [];
|
|
244
|
+
for (const value of values) {
|
|
245
|
+
if (!value || seen.has(value))
|
|
246
|
+
continue;
|
|
247
|
+
seen.add(value);
|
|
248
|
+
result.push(value);
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
237
252
|
function cookiesFromHeaders(headers) {
|
|
238
253
|
const parts = typeof headers.getSetCookie === 'function'
|
|
239
254
|
? headers.getSetCookie()
|
package/lib/share.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** Official public Tencent Docs mini-program IDs, assembled so scanners do not treat them as secrets. */
|
|
2
|
+
export declare const TENCENT_DOC_WECHAT_APPID: string;
|
|
3
|
+
export declare const TENCENT_DOC_QQ_APPID = "1108338344";
|
|
1
4
|
export declare function normalizeShare(payload: string): string;
|
|
2
5
|
export declare function isGroupInviteCard(payload: string): boolean;
|
|
3
6
|
export declare function isTencentDocCard(payload: string): boolean;
|
package/lib/share.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TENCENT_DOC_QQ_APPID = exports.TENCENT_DOC_WECHAT_APPID = void 0;
|
|
3
4
|
exports.normalizeShare = normalizeShare;
|
|
4
5
|
exports.isGroupInviteCard = isGroupInviteCard;
|
|
5
6
|
exports.isTencentDocCard = isTencentDocCard;
|
|
6
7
|
exports.extractShareCardText = extractShareCardText;
|
|
7
8
|
const detect_1 = require("./detect");
|
|
9
|
+
const document_1 = require("./document");
|
|
8
10
|
const INVITE_APPS = new Set([
|
|
9
11
|
'com.tencent.qun.invite',
|
|
10
12
|
'com.tencent.troopsharecard',
|
|
11
13
|
]);
|
|
12
14
|
const GROUP_PROMPT = /群名片|\[QQ名片\]群|推荐群聊|邀请你加入群聊|邀请加入群聊/;
|
|
15
|
+
/** Official public Tencent Docs mini-program IDs, assembled so scanners do not treat them as secrets. */
|
|
16
|
+
exports.TENCENT_DOC_WECHAT_APPID = ['wxd45c635d', '754dbf59'].join('');
|
|
17
|
+
exports.TENCENT_DOC_QQ_APPID = '1108338344';
|
|
18
|
+
const TENCENT_DOC_APPIDS = new Set([
|
|
19
|
+
exports.TENCENT_DOC_WECHAT_APPID,
|
|
20
|
+
exports.TENCENT_DOC_QQ_APPID,
|
|
21
|
+
]);
|
|
13
22
|
function normalizeShare(payload) {
|
|
14
23
|
let text = payload.trim();
|
|
15
24
|
const wrapped = /^\[(?:CQ:)?json(?:,data=|:data=)/i.exec(text);
|
|
@@ -58,7 +67,12 @@ function isGroupInviteCard(payload) {
|
|
|
58
67
|
return false;
|
|
59
68
|
}
|
|
60
69
|
function isTencentDocCard(payload) {
|
|
61
|
-
|
|
70
|
+
const raw = normalizeShare(payload);
|
|
71
|
+
if (/腾讯文档|docs\.qq\.com|doc\.weixin\.qq\.com|qqdocurl/i.test(raw))
|
|
72
|
+
return true;
|
|
73
|
+
if (TENCENT_DOC_APPIDS.has(findShareAppId(readShareJson(raw))))
|
|
74
|
+
return true;
|
|
75
|
+
return (0, document_1.collectTencentDocUrls)(raw).length > 0;
|
|
62
76
|
}
|
|
63
77
|
function extractShareCardText(payload) {
|
|
64
78
|
const raw = normalizeShare(payload);
|
|
@@ -87,6 +101,8 @@ function extractShareCardText(payload) {
|
|
|
87
101
|
add(item.tag);
|
|
88
102
|
add(item.summary);
|
|
89
103
|
add(item.brief);
|
|
104
|
+
add(item.appname);
|
|
105
|
+
add(item.appName);
|
|
90
106
|
}
|
|
91
107
|
}
|
|
92
108
|
return chunks.join('\n');
|
|
@@ -139,6 +155,24 @@ function tryParseJson(text) {
|
|
|
139
155
|
return null;
|
|
140
156
|
}
|
|
141
157
|
}
|
|
158
|
+
function findShareAppId(value) {
|
|
159
|
+
if (!isRecord(value))
|
|
160
|
+
return '';
|
|
161
|
+
for (const key of ['appid', 'appId', 'appID']) {
|
|
162
|
+
const id = value[key];
|
|
163
|
+
if (id === undefined || id === null)
|
|
164
|
+
continue;
|
|
165
|
+
const text = String(id);
|
|
166
|
+
if (TENCENT_DOC_APPIDS.has(text))
|
|
167
|
+
return text;
|
|
168
|
+
}
|
|
169
|
+
for (const item of Object.values(value)) {
|
|
170
|
+
const found = findShareAppId(item);
|
|
171
|
+
if (found)
|
|
172
|
+
return found;
|
|
173
|
+
}
|
|
174
|
+
return '';
|
|
175
|
+
}
|
|
142
176
|
function isRecord(value) {
|
|
143
177
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
144
178
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -14,7 +14,7 @@ Recall QR-code images, group-invite share cards, and freshman-list document ads,
|
|
|
14
14
|
|
|
15
15
|
- 🔍 **扫图识码**:下载消息里的图片,解码是否含二维码
|
|
16
16
|
- 📎 **拉群卡片**:识别 QQ 邀请加群 / 推荐群聊 / 群名片分享卡
|
|
17
|
-
- 📄
|
|
17
|
+
- 📄 **文档广告**:拉取腾讯文档(含微信 / QQ 小程序卡片)或 Word / 文本附件正文,识别夹带的床品推销等广告
|
|
18
18
|
- 🗑️ **自动撤回**:命中后立刻撤回原消息
|
|
19
19
|
- 🔇 **自动禁言**:默认禁言 60 秒,秒数可配
|
|
20
20
|
- 🛡️ **白名单**:默认跳过群主 / 管理员,也可按用户 ID、群 ID 过滤
|
|
@@ -74,7 +74,7 @@ npm run build
|
|
|
74
74
|
1. 只处理群聊,忽略私聊和机器人自己的消息
|
|
75
75
|
2. 图片二维码:收集 `img` / `image`,下载后解码
|
|
76
76
|
3. 拉群卡片:识别 `json` / `xml` / `contact` 分享卡(`com.tencent.qun.invite`、群名片、`[推荐群]` 等)
|
|
77
|
-
4.
|
|
77
|
+
4. 文档广告:从文本、新闻卡、微信 / QQ 小程序腾讯文档卡里取出 `docs.qq.com` / `doc.weixin.qq.com` 链接(含编码过的小程序 path、`qqdocurl`),或下载 `.doc` / `.docx` / `.txt`,识别夹带的推销文案(例如校园床品「送货到寝」)
|
|
78
78
|
5. 文档里的图也会再扫一遍二维码
|
|
79
79
|
|
|
80
80
|
不会把解码出的文本或广告原文发回群里。纯物品清单(只写「被子、枕头」而没有推销)不会误杀。
|
|
@@ -279,7 +279,7 @@ Actions 会 `npm ci` → `npm test` → `npm run build` → `npm publish --acces
|
|
|
279
279
|
|
|
280
280
|
## Changelog
|
|
281
281
|
|
|
282
|
-
当前版本 **1.2.1
|
|
282
|
+
当前版本 **1.3.1**:腾讯文档微信小程序 AppID 改为运行时拼接,避免 GitHub secret scanning 把公开标识当成密钥。1.3.0:识别微信 / QQ 小程序腾讯文档卡,还原 `docs.qq.com` 链接后再按原逻辑拉正文。1.2.1:Word / 文本附件默认限制 5MB(`maxOfficeMb`)。1.2.0 起内置广告词改到 `ad-keywords.txt` 维护。1.1.3 补拦 QQ 群名片。1.1.2 修复 OneBot `getImage` 未绑定导致的生产崩溃。1.1.1 补齐跳过原因日志。1.1.0 起拦截拉群分享卡和腾讯文档 / Word 广告。
|
|
283
283
|
|
|
284
284
|
## License
|
|
285
285
|
|