workflowService.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. /**
  2. * Workflow Service
  3. *
  4. * Integrates with external AI platform workflow to generate documents.
  5. * The workflow connects to the local backend export records API and returns document URLs.
  6. *
  7. * API Endpoint: http://114.242.25.27:3000/api/v2/chat/completions
  8. * Authentication: Bearer token in Authorization header
  9. * Workflow: Generates document → Calls local backend → Returns export record URL
  10. *
  11. * @module services/workflowService
  12. */
  13. import type { ExportRecordInfo } from '../types/chat';
  14. /**
  15. * Workflow chat message
  16. */
  17. interface WorkflowMessage {
  18. role: 'user' | 'assistant' | 'system';
  19. content: string;
  20. }
  21. /**
  22. * Workflow request
  23. */
  24. interface WorkflowRequest {
  25. chatId: string;
  26. stream?: boolean;
  27. detail?: boolean;
  28. messages: WorkflowMessage[];
  29. }
  30. /**
  31. * Workflow response from AI platform
  32. * The workflow internally calls http://192.168.0.195:8000/api/v1/export/records
  33. * and embeds the export record information in the response
  34. */
  35. interface WorkflowResponse {
  36. content?: string;
  37. choices?: Array<{
  38. message?: {
  39. content: string;
  40. };
  41. }>;
  42. // Export record data returned by workflow
  43. exportRecord?: {
  44. recordId: string;
  45. userId: string;
  46. fileName: string;
  47. fileSize: number;
  48. downloadUrl: string;
  49. documentId: string;
  50. styleId: string;
  51. createdAt: number;
  52. };
  53. // Or records array format
  54. records?: Array<{
  55. recordId: string;
  56. userId: string;
  57. fileName: string;
  58. fileSize: number;
  59. downloadUrl: string;
  60. documentId: string;
  61. styleId: string;
  62. createdAt: number;
  63. }>;
  64. }
  65. /**
  66. * Configuration for Workflow API
  67. */
  68. interface WorkflowConfig {
  69. apiUrl: string;
  70. apiKey: string;
  71. }
  72. /**
  73. * Get Workflow configuration from environment variables
  74. */
  75. const getWorkflowConfig = (): WorkflowConfig => {
  76. const apiUrl = import.meta.env.VITE_WORKFLOW_API_URL;
  77. const apiKey = import.meta.env.VITE_WORKFLOW_API_KEY;
  78. if (!apiUrl) {
  79. throw new Error('VITE_WORKFLOW_API_URL is not configured in environment variables');
  80. }
  81. if (!apiKey || apiKey === 'your_workflow_api_key_here') {
  82. // Workflow API key not configured
  83. }
  84. return { apiUrl, apiKey };
  85. };
  86. /**
  87. * Check if user input should trigger document generation workflow
  88. *
  89. * @param input - User's text input
  90. * @returns true if input matches document generation patterns
  91. */
  92. export const shouldTriggerWorkflow = (input: string): boolean => {
  93. const lowerInput = input.toLowerCase().trim();
  94. // Patterns that indicate document generation requests
  95. const generatePatterns = [
  96. '生成',
  97. '创建',
  98. '制作',
  99. '编写',
  100. 'generate',
  101. 'create',
  102. ];
  103. const documentPatterns = [
  104. '报告',
  105. '文档',
  106. '方案',
  107. '总结',
  108. '分析',
  109. 'report',
  110. 'document',
  111. 'doc',
  112. ];
  113. // Check if input contains both a generate action and document type
  114. const hasGenerateAction = generatePatterns.some((pattern) =>
  115. lowerInput.includes(pattern)
  116. );
  117. const hasDocumentType = documentPatterns.some((pattern) =>
  118. lowerInput.includes(pattern)
  119. );
  120. return hasGenerateAction && hasDocumentType;
  121. };
  122. /**
  123. * Call workflow to generate document and get export record URL
  124. *
  125. * This function calls the external AI platform workflow API which:
  126. * 1. Processes the user's document generation request
  127. * 2. Generates the document internally
  128. * 3. Calls the local backend API (http://192.168.0.195:8000/api/v1/export/records)
  129. * 4. Returns the export record with downloadUrl
  130. *
  131. * @param userInput - User's text input (e.g., "生成一个地质报告")
  132. * @param chatId - Chat session ID
  133. * @returns Promise resolving to AI response text and optional export record
  134. * @throws {Error} When workflow call fails
  135. */
  136. export const triggerDocumentWorkflow = async (
  137. userInput: string,
  138. chatId: string
  139. ): Promise<{ content: string; exportRecord?: ExportRecordInfo }> => {
  140. try {
  141. const config = getWorkflowConfig();
  142. // If API key is not configured, skip workflow
  143. if (!config.apiKey || config.apiKey === 'your_workflow_api_key_here') {
  144. return {
  145. content: '工作流未配置,无法生成文档。请配置 VITE_WORKFLOW_API_KEY。',
  146. };
  147. }
  148. // Call the workflow API
  149. const response = await fetch(config.apiUrl, {
  150. method: 'POST',
  151. headers: {
  152. 'Content-Type': 'application/json',
  153. Authorization: `Bearer ${config.apiKey}`,
  154. },
  155. body: JSON.stringify({
  156. chatId,
  157. stream: false,
  158. detail: false,
  159. messages: [
  160. {
  161. role: 'user',
  162. content: userInput,
  163. },
  164. ],
  165. } as WorkflowRequest),
  166. });
  167. if (!response.ok) {
  168. const errorText = await response.text();
  169. throw new Error(`Workflow API error (${response.status}): ${errorText}`);
  170. }
  171. const data: WorkflowResponse = await response.json();
  172. // Extract content from response
  173. let content =
  174. data.content ||
  175. data.choices?.[0]?.message?.content ||
  176. '文档已生成,请查看下方链接';
  177. // Extract export record from response
  178. let exportRecord: ExportRecordInfo | undefined;
  179. // Try to parse content as JSON if it looks like JSON
  180. if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
  181. try {
  182. const parsedContent = JSON.parse(content);
  183. // Handle nested response format: { code: 0, data: { records: [...] } }
  184. if (parsedContent.code === 0 && parsedContent.data?.records && Array.isArray(parsedContent.data.records)) {
  185. const records = parsedContent.data.records;
  186. if (records.length > 0) {
  187. const record = records[0];
  188. exportRecord = {
  189. recordId: record.recordId,
  190. fileName: record.fileName || '导出文档.docx',
  191. downloadUrl: record.downloadUrl,
  192. documentId: record.documentId || record.recordId,
  193. };
  194. // Update content to be more user-friendly
  195. content = '✅ 文档已生成,点击下方卡片预览或下载';
  196. }
  197. }
  198. // Handle direct records array format
  199. else if (parsedContent.records && Array.isArray(parsedContent.records) && parsedContent.records.length > 0) {
  200. const record = parsedContent.records[0];
  201. exportRecord = {
  202. recordId: record.recordId,
  203. fileName: record.fileName || '导出文档.docx',
  204. downloadUrl: record.downloadUrl,
  205. documentId: record.documentId || record.recordId,
  206. };
  207. // Update content to be more user-friendly
  208. content = '✅ 文档已生成,点击下方卡片预览或下载';
  209. }
  210. // Handle single record object format (not wrapped in array)
  211. else if (parsedContent.recordId && parsedContent.downloadUrl) {
  212. exportRecord = {
  213. recordId: parsedContent.recordId,
  214. fileName: parsedContent.fileName || '导出文档.docx',
  215. downloadUrl: parsedContent.downloadUrl,
  216. documentId: parsedContent.documentId || parsedContent.recordId,
  217. };
  218. // Update content to be more user-friendly
  219. content = '✅ 文档已生成,点击下方卡片预览或下载';
  220. }
  221. // Handle nested data format with single record
  222. else if (parsedContent.data && parsedContent.data.recordId) {
  223. const record = parsedContent.data;
  224. exportRecord = {
  225. recordId: record.recordId,
  226. fileName: record.fileName || '导出文档.docx',
  227. downloadUrl: record.downloadUrl,
  228. documentId: record.documentId || record.recordId,
  229. };
  230. // Update content to be more user-friendly
  231. content = '✅ 文档已生成,点击下方卡片预览或下载';
  232. }
  233. } catch (e) {
  234. // Content looks like JSON but failed to parse, log for debugging
  235. console.warn('Failed to parse JSON content:', e);
  236. console.warn('Content:', content);
  237. }
  238. }
  239. // Check for export record in top-level response
  240. if (!exportRecord && data.exportRecord) {
  241. // Single export record format
  242. exportRecord = {
  243. recordId: data.exportRecord.recordId,
  244. fileName: data.exportRecord.fileName,
  245. downloadUrl: data.exportRecord.downloadUrl,
  246. documentId: data.exportRecord.documentId,
  247. };
  248. content = '✅ 文档已生成,点击下方卡片预览或下载';
  249. } else if (!exportRecord && data.records && data.records.length > 0) {
  250. // Records array format (take the first one)
  251. const record = data.records[0];
  252. exportRecord = {
  253. recordId: record.recordId,
  254. fileName: record.fileName,
  255. downloadUrl: record.downloadUrl,
  256. documentId: record.documentId,
  257. };
  258. content = '✅ 文档已生成,点击下方卡片预览或下载';
  259. }
  260. return {
  261. content,
  262. exportRecord,
  263. };
  264. } catch (error) {
  265. // Return error message instead of throwing
  266. // This allows the chat to continue even if workflow fails
  267. if (error instanceof Error && error.message.includes('not configured')) {
  268. return {
  269. content: '⚠️ 工作流未配置,无法生成文档',
  270. };
  271. }
  272. return {
  273. content:
  274. '⚠️ 文档生成失败: ' +
  275. (error instanceof Error ? error.message : '未知错误'),
  276. };
  277. }
  278. };
  279. /**
  280. * Extract report type from user input
  281. *
  282. * @param input - User's text input
  283. * @returns Report type string (e.g., "地质报告", "技术报告")
  284. */
  285. export const extractReportType = (input: string): string => {
  286. const lowerInput = input.toLowerCase();
  287. if (lowerInput.includes('地质')) {
  288. return '地质报告';
  289. } else if (lowerInput.includes('技术')) {
  290. return '技术报告';
  291. } else if (lowerInput.includes('分析')) {
  292. return '分析报告';
  293. } else if (lowerInput.includes('项目')) {
  294. return '项目报告';
  295. } else if (lowerInput.includes('总结')) {
  296. return '总结报告';
  297. } else {
  298. return '报告';
  299. }
  300. };
  301. /**
  302. * Export the workflow service
  303. */
  304. export const workflowService = {
  305. shouldTriggerWorkflow,
  306. triggerDocumentWorkflow,
  307. extractReportType,
  308. };
  309. export default workflowService;