aiController.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. import axios from 'axios';
  2. import config from '../../config/index.js';
  3. /**
  4. * AI平台接口类
  5. * 定义了所有AI平台需要实现的方法
  6. */
  7. class AIProvider {
  8. /**
  9. * 生成文章
  10. * @param {string} content - 生成文章的提示内容
  11. * @returns {Promise<string>} - 返回生成的文章JSON字符串
  12. */
  13. async generateArticle(content) {
  14. throw new Error('Method not implemented');
  15. }
  16. }
  17. /**
  18. * 火山云AI平台实现
  19. */
  20. class VolcesAIProvider extends AIProvider {
  21. /**
  22. * 创建火山云AI提供者实例
  23. * @param {string} version - 版本号,如'1.5'或'1.6'
  24. */
  25. constructor(version = '1-5') {
  26. super();
  27. // 根据版本选择对应的API密钥和模型
  28. const versionConfig = {
  29. '1-5': {
  30. apikey: config.huoshancloud.apikeyHLR,
  31. model: "doubao-1-5-pro-32k-250115"
  32. },
  33. '1-6': {
  34. apikey: config.huoshancloud.apikeyHLR,
  35. model: "doubao-seed-1-6-250615"
  36. },
  37. };
  38. // 获取当前版本的配置,如果版本不存在则使用1.5版本
  39. const currentConfig = versionConfig[version] || versionConfig['1-5'];
  40. this.version = version;
  41. this.headers = {
  42. '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',
  43. "Authorization": "Bearer " + currentConfig.apikey,
  44. "Content-Type": "application/json"
  45. };
  46. this.url = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";
  47. this.model = currentConfig.model;
  48. }
  49. async generateArticle(content) {
  50. const postJSON = {
  51. "model": this.model,
  52. "messages": [
  53. {
  54. "role": "user",
  55. "content": content,
  56. }
  57. ]
  58. };
  59. try {
  60. console.log(`火山云${this.version}`);
  61. const response = await axios.post(encodeURI(this.url), postJSON, { headers: this.headers });
  62. return response.data.choices[0].message.content;
  63. } catch (error) {
  64. console.error("VolcesAI API error:", error);
  65. throw error;
  66. }
  67. }
  68. }
  69. // 为了保持向后兼容性,创建1.5和1.6版本的类别名
  70. class VolcesAIProvider1_5 extends VolcesAIProvider {
  71. constructor() {
  72. super('1-5');
  73. }
  74. }
  75. class VolcesAIProvider1_6 extends VolcesAIProvider {
  76. constructor() {
  77. super('1-6');
  78. }
  79. }
  80. /**
  81. * OpenAI平台实现
  82. */
  83. class OpenAIProvider extends AIProvider {
  84. constructor() {
  85. super();
  86. this.headers = {
  87. "Authorization": "Bearer " + config.openai.apikey,
  88. "Content-Type": "application/json"
  89. };
  90. this.url = "https://api.openai.com/v1/chat/completions";
  91. this.model = "gpt-3.5-turbo";
  92. }
  93. async generateArticle(content) {
  94. const postJSON = {
  95. "model": this.model,
  96. "messages": [
  97. {
  98. "role": "user",
  99. "content": content,
  100. }
  101. ]
  102. };
  103. try {
  104. const response = await axios.post(this.url, postJSON, { headers: this.headers });
  105. return response.data.choices[0].message.content;
  106. } catch (error) {
  107. console.error("OpenAI API error:", error);
  108. throw error;
  109. }
  110. }
  111. }
  112. /**
  113. * 百度文心一言平台实现
  114. */
  115. class BaiduAIProvider extends AIProvider {
  116. constructor() {
  117. super();
  118. this.headers = {
  119. "Content-Type": "application/json"
  120. };
  121. this.url = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions";
  122. this.model = "ernie-bot-4";
  123. this.accessToken = null;
  124. }
  125. async getAccessToken() {
  126. if (this.accessToken) return this.accessToken;
  127. const tokenUrl = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${config.baidu.apiKey}&client_secret=${config.baidu.secretKey}`;
  128. try {
  129. const response = await axios.post(tokenUrl);
  130. this.accessToken = response.data.access_token;
  131. return this.accessToken;
  132. } catch (error) {
  133. console.error("Baidu access token error:", error);
  134. throw error;
  135. }
  136. }
  137. async generateArticle(content) {
  138. const accessToken = await this.getAccessToken();
  139. const apiUrl = `${this.url}?access_token=${accessToken}`;
  140. const postJSON = {
  141. "model": this.model,
  142. "messages": [
  143. {
  144. "role": "user",
  145. "content": content,
  146. }
  147. ]
  148. };
  149. try {
  150. const response = await axios.post(apiUrl, postJSON, { headers: this.headers });
  151. return response.data.result;
  152. } catch (error) {
  153. console.error("Baidu AI API error:", error);
  154. throw error;
  155. }
  156. }
  157. }
  158. /**
  159. * AI提供者工厂类
  160. * 根据配置创建不同的AI平台实例
  161. */
  162. class AIProviderFactory {
  163. /**
  164. * 获取AI提供者实例
  165. * @param {string} provider - AI提供者名称,如'volces', 'volces1-5', 'volces1-6', 'openai', 'baidu'
  166. * @returns {AIProvider} - 返回对应的AI提供者实例
  167. */
  168. static getProvider(provider = 'volces1-5') {
  169. const providerLower = provider.toLowerCase();
  170. // 处理火山云通用提供者格式:volces:版本号
  171. if (providerLower.startsWith('volces:')) {
  172. const version = providerLower.split(':')[1];
  173. return new VolcesAIProvider(version);
  174. }
  175. // 处理传统提供者名称
  176. switch (providerLower) {
  177. case 'volces':
  178. case 'volces1-5':
  179. return new VolcesAIProvider1_5('1-5');
  180. case 'volces1-6':
  181. return new VolcesAIProvider1_6('1-6');
  182. case 'openai':
  183. return new OpenAIProvider();
  184. case 'baidu':
  185. return new BaiduAIProvider();
  186. default:
  187. return new VolcesAIProvider('1-5'); // 默认使用火山云1.5
  188. }
  189. }
  190. }
  191. /**
  192. * 生成文章的主函数
  193. * @param {string} content - 生成文章的提示内容
  194. * @param {string} provider - AI提供者名称,默认为'volces'
  195. * @returns {Promise<string>} - 返回生成的文章JSON字符串
  196. */
  197. export async function generateArticle(content, provider = 'volces1-5') {
  198. try {
  199. const aiProvider = AIProviderFactory.getProvider(provider);
  200. const result = await aiProvider.generateArticle(content);
  201. return result;
  202. } catch (error) {
  203. console.error("Generate article error:", error);
  204. throw error;
  205. }
  206. }
  207. /**
  208. * 计算两个字符串之间的Levenshtein距离(编辑距离)
  209. * @param {string} a - 第一个字符串
  210. * @param {string} b - 第二个字符串
  211. * @returns {number} - 编辑距离
  212. */
  213. function levenshteinDistance(a, b) {
  214. const matrix = [];
  215. // 初始化矩阵
  216. for (let i = 0; i <= b.length; i++) {
  217. matrix[i] = [i];
  218. }
  219. for (let j = 0; j <= a.length; j++) {
  220. matrix[0][j] = j;
  221. }
  222. // 填充矩阵
  223. for (let i = 1; i <= b.length; i++) {
  224. for (let j = 1; j <= a.length; j++) {
  225. if (b.charAt(i - 1) === a.charAt(j - 1)) {
  226. matrix[i][j] = matrix[i - 1][j - 1];
  227. } else {
  228. matrix[i][j] = Math.min(
  229. matrix[i - 1][j - 1] + 1, // 替换
  230. matrix[i][j - 1] + 1, // 插入
  231. matrix[i - 1][j] + 1 // 删除
  232. );
  233. }
  234. }
  235. }
  236. return matrix[b.length][a.length];
  237. }
  238. /**
  239. * 增强FormsOfWords,检测文章中单词的变形形式和拼写错误
  240. * @param {Object} jsonObj - 解析后的JSON对象
  241. * @param {string} userWords - 用户提供的单词列表,逗号分隔
  242. * @returns {Object} - 增强后的JSON对象
  243. */
  244. export function enhanceFormsOfWords(jsonObj, userWords) {
  245. if (!jsonObj || !userWords) return jsonObj;
  246. // 将用户提供的单词转换为数组并去除空格
  247. const userWordsList = userWords.split(',').map(word => word.trim().toLowerCase());
  248. // 如果没有ArticleEnglish或FormsOfWords,直接返回
  249. if (!jsonObj.ArticleEnglish || !Array.isArray(jsonObj.ArticleEnglish)) {
  250. return jsonObj;
  251. }
  252. // 确保FormsOfWords存在
  253. if (!jsonObj.FormsOfWords) {
  254. jsonObj.FormsOfWords = [];
  255. }
  256. // 从文章中提取所有单词
  257. const allWordsInArticle = [];
  258. jsonObj.ArticleEnglish.forEach(sentence => {
  259. // 移除标点符号,分割成单词
  260. const words = sentence.toLowerCase()
  261. .replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, " ")
  262. .replace(/\s+/g, " ")
  263. .split(" ");
  264. words.forEach(word => {
  265. if (word) allWordsInArticle.push(word.trim());
  266. });
  267. });
  268. // 常见的后缀列表
  269. const commonSuffixes = [
  270. 'ed', 'ing', 's', 'es', 'er', 'est', 'ful', 'ly', 'ment', 'ness', 'ity',
  271. 'tion', 'sion', 'ation', 'able', 'ible', 'al', 'ial', 'ic', 'ical', 'ious',
  272. 'ous', 'ive', 'less', 'y'
  273. ];
  274. // 不规则动词变化表(部分常见的)
  275. const irregularVerbs = {
  276. 'go': ['went', 'gone', 'goes', 'going'],
  277. 'be': ['am', 'is', 'are', 'was', 'were', 'been', 'being'],
  278. 'do': ['did', 'done', 'does', 'doing'],
  279. 'have': ['has', 'had', 'having'],
  280. 'say': ['said', 'says', 'saying'],
  281. 'make': ['made', 'makes', 'making'],
  282. 'take': ['took', 'taken', 'takes', 'taking'],
  283. 'come': ['came', 'comes', 'coming'],
  284. 'see': ['saw', 'seen', 'sees', 'seeing'],
  285. 'know': ['knew', 'known', 'knows', 'knowing'],
  286. 'get': ['got', 'gotten', 'gets', 'getting'],
  287. 'give': ['gave', 'given', 'gives', 'giving'],
  288. 'find': ['found', 'finds', 'finding'],
  289. 'think': ['thought', 'thinks', 'thinking'],
  290. 'tell': ['told', 'tells', 'telling'],
  291. 'become': ['became', 'becomes', 'becoming'],
  292. 'show': ['showed', 'shown', 'shows', 'showing'],
  293. 'leave': ['left', 'leaves', 'leaving'],
  294. 'feel': ['felt', 'feels', 'feeling'],
  295. 'put': ['puts', 'putting'],
  296. 'bring': ['brought', 'brings', 'bringing'],
  297. 'begin': ['began', 'begun', 'begins', 'beginning'],
  298. 'keep': ['kept', 'keeps', 'keeping'],
  299. 'hold': ['held', 'holds', 'holding'],
  300. 'write': ['wrote', 'written', 'writes', 'writing'],
  301. 'stand': ['stood', 'stands', 'standing'],
  302. 'hear': ['heard', 'hears', 'hearing'],
  303. 'let': ['lets', 'letting'],
  304. 'mean': ['meant', 'means', 'meaning'],
  305. 'set': ['sets', 'setting'],
  306. 'meet': ['met', 'meets', 'meeting'],
  307. 'run': ['ran', 'runs', 'running'],
  308. 'pay': ['paid', 'pays', 'paying'],
  309. 'sit': ['sat', 'sits', 'sitting'],
  310. 'speak': ['spoke', 'spoken', 'speaks', 'speaking'],
  311. 'lie': ['lay', 'lain', 'lies', 'lying'],
  312. 'lead': ['led', 'leads', 'leading'],
  313. 'read': ['read', 'reads', 'reading'],
  314. 'sleep': ['slept', 'sleeps', 'sleeping'],
  315. 'win': ['won', 'wins', 'winning'],
  316. 'understand': ['understood', 'understands', 'understanding'],
  317. 'draw': ['drew', 'drawn', 'draws', 'drawing'],
  318. 'sing': ['sang', 'sung', 'sings', 'singing'],
  319. 'fall': ['fell', 'fallen', 'falls', 'falling'],
  320. 'fly': ['flew', 'flown', 'flies', 'flying'],
  321. 'grow': ['grew', 'grown', 'grows', 'growing'],
  322. 'lose': ['lost', 'loses', 'losing'],
  323. 'teach': ['taught', 'teaches', 'teaching'],
  324. 'eat': ['ate', 'eaten', 'eats', 'eating'],
  325. 'drink': ['drank', 'drunk', 'drinks', 'drinking']
  326. };
  327. // 不规则形容词比较级和最高级
  328. const irregularAdjectives = {
  329. 'good': ['better', 'best'],
  330. 'bad': ['worse', 'worst'],
  331. 'far': ['further', 'furthest', 'farther', 'farthest'],
  332. 'little': ['less', 'least'],
  333. 'many': ['more', 'most'],
  334. 'much': ['more', 'most']
  335. };
  336. // 收集所有单词形式
  337. const allForms = new Set();
  338. userWordsList.forEach(originalWord => {
  339. // 添加原始单词
  340. allForms.add(originalWord);
  341. // 检查不规则动词
  342. if (irregularVerbs[originalWord]) {
  343. irregularVerbs[originalWord].forEach(form => allForms.add(form));
  344. }
  345. // 检查不规则形容词
  346. if (irregularAdjectives[originalWord]) {
  347. irregularAdjectives[originalWord].forEach(form => allForms.add(form));
  348. }
  349. // 检查文章中的所有单词,寻找可能的变形和拼写错误
  350. allWordsInArticle.forEach(articleWord => {
  351. // 检查是否是原始单词
  352. if (articleWord === originalWord) {
  353. allForms.add(articleWord);
  354. return;
  355. }
  356. // 检查拼写错误,使用更严格的条件
  357. // 1. 对于短单词(长度<=4),只接受编辑距离为1的情况
  358. // 2. 对于中等长度单词(4<长度<=8),只接受编辑距离为1的情况
  359. // 3. 对于长单词(长度>8),允许编辑距离为2,但有额外限制
  360. if (originalWord.length <= 8) {
  361. // 短单词和中等长度单词使用相同的严格条件
  362. if (articleWord[0] === originalWord[0] && // 首字母必须相同
  363. Math.abs(articleWord.length - originalWord.length) <= 1 && // 长度差不超过1
  364. levenshteinDistance(originalWord, articleWord) === 1) { // 编辑距离恰好为1
  365. allForms.add(articleWord);
  366. }
  367. } else {
  368. // 长单词(长度>8)的条件
  369. if (articleWord[0] === originalWord[0]) { // 首字母必须相同
  370. const editDistance = levenshteinDistance(originalWord, articleWord);
  371. if (editDistance === 1) {
  372. // 编辑距离为1的情况,长度差不超过1
  373. if (Math.abs(articleWord.length - originalWord.length) <= 1) {
  374. allForms.add(articleWord);
  375. }
  376. } else if (editDistance === 2) {
  377. // 编辑距离为2的情况,需要更严格的条件
  378. // 长度差不超过1且单词长度大于8
  379. if (Math.abs(articleWord.length - originalWord.length) <= 1) {
  380. allForms.add(articleWord);
  381. }
  382. }
  383. }
  384. }
  385. // 检查是否是通过添加后缀形成的变形
  386. for (const suffix of commonSuffixes) {
  387. if (articleWord.endsWith(suffix)) {
  388. const stem = articleWord.slice(0, -suffix.length);
  389. // 处理双写字母的情况(如:running -> run)
  390. if (stem.length > 0 && stem[stem.length-1] === stem[stem.length-2]) {
  391. const possibleStem = stem.slice(0, -1);
  392. if (possibleStem === originalWord) {
  393. allForms.add(articleWord);
  394. continue;
  395. }
  396. }
  397. // 处理去e加ing的情况(如:writing -> write)
  398. if (suffix === 'ing' && originalWord.endsWith('e') && stem + 'e' === originalWord) {
  399. allForms.add(articleWord);
  400. continue;
  401. }
  402. // 处理y变i的情况(如:studies -> study)
  403. if ((suffix === 'es' || suffix === 'ed') && originalWord.endsWith('y') &&
  404. stem + 'y' === originalWord) {
  405. allForms.add(articleWord);
  406. continue;
  407. }
  408. // 直接比较
  409. if (stem === originalWord) {
  410. allForms.add(articleWord);
  411. }
  412. }
  413. }
  414. });
  415. });
  416. // 更新FormsOfWords
  417. jsonObj.FormsOfWords = Array.from(new Set([...jsonObj.FormsOfWords, ...allForms]));
  418. return jsonObj;
  419. }
  420. /**
  421. * 校验和修复JSON结构
  422. * @param {string} jsonString - JSON字符串
  423. * @returns {string} - 修复后的JSON字符串
  424. */
  425. export function validateAndFixJSON(jsonString) {
  426. try {
  427. //console.log(jsonString);
  428. // 解析JSON字符串为对象
  429. let jsonObj = JSON.parse(jsonString);
  430. // 校验和修复Question数组中的每个问题对象
  431. if (jsonObj.Question && Array.isArray(jsonObj.Question)) {
  432. jsonObj.Question = jsonObj.Question.map(question => {
  433. // 创建一个修复后的问题对象
  434. const fixedQuestion = {};
  435. // 确保QuestionEnglish字段存在
  436. if (question.QuestionEnglish) {
  437. fixedQuestion.QuestionEnglish = question.QuestionEnglish;
  438. }
  439. // 检查QuestionChinese字段,如果不存在但有第二个QuestionEnglish,则使用它
  440. if (question.QuestionChinese) {
  441. fixedQuestion.QuestionChinese = question.QuestionChinese;
  442. } else if (Object.keys(question).filter(key => key === 'QuestionEnglish').length > 1) {
  443. // 找到第二个QuestionEnglish的值
  444. const keys = Object.keys(question);
  445. let foundFirst = false;
  446. for (const key of keys) {
  447. if (key === 'QuestionEnglish') {
  448. if (foundFirst) {
  449. fixedQuestion.QuestionChinese = question[key];
  450. break;
  451. }
  452. foundFirst = true;
  453. }
  454. }
  455. }
  456. // 确保OptionsEnglish字段存在且为数组
  457. if (question.OptionsEnglish && Array.isArray(question.OptionsEnglish)) {
  458. fixedQuestion.OptionsEnglish = question.OptionsEnglish;
  459. } else {
  460. fixedQuestion.OptionsEnglish = ["A.", "B.", "C.", "D."];
  461. }
  462. // 确保OptionsChinese字段存在且为数组
  463. if (question.OptionsChinese && Array.isArray(question.OptionsChinese)) {
  464. fixedQuestion.OptionsChinese = question.OptionsChinese;
  465. } else {
  466. fixedQuestion.OptionsChinese = ["A.", "B.", "C.", "D."];
  467. }
  468. // 确保Answer字段存在
  469. if (question.Answer) {
  470. fixedQuestion.Answer = question.Answer;
  471. } else {
  472. fixedQuestion.Answer = "A";
  473. }
  474. return fixedQuestion;
  475. });
  476. }
  477. // 确保其他必要字段存在
  478. if (!jsonObj.ArticleEnglish || !Array.isArray(jsonObj.ArticleEnglish)) {
  479. jsonObj.ArticleEnglish = ["No content available"];
  480. }
  481. if (!jsonObj.ArticleChinese || !Array.isArray(jsonObj.ArticleChinese)) {
  482. jsonObj.ArticleChinese = ["无可用内容"];
  483. }
  484. if (!jsonObj.FormsOfWords || !Array.isArray(jsonObj.FormsOfWords)) {
  485. jsonObj.FormsOfWords = [];
  486. } else {
  487. // 处理FormsOfWords数组,提取所有单词
  488. const processedFormsOfWords = [];
  489. for (const item of jsonObj.FormsOfWords) {
  490. if (typeof item !== 'string') {
  491. continue; // 跳过非字符串项
  492. }
  493. // 处理冒号分隔格式:"word1: word2"
  494. if (item.includes(':')) {
  495. const [leftWord, rightWord] = item.split(':').map(word => word.trim());
  496. if (leftWord) processedFormsOfWords.push(leftWord);
  497. if (rightWord) processedFormsOfWords.push(rightWord);
  498. continue;
  499. }
  500. // 处理括号分隔格式:"word1(word2)" 或 "word1(word2, word3)"
  501. const bracketMatch = item.match(/^([^(]+)\(([^)]+)\)$/);
  502. if (bracketMatch) {
  503. const outsideWord = bracketMatch[1].trim();
  504. const insideWords = bracketMatch[2].split(',').map(word => word.trim());
  505. if (outsideWord) processedFormsOfWords.push(outsideWord);
  506. for (const word of insideWords) {
  507. if (word) processedFormsOfWords.push(word);
  508. }
  509. continue;
  510. }
  511. // 如果不符合上述格式,检查是否包含逗号
  512. if (item.includes(',')) {
  513. // 如果包含逗号,按逗号分割并添加每个单词
  514. const words = item.split(',').map(word => word.trim());
  515. for (const word of words) {
  516. if (word) processedFormsOfWords.push(word);
  517. }
  518. } else {
  519. // 单个单词,直接添加
  520. processedFormsOfWords.push(item);
  521. }
  522. }
  523. // 去除空字符串并去重
  524. const uniqueFormsOfWords = [...new Set(processedFormsOfWords.filter(word => word))];
  525. // 用去重后的数组替换原数组
  526. jsonObj.FormsOfWords = uniqueFormsOfWords;
  527. }
  528. // 将修复后的对象转回JSON字符串
  529. return JSON.stringify(jsonObj);
  530. } catch (jsonError) {
  531. console.error("JSON解析或修复错误:", jsonError);
  532. // 如果解析失败,保留原始结果
  533. return jsonString;
  534. }
  535. }
  536. export default {
  537. generateArticle,
  538. enhanceFormsOfWords,
  539. validateAndFixJSON
  540. };