aiController.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. import axios from 'axios';
  2. import crypto from 'crypto';
  3. import config from '../../config/index.js';
  4. import { enhanceFormsOfWords } from './enhanceFormsOfWords.js';
  5. /**
  6. * AI平台接口类
  7. * 定义了所有AI平台需要实现的方法
  8. */
  9. class AIProvider {
  10. /**
  11. * 生成文章
  12. * @param {string} content - 生成文章的提示内容
  13. * @returns {Promise<string>} - 返回生成的文章JSON字符串
  14. */
  15. async generateArticle(content) {
  16. throw new Error('Method not implemented');
  17. }
  18. }
  19. /**
  20. * OpenAI平台实现
  21. */
  22. class OpenAIProvider extends AIProvider {
  23. constructor() {
  24. super();
  25. this.headers = {
  26. "Authorization": "Bearer " + config.openai.apikey,
  27. "Content-Type": "application/json"
  28. };
  29. this.url = "https://api.openai.com/v1/chat/completions";
  30. this.model = "gpt-3.5-turbo";
  31. }
  32. async generateArticle(content) {
  33. const postJSON = {
  34. "model": this.model,
  35. "messages": [
  36. {
  37. "role": "user",
  38. "content": content,
  39. }
  40. ]
  41. };
  42. try {
  43. const response = await axios.post(this.url, postJSON, { headers: this.headers });
  44. return response.data.choices[0].message.content;
  45. } catch (error) {
  46. console.error("OpenAI API error:", error);
  47. throw error;
  48. }
  49. }
  50. }
  51. /**
  52. * 火山云AI平台实现
  53. */
  54. class VolcesAIProvider extends AIProvider {
  55. /**
  56. * 创建火山云AI提供者实例
  57. * @param {string} version - 版本号
  58. */
  59. constructor(version) {
  60. super();
  61. if (version.indexOf("deepseek")>0){
  62. version=version.substring(7);
  63. }
  64. // 获取当前版本的配置,如果版本不存在则使用1.5版本
  65. const currentConfig = {
  66. apikey: config.huoshancloud.apikeyHLR,
  67. model: version
  68. };
  69. this.version = version;
  70. this.headers = {
  71. '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',
  72. "Authorization": "Bearer " + currentConfig.apikey,
  73. "Content-Type": "application/json"
  74. };
  75. this.url = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";
  76. this.model = currentConfig.model;
  77. }
  78. async generateArticle(content) {
  79. let postJSON = {
  80. "model": this.model,
  81. "messages": [
  82. {
  83. "role": "system",
  84. "content": "你是一个专业的AI助手,请以JSON格式回复。"
  85. },
  86. {
  87. "role": "user",
  88. "content": content
  89. }
  90. ]
  91. };
  92. if (this.version.indexOf("deepseek")>=0 || this.version.indexOf("seed-1")>=0){
  93. postJSON.response_format={
  94. "type": "json_object"
  95. };
  96. postJSON.thinking={
  97. "type": "disabled"
  98. };
  99. }
  100. try {
  101. console.log(`火山云${this.version}`);
  102. // 移除encodeURI,直接使用原始URL
  103. const response = await axios.post(this.url, postJSON, { headers: this.headers });
  104. return response.data.choices[0].message.content;
  105. } catch (error) {
  106. console.error("VolcesAI API error:", error);
  107. // 添加更详细的错误日志
  108. if (error.response) {
  109. console.error("错误详情:", {
  110. status: error.response.status,
  111. data: error.response.data
  112. });
  113. }
  114. throw error;
  115. }
  116. }
  117. }
  118. /**
  119. * 讯飞星火平台实现 (spark X1版本)
  120. */
  121. class XunFeiYunAIProvider extends AIProvider {
  122. constructor() {
  123. super();
  124. // 使用API Key和Secret拼接成"AK:SK"格式
  125. this.apiKey = `${config.xfyun.apikey}:${config.xfyun.apisecret}`;
  126. this.headers = {
  127. "Content-Type": "application/json",
  128. "Authorization": `Bearer ${this.apiKey}`
  129. };
  130. this.url = "https://spark-api-open.xf-yun.com/v2/chat/completions";
  131. }
  132. async generateArticle(content) {
  133. const postJSON = {
  134. "model": "x1",
  135. "messages": [
  136. {
  137. "role": "user",
  138. "content": content
  139. }
  140. ],
  141. "temperature": 0.7,
  142. "max_tokens": 2048,
  143. "user": "123456" // 固定用户ID
  144. };
  145. try {
  146. // 增加重试机制
  147. let retries = 3;
  148. let lastError = null;
  149. while (retries > 0) {
  150. try {
  151. console.log(`讯飞云`);
  152. const response = await axios.post(this.url, postJSON, {
  153. headers: this.headers,
  154. timeout: 60000 // 增加到60秒超时
  155. });
  156. // 处理OpenAI兼容格式的响应
  157. if (response.data?.choices?.[0]?.message?.content) {
  158. return response.data.choices[0].message.content;
  159. }
  160. console.error("XunFeiYun X1 API response:", response.data);
  161. throw new Error("Invalid response format from XunFeiYun X1 API");
  162. } catch (error) {
  163. lastError = error;
  164. retries--;
  165. if (retries > 0) {
  166. console.warn(`API请求失败,剩余重试次数: ${retries},等待2秒后重试...`);
  167. await new Promise(resolve => setTimeout(resolve, 2000));
  168. continue;
  169. }
  170. // 更详细的错误日志
  171. if (error.response) {
  172. console.error("XFYun API error response:", {
  173. status: error.response.status,
  174. data: error.response.data,
  175. headers: error.response.headers
  176. });
  177. } else if (error.request) {
  178. console.error("XFYun API request error:", {
  179. method: error.config.method,
  180. url: error.config.url,
  181. headers: error.config.headers,
  182. data: error.config.data
  183. });
  184. } else {
  185. console.error("XFYun API setup error:", error.message);
  186. }
  187. throw error;
  188. }
  189. }
  190. throw lastError || new Error("API请求失败");
  191. } catch (error) {
  192. console.error("XFYun API error:", error);
  193. throw error;
  194. }
  195. }
  196. }
  197. /**
  198. * 阿里云通义平台实现
  199. */
  200. class AliyunAIProvider extends AIProvider {
  201. constructor(model = 'qwen-plus') {
  202. super();
  203. let url="https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
  204. this.accessKey = config.aliyun.apikeyHLR;
  205. this.headers = {
  206. "Content-Type": "application/json",
  207. "Accept": "application/json"
  208. };
  209. this.url = url;
  210. this.model = model;
  211. }
  212. /**
  213. * 生成文章
  214. * @param {string} content - 生成文章的提示内容
  215. * @returns {Promise<string>} - 返回生成的文章内容
  216. */
  217. async generateArticle(content) {
  218. // 设置请求头中的API密钥
  219. this.headers["Authorization"] = `Bearer ${this.accessKey}`;
  220. const postJSON = {
  221. "model": this.model,
  222. "messages": [
  223. {
  224. "role": "user",
  225. "content": content
  226. }
  227. ],
  228. "temperature": 0.7,
  229. "top_p": 0.8
  230. };
  231. try {
  232. console.log(`阿里云 ${this.model}`);
  233. const response = await axios.post(this.url, postJSON, { headers: this.headers });
  234. // 根据阿里云通义API的返回格式提取内容
  235. if (response.data && response.data.choices && response.data.choices[0] && response.data.choices[0].message) {
  236. let responseContent = response.data.choices[0].message.content;
  237. // 处理可能的Markdown代码块标记
  238. if (responseContent.startsWith('```json') || responseContent.startsWith('```')) {
  239. console.log("阿里云返回了Markdown代码块,正在处理...");
  240. // 移除开头的```json或```
  241. responseContent = responseContent.replace(/^```(?:json)?\s*\n/, '');
  242. // 移除结尾的```
  243. responseContent = responseContent.replace(/\n```\s*$/, '');
  244. }
  245. return responseContent;
  246. } else {
  247. throw new Error("Unexpected response format from Aliyun API");
  248. }
  249. } catch (error) {
  250. console.error("Aliyun AI API error:", error);
  251. throw error;
  252. }
  253. }
  254. }
  255. /**
  256. * 腾讯混元大模型平台实现
  257. */
  258. class TencentHunyuanAIProvider extends AIProvider {
  259. constructor(model = 'hunyuan-turbos-latest') {
  260. super();
  261. this.headers = {
  262. "Authorization": "Bearer " + config.tencentcloudHunyuan.apikey,
  263. "Content-Type": "application/json"
  264. };
  265. this.url = "https://api.hunyuan.cloud.tencent.com/v1/chat/completions";
  266. this.model = model;
  267. }
  268. async generateArticle(content) {
  269. const postJSON = {
  270. "model": this.model,
  271. "messages": [
  272. {
  273. "role": "user",
  274. "content": content,
  275. }
  276. ]
  277. };
  278. try {
  279. console.log(`腾讯云${this.model}`);
  280. const response = await axios.post(this.url, postJSON, { headers: this.headers });
  281. return response.data.choices[0].message.content;
  282. } catch (error) {
  283. console.error("OpenAI API error:", error);
  284. throw error;
  285. }
  286. }
  287. }
  288. /**
  289. * 百度千帆平台实现
  290. */
  291. class BaiduAIProvider extends AIProvider {
  292. constructor(model = 'deepseek-v3') {
  293. super();
  294. this.headers = {
  295. "Authorization": "Bearer " + config.baiducloud.apikey,
  296. "Content-Type": "application/json"
  297. };
  298. this.url = 'https://qianfan.baidubce.com/v2/chat/completions';
  299. this.model = model;
  300. }
  301. async generateArticle(content) {
  302. const postJSON = {
  303. "model": this.model,
  304. "messages": [
  305. {
  306. "role": "user",
  307. "content": content,
  308. }
  309. ],
  310. "temperature": 0.8,
  311. "top_p": 0.8,
  312. "penalty_score": 1
  313. };
  314. try {
  315. // 增加重试机制
  316. let retries = 3;
  317. let lastError = null;
  318. let backoffTime = 1000; // 初始等待时间1秒
  319. while (retries > 0) {
  320. try {
  321. console.log(`百度千帆 ${this.model}`);
  322. const response = await axios.post(this.url, postJSON, {
  323. headers: this.headers,
  324. timeout: 60000 // 60秒超时
  325. });
  326. // 处理响应格式
  327. if (response.data?.choices?.[0]?.message?.content) {
  328. return response.data.choices[0].message.content;
  329. } else if (response.data?.result) {
  330. return response.data.result;
  331. } else {
  332. console.error("百度千帆API响应格式异常:", response.data);
  333. throw new Error("Unexpected response format from Baidu API");
  334. }
  335. } catch (error) {
  336. lastError = error;
  337. // 特别处理429错误(请求过多)
  338. if (error.response && error.response.status === 429) {
  339. console.warn(`百度千帆API限流,等待${backoffTime/1000}秒后重试...`);
  340. await new Promise(resolve => setTimeout(resolve, backoffTime));
  341. backoffTime *= 2; // 指数退避策略
  342. retries--;
  343. continue;
  344. }
  345. // 处理其他错误
  346. retries--;
  347. if (retries > 0) {
  348. console.warn(`API请求失败,剩余重试次数: ${retries},等待${backoffTime/1000}秒后重试...`);
  349. await new Promise(resolve => setTimeout(resolve, backoffTime));
  350. backoffTime *= 2; // 指数退避策略
  351. continue;
  352. }
  353. // 详细记录错误信息
  354. if (error.response) {
  355. console.error("百度千帆API错误响应:", {
  356. status: error.response.status,
  357. data: error.response.data,
  358. headers: error.response.headers
  359. });
  360. } else if (error.request) {
  361. console.error("百度千帆API请求错误:", {
  362. method: error.config.method,
  363. url: error.config.url,
  364. headers: error.config.headers,
  365. data: error.config.data
  366. });
  367. } else {
  368. console.error("百度千帆API设置错误:", error.message);
  369. }
  370. throw error;
  371. }
  372. }
  373. throw lastError || new Error("百度千帆API请求失败,已达到最大重试次数");
  374. } catch (error) {
  375. console.error("Baidu API error:", error);
  376. throw error;
  377. }
  378. }
  379. }
  380. /**
  381. * Openrouter平台实现
  382. */
  383. class OpenrouterProvider extends AIProvider {
  384. constructor() {
  385. super();
  386. this.headers = {
  387. "Authorization": "Bearer " + config.openroutercloud.apikey,
  388. "Content-Type": "application/json"
  389. };
  390. this.url = "https://openrouter.ai/api/v1/chat/completions";
  391. this.model = "moonshotai/kimi-k2:free";
  392. }
  393. async generateArticle(content) {
  394. const postJSON = {
  395. "model": this.model,
  396. "messages": [
  397. {
  398. "role": "user",
  399. "content": content,
  400. }
  401. ]
  402. };
  403. try {
  404. console.log(`Openrouter ${this.model}`);
  405. const response = await axios.post(this.url, postJSON, { headers: this.headers });
  406. return response.data.choices[0].message.content;
  407. } catch (error) {
  408. console.error("OpenAI API error:", error);
  409. throw error;
  410. }
  411. }
  412. }
  413. /**
  414. * AI提供者工厂类
  415. * 根据配置创建不同的AI平台实例
  416. */
  417. class AIProviderFactory {
  418. /**
  419. * 获取AI提供者实例
  420. * @param {string} provider - AI提供者名称
  421. * @returns {AIProvider} - 返回对应的AI提供者实例
  422. */
  423. static getProvider(provider) {
  424. const providerLower = provider.toLowerCase();
  425. // 处理传统提供者名称
  426. switch (providerLower) {
  427. case 'openai':
  428. return new OpenAIProvider();
  429. case 'openrouter-moonshotai/kimi-k2':
  430. return new OpenrouterProvider("moonshotai/kimi-k2:free");
  431. case 'doubao-1-5-pro-32k-250115':
  432. return new VolcesAIProvider(providerLower);
  433. case 'doubao-seed-1-6-250615':
  434. return new VolcesAIProvider(providerLower);
  435. case 'doubao-deepseek-v3-250324':
  436. return new VolcesAIProvider("deepseek-v3-250324");
  437. case 'doubao-deepseek-r1-250528':
  438. return new VolcesAIProvider("deepseek-r1-250528");
  439. case 'doubao-kimi-k2-250711':
  440. return new VolcesAIProvider("kimi-k2-250711");
  441. case 'ali-qwen-plus-2025-07-14':
  442. return new AliyunAIProvider("qwen-plus-2025-07-14");
  443. case 'ali-qwen-max':
  444. return new AliyunAIProvider("qwen-max");
  445. case 'llama-4-scout-17b-16e-instruct':
  446. return new AliyunAIProvider(providerLower);
  447. case 'llama-4-maverick-17b-128e-instruct':
  448. return new AliyunAIProvider(providerLower);
  449. case 'ali-deepseek-r1-0528':
  450. return new AliyunAIProvider("deepseek-r1-0528");
  451. case 'ali-deepseek-v3':
  452. return new AliyunAIProvider("deepseek-v3");
  453. case 'ali-moonshot-kimi-k2-instruct':
  454. return new AliyunAIProvider("Moonshot-Kimi-K2-Instruct");
  455. case 'xf-yun-spark-x1':
  456. return new XunFeiYunAIProvider();
  457. case 'tencent-hunyuan-turbos-latest':
  458. return new TencentHunyuanAIProvider("hunyuan-turbos-latest");
  459. case 'tencent-hunyuan-t1-latest':
  460. return new TencentHunyuanAIProvider("hunyuan-t1-latest");
  461. case 'baidu-deepseek-v3':
  462. return new BaiduAIProvider();
  463. case 'baidu-deepseek-r1':
  464. return new BaiduAIProvider("deepseek-r1");
  465. case 'baidu-ernie-4.5-turbo-vl-32k-preview':
  466. return new BaiduAIProvider("ernie-4.5-turbo-vl-32k-preview");
  467. default:
  468. return new VolcesAIProvider("doubao-1-5-pro-32k-250115"); // 默认使用火山云1.5
  469. }
  470. }
  471. }
  472. /**
  473. * 生成文章的主函数
  474. * @param {string} content - 生成文章的提示内容
  475. * @param {string} provider - AI提供者名称,默认为'volces'
  476. * @returns {Promise<string>} - 返回生成的文章JSON字符串
  477. */
  478. async function generateArticle(content, provider) {
  479. try {
  480. const aiProvider = AIProviderFactory.getProvider(provider);
  481. const result = await aiProvider.generateArticle(content);
  482. return result;
  483. } catch (error) {
  484. console.error("Generate article error:", error);
  485. throw error;
  486. }
  487. }
  488. /**
  489. * 验证并修复JSON结构
  490. * @param {string} jsonString - 需要验证和修复的JSON字符串
  491. * @returns {string} - 返回修复后的JSON字符串
  492. */
  493. function validateAndFixJSON(jsonString) {
  494. // 如果输入不是字符串,直接返回
  495. if (typeof jsonString !== 'string') {
  496. console.error("输入不是字符串类型");
  497. return jsonString;
  498. }
  499. // 预处理:移除Markdown代码块标记
  500. let processedJson = jsonString;
  501. // 检查并移除Markdown代码块标记
  502. if (processedJson.includes('```')) {
  503. console.log("检测到Markdown代码块,尝试移除标记");
  504. // 处理可能的多行代码块
  505. const codeBlockRegex = /```(?:json)?\s*\n([\s\S]*?)\n```/;
  506. const match = processedJson.match(codeBlockRegex);
  507. if (match && match[1]) {
  508. // 提取代码块内容
  509. processedJson = match[1];
  510. console.log("成功提取代码块内容");
  511. } else {
  512. // 如果正则匹配失败,尝试简单替换
  513. processedJson = processedJson.replace(/^```(?:json)?\s*\n/, '');
  514. processedJson = processedJson.replace(/\n```\s*$/, '');
  515. }
  516. }
  517. try {
  518. // 尝试解析JSON
  519. const parsed = JSON.parse(processedJson);
  520. return processedJson;
  521. } catch (error) {
  522. console.error("JSON解析错误,尝试修复:", error);
  523. // 尝试修复常见的JSON错误
  524. let fixedJson = processedJson;
  525. // 修复缺少引号的键
  526. fixedJson = fixedJson.replace(/(\s*?)(\w+)(\s*?):/g, '"$2":');
  527. // 修复单引号,但不影响缩写
  528. fixedJson = fixedJson.replace(/(?<!\w)'(?!\w)/g, '"');
  529. // 修复尾部逗号
  530. fixedJson = fixedJson.replace(/,\s*}/g, '}');
  531. fixedJson = fixedJson.replace(/,\s*\]/g, ']');
  532. // 尝试解析修复后的JSON
  533. try {
  534. JSON.parse(fixedJson);
  535. console.log("JSON修复成功");
  536. return fixedJson;
  537. } catch (error2) {
  538. console.error("JSON修复失败:", error2);
  539. // 最后尝试:如果内容看起来像JSON但解析失败,尝试提取{}之间的内容
  540. const jsonObjectRegex = /\{[\s\S]*\}/;
  541. const objectMatch = jsonString.match(jsonObjectRegex);
  542. if (objectMatch) {
  543. try {
  544. const extractedJson = objectMatch[0];
  545. JSON.parse(extractedJson);
  546. console.log("通过提取{}内容成功修复JSON");
  547. return extractedJson;
  548. } catch (error3) {
  549. console.error("提取{}内容后仍解析失败", error3);
  550. throw new Error("JSON解析失败:提取{}内容后仍无法解析");
  551. }
  552. }
  553. // 如果所有尝试都失败,抛出错误
  554. throw new Error("JSON解析失败:无法修复格式错误");
  555. }
  556. }
  557. }
  558. /**
  559. * 标准化文章字段,将修正版本的内容应用到标准字段中
  560. * @param {string|Object} jsonInput - 包含文章内容的JSON字符串或对象
  561. * @returns {string|Object} - 返回标准化后的JSON字符串或对象,与输入类型保持一致
  562. */
  563. function normalizeArticleFields(jsonInput) {
  564. // 判断输入是字符串还是对象
  565. const isString = typeof jsonInput === 'string';
  566. // 如果是字符串,先解析为对象
  567. let json = isString ? JSON.parse(jsonInput) : jsonInput;
  568. if (json.ArticleEnglishCorrected){
  569. json.ArticleEnglish=json.ArticleEnglishCorrected;
  570. delete json.ArticleEnglishCorrected;
  571. }
  572. if (json.ArticleChineseCorrected){
  573. json.ArticleChinese=json.ArticleChineseCorrected;
  574. delete json.ArticleChineseCorrected;
  575. }
  576. // 确保ArticleEnglish数组中只包含英文句子,ArticleChinese数组中只包含中文句子
  577. if (json.ArticleEnglish && Array.isArray(json.ArticleEnglish)) {
  578. const englishSentences = [];
  579. const chineseSentences = [];
  580. // 遍历ArticleEnglish数组,分离英文和中文句子
  581. json.ArticleEnglish.forEach(sentence => {
  582. // 检查句子是否包含中文字符
  583. if (/[\u4e00-\u9fa5]/.test(sentence)) {
  584. chineseSentences.push(sentence);
  585. } else {
  586. englishSentences.push(sentence);
  587. }
  588. });
  589. // 更新ArticleEnglish数组,只保留英文句子
  590. json.ArticleEnglish = englishSentences;
  591. // 如果ArticleChinese不存在或不是数组,则创建它
  592. if (!json.ArticleChinese || !Array.isArray(json.ArticleChinese)) {
  593. json.ArticleChinese = [];
  594. }
  595. // 将中文句子添加到ArticleChinese数组中
  596. chineseSentences.forEach(sentence => {
  597. if (!json.ArticleChinese.includes(sentence)) {
  598. json.ArticleChinese.push(sentence);
  599. }
  600. });
  601. }
  602. // 根据输入类型返回相应的结果
  603. return isString ? JSON.stringify(json) : json;
  604. }
  605. // 默认导出,保持向后兼容性
  606. export default {
  607. generateArticle,
  608. enhanceFormsOfWords,
  609. validateAndFixJSON,
  610. normalizeArticleFields
  611. };