aiController.js 24 KB

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