Browse Source

fix: file ai llm

ryanemax 1 day ago
parent
commit
87aaf3ca46

+ 29 - 0
angular.json

@@ -31,6 +31,35 @@
             ],
             "styles": [
               "src/styles.scss"
+            ],
+            "allowedCommonJsDependencies": [
+              "microsoft-cognitiveservices-speech-sdk",
+              "mathjax-full",
+              "recorder-core",
+              "dhtmlxscheduler",
+              "codemirror",
+              "mxgraph",
+              "plupload",
+              "qrious",
+              "wangeditor",
+              "echarts",
+              "querystring",
+              "spark-md5",
+              "jquery",
+              "markdown-it-abbr",
+              "markdown-it-deflist",
+              "markdown-it-ins",
+              "markdown-it-mark",
+              "markdown-it-ruby",
+              "markdown-it-sub",
+              "markdown-it-footnote",
+              "markdown-it-sup",
+              "plantuml-encoder",
+              "@amap/amap-jsapi-loader",
+              "jsqr",
+              "dhtmlx-gantt",
+              "@ctrl/ngx-codemirror",
+              "node_modules/wangeditor/release/wangEditor.js"
             ]
           },
           "configurations": {

File diff suppressed because it is too large
+ 94 - 208
package-lock.json


+ 59 - 0
package.json

@@ -21,6 +21,7 @@
   },
   "private": true,
   "dependencies": {
+    "@amap/amap-jsapi-loader": "^1.0.1",
     "@angular/animations": "20.1.0",
     "@angular/cdk": "20.2.2",
     "@angular/common": "^20.1.0",
@@ -30,13 +31,71 @@
     "@angular/material": "^20.2.2",
     "@angular/platform-browser": "^20.1.0",
     "@angular/router": "^20.1.0",
+    "@awesome-cordova-plugins/core": "^8.1.0",
+    "@awesome-cordova-plugins/diagnostic": "^8.1.0",
+    "@awesome-cordova-plugins/media-capture": "^8.1.0",
+    "@babylonjs/core": "^7.2.3",
+    "@babylonjs/loaders": "^7.2.3",
+    "@capacitor/camera": "^7.0.2",
+    "@capacitor/clipboard": "^7.0.2",
+    "@capacitor/core": "^7.4.3",
+    "@capacitor/filesystem": "^7.1.4",
+    "@codemirror/commands": "^6.9.0",
+    "@codemirror/fold": "^0.19.4",
+    "@codemirror/lang-cpp": "^6.0.3",
+    "@codemirror/lang-css": "^6.3.1",
+    "@codemirror/lang-html": "^6.4.11",
+    "@codemirror/lang-java": "^6.0.2",
+    "@codemirror/lang-javascript": "^6.2.4",
+    "@codemirror/lang-json": "^6.0.2",
+    "@codemirror/lang-markdown": "^6.4.0",
+    "@codemirror/lang-python": "^6.2.1",
+    "@codemirror/lang-sql": "^6.10.0",
+    "@codemirror/language": "^6.11.3",
+    "@codemirror/lint": "^6.9.0",
+    "@codemirror/search": "^6.5.11",
+    "@codemirror/state": "^6.5.2",
+    "@codemirror/theme-one-dark": "^6.1.3",
+    "@codemirror/view": "^6.38.6",
+    "@ctrl/tinycolor": "^4.2.0",
+    "@ionic/angular": "^8.7.7",
+    "@langchain/core": "^0.3.78",
+    "@types/spark-md5": "^3.0.5",
+    "@wecom/jssdk": "^2.3.1",
     "chart.js": "^4.5.0",
+    "codemirror": "^6.0.2",
     "echarts": "^6.0.0",
+    "esdk-obs-browserjs": "^3.25.6",
+    "eventemitter3": "^5.0.1",
     "fmode-ng": "^0.0.212",
+    "highlight.js": "^11.11.1",
+    "jquery": "^3.7.1",
+    "markdown-it": "^14.1.0",
+    "markdown-it-abbr": "^1.0.4",
+    "markdown-it-deflist": "^2.1.0",
+    "markdown-it-footnote": "^3.0.3",
+    "markdown-it-imsize": "^2.0.1",
+    "markdown-it-ins": "^3.0.1",
+    "markdown-it-mark": "^3.0.1",
+    "markdown-it-mathjax": "^2.0.0",
+    "markdown-it-ruby": "^0.1.1",
+    "markdown-it-sub": "^1.0.0",
+    "markdown-it-sup": "^1.0.0",
+    "mathjax-full": "^3.2.2",
+    "ng-qrcode": "^17.0.0",
+    "pako": "^2.1.0",
+    "plantuml-encoder": "^1.4.0",
+    "plupload": "^2.3.9",
+    "qiniu-js": "^2.5.5",
     "qrcode": "^1.5.4",
+    "quill": "^2.0.3",
+    "recorder-core": "^1.3.25011100",
     "roboto-fontface": "^0.10.0",
     "rxjs": "~7.8.0",
+    "spark-md5": "^3.0.2",
+    "swiper": "^11.2.10",
     "tslib": "^2.3.0",
+    "uuid": "^11.1.0",
     "zone.js": "~0.15.0"
   },
   "devDependencies": {

+ 7 - 5
src/modules/project/pages/customer-profile/customer-profile.component.ts

@@ -4,6 +4,7 @@ import { Router, ActivatedRoute } from '@angular/router';
 import { IonicModule } from '@ionic/angular';
 import { WxworkSDK, WxworkCorp } from 'fmode-ng/core';
 import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
+import { WxworkSDKService } from '../../services/wxwork-sdk.service';
 
 const Parse = FmodeParse.with('nova');
 
@@ -99,7 +100,8 @@ export class CustomerProfileComponent implements OnInit {
 
   constructor(
     private router: Router,
-    private route: ActivatedRoute
+    private route: ActivatedRoute,
+    private wxworkService: WxworkSDKService
   ) {}
 
   async ngOnInit() {
@@ -298,10 +300,10 @@ export class CustomerProfileComponent implements OnInit {
    */
   async navigateToGroupChat(chatId: string) {
     try {
-      // 使用企微SDK跳转到群聊
-      await this.wxwork!.openChat(chatId);
-    } catch (err) {
-      console.error('跳转群聊失败:', err);
+      // 使用企微SDK服务跳转到群聊
+      await this.wxworkService.openChat(chatId);
+    } catch (error: any) {
+      console.error('跳转群聊失败:', error);
       alert('跳转失败,请在企业微信中操作');
     }
   }

+ 1 - 1
src/modules/project/pages/project-detail/project-detail.component.html

@@ -65,7 +65,7 @@
           <div class="customer-info">
             <ion-avatar>
               @if (customer?.get('data')?.avatar) {
-                <img [src]="customer.get('data').avatar" alt="客户头像" />
+                <img [src]="customer?.get('data')?.avatar" alt="客户头像" />
               } @else {
                 <ion-icon name="person-circle-outline"></ion-icon>
               }

+ 11 - 11
src/modules/project/pages/project-detail/stages/stage-aftercare.component.ts

@@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
 import { IonicModule } from '@ionic/angular';
 import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
-import { NovaUploadService } from '../../../services/upload.service';
+import { ProjectUploadService } from '../../../services/upload.service';
 import { ProjectAIService } from '../../../services/ai.service';
 import { WxworkSDKService } from '../../../services/wxwork-sdk.service';
 
@@ -92,7 +92,7 @@ export class StageAftercareComponent implements OnInit {
 
   constructor(
     private route: ActivatedRoute,
-    private uploadService: NovaUploadService,
+    private uploadService: ProjectUploadService,
     private aiService: ProjectAIService,
     private wxworkService: WxworkSDKService
   ) {}
@@ -191,7 +191,7 @@ export class StageAftercareComponent implements OnInit {
       // 上传文件到Parse Server
       const url = await this.uploadService.uploadFile(file, {
         compress: true,
-        onProgress: (progress) => {
+        onProgress: (progress: number) => {
           console.log('上传进度:', progress);
         }
       });
@@ -240,9 +240,9 @@ export class StageAftercareComponent implements OnInit {
         alert('凭证已上传,但OCR识别失败,请手动核对金额和支付方式');
       }
 
-    } catch (err) {
-      console.error('上传失败:', err);
-      alert('上传失败: ' + err.message);
+    } catch (error: any) {
+      console.error('上传失败:', error);
+      alert('上传失败: ' + (error?.message || '未知错误'));
     } finally {
       this.uploading = false;
     }
@@ -307,9 +307,9 @@ export class StageAftercareComponent implements OnInit {
 
       alert('项目复盘生成成功');
 
-    } catch (err) {
-      console.error('生成失败:', err);
-      alert('生成失败: ' + err.message);
+    } catch (error: any) {
+      console.error('生成失败:', error);
+      alert('生成失败: ' + (error?.message || '未知错误'));
     } finally {
       this.generating = false;
     }
@@ -428,8 +428,8 @@ export class StageAftercareComponent implements OnInit {
         archived: true,
         archiveTime: new Date(),
         archivedBy: {
-          id: this.currentUser!.id,
-          name: this.currentUser!.get('name')
+          id: this.currentUser!.id || '',
+          name: this.currentUser!.get('name') || ''
         }
       };
 

+ 4 - 4
src/modules/project/pages/project-detail/stages/stage-delivery.component.html

@@ -18,7 +18,7 @@
         </div>
         <ion-progress-bar [value]="completionProgress / 100" color="success"></ion-progress-bar>
         <p class="progress-detail">
-          已完成 {{ deliverables.filter(d => d.status === 'approved').length }} / {{ deliverables.length }} 项
+          已完成 {{ getApprovedCount() }} / {{ deliverables.length }} 项
         </p>
       </ion-card-content>
     </ion-card>
@@ -121,7 +121,7 @@
                       expand="block"
                       color="primary"
                       (click)="deliverable.qualityCheck.checked = true; submitForReview(group.spaceName, deliverable.processType)"
-                      [disabled]="!deliverable.qualityCheck.items.every(i => i.passed)">
+                      [disabled]="!isQualityCheckAllPassed(deliverable)">
                       <ion-icon name="checkmark-circle-outline" slot="start"></ion-icon>
                       提交审核
                     </ion-button>
@@ -145,14 +145,14 @@
                     <ion-button
                       expand="block"
                       color="success"
-                      (click)="reviewDeliverable(group.spaceName, deliverable.processType, 'approved', reviewComments.value)">
+                      (click)="reviewDeliverable(group.spaceName, deliverable.processType, 'approved', reviewComments.value || '')">
                       <ion-icon name="checkmark-outline" slot="start"></ion-icon>
                       通过
                     </ion-button>
                     <ion-button
                       expand="block"
                       color="danger"
-                      (click)="reviewDeliverable(group.spaceName, deliverable.processType, 'rejected', reviewComments.value)">
+                      (click)="reviewDeliverable(group.spaceName, deliverable.processType, 'rejected', reviewComments.value || '')">
                       <ion-icon name="close-outline" slot="start"></ion-icon>
                       驳回
                     </ion-button>

+ 14 - 0
src/modules/project/pages/project-detail/stages/stage-delivery.component.ts

@@ -423,6 +423,20 @@ export class StageDeliveryComponent implements OnInit {
     }
   }
 
+  /**
+   * 获取已批准的交付物数量
+   */
+  getApprovedCount(): number {
+    return this.deliverables.filter(d => d.status === 'approved').length;
+  }
+
+  /**
+   * 检查质检是否全部通过
+   */
+  isQualityCheckAllPassed(deliverable: any): boolean {
+    return deliverable.qualityCheck?.items?.every((i: any) => i.passed) || false;
+  }
+
   /**
    * 获取工序颜色
    */

+ 10 - 10
src/modules/project/pages/project-detail/stages/stage-order.component.html

@@ -123,42 +123,42 @@
               @for (processType of processTypes; track processType.key) {
                 <div
                   class="process-item"
-                  [class.enabled]="space.processes[processType.key].enabled">
+                  [class.enabled]="isProcessEnabled(space, processType.key)">
                   <div class="process-header" (click)="canEdit && toggleProcess(space, processType.key)">
                     <ion-checkbox
-                      [checked]="space.processes[processType.key].enabled"
+                      [checked]="isProcessEnabled(space, processType.key)"
                       [disabled]="!canEdit"></ion-checkbox>
                     <ion-badge [color]="processType.color">
                       {{ processType.name }}
                     </ion-badge>
                   </div>
 
-                  @if (space.processes[processType.key].enabled) {
+                  @if (isProcessEnabled(space, processType.key)) {
                     <div class="process-inputs">
                       <ion-item lines="none">
                         <ion-label position="stacked">单价</ion-label>
                         <ion-input
                           type="number"
-                          [(ngModel)]="space.processes[processType.key].price"
-                          (ionChange)="onProcessChange()"
+                          [ngModel]="getProcessPrice(space, processType.key)"
+                          (ngModelChange)="setProcessPrice(space, processType.key, $event); onProcessChange()"
                           [disabled]="!canEdit"
                           placeholder="0"></ion-input>
-                        <ion-note slot="end">元/{{ space.processes[processType.key].unit }}</ion-note>
+                        <ion-note slot="end">元/{{ getProcessUnit(space, processType.key) }}</ion-note>
                       </ion-item>
 
                       <ion-item lines="none">
                         <ion-label position="stacked">数量</ion-label>
                         <ion-input
                           type="number"
-                          [(ngModel)]="space.processes[processType.key].quantity"
-                          (ionChange)="onProcessChange()"
+                          [ngModel]="getProcessQuantity(space, processType.key)"
+                          (ngModelChange)="setProcessQuantity(space, processType.key, $event); onProcessChange()"
                           [disabled]="!canEdit"
                           placeholder="0"></ion-input>
-                        <ion-note slot="end">{{ space.processes[processType.key].unit }}</ion-note>
+                        <ion-note slot="end">{{ getProcessUnit(space, processType.key) }}</ion-note>
                       </ion-item>
 
                       <div class="process-subtotal">
-                        小计: ¥{{ (space.processes[processType.key].price * space.processes[processType.key].quantity).toFixed(2) }}
+                        小计: ¥{{ calculateProcessSubtotal(space, processType.key).toFixed(2) }}
                       </div>
                     </div>
                   }

+ 60 - 0
src/modules/project/pages/project-detail/stages/stage-order.component.ts

@@ -344,6 +344,66 @@ export class StageOrderComponent implements OnInit {
     }
   }
 
+  /**
+   * 检查工序是否启用
+   */
+  isProcessEnabled(space: any, processKey: string): boolean {
+    const process = (space.processes as any)[processKey];
+    return process?.enabled || false;
+  }
+
+  /**
+   * 设置工序价格
+   */
+  setProcessPrice(space: any, processKey: string, value: any): void {
+    const process = (space.processes as any)[processKey];
+    if (process) {
+      process.price = value;
+    }
+  }
+
+  /**
+   * 设置工序数量
+   */
+  setProcessQuantity(space: any, processKey: string, value: any): void {
+    const process = (space.processes as any)[processKey];
+    if (process) {
+      process.quantity = value;
+    }
+  }
+
+  /**
+   * 获取工序价格
+   */
+  getProcessPrice(space: any, processKey: string): number {
+    const process = (space.processes as any)[processKey];
+    return process?.price || 0;
+  }
+
+  /**
+   * 获取工序数量
+   */
+  getProcessQuantity(space: any, processKey: string): number {
+    const process = (space.processes as any)[processKey];
+    return process?.quantity || 0;
+  }
+
+  /**
+   * 获取工序单位
+   */
+  getProcessUnit(space: any, processKey: string): string {
+    const process = (space.processes as any)[processKey];
+    return process?.unit || '';
+  }
+
+  /**
+   * 计算工序小计
+   */
+  calculateProcessSubtotal(space: any, processKey: string): number {
+    const process = (space.processes as any)[processKey];
+    return (process?.price || 0) * (process?.quantity || 0);
+  }
+
   /**
    * 获取项目类型图标
    */

+ 14 - 15
src/modules/project/pages/project-detail/stages/stage-requirements.component.ts

@@ -2,9 +2,9 @@ import { Component, OnInit, Input } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
-import { IonicModule, ModalController } from '@ionic/angular';
+import { IonicModule } from '@ionic/angular';
 import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
-import { NovaUploadService } from '../../../services/upload.service';
+import { ProjectUploadService } from '../../../services/upload.service';
 import { ProjectAIService } from '../../../services/ai.service';
 import { WxworkSDKService } from '../../../services/wxwork-sdk.service';
 
@@ -103,8 +103,7 @@ export class StageRequirementsComponent implements OnInit {
 
   constructor(
     private route: ActivatedRoute,
-    private modalController: ModalController,
-    private uploadService: NovaUploadService,
+    private uploadService: ProjectUploadService,
     private aiService: ProjectAIService,
     private wxworkService: WxworkSDKService
   ) {}
@@ -230,7 +229,7 @@ export class StageRequirementsComponent implements OnInit {
         compress: true,
         maxWidth: 1920,
         maxHeight: 1920,
-        onProgress: (progress) => {
+        onProgress: (progress: number) => {
           console.log('上传进度:', progress);
         }
       });
@@ -244,9 +243,9 @@ export class StageRequirementsComponent implements OnInit {
 
       await this.saveDraft();
 
-    } catch (err) {
-      console.error('上传失败:', err);
-      alert('上传失败: ' + err.message);
+    } catch (error: any) {
+      console.error('上传失败:', error);
+      alert('上传失败: ' + (error?.message || '未知错误'));
     } finally {
       this.uploading = false;
     }
@@ -295,7 +294,7 @@ export class StageRequirementsComponent implements OnInit {
 
       // 上传文件到Parse Server
       const url = await this.uploadService.uploadFile(file, {
-        onProgress: (progress) => {
+        onProgress: (progress: number) => {
           console.log('上传进度:', progress);
         }
       });
@@ -309,9 +308,9 @@ export class StageRequirementsComponent implements OnInit {
 
       await this.saveDraft();
 
-    } catch (err) {
-      console.error('上传失败:', err);
-      alert('上传失败: ' + err.message);
+    } catch (error: any) {
+      console.error('上传失败:', error);
+      alert('上传失败: ' + (error?.message || '未知错误'));
     } finally {
       this.uploading = false;
     }
@@ -393,9 +392,9 @@ export class StageRequirementsComponent implements OnInit {
 
       alert('AI方案生成成功');
 
-    } catch (err) {
-      console.error('生成失败:', err);
-      alert('生成失败: ' + err.message);
+    } catch (error: any) {
+      console.error('生成失败:', error);
+      alert('生成失败: ' + (error?.message || '未知错误'));
     } finally {
       this.generating = false;
     }

+ 11 - 15
src/modules/project/services/ai.service.ts

@@ -1,10 +1,7 @@
 import { Injectable } from '@angular/core';
-import { FmodeParse } from 'fmode-ng/parse';
-import { completionJSON, FmodeChatCompletion } from 'fmode-ng/lib/core/agent';
+import { completionJSON, FmodeChatCompletion } from 'fmode-ng/core/agent/chat/completion';
 import { Observable } from 'rxjs';
 
-const Parse = FmodeParse.with('nova');
-
 // 使用的AI模型
 export const ProjectAIModel = 'fmode-1.6-cn';
 
@@ -69,7 +66,7 @@ export class ProjectAIService {
       const result = await completionJSON(
         prompt,
         outputSchema,
-        (content) => {
+        (content: string) => {
           options?.onProgress?.(content);
         },
         3, // 最大重试次数
@@ -85,9 +82,9 @@ export class ProjectAIService {
         content: '基于您的需求,我们为您设计了以下方案...',
         ...result
       };
-    } catch (error) {
+    } catch (error: any) {
       console.error('生成设计方案失败:', error);
-      throw new Error('生成设计方案失败: ' + error.message);
+      throw new Error('生成设计方案失败: ' + (error?.message || '未知错误'));
     }
   }
 
@@ -123,7 +120,7 @@ export class ProjectAIService {
       const result = await completionJSON(
         prompt,
         outputSchema,
-        (content) => {
+        (content: string) => {
           options?.onProgress?.(content);
         },
         2,
@@ -136,9 +133,9 @@ export class ProjectAIService {
         generated: true,
         ...result
       };
-    } catch (error) {
+    } catch (error: any) {
       console.error('生成项目复盘失败:', error);
-      throw new Error('生成项目复盘失败: ' + error.message);
+      throw new Error('生成项目复盘失败: ' + (error?.message || '未知错误'));
     }
   }
 
@@ -167,7 +164,7 @@ export class ProjectAIService {
       const result = await completionJSON(
         prompt,
         options?.outputSchema || defaultSchema,
-        (content) => {
+        (content: string) => {
           options?.onProgress?.(content);
         },
         2,
@@ -179,9 +176,9 @@ export class ProjectAIService {
       );
 
       return result;
-    } catch (error) {
+    } catch (error: any) {
       console.error('图片识别失败:', error);
-      throw new Error('图片识别失败: ' + error.message);
+      throw new Error('图片识别失败: ' + (error?.message || '未知错误'));
     }
   }
 
@@ -206,8 +203,7 @@ export class ProjectAIService {
     ];
 
     const completion = new FmodeChatCompletion(messageList, {
-      model: options?.model || ProjectAIModel,
-      temperature: options?.temperature || 0.7
+      model: options?.model || ProjectAIModel
     });
 
     return completion.sendCompletion({

+ 12 - 16
src/modules/project/services/upload.service.ts

@@ -1,20 +1,19 @@
 import { Injectable } from '@angular/core';
-import { FmodeParse } from 'fmode-ng/parse';
-
-const Parse = FmodeParse.with('nova');
+import { NovaUploadService as FmodeUploadService } from 'fmode-ng';
 
 /**
  * 文件上传服务
  * 用于处理图片、视频、文档等文件的上传
+ * 基于 fmode-ng 的 NovaUploadService
  */
 @Injectable({
   providedIn: 'root'
 })
-export class NovaUploadService {
-  constructor() {}
+export class ProjectUploadService {
+  constructor(private uploadServ: FmodeUploadService) {}
 
   /**
-   * 上传单个文件到Parse Server
+   * 上传单个文件
    * @param file File对象
    * @param options 上传选项
    * @returns 上传后的文件URL
@@ -40,21 +39,18 @@ export class NovaUploadService {
         });
       }
 
-      // 创建Parse文件
-      const parseFile = new Parse.File(file.name, fileToUpload);
-
-      // 上传文件
-      await parseFile.save({
-        progress: (progressValue: number) => {
-          options?.onProgress?.(progressValue * 100);
+      // 使用 fmode-ng 的上传服务
+      const fileResult = await this.uploadServ.upload(fileToUpload, (progress: any) => {
+        if (options?.onProgress && progress?.percent) {
+          options.onProgress(progress.percent);
         }
       });
 
       // 返回文件URL
-      return parseFile.url();
-    } catch (error) {
+      return fileResult.url;
+    } catch (error: any) {
       console.error('文件上传失败:', error);
-      throw new Error('文件上传失败: ' + error.message);
+      throw new Error('文件上传失败: ' + (error?.message || '未知错误'));
     }
   }
 

Some files were not shown because too many files changed in this diff