twikoo-func 1.7.14 → 1.7.15
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/index.js +41 -2
- package/package.json +4 -3
- package/utils/cap.js +311 -0
- package/utils/index.js +16 -6
- package/utils/lib.js +3 -3
- package/utils/spam.js +6 -26
package/index.js
CHANGED
|
@@ -38,6 +38,13 @@ const {
|
|
|
38
38
|
checkCommentOwnership,
|
|
39
39
|
isValidEmail
|
|
40
40
|
} = require('./utils')
|
|
41
|
+
const {
|
|
42
|
+
createCap,
|
|
43
|
+
tcbStorage,
|
|
44
|
+
createChallenge,
|
|
45
|
+
redeemChallenge,
|
|
46
|
+
isBuiltinCap
|
|
47
|
+
} = require('./utils/cap')
|
|
41
48
|
const {
|
|
42
49
|
jsonParse,
|
|
43
50
|
commentImportValine,
|
|
@@ -151,6 +158,12 @@ exports.main = async (event, context) => {
|
|
|
151
158
|
case 'COMMENT_DELETE_FOR_USER':
|
|
152
159
|
res = await commentDeleteForUser(event)
|
|
153
160
|
break
|
|
161
|
+
case 'CAP_CHALLENGE': // embedded Cap.js
|
|
162
|
+
res = await capChallenge()
|
|
163
|
+
break
|
|
164
|
+
case 'CAP_REDEEM':
|
|
165
|
+
res = await capRedeem(event)
|
|
166
|
+
break
|
|
154
167
|
default:
|
|
155
168
|
if (event.event) {
|
|
156
169
|
res.code = RES_CODE.EVENT_NOT_EXIST
|
|
@@ -760,6 +773,14 @@ async function checkCaptcha (comment) {
|
|
|
760
773
|
geeTestPassToken: comment.geeTestPassToken,
|
|
761
774
|
geeTestGenTime: comment.geeTestGenTime
|
|
762
775
|
})
|
|
776
|
+
} else if (provider === 'Cap' && isBuiltinCap(config)) {
|
|
777
|
+
if (!comment.capToken) {
|
|
778
|
+
throw new Error('验证码 token 缺失,请刷新页面重试')
|
|
779
|
+
}
|
|
780
|
+
await checkCapCaptcha({
|
|
781
|
+
capToken: comment.capToken,
|
|
782
|
+
cap: createCap(tcbStorage(db))
|
|
783
|
+
})
|
|
763
784
|
} else if (provider === 'Cap' && config.CAP_API_ENDPOINT && config.CAP_SECRET_KEY) {
|
|
764
785
|
if (!comment.capToken) {
|
|
765
786
|
throw new Error('验证码 token 缺失,请刷新页面重试')
|
|
@@ -770,7 +791,7 @@ async function checkCaptcha (comment) {
|
|
|
770
791
|
capApiEndpoint: config.CAP_API_ENDPOINT
|
|
771
792
|
})
|
|
772
793
|
} else if (provider === 'Cap') {
|
|
773
|
-
throw new Error('Cap
|
|
794
|
+
throw new Error('Cap 验证码配置不完整:内嵌模式无需额外配置,外部模式需填写 CAP_API_ENDPOINT 与 CAP_SECRET_KEY')
|
|
774
795
|
} else if (provider) {
|
|
775
796
|
throw new Error(`不支持的验证码类型: ${provider}`)
|
|
776
797
|
}
|
|
@@ -1027,9 +1048,27 @@ function isRecursion (context) {
|
|
|
1027
1048
|
return envObj.TCB_SOURCE.substr(-3, 3) === 'scf'
|
|
1028
1049
|
}
|
|
1029
1050
|
|
|
1051
|
+
async function capChallenge () {
|
|
1052
|
+
if (!isBuiltinCap(config)) {
|
|
1053
|
+
return { code: RES_CODE.FAIL, message: '内嵌 Cap 未启用' }
|
|
1054
|
+
}
|
|
1055
|
+
const cap = createCap(tcbStorage(db))
|
|
1056
|
+
const data = await createChallenge(cap)
|
|
1057
|
+
return { code: RES_CODE.SUCCESS, ...data }
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
async function capRedeem (event) {
|
|
1061
|
+
if (!isBuiltinCap(config)) {
|
|
1062
|
+
return { code: RES_CODE.FAIL, message: '内嵌 Cap 未启用' }
|
|
1063
|
+
}
|
|
1064
|
+
const cap = createCap(tcbStorage(db))
|
|
1065
|
+
const data = await redeemChallenge(cap, event)
|
|
1066
|
+
return { code: RES_CODE.SUCCESS, ...data }
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1030
1069
|
// 建立数据库 collections
|
|
1031
1070
|
async function createCollections () {
|
|
1032
|
-
const collections = ['comment', 'config', 'counter']
|
|
1071
|
+
const collections = ['comment', 'config', 'counter', 'cap_challenges', 'cap_tokens']
|
|
1033
1072
|
const res = {}
|
|
1034
1073
|
for (const collection of collections) {
|
|
1035
1074
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twikoo-func",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.15",
|
|
4
4
|
"description": "A simple comment system.",
|
|
5
5
|
"author": "imaegoo <hello@imaegoo.com> (https://github.com/imaegoo)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://twikoo.js.org",
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"@cap.js/server": "^4.0.5",
|
|
14
15
|
"@cloudbase/manager-node": "^3.9.0",
|
|
15
16
|
"@cloudbase/node-sdk": "^2.5.0",
|
|
16
17
|
"@imaegoo/node-ip2region": "^2.1.1",
|
|
@@ -25,9 +26,9 @@
|
|
|
25
26
|
"jsdom": "^16.4.0",
|
|
26
27
|
"marked": "^4.0.12",
|
|
27
28
|
"nodemailer": "^7.0.11",
|
|
28
|
-
"openai": "^6.45.0",
|
|
29
29
|
"pushoo": "latest",
|
|
30
30
|
"tencentcloud-sdk-nodejs": "^4.0.65",
|
|
31
|
-
"xml2js": "^0.6.0"
|
|
31
|
+
"xml2js": "^0.6.0",
|
|
32
|
+
"xsai": "^0.2.2"
|
|
32
33
|
}
|
|
33
34
|
}
|
package/utils/cap.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedded Cap.js (https://capjs.js.org) — no external Cap Standalone required.
|
|
3
|
+
* Storage adapters follow the same hooks as @cap.js/server / ink-battles.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const Cap = require('@cap.js/server')
|
|
7
|
+
const logger = require('./logger')
|
|
8
|
+
|
|
9
|
+
const CHALLENGE_OPTS = {
|
|
10
|
+
challengeCount: 50,
|
|
11
|
+
challengeSize: 32,
|
|
12
|
+
challengeDifficulty: 4,
|
|
13
|
+
expiresMs: 600000 // 10 min
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function memoryStorage () {
|
|
17
|
+
const challenges = new Map()
|
|
18
|
+
const tokens = new Map()
|
|
19
|
+
return {
|
|
20
|
+
challenges: {
|
|
21
|
+
store: async (token, data) => { challenges.set(token, data) },
|
|
22
|
+
read: async (token) => challenges.get(token) || null,
|
|
23
|
+
delete: async (token) => { challenges.delete(token) },
|
|
24
|
+
deleteExpired: async () => {
|
|
25
|
+
const now = Date.now()
|
|
26
|
+
for (const [k, v] of challenges) {
|
|
27
|
+
if (!v || v.expires < now) challenges.delete(k)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
tokens: {
|
|
32
|
+
store: async (key, expires) => { tokens.set(key, expires) },
|
|
33
|
+
get: async (key) => {
|
|
34
|
+
const exp = tokens.get(key)
|
|
35
|
+
if (!exp) return null
|
|
36
|
+
if (exp < Date.now()) {
|
|
37
|
+
tokens.delete(key)
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
return exp
|
|
41
|
+
},
|
|
42
|
+
delete: async (key) => { tokens.delete(key) },
|
|
43
|
+
deleteExpired: async () => {
|
|
44
|
+
const now = Date.now()
|
|
45
|
+
for (const [k, v] of tokens) {
|
|
46
|
+
if (v < now) tokens.delete(k)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** MongoDB (native driver) — Vercel / self-hosted mongo */
|
|
54
|
+
function mongoStorage (db) {
|
|
55
|
+
const challenges = () => db.collection('cap_challenges')
|
|
56
|
+
const tokens = () => db.collection('cap_tokens')
|
|
57
|
+
let indexed = false
|
|
58
|
+
const ensureIndex = () => {
|
|
59
|
+
if (indexed) return
|
|
60
|
+
indexed = true
|
|
61
|
+
challenges().createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(() => {})
|
|
62
|
+
tokens().createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).catch(() => {})
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
challenges: {
|
|
66
|
+
store: async (token, data) => {
|
|
67
|
+
ensureIndex()
|
|
68
|
+
await challenges().updateOne(
|
|
69
|
+
{ token },
|
|
70
|
+
{ $set: { token, challenge: data.challenge, expiresAt: new Date(data.expires) } },
|
|
71
|
+
{ upsert: true }
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
read: async (token) => {
|
|
75
|
+
const doc = await challenges().findOne({ token, expiresAt: { $gt: new Date() } })
|
|
76
|
+
return doc ? { challenge: doc.challenge, expires: doc.expiresAt.getTime() } : null
|
|
77
|
+
},
|
|
78
|
+
delete: async (token) => { await challenges().deleteOne({ token }) },
|
|
79
|
+
deleteExpired: async () => { await challenges().deleteMany({ expiresAt: { $lte: new Date() } }) }
|
|
80
|
+
},
|
|
81
|
+
tokens: {
|
|
82
|
+
store: async (key, expires) => {
|
|
83
|
+
ensureIndex()
|
|
84
|
+
await tokens().updateOne(
|
|
85
|
+
{ key },
|
|
86
|
+
{ $set: { key, expiresAt: new Date(expires) } },
|
|
87
|
+
{ upsert: true }
|
|
88
|
+
)
|
|
89
|
+
},
|
|
90
|
+
get: async (key) => {
|
|
91
|
+
const doc = await tokens().findOne({ key, expiresAt: { $gt: new Date() } })
|
|
92
|
+
return doc ? doc.expiresAt.getTime() : null
|
|
93
|
+
},
|
|
94
|
+
delete: async (key) => { await tokens().deleteOne({ key }) },
|
|
95
|
+
deleteExpired: async () => { await tokens().deleteMany({ expiresAt: { $lte: new Date() } }) }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** LokiJS — self-hosted default */
|
|
101
|
+
function lokiStorage (db) {
|
|
102
|
+
const getCol = (name) => {
|
|
103
|
+
let col = db.getCollection(name)
|
|
104
|
+
if (!col) col = db.addCollection(name, { unique: ['key'] })
|
|
105
|
+
return col
|
|
106
|
+
}
|
|
107
|
+
// challenges use token field; tokens use key field
|
|
108
|
+
const getChallengeCol = () => {
|
|
109
|
+
let col = db.getCollection('cap_challenges')
|
|
110
|
+
if (!col) col = db.addCollection('cap_challenges', { unique: ['token'] })
|
|
111
|
+
return col
|
|
112
|
+
}
|
|
113
|
+
const getTokenCol = () => getCol('cap_tokens')
|
|
114
|
+
return {
|
|
115
|
+
challenges: {
|
|
116
|
+
store: async (token, data) => {
|
|
117
|
+
const col = getChallengeCol()
|
|
118
|
+
const existing = col.findOne({ token })
|
|
119
|
+
const row = { token, challenge: data.challenge, expires: data.expires }
|
|
120
|
+
if (existing) {
|
|
121
|
+
Object.assign(existing, row)
|
|
122
|
+
col.update(existing)
|
|
123
|
+
} else col.insert(row)
|
|
124
|
+
},
|
|
125
|
+
read: async (token) => {
|
|
126
|
+
const doc = getChallengeCol().findOne({ token })
|
|
127
|
+
if (!doc || doc.expires < Date.now()) return null
|
|
128
|
+
return { challenge: doc.challenge, expires: doc.expires }
|
|
129
|
+
},
|
|
130
|
+
delete: async (token) => { getChallengeCol().findAndRemove({ token }) },
|
|
131
|
+
deleteExpired: async () => {
|
|
132
|
+
getChallengeCol().findAndRemove({ expires: { $lte: Date.now() } })
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
tokens: {
|
|
136
|
+
store: async (key, expires) => {
|
|
137
|
+
const col = getTokenCol()
|
|
138
|
+
const existing = col.findOne({ key })
|
|
139
|
+
const row = { key, expires }
|
|
140
|
+
if (existing) {
|
|
141
|
+
Object.assign(existing, row)
|
|
142
|
+
col.update(existing)
|
|
143
|
+
} else col.insert(row)
|
|
144
|
+
},
|
|
145
|
+
get: async (key) => {
|
|
146
|
+
const doc = getTokenCol().findOne({ key })
|
|
147
|
+
if (!doc || doc.expires < Date.now()) return null
|
|
148
|
+
return doc.expires
|
|
149
|
+
},
|
|
150
|
+
delete: async (key) => { getTokenCol().findAndRemove({ key }) },
|
|
151
|
+
deleteExpired: async () => {
|
|
152
|
+
getTokenCol().findAndRemove({ expires: { $lte: Date.now() } })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** CloudBase / TCB database */
|
|
159
|
+
function tcbStorage (db) {
|
|
160
|
+
const _ = db.command
|
|
161
|
+
return {
|
|
162
|
+
challenges: {
|
|
163
|
+
store: async (token, data) => {
|
|
164
|
+
const col = db.collection('cap_challenges')
|
|
165
|
+
try {
|
|
166
|
+
await col.doc(token).set({
|
|
167
|
+
token,
|
|
168
|
+
challenge: data.challenge,
|
|
169
|
+
expires: data.expires
|
|
170
|
+
})
|
|
171
|
+
} catch (e) {
|
|
172
|
+
// doc may exist
|
|
173
|
+
await col.doc(token).update({
|
|
174
|
+
challenge: data.challenge,
|
|
175
|
+
expires: data.expires
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
read: async (token) => {
|
|
180
|
+
try {
|
|
181
|
+
const res = await db.collection('cap_challenges').doc(token).get()
|
|
182
|
+
const doc = res.data && res.data[0]
|
|
183
|
+
if (!doc || doc.expires < Date.now()) return null
|
|
184
|
+
return { challenge: doc.challenge, expires: doc.expires }
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
delete: async (token) => {
|
|
190
|
+
try { await db.collection('cap_challenges').doc(token).remove() } catch (e) {}
|
|
191
|
+
},
|
|
192
|
+
deleteExpired: async () => {
|
|
193
|
+
try {
|
|
194
|
+
await db.collection('cap_challenges').where({ expires: _.lte(Date.now()) }).remove()
|
|
195
|
+
} catch (e) {}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
tokens: {
|
|
199
|
+
store: async (key, expires) => {
|
|
200
|
+
// CloudBase doc id cannot contain ':'
|
|
201
|
+
const id = key.replace(/:/g, '_')
|
|
202
|
+
try {
|
|
203
|
+
await db.collection('cap_tokens').doc(id).set({ key, expires })
|
|
204
|
+
} catch (e) {
|
|
205
|
+
await db.collection('cap_tokens').doc(id).update({ key, expires })
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
get: async (key) => {
|
|
209
|
+
try {
|
|
210
|
+
const id = key.replace(/:/g, '_')
|
|
211
|
+
const res = await db.collection('cap_tokens').doc(id).get()
|
|
212
|
+
const doc = res.data && res.data[0]
|
|
213
|
+
if (!doc || doc.expires < Date.now()) return null
|
|
214
|
+
return doc.expires
|
|
215
|
+
} catch (e) {
|
|
216
|
+
return null
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
delete: async (key) => {
|
|
220
|
+
try {
|
|
221
|
+
const id = key.replace(/:/g, '_')
|
|
222
|
+
await db.collection('cap_tokens').doc(id).remove()
|
|
223
|
+
} catch (e) {}
|
|
224
|
+
},
|
|
225
|
+
deleteExpired: async () => {
|
|
226
|
+
try {
|
|
227
|
+
await db.collection('cap_tokens').where({ expires: _.lte(Date.now()) }).remove()
|
|
228
|
+
} catch (e) {}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* EdgeOne Blob-style store: expects { get(key), set(key, value), del(key) }
|
|
236
|
+
* value is JSON-serializable.
|
|
237
|
+
*/
|
|
238
|
+
function kvStorage (kv) {
|
|
239
|
+
return {
|
|
240
|
+
challenges: {
|
|
241
|
+
store: async (token, data) => { await kv.set(`cap:c:${token}`, data) },
|
|
242
|
+
read: async (token) => {
|
|
243
|
+
const data = await kv.get(`cap:c:${token}`)
|
|
244
|
+
if (!data || data.expires < Date.now()) return null
|
|
245
|
+
return data
|
|
246
|
+
},
|
|
247
|
+
delete: async (token) => { await kv.del(`cap:c:${token}`) },
|
|
248
|
+
deleteExpired: async () => {}
|
|
249
|
+
},
|
|
250
|
+
tokens: {
|
|
251
|
+
store: async (key, expires) => { await kv.set(`cap:t:${key}`, { expires }) },
|
|
252
|
+
get: async (key) => {
|
|
253
|
+
const data = await kv.get(`cap:t:${key}`)
|
|
254
|
+
if (!data || data.expires < Date.now()) return null
|
|
255
|
+
return data.expires
|
|
256
|
+
},
|
|
257
|
+
delete: async (key) => { await kv.del(`cap:t:${key}`) },
|
|
258
|
+
deleteExpired: async () => {}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function createCap (storage) {
|
|
264
|
+
return new Cap({
|
|
265
|
+
noFSState: true,
|
|
266
|
+
storage: storage || memoryStorage()
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function createChallenge (cap) {
|
|
271
|
+
return cap.createChallenge(CHALLENGE_OPTS)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function redeemChallenge (cap, body) {
|
|
275
|
+
const token = body && body.token
|
|
276
|
+
const solutions = body && body.solutions
|
|
277
|
+
if (!token || !solutions || !Array.isArray(solutions)) {
|
|
278
|
+
return { success: false, error: 'Missing token or solutions' }
|
|
279
|
+
}
|
|
280
|
+
return cap.redeemChallenge({ token, solutions })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function validateToken (cap, token) {
|
|
284
|
+
if (!token) return false
|
|
285
|
+
try {
|
|
286
|
+
const { success } = await cap.validateToken(token)
|
|
287
|
+
return !!success
|
|
288
|
+
} catch (e) {
|
|
289
|
+
logger.error('Cap validateToken failed:', e)
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** CAPTCHA_PROVIDER=Cap and no CAP_API_ENDPOINT → use embedded Cap */
|
|
295
|
+
function isBuiltinCap (config) {
|
|
296
|
+
return config && config.CAPTCHA_PROVIDER === 'Cap' && !config.CAP_API_ENDPOINT
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
createCap,
|
|
301
|
+
memoryStorage,
|
|
302
|
+
mongoStorage,
|
|
303
|
+
lokiStorage,
|
|
304
|
+
tcbStorage,
|
|
305
|
+
kvStorage,
|
|
306
|
+
createChallenge,
|
|
307
|
+
redeemChallenge,
|
|
308
|
+
validateToken,
|
|
309
|
+
isBuiltinCap,
|
|
310
|
+
CHALLENGE_OPTS
|
|
311
|
+
}
|
package/utils/index.js
CHANGED
|
@@ -268,7 +268,7 @@ const fn = {
|
|
|
268
268
|
if (qqApiKey) {
|
|
269
269
|
headers.Authorization = `Bearer ${qqApiKey}`
|
|
270
270
|
}
|
|
271
|
-
const result = await axios.get(`https://v1.
|
|
271
|
+
const result = await axios.get(`https://v1.tqq.me/v1/qqname?qq=${qqNum}`, { headers })
|
|
272
272
|
if (result.data?.code === 200 && result.data?.data?.nick) {
|
|
273
273
|
return result.data.data.nick
|
|
274
274
|
}
|
|
@@ -366,11 +366,17 @@ const fn = {
|
|
|
366
366
|
throw new Error('极验验证码检测失败: ' + e.message)
|
|
367
367
|
}
|
|
368
368
|
},
|
|
369
|
-
async checkCapCaptcha ({ capToken, capSecretKey, capApiEndpoint }) {
|
|
369
|
+
async checkCapCaptcha ({ capToken, capSecretKey, capApiEndpoint, cap }) {
|
|
370
370
|
try {
|
|
371
|
-
//
|
|
371
|
+
// 内嵌 Cap:直接 validateToken,无需外部 Standalone
|
|
372
|
+
if (cap) {
|
|
373
|
+
const { validateToken } = require('./cap')
|
|
374
|
+
const ok = await validateToken(cap, capToken)
|
|
375
|
+
if (!ok) throw new Error('验证码错误')
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
// 外部 Cap Standalone:HTTP siteverify
|
|
372
379
|
const endpoint = capApiEndpoint.replace(/\/$/, '')
|
|
373
|
-
// Cap 的 siteverify 端点
|
|
374
380
|
const url = `${endpoint}/siteverify`
|
|
375
381
|
logger.log('Cap验证码验证URL:', url)
|
|
376
382
|
logger.log('Cap验证码验证参数:', { secret: capSecretKey ? '***' : undefined, response: capToken.substring(0, 20) + '...' })
|
|
@@ -426,9 +432,13 @@ const fn = {
|
|
|
426
432
|
baseConfig.GEETEST_CAPTCHA_ID = config.GEETEST_CAPTCHA_ID
|
|
427
433
|
}
|
|
428
434
|
|
|
429
|
-
//
|
|
435
|
+
// Cap:有外部 endpoint 则下发;否则标记 builtin,前端走 twikoo 事件代理
|
|
430
436
|
if (config.CAPTCHA_PROVIDER === 'Cap') {
|
|
431
|
-
|
|
437
|
+
if (config.CAP_API_ENDPOINT) {
|
|
438
|
+
baseConfig.CAP_API_ENDPOINT = config.CAP_API_ENDPOINT
|
|
439
|
+
} else {
|
|
440
|
+
baseConfig.CAP_BUILTIN = true
|
|
441
|
+
}
|
|
432
442
|
}
|
|
433
443
|
|
|
434
444
|
return {
|
package/utils/lib.js
CHANGED
|
@@ -73,10 +73,10 @@ module.exports = {
|
|
|
73
73
|
return xml2js
|
|
74
74
|
},
|
|
75
75
|
getOpenAIClient (config) {
|
|
76
|
-
const
|
|
77
|
-
const openaiClient =
|
|
76
|
+
const { createXSClient } = require('xsai') // xsai SDK
|
|
77
|
+
const openaiClient = createXSClient({
|
|
78
78
|
apiKey: config.LLM_API_KEY,
|
|
79
|
-
baseURL: config.LLM_API_ENDPOINT || 'https://api.deepseek.com'
|
|
79
|
+
baseURL: config.LLM_API_ENDPOINT || 'https://api.deepseek.com/v1'
|
|
80
80
|
})
|
|
81
81
|
return openaiClient
|
|
82
82
|
}
|
package/utils/spam.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
const {
|
|
2
2
|
getAkismetClient,
|
|
3
3
|
getCryptoJS,
|
|
4
|
-
getTencentcloud
|
|
5
|
-
getOpenAIClient
|
|
4
|
+
getTencentcloud
|
|
6
5
|
} = require('./lib')
|
|
7
6
|
const {
|
|
8
7
|
equalsMail
|
|
@@ -13,9 +12,6 @@ const CryptoJS = getCryptoJS()
|
|
|
13
12
|
const logger = require('./logger')
|
|
14
13
|
|
|
15
14
|
let tencentcloud
|
|
16
|
-
let openai
|
|
17
|
-
let _openaiApiKey
|
|
18
|
-
let _openaiEndpoint
|
|
19
15
|
|
|
20
16
|
function getTencentCloud () {
|
|
21
17
|
if (!tencentcloud) {
|
|
@@ -28,21 +24,6 @@ function getTencentCloud () {
|
|
|
28
24
|
return tencentcloud
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
function getOpenAI (config) {
|
|
32
|
-
if (isConfigChanged(config)) {
|
|
33
|
-
_openaiApiKey = config.LLM_API_KEY || ''
|
|
34
|
-
_openaiEndpoint = config.LLM_API_ENDPOINT || ''
|
|
35
|
-
openai = getOpenAIClient(config)
|
|
36
|
-
}
|
|
37
|
-
return openai
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function isConfigChanged (config) {
|
|
41
|
-
return !openai ||
|
|
42
|
-
_openaiApiKey !== (config.LLM_API_KEY || '') ||
|
|
43
|
-
_openaiEndpoint !== (config.LLM_API_ENDPOINT || '')
|
|
44
|
-
}
|
|
45
|
-
|
|
46
27
|
// 提取json结构的函数
|
|
47
28
|
function extractJson (rawText) {
|
|
48
29
|
if (!rawText) return ''
|
|
@@ -126,8 +107,6 @@ async function checkByLLM (comment, config) {
|
|
|
126
107
|
const maxRetries = Number(config.LLM_MAX_RETRIES) || 3
|
|
127
108
|
let lastError = ''
|
|
128
109
|
|
|
129
|
-
const openai = getOpenAI(config)
|
|
130
|
-
|
|
131
110
|
// 网络/Provider 异常或者格式校验不通过会进入重试逻辑
|
|
132
111
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
133
112
|
if (attempt > 1) {
|
|
@@ -142,13 +121,14 @@ async function checkByLLM (comment, config) {
|
|
|
142
121
|
messages = buildMessages(comment, lastError)
|
|
143
122
|
}
|
|
144
123
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
124
|
+
const { generateText } = require('xsai')
|
|
125
|
+
const chatCompletion = await generateText({
|
|
126
|
+
model: config.LLM_MODEL || 'deepseek-chat',
|
|
127
|
+
responseFormat: { type: 'json_object' },
|
|
148
128
|
messages
|
|
149
129
|
})
|
|
150
130
|
|
|
151
|
-
const rawText = chatCompletion.
|
|
131
|
+
const rawText = chatCompletion.text || ''
|
|
152
132
|
|
|
153
133
|
const extracted = extractJson(rawText)
|
|
154
134
|
const repaired = repairJson(extracted)
|