@anyul/koishi-plugin-rss 5.2.1 → 5.2.3
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/commands/error-handler.js +2 -5
- package/lib/commands/index.d.ts +17 -1
- package/lib/commands/index.js +388 -2
- package/lib/commands/subscription-edit.d.ts +7 -0
- package/lib/commands/subscription-edit.js +177 -0
- package/lib/commands/subscription-management.d.ts +12 -0
- package/lib/commands/subscription-management.js +176 -0
- package/lib/commands/utils.d.ts +13 -1
- package/lib/commands/utils.js +43 -2
- package/lib/config.js +19 -0
- package/lib/core/ai.d.ts +16 -2
- package/lib/core/ai.js +73 -6
- package/lib/core/feeder.d.ts +1 -1
- package/lib/core/feeder.js +238 -125
- package/lib/core/item-processor.d.ts +5 -0
- package/lib/core/item-processor.js +66 -136
- package/lib/core/notification-queue.d.ts +2 -0
- package/lib/core/notification-queue.js +80 -33
- package/lib/core/parser.js +12 -0
- package/lib/core/renderer.d.ts +15 -0
- package/lib/core/renderer.js +105 -16
- package/lib/index.js +28 -784
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types.d.ts +24 -0
- package/lib/utils/common.js +52 -3
- package/lib/utils/error-handler.d.ts +8 -0
- package/lib/utils/error-handler.js +27 -0
- package/lib/utils/error-tracker.js +24 -8
- package/lib/utils/fetcher.js +68 -9
- package/lib/utils/logger.d.ts +4 -2
- package/lib/utils/logger.js +144 -6
- package/lib/utils/media.js +3 -6
- package/lib/utils/sanitizer.d.ts +58 -0
- package/lib/utils/sanitizer.js +227 -0
- package/lib/utils/security.d.ts +75 -0
- package/lib/utils/security.js +312 -0
- package/lib/utils/structured-logger.js +3 -20
- package/package.json +2 -1
|
@@ -19,16 +19,28 @@ exports.setUser = setUser;
|
|
|
19
19
|
exports.TracePerformance = TracePerformance;
|
|
20
20
|
exports.wrapAsyncFunction = wrapAsyncFunction;
|
|
21
21
|
exports.withErrorTracking = withErrorTracking;
|
|
22
|
+
const error_handler_1 = require("./error-handler");
|
|
23
|
+
const logger_1 = require("./logger");
|
|
22
24
|
// 动态导入 Sentry,避免未安装时编译失败
|
|
23
25
|
let Sentry = null;
|
|
26
|
+
let sentryDependencyWarningShown = false;
|
|
24
27
|
try {
|
|
25
28
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
26
29
|
Sentry = require('@sentry/node');
|
|
27
30
|
}
|
|
28
31
|
catch {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
Sentry = null;
|
|
33
|
+
}
|
|
34
|
+
function warnMissingSentryDependency() {
|
|
35
|
+
if (sentryDependencyWarningShown)
|
|
36
|
+
return;
|
|
37
|
+
sentryDependencyWarningShown = true;
|
|
38
|
+
logger_1.logger.warn('[error-tracker] Sentry not installed. Error tracking will be disabled.');
|
|
39
|
+
logger_1.logger.warn('[error-tracker] To enable error tracking, install: npm install @sentry/node');
|
|
40
|
+
}
|
|
41
|
+
function logSentryInitError(error) {
|
|
42
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
43
|
+
logger_1.logger.error(`[error-tracker] Failed to initialize Sentry: ${errorMessage}`);
|
|
32
44
|
}
|
|
33
45
|
/**
|
|
34
46
|
* 错误追踪器类
|
|
@@ -43,7 +55,11 @@ class ErrorTracker {
|
|
|
43
55
|
* 初始化 Sentry
|
|
44
56
|
*/
|
|
45
57
|
init() {
|
|
46
|
-
if (!this.config.enabled || !this.config.dsn
|
|
58
|
+
if (!this.config.enabled || !this.config.dsn) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!Sentry) {
|
|
62
|
+
warnMissingSentryDependency();
|
|
47
63
|
return;
|
|
48
64
|
}
|
|
49
65
|
try {
|
|
@@ -72,7 +88,7 @@ class ErrorTracker {
|
|
|
72
88
|
this.initialized = true;
|
|
73
89
|
}
|
|
74
90
|
catch (error) {
|
|
75
|
-
|
|
91
|
+
logSentryInitError(error);
|
|
76
92
|
}
|
|
77
93
|
}
|
|
78
94
|
/**
|
|
@@ -190,7 +206,7 @@ let globalErrorTracker = null;
|
|
|
190
206
|
* 初始化全局错误追踪器
|
|
191
207
|
*/
|
|
192
208
|
function initErrorTracker(config) {
|
|
193
|
-
if (!globalErrorTracker) {
|
|
209
|
+
if (!globalErrorTracker || !globalErrorTracker.isInitialized()) {
|
|
194
210
|
globalErrorTracker = new ErrorTracker(config);
|
|
195
211
|
globalErrorTracker.init();
|
|
196
212
|
}
|
|
@@ -322,14 +338,14 @@ function withErrorTracking(fn, context, errorMessage) {
|
|
|
322
338
|
// 处理 Promise
|
|
323
339
|
if (result instanceof Promise) {
|
|
324
340
|
return result.catch((error) => {
|
|
325
|
-
trackError(error, context);
|
|
341
|
+
trackError((0, error_handler_1.normalizeError)(error, errorMessage || 'Unknown error'), context);
|
|
326
342
|
throw error;
|
|
327
343
|
});
|
|
328
344
|
}
|
|
329
345
|
return result;
|
|
330
346
|
}
|
|
331
347
|
catch (error) {
|
|
332
|
-
trackError(error, context);
|
|
348
|
+
trackError((0, error_handler_1.normalizeError)(error, errorMessage || 'Unknown error'), context);
|
|
333
349
|
throw error;
|
|
334
350
|
}
|
|
335
351
|
});
|
package/lib/utils/fetcher.js
CHANGED
|
@@ -6,8 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.createHttpFunction = exports.RequestManager = void 0;
|
|
7
7
|
const axios_1 = __importDefault(require("axios"));
|
|
8
8
|
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
9
|
+
const error_handler_1 = require("./error-handler");
|
|
10
|
+
const error_tracker_1 = require("./error-tracker");
|
|
9
11
|
const logger_1 = require("./logger");
|
|
10
12
|
const common_1 = require("./common");
|
|
13
|
+
const security_1 = require("./security");
|
|
11
14
|
// 简化版 RequestManager,仅用于普通 API 请求,大文件下载建议绕过
|
|
12
15
|
class RequestManager {
|
|
13
16
|
queue = [];
|
|
@@ -69,14 +72,42 @@ class RequestManager {
|
|
|
69
72
|
exports.RequestManager = RequestManager;
|
|
70
73
|
const createHttpFunction = (ctx, config, requestManager) => {
|
|
71
74
|
return async (url, arg, requestConfig = {}) => {
|
|
75
|
+
const isHeavyRequest = requestConfig.responseType === 'arraybuffer' || requestConfig.responseType === 'stream';
|
|
76
|
+
const requestType = isHeavyRequest ? 'heavy' : 'normal';
|
|
77
|
+
const requestTimeout = (arg.timeout || 60) * 1000;
|
|
78
|
+
const requestDebug = (0, logger_1.createDebugWithContext)(config, {
|
|
79
|
+
url,
|
|
80
|
+
requestType,
|
|
81
|
+
isHeavyRequest,
|
|
82
|
+
timeout: requestTimeout,
|
|
83
|
+
});
|
|
84
|
+
// URL 安全验证
|
|
85
|
+
try {
|
|
86
|
+
(0, security_1.validateUrlOrThrow)(url, (0, security_1.getSecurityOptions)(config));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof security_1.SecurityError) {
|
|
90
|
+
const normalizedError = (0, error_handler_1.normalizeError)(error);
|
|
91
|
+
requestDebug(`URL 安全验证失败: ${normalizedError.message}`, 'security', 'error', {
|
|
92
|
+
stage: 'security-validation',
|
|
93
|
+
});
|
|
94
|
+
(0, error_tracker_1.trackError)(normalizedError, {
|
|
95
|
+
url,
|
|
96
|
+
requestType,
|
|
97
|
+
isHeavyRequest,
|
|
98
|
+
stage: 'security-validation',
|
|
99
|
+
});
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
72
104
|
// 关键修改:如果检测到是大文件下载(responseType 为 arraybuffer 或 stream),绕过队列直接请求
|
|
73
105
|
// 这样避免视频下载阻塞 RSS 轮询,也避免因队列超时导致下载失败
|
|
74
|
-
const isHeavyRequest = requestConfig.responseType === 'arraybuffer' || requestConfig.responseType === 'stream';
|
|
75
106
|
const makeRequest = async () => {
|
|
76
107
|
// 基础配置
|
|
77
|
-
(
|
|
108
|
+
requestDebug(`[DEBUG_PROXY] fetcher makeRequest arg.proxyAgent: ${JSON.stringify(arg?.proxyAgent)}`, 'request', 'details');
|
|
78
109
|
let configObj = {
|
|
79
|
-
timeout:
|
|
110
|
+
timeout: requestTimeout,
|
|
80
111
|
headers: {
|
|
81
112
|
'User-Agent': config.net.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
82
113
|
},
|
|
@@ -95,10 +126,15 @@ const createHttpFunction = (ctx, config, requestManager) => {
|
|
|
95
126
|
port: config.net.proxyAgent.port,
|
|
96
127
|
auth: config.net.proxyAgent.auth?.enabled ? config.net.proxyAgent.auth : undefined
|
|
97
128
|
};
|
|
98
|
-
(
|
|
129
|
+
requestDebug(`[DEBUG_PROXY] fetcher 使用防御性全局代理`, 'request', 'details', {
|
|
130
|
+
proxyEnabled: true,
|
|
131
|
+
proxyUrl: config.net.proxyAgent.host
|
|
132
|
+
? `${config.net.proxyAgent.protocol}://${config.net.proxyAgent.host}:${config.net.proxyAgent.port}`
|
|
133
|
+
: '',
|
|
134
|
+
});
|
|
99
135
|
}
|
|
100
136
|
}
|
|
101
|
-
const proxyEnabled = currentProxyAgent?.enabled;
|
|
137
|
+
const proxyEnabled = Boolean(currentProxyAgent?.enabled);
|
|
102
138
|
let proxyUrl = '';
|
|
103
139
|
if (proxyEnabled && currentProxyAgent.host) {
|
|
104
140
|
proxyUrl = `${currentProxyAgent.protocol}://${currentProxyAgent.host}:${currentProxyAgent.port}`;
|
|
@@ -113,13 +149,17 @@ const createHttpFunction = (ctx, config, requestManager) => {
|
|
|
113
149
|
delete requestConfig.headers;
|
|
114
150
|
}
|
|
115
151
|
const finalConfig = { ...configObj, ...requestConfig };
|
|
152
|
+
const requestContext = {
|
|
153
|
+
proxyEnabled,
|
|
154
|
+
proxyUrl,
|
|
155
|
+
};
|
|
116
156
|
// 对于重请求,打印更详细的日志(包含代理状态)
|
|
117
157
|
if (isHeavyRequest) {
|
|
118
158
|
const proxyInfo = proxyEnabled ? ` [代理: ${proxyUrl}]` : ' [直连]';
|
|
119
|
-
(
|
|
159
|
+
requestDebug(`Heavy Request${proxyInfo}: ${url}`, 'request', 'details', requestContext);
|
|
120
160
|
}
|
|
121
161
|
else {
|
|
122
|
-
(
|
|
162
|
+
requestDebug(`Request: ${url}`, 'request', 'details', requestContext);
|
|
123
163
|
}
|
|
124
164
|
let retries = 3;
|
|
125
165
|
let lastError;
|
|
@@ -133,12 +173,31 @@ const createHttpFunction = (ctx, config, requestManager) => {
|
|
|
133
173
|
const status = error.response?.status || 'Unknown';
|
|
134
174
|
const errMsg = error.message || error.code || error;
|
|
135
175
|
if (retries > 0) {
|
|
136
|
-
(
|
|
176
|
+
requestDebug(`[Request Retry] 剩余 ${retries} 次: ${url} [Status: ${status}] ${errMsg}`, 'request', 'info', {
|
|
177
|
+
...requestContext,
|
|
178
|
+
retryCount: 3 - retries,
|
|
179
|
+
retriesRemaining: retries,
|
|
180
|
+
status,
|
|
181
|
+
});
|
|
137
182
|
await (0, common_1.sleep)(1500); // 增加重试间隔
|
|
138
183
|
}
|
|
139
184
|
}
|
|
140
185
|
}
|
|
141
|
-
|
|
186
|
+
const normalizedError = (0, error_handler_1.normalizeError)(lastError || new Error('Max retries reached'));
|
|
187
|
+
const status = lastError?.response?.status || 'Unknown';
|
|
188
|
+
requestDebug(`Request failed after retries: ${normalizedError.message}`, 'request', 'error', {
|
|
189
|
+
...requestContext,
|
|
190
|
+
status,
|
|
191
|
+
retriesRemaining: 0,
|
|
192
|
+
});
|
|
193
|
+
(0, error_tracker_1.trackError)(normalizedError, {
|
|
194
|
+
url,
|
|
195
|
+
requestType,
|
|
196
|
+
isHeavyRequest,
|
|
197
|
+
status,
|
|
198
|
+
...requestContext,
|
|
199
|
+
});
|
|
200
|
+
throw normalizedError;
|
|
142
201
|
};
|
|
143
202
|
if (isHeavyRequest) {
|
|
144
203
|
return makeRequest(); // 直接执行,不走队列
|
package/lib/utils/logger.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Logger } from 'koishi';
|
|
2
2
|
import { Config } from '../types';
|
|
3
3
|
declare const logger: Logger;
|
|
4
|
+
export type DebugLogType = "disable" | "error" | "info" | "details";
|
|
5
|
+
export declare function shouldLog(config: Config, type: DebugLogType): boolean;
|
|
4
6
|
/**
|
|
5
7
|
* 增强的调试日志函数
|
|
6
8
|
*
|
|
@@ -10,7 +12,7 @@ declare const logger: Logger;
|
|
|
10
12
|
* @param type - 日志级别
|
|
11
13
|
* @param context - 额外的上下文信息
|
|
12
14
|
*/
|
|
13
|
-
export declare function debug(config: Config, message: any, name?: string, type?:
|
|
15
|
+
export declare function debug(config: Config, message: any, name?: string, type?: DebugLogType, context?: Record<string, any>): void;
|
|
14
16
|
/**
|
|
15
17
|
* 便捷函数:记录错误日志
|
|
16
18
|
*/
|
|
@@ -31,5 +33,5 @@ export declare function debugInfo(config: Config, message: any, name?: string, c
|
|
|
31
33
|
* feedDebug('Processing feed', 'feeder', 'info')
|
|
32
34
|
* // 输出会自动包含 guildId 和 platform
|
|
33
35
|
*/
|
|
34
|
-
export declare function createDebugWithContext(config: Config, fixedContext: Record<string, any>): (message: any, name?: string, type?:
|
|
36
|
+
export declare function createDebugWithContext(config: Config, fixedContext: Record<string, any>): (message: any, name?: string, type?: DebugLogType, additionalContext?: Record<string, any>) => void;
|
|
35
37
|
export { logger };
|
package/lib/utils/logger.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.logger = void 0;
|
|
4
|
+
exports.shouldLog = shouldLog;
|
|
4
5
|
exports.debug = debug;
|
|
5
6
|
exports.debugError = debugError;
|
|
6
7
|
exports.debugInfo = debugInfo;
|
|
@@ -9,6 +10,132 @@ const koishi_1 = require("koishi");
|
|
|
9
10
|
const types_1 = require("../types");
|
|
10
11
|
const logger = new koishi_1.Logger('rss-owl');
|
|
11
12
|
exports.logger = logger;
|
|
13
|
+
function shouldLog(config, type) {
|
|
14
|
+
const typeLevel = types_1.debugLevel.findIndex(i => i === type);
|
|
15
|
+
if (typeLevel < 1)
|
|
16
|
+
return false;
|
|
17
|
+
const configLevel = types_1.debugLevel.findIndex(i => i === config.debug);
|
|
18
|
+
if (configLevel < 0)
|
|
19
|
+
return false;
|
|
20
|
+
return typeLevel <= configLevel;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 敏感信息模式定义
|
|
24
|
+
*/
|
|
25
|
+
const SENSITIVE_PATTERNS = [
|
|
26
|
+
// API Key 模式
|
|
27
|
+
{ pattern: /api[_-]?key["']?\s*[:=]\s*["']?([^"'&\s,}]+)/gi, replacement: 'api_key=***' },
|
|
28
|
+
// Bearer Token
|
|
29
|
+
{ pattern: /Bearer\s+([A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+)/gi, replacement: 'Bearer ***' },
|
|
30
|
+
// Basic Auth
|
|
31
|
+
{ pattern: /Basic\s+([A-Za-z0-9+/=]+)/gi, replacement: 'Basic ***' },
|
|
32
|
+
// 代理认证
|
|
33
|
+
{ pattern: /([a-zA-Z]+):\/\/([^:]+):([^@]+)@/gi, replacement: '$1://$2:***@' },
|
|
34
|
+
// 密码字段
|
|
35
|
+
{ pattern: /["']?password["']?\s*[:=]\s*["']?([^"'&\s,}]+)/gi, replacement: 'password=***' },
|
|
36
|
+
// 密钥字段
|
|
37
|
+
{ pattern: /["']?secret["']?\s*[:=]\s*["']?([^"'&\s,}]+)/gi, replacement: 'secret=***' },
|
|
38
|
+
{ pattern: /["']?token["']?\s*[:=]\s*["']?([^"'&\s,}]+)/gi, replacement: 'token=***' },
|
|
39
|
+
// AWS Access Key
|
|
40
|
+
{ pattern: /(AKIA|ABIA|ACCA|ASIA)[0-9A-Z]{16}/g, replacement: '***' },
|
|
41
|
+
// GitHub Token
|
|
42
|
+
{ pattern: /(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}/g, replacement: '***' },
|
|
43
|
+
// JWT Token (更精确的匹配)
|
|
44
|
+
{ pattern: /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, replacement: '***' },
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* 脱敏日志消息,移除敏感信息
|
|
48
|
+
*
|
|
49
|
+
* @param message - 原始消息(字符串或对象)
|
|
50
|
+
* @returns 脱敏后的消息
|
|
51
|
+
*/
|
|
52
|
+
function sanitizeLogMessage(message) {
|
|
53
|
+
if (typeof message === 'string') {
|
|
54
|
+
let sanitized = message;
|
|
55
|
+
for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
|
|
56
|
+
sanitized = sanitized.replace(pattern, replacement);
|
|
57
|
+
}
|
|
58
|
+
return sanitized;
|
|
59
|
+
}
|
|
60
|
+
if (message === null || message === undefined) {
|
|
61
|
+
return message;
|
|
62
|
+
}
|
|
63
|
+
if (message instanceof Error) {
|
|
64
|
+
const sanitizedError = new Error(sanitizeLogMessage(message.message));
|
|
65
|
+
sanitizedError.name = message.name;
|
|
66
|
+
return sanitizedError;
|
|
67
|
+
}
|
|
68
|
+
// 如果是对象,深度脱敏
|
|
69
|
+
if (typeof message === 'object') {
|
|
70
|
+
try {
|
|
71
|
+
// 先序列化再脱敏,然后反序列化
|
|
72
|
+
const jsonStr = JSON.stringify(message);
|
|
73
|
+
let sanitized = jsonStr;
|
|
74
|
+
for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
|
|
75
|
+
sanitized = sanitized.replace(pattern, replacement);
|
|
76
|
+
}
|
|
77
|
+
return JSON.parse(sanitized);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// 如果序列化失败,返回原始对象的脱敏版本
|
|
81
|
+
return sanitizeObject(message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return message;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 递归脱敏对象中的敏感字段
|
|
88
|
+
*/
|
|
89
|
+
function sanitizeObject(obj, depth = 0) {
|
|
90
|
+
// 防止无限递归
|
|
91
|
+
if (depth > 5 || obj === null || obj === undefined) {
|
|
92
|
+
return obj;
|
|
93
|
+
}
|
|
94
|
+
// 敏感字段列表
|
|
95
|
+
const sensitiveFields = new Set([
|
|
96
|
+
'password', 'passwd', 'secret', 'token', 'apiKey', 'api_key',
|
|
97
|
+
'apikey', 'accessToken', 'access_token', 'refreshToken', 'refresh_token',
|
|
98
|
+
'auth', 'authorization', 'credential', 'privateKey', 'private_key',
|
|
99
|
+
'sessionId', 'session_id', 'sessionid', 'cookie', 'x-api-key',
|
|
100
|
+
'x-api-key', 'bearer', 'basic'
|
|
101
|
+
]);
|
|
102
|
+
if (Array.isArray(obj)) {
|
|
103
|
+
return obj.map(item => sanitizeObject(item, depth + 1));
|
|
104
|
+
}
|
|
105
|
+
if (typeof obj === 'object') {
|
|
106
|
+
const result = {};
|
|
107
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
108
|
+
const lowerKey = key.toLowerCase();
|
|
109
|
+
if (sensitiveFields.has(lowerKey) || lowerKey.includes('secret') || lowerKey.includes('token')) {
|
|
110
|
+
result[key] = '***';
|
|
111
|
+
}
|
|
112
|
+
else if (typeof value === 'object') {
|
|
113
|
+
result[key] = sanitizeObject(value, depth + 1);
|
|
114
|
+
}
|
|
115
|
+
else if (typeof value === 'string') {
|
|
116
|
+
// 检查字符串值是否包含可能的 token
|
|
117
|
+
if (value.length > 30 && /[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/.test(value)) {
|
|
118
|
+
result[key] = '***';
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
result[key] = value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
result[key] = value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
return obj;
|
|
131
|
+
}
|
|
132
|
+
function emitLog(type, content) {
|
|
133
|
+
if (type === 'error') {
|
|
134
|
+
logger.error(content);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
logger.info(content);
|
|
138
|
+
}
|
|
12
139
|
/**
|
|
13
140
|
* 增强的调试日志函数
|
|
14
141
|
*
|
|
@@ -19,11 +146,16 @@ exports.logger = logger;
|
|
|
19
146
|
* @param context - 额外的上下文信息
|
|
20
147
|
*/
|
|
21
148
|
function debug(config, message, name = '', type = 'details', context) {
|
|
22
|
-
|
|
23
|
-
if (typeLevel < 1)
|
|
24
|
-
return;
|
|
25
|
-
if (typeLevel > types_1.debugLevel.findIndex(i => i == config.debug))
|
|
149
|
+
if (!shouldLog(config, type))
|
|
26
150
|
return;
|
|
151
|
+
// 检查是否启用日志脱敏(默认启用)
|
|
152
|
+
const sanitizeEnabled = config.logging?.sanitizeLogs !== false;
|
|
153
|
+
if (sanitizeEnabled) {
|
|
154
|
+
message = sanitizeLogMessage(message);
|
|
155
|
+
if (context) {
|
|
156
|
+
context = sanitizeObject(context);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
27
159
|
// 获取日志配置
|
|
28
160
|
const loggingConfig = config.logging || {};
|
|
29
161
|
// 格式化消息内容
|
|
@@ -31,6 +163,12 @@ function debug(config, message, name = '', type = 'details', context) {
|
|
|
31
163
|
if (typeof message === 'string') {
|
|
32
164
|
formattedMessage = message;
|
|
33
165
|
}
|
|
166
|
+
else if (message instanceof Error) {
|
|
167
|
+
formattedMessage = message.message || String(message);
|
|
168
|
+
}
|
|
169
|
+
else if (typeof message === 'function') {
|
|
170
|
+
formattedMessage = String(message);
|
|
171
|
+
}
|
|
34
172
|
else if (message === null || message === undefined) {
|
|
35
173
|
formattedMessage = String(message);
|
|
36
174
|
}
|
|
@@ -86,7 +224,7 @@ function debug(config, message, name = '', type = 'details', context) {
|
|
|
86
224
|
}
|
|
87
225
|
}
|
|
88
226
|
// 输出结构化日志
|
|
89
|
-
|
|
227
|
+
emitLog(type, JSON.stringify(logEntry));
|
|
90
228
|
}
|
|
91
229
|
else {
|
|
92
230
|
// 传统文本格式
|
|
@@ -102,7 +240,7 @@ function debug(config, message, name = '', type = 'details', context) {
|
|
|
102
240
|
.join(' ');
|
|
103
241
|
textOutput += ` | ${contextStr}`;
|
|
104
242
|
}
|
|
105
|
-
|
|
243
|
+
emitLog(type, textOutput);
|
|
106
244
|
}
|
|
107
245
|
}
|
|
108
246
|
/**
|
package/lib/utils/media.js
CHANGED
|
@@ -57,12 +57,9 @@ exports.getCacheDir = getCacheDir;
|
|
|
57
57
|
const writeCacheFile = async (fileUrl, config) => {
|
|
58
58
|
const cacheDir = (0, exports.getCacheDir)(config);
|
|
59
59
|
(0, logger_1.debug)(config, cacheDir, 'cacheDir', 'details');
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
let fileName = `${
|
|
63
|
-
while (fileList.find(i => i == fileName)) {
|
|
64
|
-
fileName = `${parseInt((Math.random() * 10000000).toString()).toString()}.${suffix}`;
|
|
65
|
-
}
|
|
60
|
+
let suffix = /(?<=^data:.+?\/).+?(?=;base64)/.exec(fileUrl)?.[0] || 'bin';
|
|
61
|
+
// 使用时间戳 + 随机数生成唯一文件名,避免竞态条件
|
|
62
|
+
let fileName = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}.${suffix}`;
|
|
66
63
|
let base64Data = fileUrl.replace(/^data:.+?;base64,/, "");
|
|
67
64
|
let filePath = `${cacheDir}/${fileName}`;
|
|
68
65
|
fs.writeFileSync(filePath, base64Data, 'base64');
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Config } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* 清理 HTML 内容,移除潜在恶意代码
|
|
4
|
+
*
|
|
5
|
+
* @param html - 要清理的 HTML 字符串
|
|
6
|
+
* @param config - 可选的额外配置
|
|
7
|
+
* @returns 清理后的 HTML 字符串
|
|
8
|
+
*/
|
|
9
|
+
export declare function sanitizeHtml(html: string, config?: Record<string, unknown>): string;
|
|
10
|
+
/**
|
|
11
|
+
* 清理并返回纯文本(用于需要提取文本的场景)
|
|
12
|
+
*
|
|
13
|
+
* @param html - 要清理的 HTML 字符串
|
|
14
|
+
* @returns 清理后的纯文本
|
|
15
|
+
*/
|
|
16
|
+
export declare function sanitizeToText(html: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* 清理 HTML 中的图片 URL
|
|
19
|
+
*
|
|
20
|
+
* @param html - 包含图片的 HTML 字符串
|
|
21
|
+
* @returns 清理后的 HTML(图片 URL 已验证)
|
|
22
|
+
*/
|
|
23
|
+
export declare function sanitizeImageUrls(html: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* 清理链接(a 标签)
|
|
26
|
+
*
|
|
27
|
+
* @param html - 包含链接的 HTML 字符串
|
|
28
|
+
* @returns 清理后的 HTML(链接已安全化)
|
|
29
|
+
*/
|
|
30
|
+
export declare function sanitizeLinks(html: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* 创建带有 HTML 清理功能的模板内容处理函数
|
|
33
|
+
*
|
|
34
|
+
* @param config - 配置对象
|
|
35
|
+
* @returns 清理函数
|
|
36
|
+
*/
|
|
37
|
+
export declare function createSanitizer(config: Config): {
|
|
38
|
+
/**
|
|
39
|
+
* 清理 HTML 内容
|
|
40
|
+
*/
|
|
41
|
+
sanitize: (html: string) => string;
|
|
42
|
+
/**
|
|
43
|
+
* 清理并返回纯文本
|
|
44
|
+
*/
|
|
45
|
+
sanitizeToText: (html: string) => string;
|
|
46
|
+
/**
|
|
47
|
+
* 清理图片
|
|
48
|
+
*/
|
|
49
|
+
sanitizeImages: (html: string) => string;
|
|
50
|
+
/**
|
|
51
|
+
* 清理链接
|
|
52
|
+
*/
|
|
53
|
+
sanitizeLinks: (html: string) => string;
|
|
54
|
+
/**
|
|
55
|
+
* 检查是否启用清理
|
|
56
|
+
*/
|
|
57
|
+
isEnabled: () => boolean;
|
|
58
|
+
};
|