koishi-plugin-wordpress-notifier 2.9.0 → 2.9.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.
@@ -0,0 +1,395 @@
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.WordPressService = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ // 简化实现,避免继承 Service 类的问题
9
+ class WordPressService {
10
+ constructor(ctx, config) {
11
+ this.ctx = ctx;
12
+ this.isAuthenticated = true; // 认证状态缓存
13
+ this.authFailureTime = 0; // 认证失败时间戳
14
+ this.AUTH_FAILURE_COOLDOWN = 5 * 60 * 1000; // 认证失败冷却时间(5分钟)
15
+ this.interceptorRegistered = false; // 拦截器注册状态
16
+ this.MAX_RETRIES = 2; // 最大重试次数
17
+ this.retryCounts = new WeakMap(); // 用于跟踪请求重试次数
18
+ this.config = config;
19
+ this.client = this.createClient();
20
+ this.setupResponseInterceptor();
21
+ }
22
+ createClient() {
23
+ const { wordpressUrl, authType, username, password } = this.config;
24
+ const client = axios_1.default.create({
25
+ baseURL: wordpressUrl,
26
+ timeout: 10000,
27
+ });
28
+ // 添加响应重试拦截器
29
+ client.interceptors.response.use((response) => {
30
+ // 请求成功,清除重试计数
31
+ this.retryCounts.delete(response.config);
32
+ return response;
33
+ }, async (error) => {
34
+ const { config } = error;
35
+ // 如果不是网络错误或超时,不重试
36
+ if (!error.code || !['ECONNABORTED', 'ETIMEDOUT', 'ECONNREFUSED', 'NETWORK_ERROR'].includes(error.code)) {
37
+ return Promise.reject(error);
38
+ }
39
+ // 检查是否已经达到最大重试次数
40
+ const currentRetryCount = this.retryCounts.get(config) || 0;
41
+ const newRetryCount = currentRetryCount + 1;
42
+ if (newRetryCount > this.MAX_RETRIES) {
43
+ // 达到最大重试次数,清除计数并拒绝
44
+ this.retryCounts.delete(config);
45
+ return Promise.reject(error);
46
+ }
47
+ // 更新重试计数
48
+ this.retryCounts.set(config, newRetryCount);
49
+ // 指数退避策略
50
+ const delay = Math.pow(2, newRetryCount - 1) * 1000;
51
+ this.ctx.logger.info(`Retrying request (${newRetryCount}/${this.MAX_RETRIES}) after ${delay}ms...`);
52
+ await new Promise(resolve => setTimeout(resolve, delay));
53
+ return client(config);
54
+ });
55
+ // 配置认证
56
+ if (authType === 'basic' && username && password) {
57
+ client.defaults.auth = {
58
+ username,
59
+ password
60
+ };
61
+ }
62
+ else if (authType === 'application-password' && username && password) {
63
+ // Use btoa for better compatibility instead of Buffer
64
+ const credentials = `${username}:${password}`;
65
+ const encodedCredentials = typeof btoa === 'function'
66
+ ? btoa(credentials)
67
+ : Buffer.from(credentials).toString('base64');
68
+ client.defaults.headers.common['Authorization'] = `Basic ${encodedCredentials}`;
69
+ }
70
+ return client;
71
+ }
72
+ // 设置响应拦截器
73
+ setupResponseInterceptor() {
74
+ if (this.interceptorRegistered) {
75
+ return; // 拦截器已经注册,避免重复注册
76
+ }
77
+ this.client.interceptors.response.use((response) => {
78
+ // 认证成功,重置认证状态
79
+ this.isAuthenticated = true;
80
+ this.authFailureTime = 0;
81
+ return response;
82
+ }, (error) => {
83
+ // 检测认证失败
84
+ if (error.response && (error.response.status === 401 || error.response.status === 403)) {
85
+ this.isAuthenticated = false;
86
+ this.authFailureTime = Date.now();
87
+ this.ctx.logger.warn('WordPress API authentication failed, blocking subsequent requests');
88
+ }
89
+ return Promise.reject(error);
90
+ });
91
+ this.interceptorRegistered = true;
92
+ }
93
+ // 检查认证状态
94
+ checkAuthStatus() {
95
+ // 如果未认证且在冷却期内,阻止请求
96
+ if (!this.isAuthenticated && (Date.now() - this.authFailureTime) < this.AUTH_FAILURE_COOLDOWN) {
97
+ this.ctx.logger.warn('Skipping request due to authentication failure cooldown');
98
+ return false;
99
+ }
100
+ // 冷却期过后,尝试重新认证
101
+ if (!this.isAuthenticated && (Date.now() - this.authFailureTime) >= this.AUTH_FAILURE_COOLDOWN) {
102
+ this.isAuthenticated = true;
103
+ this.ctx.logger.info('Authentication failure cooldown expired, allowing new requests');
104
+ }
105
+ return true;
106
+ }
107
+ // 获取最新文章
108
+ async getLatestPosts(perPage = 10, page = 1) {
109
+ if (!this.checkAuthStatus()) {
110
+ throw new Error('Authentication failed and in cooldown period');
111
+ }
112
+ try {
113
+ const response = await this.client.get(`${this.config.apiEndpoint || '/wp-json/wp/v2'}/posts`, {
114
+ params: {
115
+ per_page: Math.min(perPage, 100), // 限制每页最多100条
116
+ page: Math.max(1, page), // 确保页码至少为1
117
+ orderby: 'date',
118
+ order: 'desc',
119
+ _embed: true // 包含关联数据,如作者信息
120
+ }
121
+ });
122
+ // 处理响应数据,确保数据结构一致
123
+ return response.data.map((post) => ({
124
+ id: post.id,
125
+ date: post.date,
126
+ modified: post.modified,
127
+ slug: post.slug,
128
+ status: post.status,
129
+ type: post.type,
130
+ title: post.title,
131
+ content: post.content,
132
+ excerpt: post.excerpt,
133
+ author: post.author,
134
+ featured_media: post.featured_media,
135
+ link: post.link,
136
+ _embedded: post._embedded,
137
+ author_name: post._embedded?.author?.[0]?.name || '未知作者',
138
+ featured_image: post._embedded?.['wp:featuredmedia']?.[0]?.source_url
139
+ }));
140
+ }
141
+ catch (error) {
142
+ this.logError('Failed to get latest posts:', error);
143
+ throw error;
144
+ }
145
+ }
146
+ // 获取文章详情
147
+ async getPostById(id) {
148
+ if (!this.checkAuthStatus()) {
149
+ throw new Error('Authentication failed and in cooldown period');
150
+ }
151
+ try {
152
+ const response = await this.client.get(`${this.config.apiEndpoint || '/wp-json/wp/v2'}/posts/${id}`, {
153
+ params: {
154
+ _embed: true // 包含关联数据
155
+ }
156
+ });
157
+ const post = response.data;
158
+ return {
159
+ id: post.id,
160
+ date: post.date,
161
+ modified: post.modified,
162
+ slug: post.slug,
163
+ status: post.status,
164
+ type: post.type,
165
+ title: post.title,
166
+ content: post.content,
167
+ excerpt: post.excerpt,
168
+ author: post.author,
169
+ featured_media: post.featured_media,
170
+ link: post.link,
171
+ _embedded: post._embedded,
172
+ author_name: post._embedded?.author?.[0]?.name || '未知作者',
173
+ featured_image: post._embedded?.['wp:featuredmedia']?.[0]?.source_url
174
+ };
175
+ }
176
+ catch (error) {
177
+ this.logError(`Failed to get post ${id}:`, error);
178
+ throw error;
179
+ }
180
+ }
181
+ // 获取用户列表
182
+ async getUsers() {
183
+ if (!this.checkAuthStatus()) {
184
+ throw new Error('Authentication failed and in cooldown period');
185
+ }
186
+ try {
187
+ const response = await this.client.get(`${this.config.apiEndpoint || '/wp-json/wp/v2'}/users`, {
188
+ params: {
189
+ per_page: 100 // 限制最多100个用户
190
+ }
191
+ });
192
+ return response.data.map((user) => ({
193
+ id: user.id,
194
+ name: user.name,
195
+ username: user.username,
196
+ email: user.email,
197
+ registered_date: user.register_date || user.registered_date,
198
+ updated_date: user.updated_date || user.modified || user.register_date || user.registered_date,
199
+ display_name: user.display_name || user.name
200
+ }));
201
+ }
202
+ catch (error) {
203
+ this.logError('Failed to get users:', error);
204
+ throw error;
205
+ }
206
+ }
207
+ // 获取单个用户信息
208
+ async getUser(userId) {
209
+ if (!this.checkAuthStatus()) {
210
+ throw new Error('Authentication failed and in cooldown period');
211
+ }
212
+ try {
213
+ const response = await this.client.get(`${this.config.apiEndpoint || '/wp-json/wp/v2'}/users/${userId}`);
214
+ const user = response.data;
215
+ return {
216
+ id: user.id,
217
+ name: user.name,
218
+ username: user.username,
219
+ email: user.email,
220
+ registered_date: user.register_date || user.registered_date,
221
+ updated_date: user.updated_date || user.modified || user.register_date || user.registered_date,
222
+ display_name: user.display_name || user.name
223
+ };
224
+ }
225
+ catch (error) {
226
+ this.logError(`Failed to get user ${userId}:`, error);
227
+ return null;
228
+ }
229
+ }
230
+ // 搜索文章
231
+ async searchPosts(query, perPage = 10) {
232
+ if (!this.checkAuthStatus()) {
233
+ throw new Error('Authentication failed and in cooldown period');
234
+ }
235
+ try {
236
+ const response = await this.client.get(`${this.config.apiEndpoint || '/wp-json/wp/v2'}/posts`, {
237
+ params: {
238
+ search: query,
239
+ per_page: Math.min(perPage, 100),
240
+ orderby: 'relevance',
241
+ _embed: true
242
+ }
243
+ });
244
+ return response.data.map((post) => ({
245
+ id: post.id,
246
+ date: post.date,
247
+ modified: post.modified,
248
+ slug: post.slug,
249
+ status: post.status,
250
+ type: post.type,
251
+ title: post.title,
252
+ content: post.content,
253
+ excerpt: post.excerpt,
254
+ author: post.author,
255
+ featured_media: post.featured_media,
256
+ link: post.link,
257
+ _embedded: post._embedded,
258
+ author_name: post._embedded?.author?.[0]?.name || '未知作者',
259
+ featured_image: post._embedded?.['wp:featuredmedia']?.[0]?.source_url
260
+ }));
261
+ }
262
+ catch (error) {
263
+ this.logError('Failed to search posts:', error);
264
+ throw error;
265
+ }
266
+ }
267
+ // 获取分类列表
268
+ async getCategories() {
269
+ if (!this.checkAuthStatus()) {
270
+ throw new Error('Authentication failed and in cooldown period');
271
+ }
272
+ try {
273
+ const response = await this.client.get(`${this.config.apiEndpoint || '/wp-json/wp/v2'}/categories`);
274
+ return response.data;
275
+ }
276
+ catch (error) {
277
+ this.logError('Failed to get categories:', error);
278
+ throw error;
279
+ }
280
+ }
281
+ // 获取标签列表
282
+ async getTags() {
283
+ if (!this.checkAuthStatus()) {
284
+ throw new Error('Authentication failed and in cooldown period');
285
+ }
286
+ try {
287
+ const response = await this.client.get(`${this.config.apiEndpoint || '/wp-json/wp/v2'}/tags`);
288
+ return response.data;
289
+ }
290
+ catch (error) {
291
+ this.logError('Failed to get tags:', error);
292
+ throw error;
293
+ }
294
+ }
295
+ // 脱敏日志记录
296
+ logError(message, error) {
297
+ // 检查并脱敏错误对象中的敏感信息
298
+ const sanitizedError = this.sanitizeError(error);
299
+ this.ctx.logger.error(message, sanitizedError);
300
+ }
301
+ // 脱敏错误对象
302
+ sanitizeError(error) {
303
+ if (!error)
304
+ return error;
305
+ // 创建错误副本
306
+ const sanitized = { ...error };
307
+ // 脱敏 config 中的敏感信息
308
+ if (sanitized.config) {
309
+ sanitized.config = {
310
+ ...sanitized.config,
311
+ auth: sanitized.config.auth ? { username: '***', password: '***' } : undefined,
312
+ headers: sanitized.config.headers ? {
313
+ ...sanitized.config.headers,
314
+ Authorization: sanitized.config.headers.Authorization ? '***' : undefined
315
+ } : undefined
316
+ };
317
+ }
318
+ // 脱敏 response 中的敏感信息
319
+ if (sanitized.response) {
320
+ sanitized.response = {
321
+ ...sanitized.response,
322
+ config: sanitized.response.config ? this.sanitizeError(sanitized.response.config) : undefined
323
+ };
324
+ }
325
+ return sanitized;
326
+ }
327
+ // 重置认证状态
328
+ resetAuthStatus() {
329
+ this.isAuthenticated = true;
330
+ this.authFailureTime = 0;
331
+ this.ctx.logger.info('Authentication status reset');
332
+ }
333
+ // 获取认证状态
334
+ getAuthStatus() {
335
+ return this.isAuthenticated;
336
+ }
337
+ // 检查 WordPress 连接状态
338
+ async checkConnection() {
339
+ try {
340
+ // 尝试访问 WordPress 站点的根目录
341
+ const rootResponse = await this.client.get('/', {
342
+ timeout: 5000
343
+ });
344
+ // 尝试访问 WordPress REST API
345
+ const apiResponse = await this.client.get(`${this.config.apiEndpoint || '/wp-json/wp/v2'}/posts`, {
346
+ params: {
347
+ per_page: 1,
348
+ _embed: false
349
+ },
350
+ timeout: 5000
351
+ });
352
+ return {
353
+ success: true,
354
+ message: 'WordPress site is accessible and API is working',
355
+ details: {
356
+ rootStatus: rootResponse.status,
357
+ apiStatus: apiResponse.status,
358
+ apiVersion: this.config.apiEndpoint || 'wp/v2'
359
+ }
360
+ };
361
+ }
362
+ catch (error) {
363
+ this.logError('Connection check failed:', error);
364
+ if (error.code === 'ECONNREFUSED') {
365
+ return {
366
+ success: false,
367
+ message: 'Connection refused: WordPress site is not reachable'
368
+ };
369
+ }
370
+ else if (error.code === 'ETIMEDOUT') {
371
+ return {
372
+ success: false,
373
+ message: 'Connection timed out: WordPress site is slow or unresponsive'
374
+ };
375
+ }
376
+ else if (error.response?.status === 401 || error.response?.status === 403) {
377
+ return {
378
+ success: false,
379
+ message: 'Authentication failed: Invalid credentials or insufficient permissions'
380
+ };
381
+ }
382
+ else if (error.response?.status === 404) {
383
+ return {
384
+ success: false,
385
+ message: 'API endpoint not found: WordPress REST API may not be enabled'
386
+ };
387
+ }
388
+ return {
389
+ success: false,
390
+ message: `Connection failed: ${error.message || 'Unknown error'}`
391
+ };
392
+ }
393
+ }
394
+ }
395
+ exports.WordPressService = WordPressService;
package/package.json CHANGED
@@ -1,46 +1,39 @@
1
1
  {
2
2
  "name": "koishi-plugin-wordpress-notifier",
3
- "version": "2.9.0",
4
- "description": "WordPress 文章自动推送到 QQ",
3
+ "version": "2.9.1",
4
+ "description": "WordPress 文章自动推送插件",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
7
7
  "files": [
8
- "lib",
9
- "data"
8
+ "lib"
10
9
  ],
11
- "license": "MIT",
12
- "author": "kate522@88.com",
13
- "scripts": {
14
- "build": "tsc",
15
- "build:watch": "tsc --watch",
16
- "prepublishOnly": "npm run build"
17
- },
18
- "dependencies": {
19
- "koishi": "^4.18.10"
20
- },
21
- "devDependencies": {
22
- "@types/node": "^20.0.0",
23
- "typescript": "^5.0.0"
24
- },
25
- "peerDependencies": {
26
- "koishi": "^4.18.10"
27
- },
28
10
  "keywords": [
29
11
  "koishi",
30
12
  "plugin",
31
13
  "wordpress",
32
- "notifier",
33
- "qq"
14
+ "push"
34
15
  ],
35
- "repository": {
36
- "type": "git",
37
- "url": "git+https://github.com/Lexo0522/koishi-plugin-wordpress-notifier.git"
16
+ "author": {
17
+ "name": "kate522",
18
+ "email": "kate522@88.com",
19
+ "url": "https://github.com/Lexo0522/koishi-plugin-wordpress-notifier"
20
+ },
21
+ "license": "MIT",
22
+ "peerDependencies": {
23
+ "koishi": "^4.15.0"
24
+ },
25
+ "dependencies": {
26
+ "axios": "^1.13.5"
38
27
  },
39
- "homepage": "https://github.com/Lexo0522/koishi-plugin-wordpress-notifier#readme",
40
- "bugs": {
41
- "url": "https://github.com/Lexo0522/koishi-plugin-wordpress-notifier/issues"
28
+ "devDependencies": {
29
+ "jest": "^29.7.0",
30
+ "koishi": "^4.15.0",
31
+ "koishi-test-utils": "^3.0.0",
32
+ "typescript": "^5.0.0"
42
33
  },
43
- "engines": {
44
- "node": ">=14.0.0"
34
+ "scripts": {
35
+ "build": "tsc",
36
+ "dev": "tsc -w",
37
+ "test": "jest"
45
38
  }
46
39
  }