|
|
@@ -3,6 +3,98 @@ import crypto from 'crypto';
|
|
3
|
3
|
import config from '../../config/index.js';
|
|
4
|
4
|
import { enhanceFormsOfWords } from './enhanceFormsOfWords.js';
|
|
5
|
5
|
|
|
|
6
|
+const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
7
|
+
|
|
|
8
|
+const toPositiveInteger = (value, fallback) => {
|
|
|
9
|
+ const number = Number(value);
|
|
|
10
|
+ return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
|
11
|
+};
|
|
|
12
|
+
|
|
|
13
|
+const VOLCES_MIN_INTERVAL_MS = toPositiveInteger(config.aiGeneration?.volcesMinIntervalMs, 15000);
|
|
|
14
|
+const VOLCES_MAX_RETRIES = toPositiveInteger(config.aiGeneration?.volcesMaxRetries, 2);
|
|
|
15
|
+const VOLCES_TIMEOUT_MS = toPositiveInteger(config.aiGeneration?.volcesTimeoutMs, 90000);
|
|
|
16
|
+
|
|
|
17
|
+let volcesNextRequestAt = 0;
|
|
|
18
|
+let volcesQueue = Promise.resolve();
|
|
|
19
|
+
|
|
|
20
|
+function getAxiosStatus(error) {
|
|
|
21
|
+ return error?.response?.status || error?.status;
|
|
|
22
|
+}
|
|
|
23
|
+
|
|
|
24
|
+function getExternalErrorCode(error) {
|
|
|
25
|
+ return error?.response?.data?.error?.code || error?.code || '';
|
|
|
26
|
+}
|
|
|
27
|
+
|
|
|
28
|
+function isRateLimitError(error) {
|
|
|
29
|
+ return getAxiosStatus(error) === 429 || String(getExternalErrorCode(error)).includes('RateLimit');
|
|
|
30
|
+}
|
|
|
31
|
+
|
|
|
32
|
+function isModelUnavailableError(error) {
|
|
|
33
|
+ return getAxiosStatus(error) === 404 || String(getExternalErrorCode(error)).includes('InvalidEndpointOrModel');
|
|
|
34
|
+}
|
|
|
35
|
+
|
|
|
36
|
+function sanitizeExternalError(error) {
|
|
|
37
|
+ return {
|
|
|
38
|
+ status: getAxiosStatus(error),
|
|
|
39
|
+ code: getExternalErrorCode(error),
|
|
|
40
|
+ message: error?.response?.data?.error?.message || error?.message
|
|
|
41
|
+ };
|
|
|
42
|
+}
|
|
|
43
|
+
|
|
|
44
|
+function createAIProviderError(provider, model, error) {
|
|
|
45
|
+ const status = getAxiosStatus(error);
|
|
|
46
|
+ const code = getExternalErrorCode(error);
|
|
|
47
|
+ const messageParts = [`${provider} API failed`];
|
|
|
48
|
+
|
|
|
49
|
+ if (status) {
|
|
|
50
|
+ messageParts.push(`status ${status}`);
|
|
|
51
|
+ }
|
|
|
52
|
+ if (code) {
|
|
|
53
|
+ messageParts.push(code);
|
|
|
54
|
+ }
|
|
|
55
|
+ if (error?.message) {
|
|
|
56
|
+ messageParts.push(error.message);
|
|
|
57
|
+ }
|
|
|
58
|
+
|
|
|
59
|
+ const providerError = new Error(messageParts.join(': '));
|
|
|
60
|
+ providerError.name = 'AIProviderError';
|
|
|
61
|
+ providerError.provider = provider;
|
|
|
62
|
+ providerError.model = model;
|
|
|
63
|
+ providerError.status = status;
|
|
|
64
|
+ providerError.code = code;
|
|
|
65
|
+ providerError.isExternalAIProviderError = true;
|
|
|
66
|
+ providerError.isRateLimit = isRateLimitError(error);
|
|
|
67
|
+ providerError.isModelUnavailable = isModelUnavailableError(error);
|
|
|
68
|
+ providerError.details = sanitizeExternalError(error);
|
|
|
69
|
+ providerError.cause = error;
|
|
|
70
|
+ return providerError;
|
|
|
71
|
+}
|
|
|
72
|
+
|
|
|
73
|
+async function waitForVolcesSlot() {
|
|
|
74
|
+ const previousQueue = volcesQueue;
|
|
|
75
|
+
|
|
|
76
|
+ let release;
|
|
|
77
|
+ volcesQueue = new Promise(resolve => {
|
|
|
78
|
+ release = resolve;
|
|
|
79
|
+ });
|
|
|
80
|
+
|
|
|
81
|
+ await previousQueue.catch(() => {});
|
|
|
82
|
+ try {
|
|
|
83
|
+ const waitMs = Math.max(0, volcesNextRequestAt - Date.now());
|
|
|
84
|
+ if (waitMs > 0) {
|
|
|
85
|
+ console.warn(`火山云AI请求节流,等待${Math.ceil(waitMs / 1000)}秒`);
|
|
|
86
|
+ await sleep(waitMs);
|
|
|
87
|
+ }
|
|
|
88
|
+ volcesNextRequestAt = Date.now() + VOLCES_MIN_INTERVAL_MS;
|
|
|
89
|
+ } finally {
|
|
|
90
|
+ release();
|
|
|
91
|
+ }
|
|
|
92
|
+}
|
|
|
93
|
+
|
|
|
94
|
+function delayVolcesRequests(delayMs) {
|
|
|
95
|
+ volcesNextRequestAt = Math.max(volcesNextRequestAt, Date.now() + delayMs);
|
|
|
96
|
+}
|
|
|
97
|
+
|
|
6
|
98
|
/**
|
|
7
|
99
|
* AI平台接口类
|
|
8
|
100
|
* 定义了所有AI平台需要实现的方法
|
|
|
@@ -110,23 +202,35 @@ class VolcesAIProvider extends AIProvider {
|
|
110
|
202
|
};
|
|
111
|
203
|
}
|
|
112
|
204
|
|
|
113
|
|
- try {
|
|
114
|
|
- console.log(`火山云${this.version}`);
|
|
115
|
|
- //console.log(JSON.stringify(postJSON));
|
|
116
|
|
- // 移除encodeURI,直接使用原始URL
|
|
117
|
|
- const response = await axios.post(this.url, postJSON, { headers: this.headers });
|
|
118
|
|
- return response.data.choices[0].message.content;
|
|
119
|
|
- } catch (error) {
|
|
120
|
|
- console.error("VolcesAI API error:", error);
|
|
121
|
|
- // 添加更详细的错误日志
|
|
122
|
|
- if (error.response) {
|
|
123
|
|
- console.error("错误详情:", {
|
|
124
|
|
- status: error.response.status,
|
|
125
|
|
- data: error.response.data
|
|
|
205
|
+ let lastError = null;
|
|
|
206
|
+
|
|
|
207
|
+ for (let attempt = 1; attempt <= VOLCES_MAX_RETRIES + 1; attempt++) {
|
|
|
208
|
+ try {
|
|
|
209
|
+ await waitForVolcesSlot();
|
|
|
210
|
+ console.log(`火山云${this.version}`);
|
|
|
211
|
+ //console.log(JSON.stringify(postJSON));
|
|
|
212
|
+ // 移除encodeURI,直接使用原始URL
|
|
|
213
|
+ const response = await axios.post(this.url, postJSON, {
|
|
|
214
|
+ headers: this.headers,
|
|
|
215
|
+ timeout: VOLCES_TIMEOUT_MS
|
|
126
|
216
|
});
|
|
|
217
|
+ return response.data.choices[0].message.content;
|
|
|
218
|
+ } catch (error) {
|
|
|
219
|
+ lastError = error;
|
|
|
220
|
+
|
|
|
221
|
+ if (isRateLimitError(error) && attempt <= VOLCES_MAX_RETRIES) {
|
|
|
222
|
+ const backoffMs = Math.max(60000, VOLCES_MIN_INTERVAL_MS * (attempt + 1));
|
|
|
223
|
+ delayVolcesRequests(backoffMs);
|
|
|
224
|
+ console.warn(`火山云AI接口限流,第${attempt}次失败,等待${Math.ceil(backoffMs / 1000)}秒后重试`);
|
|
|
225
|
+ await sleep(backoffMs);
|
|
|
226
|
+ continue;
|
|
|
227
|
+ }
|
|
|
228
|
+
|
|
|
229
|
+ throw createAIProviderError('VolcesAI', this.model, error);
|
|
127
|
230
|
}
|
|
128
|
|
- throw error;
|
|
129
|
231
|
}
|
|
|
232
|
+
|
|
|
233
|
+ throw createAIProviderError('VolcesAI', this.model, lastError);
|
|
130
|
234
|
}
|
|
131
|
235
|
}
|
|
132
|
236
|
|
|
|
@@ -538,25 +642,39 @@ class AIProviderFactory {
|
|
538
|
642
|
case 'baidu-ernie-4.5-turbo-vl-32k-preview':
|
|
539
|
643
|
return new BaiduAIProvider("ernie-4.5-turbo-vl-32k-preview");
|
|
540
|
644
|
default:
|
|
541
|
|
- return new VolcesAIProvider("doubao-kimi-k2-250711"); // 默认使用火山云1.5
|
|
|
645
|
+ throw new Error(`Unsupported AI provider: ${provider}`);
|
|
542
|
646
|
}
|
|
543
|
647
|
}
|
|
544
|
648
|
}
|
|
545
|
649
|
|
|
|
650
|
+AIProviderFactory.getProvider(config.aiGeneration?.articleProvider || 'doubao-deepseek-v4-pro-260425');
|
|
|
651
|
+
|
|
546
|
652
|
/**
|
|
547
|
653
|
* 生成文章的主函数
|
|
548
|
654
|
* @param {string} content - 生成文章的提示内容
|
|
549
|
|
- * @param {string} provider - AI提供者名称,默认为'volces'
|
|
|
655
|
+ * @param {string} provider - 保留兼容旧调用,实际提供者统一读取 config.aiGeneration.articleProvider
|
|
550
|
656
|
* @returns {Promise<string>} - 返回生成的文章JSON字符串
|
|
551
|
657
|
*/
|
|
552
|
658
|
async function generateArticle(content, provider) {
|
|
|
659
|
+ const currentProvider = config.aiGeneration?.articleProvider || 'doubao-deepseek-v4-pro-260425';
|
|
|
660
|
+
|
|
553
|
661
|
try {
|
|
554
|
|
- const aiProvider = AIProviderFactory.getProvider(provider);
|
|
|
662
|
+ const aiProvider = AIProviderFactory.getProvider(currentProvider);
|
|
555
|
663
|
//console.log(JSON.stringify(aiProvider));
|
|
556
|
664
|
const result = await aiProvider.generateArticle(content);
|
|
557
|
665
|
return result;
|
|
558
|
666
|
} catch (error) {
|
|
559
|
|
- console.error("Generate article error:", error);
|
|
|
667
|
+ if (error?.isExternalAIProviderError) {
|
|
|
668
|
+ console.error("Generate article error:", {
|
|
|
669
|
+ provider: error.provider,
|
|
|
670
|
+ model: error.model,
|
|
|
671
|
+ status: error.status,
|
|
|
672
|
+ code: error.code,
|
|
|
673
|
+ message: error.message
|
|
|
674
|
+ });
|
|
|
675
|
+ } else {
|
|
|
676
|
+ console.error("Generate article error:", error);
|
|
|
677
|
+ }
|
|
560
|
678
|
throw error;
|
|
561
|
679
|
}
|
|
562
|
680
|
}
|
|
|
@@ -710,4 +828,4 @@ export default {
|
|
710
|
828
|
enhanceFormsOfWords,
|
|
711
|
829
|
validateAndFixJSON,
|
|
712
|
830
|
normalizeArticleFields
|
|
713
|
|
-};
|
|
|
831
|
+};
|