koishi-plugin-wordpress-notifier 2.9.1 → 2.9.4
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/README.md +83 -14
- package/lib/commands.d.ts +5 -1
- package/lib/commands.js +214 -280
- package/lib/index.d.ts +4 -0
- package/lib/index.js +120 -40
- package/lib/push.d.ts +4 -5
- package/lib/push.js +86 -242
- package/lib/schedule.d.ts +6 -0
- package/lib/schedule.js +239 -43
- package/lib/storage.d.ts +4 -0
- package/lib/storage.js +93 -54
- package/lib/wordpress.d.ts +78 -54
- package/lib/wordpress.js +372 -241
- package/package.json +40 -39
package/lib/wordpress.js
CHANGED
|
@@ -1,23 +1,103 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.WordPressService = void 0;
|
|
39
|
+
exports.WordPressService = exports.AuthCooldownError = void 0;
|
|
7
40
|
const axios_1 = __importDefault(require("axios"));
|
|
41
|
+
// 动态导入 p-limit
|
|
42
|
+
let pLimit = null;
|
|
43
|
+
// 异步初始化 p-limit
|
|
44
|
+
async function initPLimit() {
|
|
45
|
+
try {
|
|
46
|
+
const module = await Promise.resolve().then(() => __importStar(require('p-limit')));
|
|
47
|
+
pLimit = module.default || module;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
// 降级为内部队列实现
|
|
51
|
+
pLimit = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 立即初始化
|
|
55
|
+
initPLimit();
|
|
56
|
+
// 自定义认证冷却错误
|
|
57
|
+
class AuthCooldownError extends Error {
|
|
58
|
+
constructor(message) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = 'AuthCooldownError';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.AuthCooldownError = AuthCooldownError;
|
|
8
64
|
// 简化实现,避免继承 Service 类的问题
|
|
9
65
|
class WordPressService {
|
|
10
66
|
constructor(ctx, config) {
|
|
11
67
|
this.ctx = ctx;
|
|
12
|
-
this.isAuthenticated = true;
|
|
13
|
-
this.authFailureTime = 0;
|
|
14
|
-
this.AUTH_FAILURE_COOLDOWN = 5 * 60 * 1000;
|
|
15
|
-
this.
|
|
16
|
-
this.MAX_RETRIES = 2;
|
|
17
|
-
this.
|
|
68
|
+
this.isAuthenticated = true;
|
|
69
|
+
this.authFailureTime = 0;
|
|
70
|
+
this.AUTH_FAILURE_COOLDOWN = 5 * 60 * 1000;
|
|
71
|
+
this.interceptorId = null;
|
|
72
|
+
this.MAX_RETRIES = 2;
|
|
73
|
+
this.MAX_CONCURRENT_REQUESTS = 5;
|
|
74
|
+
this.activeRequests = 0;
|
|
75
|
+
this.requestQueue = [];
|
|
76
|
+
this.cache = new Map();
|
|
77
|
+
this.CACHE_DURATION = 10 * 1000; // 10s 缓存
|
|
78
|
+
this.cacheCleanupInterval = null;
|
|
79
|
+
// 监控统计
|
|
80
|
+
this.stats = {
|
|
81
|
+
totalRequests: 0,
|
|
82
|
+
successfulRequests: 0,
|
|
83
|
+
failedRequests: 0,
|
|
84
|
+
totalResponseTime: 0,
|
|
85
|
+
commandCount: 0
|
|
86
|
+
};
|
|
18
87
|
this.config = config;
|
|
88
|
+
// 初始化时使用内部队列实现,p-limit 会在异步初始化后替换
|
|
89
|
+
this.limit = null;
|
|
90
|
+
this.ctx.logger.info('初始化 WordPressService,使用内部队列实现');
|
|
19
91
|
this.client = this.createClient();
|
|
20
92
|
this.setupResponseInterceptor();
|
|
93
|
+
this.setupCacheCleanup();
|
|
94
|
+
// 等待 p-limit 初始化完成
|
|
95
|
+
initPLimit().then(() => {
|
|
96
|
+
if (pLimit) {
|
|
97
|
+
this.limit = pLimit(this.MAX_CONCURRENT_REQUESTS);
|
|
98
|
+
this.ctx.logger.info('p-limit 初始化成功,切换到 p-limit 实现');
|
|
99
|
+
}
|
|
100
|
+
});
|
|
21
101
|
}
|
|
22
102
|
createClient() {
|
|
23
103
|
const { wordpressUrl, authType, username, password } = this.config;
|
|
@@ -25,33 +105,6 @@ class WordPressService {
|
|
|
25
105
|
baseURL: wordpressUrl,
|
|
26
106
|
timeout: 10000,
|
|
27
107
|
});
|
|
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
108
|
// 配置认证
|
|
56
109
|
if (authType === 'basic' && username && password) {
|
|
57
110
|
client.defaults.auth = {
|
|
@@ -60,67 +113,219 @@ class WordPressService {
|
|
|
60
113
|
};
|
|
61
114
|
}
|
|
62
115
|
else if (authType === 'application-password' && username && password) {
|
|
63
|
-
// Use btoa for better compatibility instead of Buffer
|
|
64
116
|
const credentials = `${username}:${password}`;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
117
|
+
let encodedCredentials;
|
|
118
|
+
try {
|
|
119
|
+
if (typeof Buffer !== 'undefined') {
|
|
120
|
+
encodedCredentials = Buffer.from(credentials).toString('base64');
|
|
121
|
+
}
|
|
122
|
+
else if (typeof btoa === 'function') {
|
|
123
|
+
encodedCredentials = btoa(credentials);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
throw new Error('No Base64 encoding method available');
|
|
127
|
+
}
|
|
128
|
+
client.defaults.headers.common['Authorization'] = `Basic ${encodedCredentials}`;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.ctx.logger.error('Failed to configure authentication:', error);
|
|
132
|
+
}
|
|
69
133
|
}
|
|
70
134
|
return client;
|
|
71
135
|
}
|
|
72
136
|
// 设置响应拦截器
|
|
73
137
|
setupResponseInterceptor() {
|
|
74
|
-
if (this.
|
|
75
|
-
return;
|
|
138
|
+
if (this.interceptorId !== null) {
|
|
139
|
+
return;
|
|
76
140
|
}
|
|
77
|
-
this.client.interceptors.response.use((response) => {
|
|
78
|
-
// 认证成功,重置认证状态
|
|
141
|
+
this.interceptorId = this.client.interceptors.response.use((response) => {
|
|
79
142
|
this.isAuthenticated = true;
|
|
80
143
|
this.authFailureTime = 0;
|
|
81
144
|
return response;
|
|
82
145
|
}, (error) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
146
|
+
if (error.response) {
|
|
147
|
+
const status = error.response.status;
|
|
148
|
+
const url = error.config?.url || '';
|
|
149
|
+
if (status === 401 && (url.includes('/posts') || url.includes('/users'))) {
|
|
150
|
+
this.isAuthenticated = false;
|
|
151
|
+
this.authFailureTime = Date.now();
|
|
152
|
+
this.ctx.logger.warn('WordPress API authentication failed for public endpoint, blocking subsequent requests');
|
|
153
|
+
}
|
|
154
|
+
else if (status === 403) {
|
|
155
|
+
this.ctx.logger.warn(`WordPress API permission denied for resource: ${url}`);
|
|
156
|
+
}
|
|
88
157
|
}
|
|
89
158
|
return Promise.reject(error);
|
|
90
159
|
});
|
|
91
|
-
|
|
160
|
+
}
|
|
161
|
+
// 移除响应拦截器
|
|
162
|
+
removeResponseInterceptor() {
|
|
163
|
+
if (this.interceptorId !== null) {
|
|
164
|
+
this.client.interceptors.response.eject(this.interceptorId);
|
|
165
|
+
this.interceptorId = null;
|
|
166
|
+
}
|
|
92
167
|
}
|
|
93
168
|
// 检查认证状态
|
|
94
169
|
checkAuthStatus() {
|
|
95
|
-
// 如果未认证且在冷却期内,阻止请求
|
|
96
170
|
if (!this.isAuthenticated && (Date.now() - this.authFailureTime) < this.AUTH_FAILURE_COOLDOWN) {
|
|
97
171
|
this.ctx.logger.warn('Skipping request due to authentication failure cooldown');
|
|
98
172
|
return false;
|
|
99
173
|
}
|
|
100
|
-
// 冷却期过后,尝试重新认证
|
|
101
174
|
if (!this.isAuthenticated && (Date.now() - this.authFailureTime) >= this.AUTH_FAILURE_COOLDOWN) {
|
|
102
175
|
this.isAuthenticated = true;
|
|
103
176
|
this.ctx.logger.info('Authentication failure cooldown expired, allowing new requests');
|
|
104
177
|
}
|
|
105
178
|
return true;
|
|
106
179
|
}
|
|
107
|
-
//
|
|
108
|
-
async
|
|
180
|
+
// 执行请求(带并发控制和重试)
|
|
181
|
+
async executeWithConcurrencyControl(requestFn, config) {
|
|
109
182
|
if (!this.checkAuthStatus()) {
|
|
110
|
-
throw new
|
|
183
|
+
throw new AuthCooldownError('Authentication failed and in cooldown period');
|
|
111
184
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
185
|
+
// 使用 p-limit 或内部队列实现
|
|
186
|
+
if (this.limit) {
|
|
187
|
+
return this.limit(async () => {
|
|
188
|
+
return this.executeWithRetry(requestFn, config);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// 使用内部队列实现
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const executeRequest = async () => {
|
|
195
|
+
try {
|
|
196
|
+
this.activeRequests++;
|
|
197
|
+
const result = await this.executeWithRetry(requestFn, config);
|
|
198
|
+
resolve(result);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
reject(error);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
this.activeRequests--;
|
|
205
|
+
this.processNextRequest();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
if (this.activeRequests < this.MAX_CONCURRENT_REQUESTS) {
|
|
209
|
+
executeRequest();
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
this.ctx.logger.info(`Request queued. Active: ${this.activeRequests}, Queue: ${this.requestQueue.length + 1}`);
|
|
213
|
+
this.requestQueue.push(executeRequest);
|
|
120
214
|
}
|
|
121
215
|
});
|
|
122
|
-
|
|
123
|
-
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// 执行请求(带重试)
|
|
219
|
+
async executeWithRetry(requestFn, config) {
|
|
220
|
+
let lastError;
|
|
221
|
+
for (let attempt = 0; attempt <= this.MAX_RETRIES; attempt++) {
|
|
222
|
+
const startTime = Date.now();
|
|
223
|
+
this.stats.totalRequests++;
|
|
224
|
+
try {
|
|
225
|
+
const result = await requestFn();
|
|
226
|
+
const endTime = Date.now();
|
|
227
|
+
this.stats.successfulRequests++;
|
|
228
|
+
this.stats.totalResponseTime += endTime - startTime;
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
const endTime = Date.now();
|
|
233
|
+
this.stats.failedRequests++;
|
|
234
|
+
this.stats.totalResponseTime += endTime - startTime;
|
|
235
|
+
lastError = error;
|
|
236
|
+
// 仅对 GET 请求进行重试,防止重复写操作
|
|
237
|
+
const isGetRequest = !config || (config.method?.toUpperCase() ?? 'GET') === 'GET';
|
|
238
|
+
if (attempt < this.MAX_RETRIES && this.isNetworkError(error) && isGetRequest) {
|
|
239
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
240
|
+
this.ctx.logger.info(`Retrying request (${attempt + 1}/${this.MAX_RETRIES}) after ${delay}ms...`);
|
|
241
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
throw lastError;
|
|
249
|
+
}
|
|
250
|
+
// 处理下一个请求
|
|
251
|
+
processNextRequest() {
|
|
252
|
+
if (this.requestQueue.length > 0 && this.activeRequests < this.MAX_CONCURRENT_REQUESTS) {
|
|
253
|
+
const nextRequest = this.requestQueue.shift();
|
|
254
|
+
if (nextRequest) {
|
|
255
|
+
this.ctx.logger.info(`Processing next request. Active: ${this.activeRequests}, Queue: ${this.requestQueue.length}`);
|
|
256
|
+
nextRequest();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// 检查是否为网络错误
|
|
261
|
+
isNetworkError(error) {
|
|
262
|
+
const axiosError = error;
|
|
263
|
+
return !axiosError.response && (axiosError.code === 'ECONNABORTED' ||
|
|
264
|
+
axiosError.code === 'ETIMEDOUT' ||
|
|
265
|
+
axiosError.code === 'ECONNREFUSED' ||
|
|
266
|
+
axiosError.code === 'NETWORK_ERROR');
|
|
267
|
+
}
|
|
268
|
+
// 获取缓存
|
|
269
|
+
getCache(key) {
|
|
270
|
+
const item = this.cache.get(key);
|
|
271
|
+
if (item && (Date.now() - item.timestamp) < this.CACHE_DURATION) {
|
|
272
|
+
return item.data;
|
|
273
|
+
}
|
|
274
|
+
this.cache.delete(key);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
// 设置缓存
|
|
278
|
+
setCache(key, data) {
|
|
279
|
+
this.cache.set(key, {
|
|
280
|
+
data,
|
|
281
|
+
timestamp: Date.now()
|
|
282
|
+
});
|
|
283
|
+
// 清理过期缓存
|
|
284
|
+
this.cleanupCache();
|
|
285
|
+
}
|
|
286
|
+
// 设置缓存定时清理
|
|
287
|
+
setupCacheCleanup() {
|
|
288
|
+
// 每30秒清理一次过期缓存
|
|
289
|
+
this.cacheCleanupInterval = setInterval(() => {
|
|
290
|
+
this.cleanupCache();
|
|
291
|
+
}, 30 * 1000);
|
|
292
|
+
}
|
|
293
|
+
// 清理过期缓存
|
|
294
|
+
cleanupCache() {
|
|
295
|
+
const now = Date.now();
|
|
296
|
+
let cleanedCount = 0;
|
|
297
|
+
for (const [key, item] of this.cache.entries()) {
|
|
298
|
+
if (now - item.timestamp >= this.CACHE_DURATION) {
|
|
299
|
+
this.cache.delete(key);
|
|
300
|
+
cleanedCount++;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (cleanedCount > 0) {
|
|
304
|
+
this.ctx.logger.debug(`Cleaned ${cleanedCount} expired cache items`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// 获取最新文章
|
|
308
|
+
async getLatestPosts(perPage = 10, page = 1) {
|
|
309
|
+
this.stats.commandCount++;
|
|
310
|
+
const cacheKey = `latest-posts-${perPage}-${page}`;
|
|
311
|
+
const cachedData = this.getCache(cacheKey);
|
|
312
|
+
if (cachedData) {
|
|
313
|
+
return cachedData;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
// 简化 config,只保留 params
|
|
317
|
+
const requestParams = {
|
|
318
|
+
per_page: Math.min(perPage, 100),
|
|
319
|
+
page: Math.max(1, page),
|
|
320
|
+
orderby: 'date',
|
|
321
|
+
order: 'desc',
|
|
322
|
+
_embed: true
|
|
323
|
+
};
|
|
324
|
+
// 修复:axios.get 第二个参数只传 params
|
|
325
|
+
const response = await this.executeWithConcurrencyControl(() => this.client.get(`${this.config.apiEndpoint || '/wp-json/wp/v2'}/posts`, {
|
|
326
|
+
params: requestParams
|
|
327
|
+
}), { method: 'GET', params: requestParams }); // 仅传递必要的 config 信息
|
|
328
|
+
const posts = response.data.map((post) => ({
|
|
124
329
|
id: post.id,
|
|
125
330
|
date: post.date,
|
|
126
331
|
modified: post.modified,
|
|
@@ -137,6 +342,8 @@ class WordPressService {
|
|
|
137
342
|
author_name: post._embedded?.author?.[0]?.name || '未知作者',
|
|
138
343
|
featured_image: post._embedded?.['wp:featuredmedia']?.[0]?.source_url
|
|
139
344
|
}));
|
|
345
|
+
this.setCache(cacheKey, posts);
|
|
346
|
+
return posts;
|
|
140
347
|
}
|
|
141
348
|
catch (error) {
|
|
142
349
|
this.logError('Failed to get latest posts:', error);
|
|
@@ -145,17 +352,23 @@ class WordPressService {
|
|
|
145
352
|
}
|
|
146
353
|
// 获取文章详情
|
|
147
354
|
async getPostById(id) {
|
|
148
|
-
|
|
149
|
-
|
|
355
|
+
this.stats.commandCount++;
|
|
356
|
+
const cacheKey = `post-${id}`;
|
|
357
|
+
const cachedData = this.getCache(cacheKey);
|
|
358
|
+
if (cachedData) {
|
|
359
|
+
return cachedData;
|
|
150
360
|
}
|
|
151
361
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
362
|
+
// 简化 config,只保留 params
|
|
363
|
+
const requestParams = {
|
|
364
|
+
_embed: true
|
|
365
|
+
};
|
|
366
|
+
// 修复:axios.get 第二个参数只传 params
|
|
367
|
+
const response = await this.executeWithConcurrencyControl(() => this.client.get(`${this.config.apiEndpoint || '/wp-json/wp/v2'}/posts/${id}`, {
|
|
368
|
+
params: requestParams
|
|
369
|
+
}), { method: 'GET', params: requestParams }); // 仅传递必要的 config 信息
|
|
157
370
|
const post = response.data;
|
|
158
|
-
|
|
371
|
+
const result = {
|
|
159
372
|
id: post.id,
|
|
160
373
|
date: post.date,
|
|
161
374
|
modified: post.modified,
|
|
@@ -172,129 +385,117 @@ class WordPressService {
|
|
|
172
385
|
author_name: post._embedded?.author?.[0]?.name || '未知作者',
|
|
173
386
|
featured_image: post._embedded?.['wp:featuredmedia']?.[0]?.source_url
|
|
174
387
|
};
|
|
388
|
+
this.setCache(cacheKey, result);
|
|
389
|
+
return result;
|
|
175
390
|
}
|
|
176
391
|
catch (error) {
|
|
177
392
|
this.logError(`Failed to get post ${id}:`, error);
|
|
178
393
|
throw error;
|
|
179
394
|
}
|
|
180
395
|
}
|
|
181
|
-
//
|
|
182
|
-
async
|
|
183
|
-
|
|
184
|
-
|
|
396
|
+
// 获取最新注册的用户
|
|
397
|
+
async getLatestUsers(perPage = 10, page = 1) {
|
|
398
|
+
this.stats.commandCount++;
|
|
399
|
+
const cacheKey = `latest-users-${perPage}-${page}`;
|
|
400
|
+
const cachedData = this.getCache(cacheKey);
|
|
401
|
+
if (cachedData) {
|
|
402
|
+
return cachedData;
|
|
185
403
|
}
|
|
186
404
|
try {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
405
|
+
// 获取最新注册的用户
|
|
406
|
+
const requestParams = {
|
|
407
|
+
per_page: Math.min(perPage, 100),
|
|
408
|
+
page: Math.max(1, page),
|
|
409
|
+
orderby: 'registered_date',
|
|
410
|
+
order: 'desc'
|
|
411
|
+
};
|
|
412
|
+
const response = await this.executeWithConcurrencyControl(() => this.client.get(`${this.config.apiEndpoint || '/wp-json/wp/v2'}/users`, {
|
|
413
|
+
params: requestParams
|
|
414
|
+
}), { method: 'GET', params: requestParams });
|
|
415
|
+
const users = response.data.map((user) => ({
|
|
193
416
|
id: user.id,
|
|
194
417
|
name: user.name,
|
|
195
|
-
|
|
418
|
+
registered_date: user.registered_date,
|
|
196
419
|
email: user.email,
|
|
197
|
-
|
|
198
|
-
updated_date: user.updated_date || user.modified || user.register_date || user.registered_date,
|
|
199
|
-
display_name: user.display_name || user.name
|
|
420
|
+
link: user.link
|
|
200
421
|
}));
|
|
422
|
+
this.setCache(cacheKey, users);
|
|
423
|
+
return users;
|
|
201
424
|
}
|
|
202
425
|
catch (error) {
|
|
203
|
-
this.logError('Failed to get users:', error);
|
|
426
|
+
this.logError('Failed to get latest users:', error);
|
|
204
427
|
throw error;
|
|
205
428
|
}
|
|
206
429
|
}
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
430
|
+
// 获取服务状态
|
|
431
|
+
getStatus() {
|
|
432
|
+
const successRate = this.stats.totalRequests > 0
|
|
433
|
+
? (this.stats.successfulRequests / this.stats.totalRequests * 100).toFixed(2)
|
|
434
|
+
: '0.00';
|
|
435
|
+
const avgResponseTime = this.stats.successfulRequests > 0
|
|
436
|
+
? (this.stats.totalResponseTime / this.stats.successfulRequests).toFixed(2)
|
|
437
|
+
: '0.00';
|
|
438
|
+
return {
|
|
439
|
+
wordpressUrl: this.config.wordpressUrl,
|
|
440
|
+
apiEndpoint: this.config.apiEndpoint || '/wp-json/wp/v2',
|
|
441
|
+
isAuthenticated: this.isAuthenticated,
|
|
442
|
+
inCooldown: !this.isAuthenticated && (Date.now() - this.authFailureTime) < this.AUTH_FAILURE_COOLDOWN,
|
|
443
|
+
cooldownTimeRemaining: Math.max(0, Math.floor((this.AUTH_FAILURE_COOLDOWN - (Date.now() - this.authFailureTime)) / 1000)),
|
|
444
|
+
stats: {
|
|
445
|
+
totalRequests: this.stats.totalRequests,
|
|
446
|
+
successfulRequests: this.stats.successfulRequests,
|
|
447
|
+
failedRequests: this.stats.failedRequests,
|
|
448
|
+
successRate: `${successRate}%`,
|
|
449
|
+
averageResponseTime: `${avgResponseTime}ms`,
|
|
450
|
+
commandCount: this.stats.commandCount
|
|
451
|
+
},
|
|
452
|
+
cache: {
|
|
453
|
+
size: this.cache.size,
|
|
454
|
+
duration: this.CACHE_DURATION / 1000,
|
|
455
|
+
cleanupInterval: 30 // 30秒
|
|
456
|
+
},
|
|
457
|
+
concurrency: {
|
|
458
|
+
max: this.MAX_CONCURRENT_REQUESTS
|
|
459
|
+
},
|
|
460
|
+
features: {
|
|
461
|
+
retryEnabled: true,
|
|
462
|
+
retryLimit: this.MAX_RETRIES,
|
|
463
|
+
cacheEnabled: true,
|
|
464
|
+
concurrencyControl: true
|
|
465
|
+
}
|
|
466
|
+
};
|
|
229
467
|
}
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
}));
|
|
468
|
+
// 清理资源
|
|
469
|
+
cleanup() {
|
|
470
|
+
this.removeResponseInterceptor();
|
|
471
|
+
this.cache.clear();
|
|
472
|
+
// 清理定时任务
|
|
473
|
+
if (this.cacheCleanupInterval) {
|
|
474
|
+
clearInterval(this.cacheCleanupInterval);
|
|
475
|
+
this.cacheCleanupInterval = null;
|
|
261
476
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
477
|
+
// 重置所有并发相关状态
|
|
478
|
+
this.requestQueue = [];
|
|
479
|
+
this.activeRequests = 0;
|
|
480
|
+
// 重置 p-limit(可选)
|
|
481
|
+
if (pLimit) {
|
|
482
|
+
this.limit = pLimit(this.MAX_CONCURRENT_REQUESTS);
|
|
265
483
|
}
|
|
484
|
+
this.ctx.logger.info('WordPressService resources cleaned up');
|
|
266
485
|
}
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
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
|
-
}
|
|
486
|
+
// 重置统计数据
|
|
487
|
+
resetStats() {
|
|
488
|
+
this.stats = {
|
|
489
|
+
totalRequests: 0,
|
|
490
|
+
successfulRequests: 0,
|
|
491
|
+
failedRequests: 0,
|
|
492
|
+
totalResponseTime: 0,
|
|
493
|
+
commandCount: 0
|
|
494
|
+
};
|
|
495
|
+
this.ctx.logger.info('Statistics reset');
|
|
294
496
|
}
|
|
295
497
|
// 脱敏日志记录
|
|
296
498
|
logError(message, error) {
|
|
297
|
-
// 检查并脱敏错误对象中的敏感信息
|
|
298
499
|
const sanitizedError = this.sanitizeError(error);
|
|
299
500
|
this.ctx.logger.error(message, sanitizedError);
|
|
300
501
|
}
|
|
@@ -302,9 +503,7 @@ class WordPressService {
|
|
|
302
503
|
sanitizeError(error) {
|
|
303
504
|
if (!error)
|
|
304
505
|
return error;
|
|
305
|
-
// 创建错误副本
|
|
306
506
|
const sanitized = { ...error };
|
|
307
|
-
// 脱敏 config 中的敏感信息
|
|
308
507
|
if (sanitized.config) {
|
|
309
508
|
sanitized.config = {
|
|
310
509
|
...sanitized.config,
|
|
@@ -315,7 +514,6 @@ class WordPressService {
|
|
|
315
514
|
} : undefined
|
|
316
515
|
};
|
|
317
516
|
}
|
|
318
|
-
// 脱敏 response 中的敏感信息
|
|
319
517
|
if (sanitized.response) {
|
|
320
518
|
sanitized.response = {
|
|
321
519
|
...sanitized.response,
|
|
@@ -324,72 +522,5 @@ class WordPressService {
|
|
|
324
522
|
}
|
|
325
523
|
return sanitized;
|
|
326
524
|
}
|
|
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
525
|
}
|
|
395
526
|
exports.WordPressService = WordPressService;
|