chengjie 4 月之前
父節點
當前提交
d2f273a8b7
共有 4 個文件被更改,包括 404 次插入103 次删除
  1. 369 0
      src/api/yjbdc/aiController.js
  2. 32 101
      src/api/yjbdc/yjbdcController.js
  3. 2 1
      src/config/index.js
  4. 1 1
      src/model/mps.js

+ 369 - 0
src/api/yjbdc/aiController.js

@@ -0,0 +1,369 @@
1
+import axios from 'axios';
2
+import config from '../../config/index.js';
3
+
4
+/**
5
+ * AI平台接口类
6
+ * 定义了所有AI平台需要实现的方法
7
+ */
8
+class AIProvider {
9
+    /**
10
+     * 生成文章
11
+     * @param {string} content - 生成文章的提示内容
12
+     * @returns {Promise<string>} - 返回生成的文章JSON字符串
13
+     */
14
+    async generateArticle(content) {
15
+        throw new Error('Method not implemented');
16
+    }
17
+}
18
+
19
+/**
20
+ * 火山云AI平台实现
21
+ */
22
+class VolcesAIProvider extends AIProvider {
23
+    /**
24
+     * 创建火山云AI提供者实例
25
+     * @param {string} version - 版本号,如'1.5'或'1.6'
26
+     */
27
+    constructor(version = '1-5') {
28
+        super();
29
+        
30
+        // 根据版本选择对应的API密钥和模型
31
+        const versionConfig = {
32
+            '1-5': {
33
+                apikey: config.huoshancloud.apikeyHLR,
34
+                model: "doubao-1-5-pro-32k-250115"
35
+            },
36
+            '1-6': {
37
+                apikey: config.huoshancloud.apikeyHLR,
38
+                model: "doubao-seed-1-6-250615"
39
+            },
40
+        };
41
+        
42
+        // 获取当前版本的配置,如果版本不存在则使用1.5版本
43
+        const currentConfig = versionConfig[version] || versionConfig['1-5'];
44
+        
45
+        this.version = version;
46
+        this.headers = {
47
+            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36',
48
+            "Authorization": "Bearer " + currentConfig.apikey,
49
+            "Content-Type": "application/json"
50
+        };
51
+        this.url = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";
52
+        this.model = currentConfig.model;
53
+    }
54
+
55
+    async generateArticle(content) {
56
+        const postJSON = {
57
+            "model": this.model,
58
+            "messages": [
59
+                {
60
+                    "role": "user",
61
+                    "content": content,
62
+                }
63
+            ]
64
+        };
65
+
66
+        try {
67
+            console.log(`火山云${this.version}`);
68
+            const response = await axios.post(encodeURI(this.url), postJSON, { headers: this.headers });
69
+            return response.data.choices[0].message.content;
70
+        } catch (error) {
71
+            console.error("VolcesAI API error:", error);
72
+            throw error;
73
+        }
74
+    }
75
+}
76
+
77
+// 为了保持向后兼容性,创建1.5和1.6版本的类别名
78
+class VolcesAIProvider1_5 extends VolcesAIProvider {
79
+    constructor() {
80
+        super('1-5');
81
+    }
82
+}
83
+
84
+class VolcesAIProvider1_6 extends VolcesAIProvider {
85
+    constructor() {
86
+        super('1-6');
87
+    }
88
+}
89
+
90
+/**
91
+ * OpenAI平台实现
92
+ */
93
+class OpenAIProvider extends AIProvider {
94
+    constructor() {
95
+        super();
96
+        this.headers = {
97
+            "Authorization": "Bearer " + config.openai.apikey,
98
+            "Content-Type": "application/json"
99
+        };
100
+        this.url = "https://api.openai.com/v1/chat/completions";
101
+        this.model = "gpt-3.5-turbo";
102
+    }
103
+
104
+    async generateArticle(content) {
105
+        const postJSON = {
106
+            "model": this.model,
107
+            "messages": [
108
+                {
109
+                    "role": "user",
110
+                    "content": content,
111
+                }
112
+            ]
113
+        };
114
+
115
+        try {
116
+            const response = await axios.post(this.url, postJSON, { headers: this.headers });
117
+            return response.data.choices[0].message.content;
118
+        } catch (error) {
119
+            console.error("OpenAI API error:", error);
120
+            throw error;
121
+        }
122
+    }
123
+}
124
+
125
+/**
126
+ * 百度文心一言平台实现
127
+ */
128
+class BaiduAIProvider extends AIProvider {
129
+    constructor() {
130
+        super();
131
+        this.headers = {
132
+            "Content-Type": "application/json"
133
+        };
134
+        this.url = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions";
135
+        this.model = "ernie-bot-4";
136
+        this.accessToken = null;
137
+    }
138
+
139
+    async getAccessToken() {
140
+        if (this.accessToken) return this.accessToken;
141
+        
142
+        const tokenUrl = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${config.baidu.apiKey}&client_secret=${config.baidu.secretKey}`;
143
+        
144
+        try {
145
+            const response = await axios.post(tokenUrl);
146
+            this.accessToken = response.data.access_token;
147
+            return this.accessToken;
148
+        } catch (error) {
149
+            console.error("Baidu access token error:", error);
150
+            throw error;
151
+        }
152
+    }
153
+
154
+    async generateArticle(content) {
155
+        const accessToken = await this.getAccessToken();
156
+        const apiUrl = `${this.url}?access_token=${accessToken}`;
157
+        
158
+        const postJSON = {
159
+            "model": this.model,
160
+            "messages": [
161
+                {
162
+                    "role": "user",
163
+                    "content": content,
164
+                }
165
+            ]
166
+        };
167
+
168
+        try {
169
+            const response = await axios.post(apiUrl, postJSON, { headers: this.headers });
170
+            return response.data.result;
171
+        } catch (error) {
172
+            console.error("Baidu AI API error:", error);
173
+            throw error;
174
+        }
175
+    }
176
+}
177
+
178
+/**
179
+ * AI提供者工厂类
180
+ * 根据配置创建不同的AI平台实例
181
+ */
182
+class AIProviderFactory {
183
+    /**
184
+     * 获取AI提供者实例
185
+     * @param {string} provider - AI提供者名称,如'volces', 'volces1-5', 'volces1-6', 'openai', 'baidu'
186
+     * @returns {AIProvider} - 返回对应的AI提供者实例
187
+     */
188
+    static getProvider(provider = 'volces1-5') {
189
+        const providerLower = provider.toLowerCase();
190
+        
191
+        // 处理火山云通用提供者格式:volces:版本号
192
+        if (providerLower.startsWith('volces:')) {
193
+            const version = providerLower.split(':')[1];
194
+            return new VolcesAIProvider(version);
195
+        }
196
+        
197
+        // 处理传统提供者名称
198
+        switch (providerLower) {
199
+            case 'volces':
200
+            case 'volces1-5':
201
+                return new VolcesAIProvider1_5('1-5');
202
+            case 'volces1-6':
203
+                return new VolcesAIProvider1_6('1-6');
204
+            case 'openai':
205
+                return new OpenAIProvider();
206
+            case 'baidu':
207
+                return new BaiduAIProvider();
208
+            default:
209
+                return new VolcesAIProvider('1-5'); // 默认使用火山云1.5
210
+        }
211
+    }
212
+}
213
+
214
+/**
215
+ * 生成文章的主函数
216
+ * @param {string} content - 生成文章的提示内容
217
+ * @param {string} provider - AI提供者名称,默认为'volces'
218
+ * @returns {Promise<string>} - 返回生成的文章JSON字符串
219
+ */
220
+export async function generateArticle(content, provider = 'volces1-5') {
221
+    try {
222
+        const aiProvider = AIProviderFactory.getProvider(provider);
223
+        const result = await aiProvider.generateArticle(content);
224
+        return result;
225
+    } catch (error) {
226
+        console.error("Generate article error:", error);
227
+        throw error;
228
+    }
229
+}
230
+
231
+/**
232
+ * 校验和修复JSON结构
233
+ * @param {string} jsonString - JSON字符串
234
+ * @returns {string} - 修复后的JSON字符串
235
+ */
236
+export function validateAndFixJSON(jsonString) {
237
+    try {
238
+        //console.log(jsonString);
239
+        // 解析JSON字符串为对象
240
+        let jsonObj = JSON.parse(jsonString);
241
+        
242
+        // 校验和修复Question数组中的每个问题对象
243
+        if (jsonObj.Question && Array.isArray(jsonObj.Question)) {
244
+            jsonObj.Question = jsonObj.Question.map(question => {
245
+                // 创建一个修复后的问题对象
246
+                const fixedQuestion = {};
247
+                
248
+                // 确保QuestionEnglish字段存在
249
+                if (question.QuestionEnglish) {
250
+                    fixedQuestion.QuestionEnglish = question.QuestionEnglish;
251
+                }
252
+                
253
+                // 检查QuestionChinese字段,如果不存在但有第二个QuestionEnglish,则使用它
254
+                if (question.QuestionChinese) {
255
+                    fixedQuestion.QuestionChinese = question.QuestionChinese;
256
+                } else if (Object.keys(question).filter(key => key === 'QuestionEnglish').length > 1) {
257
+                    // 找到第二个QuestionEnglish的值
258
+                    const keys = Object.keys(question);
259
+                    let foundFirst = false;
260
+                    for (const key of keys) {
261
+                        if (key === 'QuestionEnglish') {
262
+                            if (foundFirst) {
263
+                                fixedQuestion.QuestionChinese = question[key];
264
+                                break;
265
+                            }
266
+                            foundFirst = true;
267
+                        }
268
+                    }
269
+                }
270
+                
271
+                // 确保OptionsEnglish字段存在且为数组
272
+                if (question.OptionsEnglish && Array.isArray(question.OptionsEnglish)) {
273
+                    fixedQuestion.OptionsEnglish = question.OptionsEnglish;
274
+                } else {
275
+                    fixedQuestion.OptionsEnglish = ["A.", "B.", "C.", "D."];
276
+                }
277
+                
278
+                // 确保OptionsChinese字段存在且为数组
279
+                if (question.OptionsChinese && Array.isArray(question.OptionsChinese)) {
280
+                    fixedQuestion.OptionsChinese = question.OptionsChinese;
281
+                } else {
282
+                    fixedQuestion.OptionsChinese = ["A.", "B.", "C.", "D."];
283
+                }
284
+                
285
+                // 确保Answer字段存在
286
+                if (question.Answer) {
287
+                    fixedQuestion.Answer = question.Answer;
288
+                } else {
289
+                    fixedQuestion.Answer = "A";
290
+                }
291
+                
292
+                return fixedQuestion;
293
+            });
294
+        }
295
+        
296
+        // 确保其他必要字段存在
297
+        if (!jsonObj.ArticleEnglish || !Array.isArray(jsonObj.ArticleEnglish)) {
298
+            jsonObj.ArticleEnglish = ["No content available"];
299
+        }
300
+        
301
+        if (!jsonObj.ArticleChinese || !Array.isArray(jsonObj.ArticleChinese)) {
302
+            jsonObj.ArticleChinese = ["无可用内容"];
303
+        }
304
+        
305
+        if (!jsonObj.FormsOfWords || !Array.isArray(jsonObj.FormsOfWords)) {
306
+            jsonObj.FormsOfWords = [];
307
+        } else {
308
+            // 处理FormsOfWords数组,提取所有单词
309
+            const processedFormsOfWords = [];
310
+            
311
+            for (const item of jsonObj.FormsOfWords) {
312
+                if (typeof item !== 'string') {
313
+                    continue; // 跳过非字符串项
314
+                }
315
+                
316
+                // 处理冒号分隔格式:"word1: word2"
317
+                if (item.includes(':')) {
318
+                    const [leftWord, rightWord] = item.split(':').map(word => word.trim());
319
+                    if (leftWord) processedFormsOfWords.push(leftWord);
320
+                    if (rightWord) processedFormsOfWords.push(rightWord);
321
+                    continue;
322
+                }
323
+                
324
+                // 处理括号分隔格式:"word1(word2)" 或 "word1(word2, word3)"
325
+                const bracketMatch = item.match(/^([^(]+)\(([^)]+)\)$/);
326
+                if (bracketMatch) {
327
+                    const outsideWord = bracketMatch[1].trim();
328
+                    const insideWords = bracketMatch[2].split(',').map(word => word.trim());
329
+                    
330
+                    if (outsideWord) processedFormsOfWords.push(outsideWord);
331
+                    for (const word of insideWords) {
332
+                        if (word) processedFormsOfWords.push(word);
333
+                    }
334
+                    continue;
335
+                }
336
+                
337
+                // 如果不符合上述格式,检查是否包含逗号
338
+                if (item.includes(',')) {
339
+                    // 如果包含逗号,按逗号分割并添加每个单词
340
+                    const words = item.split(',').map(word => word.trim());
341
+                    for (const word of words) {
342
+                        if (word) processedFormsOfWords.push(word);
343
+                    }
344
+                } else {
345
+                    // 单个单词,直接添加
346
+                    processedFormsOfWords.push(item);
347
+                }
348
+            }
349
+            
350
+            // 去除空字符串并去重
351
+            const uniqueFormsOfWords = [...new Set(processedFormsOfWords.filter(word => word))];
352
+            
353
+            // 用去重后的数组替换原数组
354
+            jsonObj.FormsOfWords = uniqueFormsOfWords;
355
+        }
356
+        
357
+        // 将修复后的对象转回JSON字符串
358
+        return JSON.stringify(jsonObj);
359
+    } catch (jsonError) {
360
+        console.error("JSON解析或修复错误:", jsonError);
361
+        // 如果解析失败,保留原始结果
362
+        return jsonString;
363
+    }
364
+}
365
+
366
+export default {
367
+    generateArticle,
368
+    validateAndFixJSON
369
+};

+ 32 - 101
src/api/yjbdc/yjbdcController.js

@@ -9,6 +9,7 @@ import { stringUtils } from '../../util/stringClass.js';
9 9
 import WXBizDataCrypt from '../../util/WXBizDataCrypt.js';
10 10
 import { globalCache } from '../../util/GlobalCache.js';
11 11
 import yjbdc from '../../model/yjbdc.js';
12
+import aiController from './aiController.js';
12 13
 
13 14
 import PDFDocument from 'pdfkit';
14 15
 import tencentcloud from 'tencentcloud-sdk-nodejs-ocr';
@@ -16,11 +17,8 @@ import tencentcloud from 'tencentcloud-sdk-nodejs-ocr';
16 17
 const ONE_DAY_MAX_BUILD_COUNT=12;//一天最大生成数
17 18
 
18 19
 const OcrClient = tencentcloud.ocr.v20181119.Client;
19
-const headers = {
20
-    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36',
21
-    "Authorization": "Bearer "+config.huoshancloud.apikey,
22
-    "Content-Type": "application/json"
23
-}
20
+// AI平台配置
21
+const DEFAULT_AI_PROVIDER = 'volces1-6'; // 默认使用火山云AI
24 22
 
25 23
 
26 24
 export async function YJBDCLogin(ctx) {
@@ -262,102 +260,17 @@ export async function GenerateArticle(ctx) {
262 260
         
263 261
 
264 262
         //console.log("content:"+content);
265
-        const url = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";
266
-        const postJSON = {
267
-            "model": "doubao-1.5-pro-32k-250115",
268
-            "messages": [
269
-            {
270
-                "role": "user",
271
-                "content": content,
272
-            }
273
-        ]};
274
-        try{
275
-            const response = await axios.post(encodeURI(url), postJSON, { headers });
276
-
277
-            
278
-            //console.log("parsedBody:" + JSON.stringify(response.data.choices[0].message.content));
279
-
280
-            let result2=response.data.choices[0].message.content;
263
+        
264
+        // 从请求参数中获取AI提供者,如果没有指定则使用默认值
265
+        const aiProvider = params.aiProvider || DEFAULT_AI_PROVIDER;
266
+        
267
+        try {
268
+            // 使用aiController生成文章
269
+            let result2 = await aiController.generateArticle(content, aiProvider);
281 270
             
282 271
             // 校验和修复JSON结构
283
-            try {
284
-                // 解析JSON字符串为对象
285
-                let jsonObj = JSON.parse(result2);
286
-                
287
-                // 校验和修复Question数组中的每个问题对象
288
-                if (jsonObj.Question && Array.isArray(jsonObj.Question)) {
289
-                    jsonObj.Question = jsonObj.Question.map(question => {
290
-                        // 创建一个修复后的问题对象
291
-                        const fixedQuestion = {};
292
-                        
293
-                        // 确保QuestionEnglish字段存在
294
-                        if (question.QuestionEnglish) {
295
-                            fixedQuestion.QuestionEnglish = question.QuestionEnglish;
296
-                        }
297
-                        
298
-                        // 检查QuestionChinese字段,如果不存在但有第二个QuestionEnglish,则使用它
299
-                        if (question.QuestionChinese) {
300
-                            fixedQuestion.QuestionChinese = question.QuestionChinese;
301
-                        } else if (Object.keys(question).filter(key => key === 'QuestionEnglish').length > 1) {
302
-                            // 找到第二个QuestionEnglish的值
303
-                            const keys = Object.keys(question);
304
-                            let foundFirst = false;
305
-                            for (const key of keys) {
306
-                                if (key === 'QuestionEnglish') {
307
-                                    if (foundFirst) {
308
-                                        fixedQuestion.QuestionChinese = question[key];
309
-                                        break;
310
-                                    }
311
-                                    foundFirst = true;
312
-                                }
313
-                            }
314
-                        }
315
-                        
316
-                        // 确保OptionsEnglish字段存在且为数组
317
-                        if (question.OptionsEnglish && Array.isArray(question.OptionsEnglish)) {
318
-                            fixedQuestion.OptionsEnglish = question.OptionsEnglish;
319
-                        } else {
320
-                            fixedQuestion.OptionsEnglish = ["A.", "B.", "C.", "D."];
321
-                        }
322
-                        
323
-                        // 确保OptionsChinese字段存在且为数组
324
-                        if (question.OptionsChinese && Array.isArray(question.OptionsChinese)) {
325
-                            fixedQuestion.OptionsChinese = question.OptionsChinese;
326
-                        } else {
327
-                            fixedQuestion.OptionsChinese = ["A.", "B.", "C.", "D."];
328
-                        }
329
-                        
330
-                        // 确保Answer字段存在
331
-                        if (question.Answer) {
332
-                            fixedQuestion.Answer = question.Answer;
333
-                        } else {
334
-                            fixedQuestion.Answer = "A";
335
-                        }
336
-                        
337
-                        return fixedQuestion;
338
-                    });
339
-                }
340
-                
341
-                // 确保其他必要字段存在
342
-                if (!jsonObj.ArticleEnglish || !Array.isArray(jsonObj.ArticleEnglish)) {
343
-                    jsonObj.ArticleEnglish = ["No content available"];
344
-                }
345
-                
346
-                if (!jsonObj.ArticleChinese || !Array.isArray(jsonObj.ArticleChinese)) {
347
-                    jsonObj.ArticleChinese = ["无可用内容"];
348
-                }
349
-                
350
-                if (!jsonObj.FormsOfWords || !Array.isArray(jsonObj.FormsOfWords)) {
351
-                    jsonObj.FormsOfWords = [];
352
-                }
353
-                
354
-                // 将修复后的对象转回JSON字符串
355
-                result2 = JSON.stringify(jsonObj);
356
-                console.log("JSON结构已校验和修复");
357
-            } catch (jsonError) {
358
-                console.error("JSON解析或修复错误:", jsonError);
359
-                // 如果解析失败,保留原始结果
360
-            }
272
+            result2 = aiController.validateAndFixJSON(result2);
273
+            console.log("JSON结构已校验和修复");
361 274
             
362 275
             let param2={};
363 276
             param2.UserID=ctx.query.UserID;
@@ -604,8 +517,26 @@ export async function GeneratePDF(ctx) {
604 517
         });
605 518
 
606 519
         // 6. 文章内容
607
-        doc.font('Helvetica')
608
-           .fontSize(pixelToPt(48))
520
+        // 先计算文章内容的行数
521
+        doc.font('Helvetica');
522
+        
523
+        // 使用48字号计算文本高度
524
+        const fontSize48 = pixelToPt(48);
525
+        doc.fontSize(fontSize48);
526
+        const textHeight48 = doc.heightOfString(articleText, {
527
+            width: pixelToPt(1240),
528
+            lineGap: pixelToPt(40.5)
529
+        });
530
+        
531
+        // 计算行数 (文本高度 / (字体大小 + 行间距))
532
+        const lineHeight = fontSize48 + pixelToPt(40.5);
533
+        const lineCount = Math.ceil(textHeight48 / lineHeight);
534
+        
535
+        // 如果行数超过18行,则使用42字号
536
+        const fontSize = lineCount > 18 ? pixelToPt(42) : pixelToPt(48);
537
+        
538
+        // 渲染文章内容
539
+        doc.fontSize(fontSize)
609 540
            .text(articleText, pixelToPt(740), pixelToPt(105), {
610 541
                width: pixelToPt(1240),
611 542
                lineGap: pixelToPt(40.5)

+ 2 - 1
src/config/index.js

@@ -73,7 +73,8 @@ const commonConfig = {
73 73
         secretKey: "JTUK0WsBKJ8nIdopfM0TnxwkOsBJ75vp",
74 74
     },
75 75
     huoshancloud:{
76
-        apikey:"d276e65d-4c39-49ad-a2e2-5060c30ca9a0",
76
+        apikeyCJ:"d276e65d-4c39-49ad-a2e2-5060c30ca9a0",
77
+        apikeyHLR:"2b7d737e-6773-4d55-ae77-da8d4e819c96",
77 78
     },
78 79
     BufferMemoryTimeSuperLower:1,//缓存时间1秒
79 80
     BufferMemoryTimeLower:5,//缓存时间5秒

+ 1 - 1
src/model/mps.js

@@ -225,7 +225,7 @@ class MPS {
225 225
 // 当前年份配置
226 226
 export const ArrYear = [{
227 227
     Name: "2025",
228
-    IsNullScoreLine: (moment().format("MM-DD") >= "05-30" && moment().format("MM-DD") <= "07-14") ? true : false, // 当只有计划没有分数线时
228
+    IsNullScoreLine: (moment().format("MM-DD") >= "05-30" && moment().format("MM-DD") < "07-14") ? true : false, // 当只有计划没有分数线时
229 229
     CSS: "Selected"
230 230
 }, {
231 231
     Name: "2024",