koishi-plugin-guild-request-approval 0.0.0-stage → 0.1.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.
Files changed (4) hide show
  1. package/index.js +427 -0
  2. package/package.json +44 -4
  3. package/readme.md +43 -0
  4. package/README.md +0 -3
package/index.js ADDED
@@ -0,0 +1,427 @@
1
+ // ESM 模块(见 package.json 的 "type": "module")。想用 CommonJS 就把下面这行换成
2
+ // const { Schema } = require('koishi'),再把两个 export const 改成 module.exports = { ... }
3
+ import { Schema } from 'koishi'
4
+
5
+ export const name = 'guild-request-approval'
6
+
7
+ // 故意不 inject database / http:所有状态放内存,装上就能跑,进程重启后挂起的申请会丢。
8
+ export const inject = []
9
+
10
+ export const Config = Schema.object({
11
+ mode: Schema.union([
12
+ Schema.const('accept').description('自动通过'),
13
+ Schema.const('reject').description('自动拒绝'),
14
+ Schema.const('manual').description('转发给管理员,人工回复审批'),
15
+ Schema.const('auto').description('按条件筛选,不满足时走 fallback'),
16
+ ]).default('manual').description('审批模式'),
17
+
18
+ target: Schema.string().default('')
19
+ .description('人工审批的转发目标:`group:123456`、`private:10001`,只写数字默认当群号。'),
20
+
21
+ admins: Schema.array(Schema.string()).default([])
22
+ .description('有权回复审批的管理员 QQ;留空表示“转发目标所在会话里的任何人都能批”。'),
23
+
24
+ timeout: Schema.natural().default(300).description('人工审批等待时长(秒),0 = 永不超时。'),
25
+
26
+ timeoutAction: Schema.union([
27
+ Schema.const('reject').description('自动拒绝'),
28
+ Schema.const('accept').description('自动通过'),
29
+ Schema.const('none').description('不处理,留在 QQ 那边自然过期'),
30
+ ]).default('reject').description('超时后的动作'),
31
+
32
+ minLevel: Schema.natural().default(0).description('auto 模式:最低 QQ 等级,0 = 不检查。'),
33
+
34
+ minAgeYears: Schema.number().default(0).description('auto 模式:最短注册年限(可写 0.5),0 = 不检查。'),
35
+
36
+ unknownAs: Schema.union([
37
+ Schema.const('fail').description('当作不满足,转 fallback'),
38
+ Schema.const('pass').description('当作满足,放行'),
39
+ ]).default('fail').description('auto 模式拿不到等级/注册时间时怎么办'),
40
+
41
+ fallback: Schema.union([
42
+ Schema.const('manual').description('转人工审批'),
43
+ Schema.const('reject').description('直接拒绝'),
44
+ ]).default('manual').description('auto 模式条件不满足时的去向'),
45
+
46
+ maxPending: Schema.natural().default(200).description('同时挂起的申请上限,超了就先结算最旧的。'),
47
+
48
+ acceptComment: Schema.string().default('').description('通过时的附言,可空。'),
49
+
50
+ rejectComment: Schema.string().default('').description('拒绝 / 超时拒绝的默认理由。'),
51
+
52
+ forwardTemplate: Schema.string().role('textarea').default(
53
+ '【入群申请 #{id}】\n群号:{guildId}\n申请人:{userId}\n附言:{comment}\n{reason}\n时间:{time}\n\n回复「同意 {id}」或「拒绝 {id} 理由」',
54
+ ).description('转发文案,支持 {id} {guildId} {userId} {comment} {reason} {time}'),
55
+
56
+ replyOnResult: Schema.boolean().default(true).description('处理完是否在回复处回执一句。'),
57
+ })
58
+
59
+ const MODES = ['accept', 'reject', 'manual', 'auto']
60
+ const YEAR_MS = 365.25 * 24 * 3600 * 1000
61
+
62
+ // `group:123` / `123` / `private:456` / `qq:789`(中文冒号也认)
63
+ function parseTarget(raw) {
64
+ const s = String(raw ?? '').trim()
65
+ if (!s) return { type: 'group', id: '' }
66
+ const m = /^(group|private|user|qq|guild|channel)\s*[::]\s*(.+)$/i.exec(s)
67
+ if (!m) return { type: 'group', id: s }
68
+ const type = /^(private|user|qq)$/i.test(m[1]) ? 'private' : 'group'
69
+ return { type, id: m[2].trim() }
70
+ }
71
+
72
+ function firstNumber(obj, keys) {
73
+ if (!obj) return null
74
+ for (const key of keys) {
75
+ const v = obj[key]
76
+ if (v === null || v === undefined || v === '') continue
77
+ const n = Number(v)
78
+ if (Number.isFinite(n)) return n
79
+ }
80
+ return null
81
+ }
82
+
83
+ // 秒 / 毫秒 / YYYYMMDD / 日期字符串,都当成时间戳吃掉
84
+ function toTimestamp(input) {
85
+ if (input === null || input === undefined || input === '') return null
86
+ if (input instanceof Date) return Number.isNaN(input.getTime()) ? null : input.getTime()
87
+ const n = Number(input)
88
+ if (Number.isFinite(n)) {
89
+ if (n > 1e12) return n
90
+ if (n > 1e9) return n * 1000
91
+ if (n >= 19000101 && n <= 29991231) {
92
+ const s = String(Math.trunc(n))
93
+ return new Date(+s.slice(0, 4), +s.slice(4, 6) - 1, +s.slice(6, 8)).getTime()
94
+ }
95
+ return null
96
+ }
97
+ const t = Date.parse(String(input))
98
+ return Number.isNaN(t) ? null : t
99
+ }
100
+
101
+ function renderTemplate(tpl, rec) {
102
+ return String(tpl ?? '').replace(/\{(\w+)\}/g, (_, key) => {
103
+ if (key === 'time') return new Date(rec.createdAt).toLocaleString('zh-CN', { hour12: false })
104
+ if (key === 'comment') return rec.comment || '(无)'
105
+ if (key === 'reason') return rec.reason ? `筛选未过:${rec.reason}` : ''
106
+ const v = rec[key]
107
+ return v === null || v === undefined ? '' : String(v)
108
+ })
109
+ }
110
+
111
+ // 等级 / 注册时间只能“有就用”:官方 QQ 机器人接口根本不给这两项,
112
+ // 这里按 事件字段 → bot.internal(OneBot 的 getGroupMemberInfo)的顺序碰运气。
113
+ async function resolveProfile(bot, guildId, userId, event) {
114
+ const out = { level: null, ageYears: null }
115
+ const pools = [event?.user, event?.member, event?.d, event].filter(Boolean)
116
+
117
+ for (const pool of pools) {
118
+ if (out.level === null) {
119
+ out.level = firstNumber(pool, ['level', 'qqLevel', 'qq_level', 'userLevel', 'user_level'])
120
+ }
121
+ if (out.ageYears === null) {
122
+ const raws = ['regTime', 'registerTime', 'register_time', 'regTimestamp', 'createTime', 'createdAt', 'created_at']
123
+ for (const key of raws) {
124
+ const ts = toTimestamp(pool[key])
125
+ if (ts) {
126
+ out.ageYears = (Date.now() - ts) / YEAR_MS
127
+ break
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ if (out.level === null && bot?.internal && typeof bot.internal.getGroupMemberInfo === 'function') {
134
+ try {
135
+ const info = await bot.internal.getGroupMemberInfo(guildId, userId)
136
+ out.level = firstNumber(info, ['level', 'qq_level', 'qqLevel'])
137
+ } catch (e) {
138
+ // 拿不到很正常(官方接口没有这个能力),不用当错误
139
+ }
140
+ }
141
+
142
+ return out
143
+ }
144
+
145
+ function evaluate(profile, config) {
146
+ const fails = []
147
+ const minLevel = Number(config.minLevel) || 0
148
+ const minAgeYears = Number(config.minAgeYears) || 0
149
+
150
+ if (minLevel > 0) {
151
+ if (profile.level === null) {
152
+ if (config.unknownAs === 'fail') fails.push('拿不到 QQ 等级')
153
+ } else if (profile.level < minLevel) {
154
+ fails.push(`QQ 等级 ${profile.level} 低于 ${minLevel}`)
155
+ }
156
+ }
157
+
158
+ if (minAgeYears > 0) {
159
+ if (profile.ageYears === null) {
160
+ if (config.unknownAs === 'fail') fails.push('拿不到注册时间')
161
+ } else if (profile.ageYears < minAgeYears) {
162
+ fails.push(`注册 ${profile.ageYears.toFixed(1)} 年,不足 ${minAgeYears} 年`)
163
+ }
164
+ }
165
+
166
+ return fails
167
+ }
168
+
169
+ async function sendTo(bot, target, content) {
170
+ const { type, id } = parseTarget(target)
171
+ if (!id) throw new Error('没有配置转发目标 target')
172
+ if (!bot) throw new Error('没有可用的 bot')
173
+ if (type === 'private') {
174
+ // 官方 QQ 适配器的私聊必须走 sendPrivateMessage,sendMessage('private:x') 在它上面不成立
175
+ if (typeof bot.sendPrivateMessage !== 'function') throw new Error('当前适配器不支持私聊,改成 group:<群号>')
176
+ return bot.sendPrivateMessage(id, content)
177
+ }
178
+ return bot.sendMessage(id, content)
179
+ }
180
+
181
+ export function apply(ctx, config) {
182
+ const logger = ctx.logger('guild-request-approval')
183
+ const pending = new Map()
184
+ const seen = new Set() // adapter-qq-crack 偶尔会把同一条申请推两次
185
+ let seq = 0
186
+
187
+ const safeSend = async (session, text) => {
188
+ try {
189
+ await session.send(text)
190
+ } catch (e) {
191
+ logger.warn('回执发送失败:%s', e?.message ?? e)
192
+ }
193
+ }
194
+
195
+ const findPending = (input) => {
196
+ const key = String(input ?? '').trim().replace(/^[##]/, '')
197
+ if (!key) return null
198
+ if (pending.has(key)) return pending.get(key)
199
+ for (const rec of pending.values()) {
200
+ if (String(rec.messageId) === key) return rec
201
+ }
202
+ return null
203
+ }
204
+
205
+ const settle = async (rec, approve, comment, by) => {
206
+ if (rec.status !== 'pending') return false
207
+ rec.status = approve ? 'accepted' : 'rejected'
208
+ if (rec.timer) {
209
+ clearTimeout(rec.timer)
210
+ rec.timer = null
211
+ }
212
+ pending.delete(rec.id)
213
+ try {
214
+ await rec.bot.handleGuildMemberRequest(rec.messageId, approve, comment || undefined)
215
+ logger.info('申请 #%s %s@%s → %s%s', rec.id, rec.userId, rec.guildId,
216
+ approve ? '通过' : '拒绝', by ? `(${by})` : '')
217
+ return true
218
+ } catch (e) {
219
+ // 适配器没实现 handleGuildMemberRequest 时只会在这里炸,不会把插件带崩
220
+ logger.warn('申请 #%s 调用 handleGuildMemberRequest 失败:%s', rec.id, e?.message ?? e)
221
+ rec.status = 'error'
222
+ return false
223
+ }
224
+ }
225
+
226
+ const armTimeout = (rec) => {
227
+ const seconds = Number(config.timeout) || 0
228
+ if (seconds <= 0) return
229
+ // unref:别让挂起的定时器拖住进程退出
230
+ rec.timer = setTimeout(async () => {
231
+ rec.timer = null
232
+ if (config.timeoutAction === 'none') {
233
+ rec.status = 'expired'
234
+ pending.delete(rec.id)
235
+ logger.info('申请 #%s 超时,按配置留在 QQ 侧', rec.id)
236
+ return
237
+ }
238
+ const ok = await settle(rec, config.timeoutAction === 'accept', config.rejectComment || '超时未处理', '超时')
239
+ if (ok && config.replyOnResult) {
240
+ try {
241
+ await sendTo(rec.bot, config.target, `申请 #${rec.id} 超时,已自动${config.timeoutAction === 'accept' ? '通过' : '拒绝'}`)
242
+ } catch (e) {
243
+ // 回执失败无所谓,主流程已经完成了
244
+ }
245
+ }
246
+ }, seconds * 1000)
247
+ rec.timer.unref?.()
248
+ }
249
+
250
+ const fromTarget = (session) => {
251
+ const { type, id } = parseTarget(config.target)
252
+ if (!id) return false
253
+ if (type === 'private') return String(session.userId) === id
254
+ return [session.guildId, session.channelId].some((v) => v !== null && v !== undefined && String(v) === id)
255
+ }
256
+
257
+ const allowed = (session) => {
258
+ const admins = (config.admins ?? []).map(String).map((s) => s.trim()).filter(Boolean)
259
+ if (admins.length) return admins.includes(String(session.userId))
260
+ return fromTarget(session)
261
+ }
262
+
263
+ const forward = async (rec) => {
264
+ const text = renderTemplate(config.forwardTemplate, rec)
265
+ const { id } = parseTarget(config.target)
266
+ if (!id) throw new Error('没有配置转发目标 target')
267
+ await sendTo(rec.bot || ctx.bots[0], config.target, text)
268
+ }
269
+
270
+ ctx.on('guild-member-request', async (session) => {
271
+ // adapter-qq-crack 的事件字段不保证齐全:缺一个就跳,别猜
272
+ const guildId = session.guildId
273
+ const userId = session.userId
274
+ const messageId = session.messageId || session.event?.message?.id || session.event?.messageId
275
+ const comment = session.event?.comment || session.event?.message?.content || session.content || ''
276
+
277
+ if (!guildId || !userId || !messageId) {
278
+ logger.warn('入群申请字段缺失,已跳过:guildId=%o userId=%o messageId=%o', guildId, userId, messageId)
279
+ return
280
+ }
281
+
282
+ if (seen.has(String(messageId))) {
283
+ logger.debug('重复的申请事件 %s,忽略', messageId)
284
+ return
285
+ }
286
+ seen.add(String(messageId))
287
+ if (seen.size > 1000) seen.delete(seen.values().next().value)
288
+
289
+ const bot = session.bot || ctx.bots[0]
290
+ if (!bot) {
291
+ logger.warn('没有可用的 bot,跳过 %s 的申请', userId)
292
+ return
293
+ }
294
+
295
+ const mode = MODES.includes(config.mode) ? config.mode : 'manual'
296
+ seq += 1
297
+ const rec = {
298
+ id: String(seq),
299
+ guildId,
300
+ userId,
301
+ messageId,
302
+ comment,
303
+ reason: '',
304
+ bot,
305
+ createdAt: Date.now(),
306
+ status: 'pending',
307
+ timer: null,
308
+ }
309
+
310
+ if (mode === 'accept') {
311
+ await settle(rec, true, config.acceptComment)
312
+ return
313
+ }
314
+
315
+ if (mode === 'reject') {
316
+ await settle(rec, false, config.rejectComment || '本群暂不开放')
317
+ return
318
+ }
319
+
320
+ if (mode === 'auto') {
321
+ const profile = await resolveProfile(bot, guildId, userId, session.event)
322
+ const fails = evaluate(profile, config)
323
+ logger.debug('auto 判定 #%s level=%o ageYears=%o fails=%o', rec.id, profile.level, profile.ageYears, fails)
324
+ if (!fails.length) {
325
+ await settle(rec, true, config.acceptComment)
326
+ return
327
+ }
328
+ if (config.fallback === 'reject') {
329
+ await settle(rec, false, fails.join(';') || config.rejectComment)
330
+ return
331
+ }
332
+ rec.reason = fails.join(';')
333
+ }
334
+
335
+ // manual(auto 转人工也走这儿)
336
+ pending.set(rec.id, rec)
337
+ const cap = Number(config.maxPending) || 200
338
+ if (pending.size > cap) {
339
+ const oldest = pending.values().next().value
340
+ if (oldest && oldest.id !== rec.id) {
341
+ logger.warn('挂起申请超过 %s 条,先结算最旧的 #%s', cap, oldest.id)
342
+ if (config.timeoutAction === 'none') {
343
+ oldest.status = 'expired'
344
+ pending.delete(oldest.id)
345
+ } else {
346
+ await settle(oldest, config.timeoutAction === 'accept', config.rejectComment || '积压超限')
347
+ }
348
+ }
349
+ }
350
+
351
+ try {
352
+ await forward(rec)
353
+ } catch (e) {
354
+ // 转不出去就等于没人能批,按超时策略收掉,别让申请一直吊在 QQ 那边
355
+ logger.warn('转发申请 #%s 失败:%s', rec.id, e?.message ?? e)
356
+ if (config.timeoutAction === 'none') {
357
+ rec.status = 'expired'
358
+ pending.delete(rec.id)
359
+ } else {
360
+ await settle(rec, config.timeoutAction === 'accept', config.rejectComment || '无人可审批')
361
+ }
362
+ return
363
+ }
364
+
365
+ armTimeout(rec)
366
+ })
367
+
368
+ // 手写正则而不是 ctx.command:审批人是在群里随口回一句,不该被全局前缀绑住
369
+ const REJECT_RE = /^(?:不同意|拒绝|驳回|reject|deny)\s*[##]?([0-9A-Za-z_-]+)\s*([\s\S]*)$/i
370
+ const APPROVE_RE = /^(?:同意|通过|批准|approve|accept)\s*[##]?([0-9A-Za-z_-]+)\s*([\s\S]*)$/i
371
+
372
+ ctx.on('message', async (session) => {
373
+ if (!session.content) return
374
+ if (session.userId && session.selfId && session.userId === session.selfId) return
375
+
376
+ // QQ 消息里会混进 <at id="..."/> 这类片段,先剥掉再匹配
377
+ let text = String(session.content).replace(/<[^>]*>/g, ' ').trim()
378
+ text = text.replace(/^[!!//.。、,,\s]+/, '')
379
+ if (!text) return
380
+
381
+ let approved = false
382
+ let matched = REJECT_RE.exec(text)
383
+ if (matched) {
384
+ approved = false
385
+ } else {
386
+ matched = APPROVE_RE.exec(text)
387
+ if (matched) approved = true
388
+ }
389
+ if (!matched) return
390
+
391
+ const input = matched[1]
392
+ const comment = (matched[2] || '').trim()
393
+ const rec = findPending(input)
394
+
395
+ if (!rec) {
396
+ if (allowed(session)) await safeSend(session, `没找到待处理的申请 #${input.replace(/^[##]/, '')}`)
397
+ return
398
+ }
399
+ if (!allowed(session)) {
400
+ await safeSend(session, '你没有审批权限')
401
+ return
402
+ }
403
+
404
+ const ok = await settle(rec, approved, approved ? (comment || config.acceptComment) : (comment || config.rejectComment), `by ${session.userId}`)
405
+ if (!ok) {
406
+ await safeSend(session, `申请 #${rec.id} 处理失败,看看日志`)
407
+ return
408
+ }
409
+ if (config.replyOnResult) {
410
+ await safeSend(session, approved
411
+ ? `已同意 #${rec.id}`
412
+ : `已拒绝 #${rec.id}${comment ? ':' + comment : ''}`)
413
+ }
414
+ // 私聊里批的,顺手在转发目标那边播报一句,其他管理员不至于重复操作
415
+ if (!fromTarget(session)) {
416
+ try {
417
+ await sendTo(rec.bot, config.target, `申请 #${rec.id} 已被 ${session.userId} ${approved ? '同意' : '拒绝'}`)
418
+ } catch (e) {
419
+ // 播报失败不影响结果
420
+ }
421
+ }
422
+ })
423
+
424
+ if (['manual', 'auto'].includes(config.mode) && !parseTarget(config.target).id) {
425
+ logger.warn('当前是 %s 模式但没配置 target,申请会被直接按超时策略处理', config.mode)
426
+ }
427
+ }
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "koishi-plugin-guild-request-approval",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "QQ 群入群申请审批:自动通过 / 自动拒绝 / 转发人工审批 / 条件筛选,适配 adapter-qq",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "files": [
8
+ "index.js",
9
+ "readme.md"
10
+ ],
11
+ "keywords": [
12
+ "chatbot",
13
+ "koishi",
14
+ "plugin",
15
+ "qq",
16
+ "guild",
17
+ "request",
18
+ "adapter-qq"
19
+ ],
20
+ "license": "MIT",
21
+ "author": "7-Ice",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/7-Ice/koishi-plugin-guild-request-approval.git"
25
+ },
26
+ "homepage": "https://github.com/7-Ice/koishi-plugin-guild-request-approval",
27
+ "bugs": {
28
+ "url": "https://github.com/7-Ice/koishi-plugin-guild-request-approval/issues"
29
+ },
30
+ "peerDependencies": {
31
+ "koishi": "^4.0.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "koishi": {
37
+ "description": {
38
+ "zh": "群入群申请审批:accept / reject / manual / auto 四种模式,可转发给管理员回复「同意 1」「拒绝 1 理由」,支持超时兜底",
39
+ "en": "Guild member request approval with accept / reject / manual / auto modes and timeout fallback"
40
+ },
41
+ "service": {
42
+ "required": [],
43
+ "optional": []
44
+ }
45
+ }
46
+ }
package/readme.md ADDED
@@ -0,0 +1,43 @@
1
+ # koishi-plugin-guild-request-approval
2
+
3
+ 处理 QQ 群入群申请,四种模式随便挑:直接过、直接拒、丢给管理员、按条件筛。
4
+
5
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-guild-request-approval?style=flat-square&logo=npm&color=cb3837)](https://www.npmjs.com/package/koishi-plugin-guild-request-approval)
6
+ [![downloads](https://img.shields.io/npm/dm/koishi-plugin-guild-request-approval?style=flat-square&color=blue)](https://www.npmjs.com/package/koishi-plugin-guild-request-approval)
7
+ [![license](https://img.shields.io/npm/l/koishi-plugin-guild-request-approval?style=flat-square&color=3da639)](package.json)
8
+ [![koishi](https://img.shields.io/badge/koishi-%5E4.0.0-ff6b6b?style=flat-square)](https://koishi.chat)
9
+
10
+ ## 装
11
+
12
+ ```bash
13
+ npm i koishi-plugin-guild-request-approval
14
+ ```
15
+
16
+ 或者 Koishi 控制台 → 插件市场搜 `guild-request-approval`。装完点开配置,选模式、填目标就行,不用 database。
17
+
18
+ ## 配置项
19
+
20
+ | 项 | 说明 |
21
+ | --- | --- |
22
+ | `mode` | `accept` 直接过 / `reject` 直接拒 / `manual` 转人工 / `auto` 条件筛 |
23
+ | `target` | 转人工的落地位置:`group:123456`、`private:10001`,只写数字当群号 |
24
+ | `admins` | 能回复审批的 QQ 列表;留空 = 转发目标会话里谁都能批 |
25
+ | `timeout` | 等多久(秒),`0` = 一直等 |
26
+ | `timeoutAction` | 超时后 `reject` / `accept` / `none` |
27
+ | `minLevel` | `auto` 用,最低 QQ 等级,`0` 不检查 |
28
+ | `minAgeYears` | `auto` 用,最短注册年限,`0` 不检查 |
29
+ | `unknownAs` | `auto` 拿不到等级/注册时间时:`fail` 当不满足,`pass` 放行 |
30
+ | `fallback` | `auto` 不满足条件时:`manual` 转人工,`reject` 直接拒 |
31
+ | `maxPending` | 最多挂起多少条,超了先结算最旧的 |
32
+ | `acceptComment` / `rejectComment` | 通过/拒绝附言 |
33
+ | `forwardTemplate` | 转发文案,占位 `{id} {guildId} {userId} {comment} {reason} {time}` |
34
+ | `replyOnResult` | 处理完回不回一句 |
35
+
36
+ 官方机器人接口不提供 QQ 等级和注册时间,`auto` 模式在这类适配器上会一直走“拿不到数据”分支——要么把 `unknownAs` 设成 `pass`,要么就用 `manual`。
37
+
38
+ ## 验证
39
+
40
+ - `accept` / `reject`:小号发申请,群里立刻进人 / 立刻被拒。
41
+ - `manual`:申请转发到 `target`,回复 `同意 1` 或 `拒绝 1 太新了`,回执出现即生效;不管它,到点看 `timeoutAction` 有没有执行。
42
+ - `auto`:把 `minLevel` 设成 999,申请必然转人工或直接拒,然后把 `unknownAs` 改成 `pass` 再试一次,行为应当翻转。
43
+ - 日志标签 `guild-request-approval` 里能看到每条申请的判定结果,字段缺失会打 warn。
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.