Browse Source

feat: new lib/ncloud & user/

未来全栈 1 year ago
parent
commit
9979f2d967

+ 120 - 37
heartvoice-app/src/lib/ncloud.ts

@@ -1,5 +1,4 @@
 // CloudObject.ts
-
 export class CloudObject {
     className: string;
     id: string | null = null;
@@ -7,8 +6,6 @@ export class CloudObject {
     updatedAt:any;
     data: Record<string, any> = {};
 
-
-
     constructor(className: string) {
         this.className = className;
     }
@@ -19,7 +16,7 @@ export class CloudObject {
 
     set(json: Record<string, any>) {
         Object.keys(json).forEach(key => {
-            if (["objectId", "id", "createdAt", "updatedAt", "ACL"].indexOf(key) > -1) {
+            if (["objectId", "id", "createdAt", "updatedAt"].indexOf(key) > -1) {
                 return;
             }
             this.data[key] = json[key];
@@ -85,34 +82,38 @@ export class CloudObject {
 // CloudQuery.ts
 export class CloudQuery {
     className: string;
-    whereOptions: Record<string, any> = {};
+    queryParams: Record<string, any> = {};
 
     constructor(className: string) {
         this.className = className;
     }
 
+    include(...fileds:string[]) {
+        this.queryParams["include"] = fileds;
+    }
     greaterThan(key: string, value: any) {
-        if (!this.whereOptions[key]) this.whereOptions[key] = {};
-        this.whereOptions[key]["$gt"] = value;
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$gt"] = value;
     }
 
     greaterThanAndEqualTo(key: string, value: any) {
-        if (!this.whereOptions[key]) this.whereOptions[key] = {};
-        this.whereOptions[key]["$gte"] = value;
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$gte"] = value;
     }
 
     lessThan(key: string, value: any) {
-        if (!this.whereOptions[key]) this.whereOptions[key] = {};
-        this.whereOptions[key]["$lt"] = value;
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$lt"] = value;
     }
 
     lessThanAndEqualTo(key: string, value: any) {
-        if (!this.whereOptions[key]) this.whereOptions[key] = {};
-        this.whereOptions[key]["$lte"] = value;
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$lte"] = value;
     }
 
     equalTo(key: string, value: any) {
-        this.whereOptions[key] = value;
+        if (!this.queryParams["where"]) this.queryParams["where"] = {};
+        this.queryParams["where"][key] = value;
     }
 
     async get(id: string) {
@@ -133,13 +134,24 @@ export class CloudQuery {
         return json || {};
     }
 
-    async find() {
+    async find():Promise<Array<CloudObject>> {
         let url = `https://dev.fmode.cn/parse/classes/${this.className}?`;
 
-        if (Object.keys(this.whereOptions).length) {
-            const whereStr = JSON.stringify(this.whereOptions);
-            url += `where=${whereStr}`;
-        }
+        let queryStr = ``
+        Object.keys(this.queryParams).forEach(key=>{
+            let paramStr = JSON.stringify(this.queryParams[key]);
+            if(key=="include"){
+                paramStr = this.queryParams[key]?.join(",")
+            }
+            if(queryStr) {
+                url += `${key}=${paramStr}`;
+            }else{
+                url += `&${key}=${paramStr}`;
+            }
+        })
+        // if (Object.keys(this.queryParams["where"]).length) {
+            
+        // }
 
         const response = await fetch(url, {
             headers: {
@@ -158,11 +170,12 @@ export class CloudQuery {
         return objList || [];
     }
 
+
     async first() {
         let url = `https://dev.fmode.cn/parse/classes/${this.className}?`;
 
-        if (Object.keys(this.whereOptions).length) {
-            const whereStr = JSON.stringify(this.whereOptions);
+        if (Object.keys(this.queryParams["where"]).length) {
+            const whereStr = JSON.stringify(this.queryParams["where"]);
             url += `where=${whereStr}`;
         }
 
@@ -219,23 +232,21 @@ export class CloudUser extends CloudObject {
             return null;
         }
         return this;
+        // const response = await fetch(`https://dev.fmode.cn/parse/users/me`, {
+        //     headers: {
+        //         "x-parse-application-id": "dev",
+        //         "x-parse-session-token": this.sessionToken // 使用sessionToken进行身份验证
+        //     },
+        //     method: "GET"
+        // });
+
+        // const result = await response?.json();
+        // if (result?.error) {
+        //     console.error(result?.error);
+        //     return null;
+        // }
+        // return result;
     }
-        
-    //     const response = await fetch(`https://dev.fmode.cn/parse/users/me`, {
-    //         headers: {
-    //             "x-parse-application-id": "dev",
-    //             "x-parse-session-token": this.sessionToken // 使用sessionToken进行身份验证
-    //         },
-    //         method: "GET"
-    //     });
-
-    //     const result = await response?.json();
-    //     if (result?.error) {
-    //         console.error(result?.error);
-    //         return null;
-    //     }
-    //     return result;
-    // }
 
     /** 登录 */
     async login(username: string, password: string):Promise<CloudUser|null> {
@@ -317,9 +328,81 @@ export class CloudUser extends CloudObject {
         }
 
         // 设置用户信息
+        // 缓存用户信息
+        console.log(result)
+        localStorage.setItem("NCloud/dev/User",JSON.stringify(result))
         this.id = result?.objectId;
         this.sessionToken = result?.sessionToken;
         this.data = result; // 保存用户数据
         return this;
     }
+
+    override async save() {
+        let method = "POST";
+        let url = `https://dev.fmode.cn/parse/users`;
+    
+        // 更新用户信息
+        if (this.id) {
+            url += `/${this.id}`;
+            method = "PUT";
+        }
+    
+        let data:any = JSON.parse(JSON.stringify(this.data))
+        delete data.createdAt
+        delete data.updatedAt
+        delete data.ACL
+        delete data.objectId
+        const body = JSON.stringify(data);
+        let headersOptions:any = {
+            "content-type": "application/json;charset=UTF-8",
+            "x-parse-application-id": "dev",
+            "x-parse-session-token": this.sessionToken, // 添加sessionToken以进行身份验证
+        }
+        const response = await fetch(url, {
+            headers: headersOptions,
+            body: body,
+            method: method,
+            mode: "cors",
+            credentials: "omit"
+        });
+    
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+        }
+        if (result?.objectId) {
+            this.id = result?.objectId;
+        }
+        localStorage.setItem("NCloud/dev/User",JSON.stringify(this.data))
+        return this;
+    }
+}
+
+export class CloudApi{
+    async fetch(path:string,body:any,options?:{
+        method:string
+        body:any
+    }){
+
+        let reqOpts:any =  {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            method: options?.method || "POST",
+            mode: "cors",
+            credentials: "omit"
+        }
+        if(body||options?.body){
+            reqOpts.body = JSON.stringify(body || options?.body);
+            reqOpts.json = true;
+        }
+        let host = `https://dev.fmode.cn`
+        // host = `http://127.0.0.1:1337`
+        let url = `${host}/api/`+path
+        console.log(url,reqOpts)
+        const response = await fetch(url,reqOpts);
+        let json = await response.json();
+        return json
+    }
 }

+ 262 - 0
heartvoice-app/src/lib/story.ts

@@ -0,0 +1,262 @@
+// import pdf from 'pdf-parse';
+// import fs from 'fs';
+import { CloudApi, CloudObject, CloudQuery } from 'src/lib/ncloud';
+import mammoth from "mammoth";
+import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
+import { Document } from '@langchain/core/documents';
+
+import * as tf from '@tensorflow/tfjs-core';
+// import "@tensorflow/tfjs-backend-cpu";
+// import '@tensorflow/tfjs-backend-webgpu';
+import '@tensorflow/tfjs-backend-webgl';
+// import '@tensorflow/tfjs-backend-wasm';
+import { TensorFlowEmbeddings } from "@langchain/community/embeddings/tensorflow";
+
+export class AgentStory{
+
+    story:CloudObject|undefined
+    // 文件标题
+    title:string|undefined = ""
+    // 文档标签
+    tags:Array<string>|undefined
+    // 文件源地址
+    url:string|undefined = ""
+    // 文档完整纯文本内容
+    content:string|undefined = ""
+    // 文档hash唯一值
+    hash:string|undefined = ""
+    // 文档分割后的列表
+    docList:Array<Document|any> = []
+
+    constructor(metadata:{
+        url:string,
+        title?:string,
+        tags?:Array<string>
+    }){
+        this.url = metadata.url
+        this.title = metadata.title
+        this.tags = metadata.tags
+        setBackend()
+    }
+    async save(){
+        if(!this.hash){ return }
+        let query = new CloudQuery("Story");
+        query.equalTo("hash",this.hash);
+        let story = await query.first();
+        if(!story?.id){
+            story = new CloudObject("Story");
+        }
+        story.set({
+            title: this.title,
+            url: this.url,
+            content: this.content,
+            hash: this.hash,
+            tags:this.tags
+        })
+        this.story = await story.save();
+    }
+    async loader(url:string){
+        let api = new CloudApi();
+
+        let result;
+        if(url?.endsWith(".docx")){
+            result = await this.loadDocx(url)
+        }
+        if(!result){
+            result = await api.fetch("agent/loader",{url:url})
+        }
+        this.content = result?.data || null
+        if(this.content){
+            this.url = url
+        }
+        this.save();
+        return this.content
+    }
+
+    async loadDocx(url:string){
+        let data:any
+        const response = await fetch(url);
+
+        const arrayBuffer:any = await response.arrayBuffer();
+        
+        let text;
+        try {
+            text = await mammoth.extractRawText({arrayBuffer:arrayBuffer}); // 浏览器 直接传递 arrayBuffer
+        } catch (err) {
+            console.error(err);
+        }
+
+        this.hash = await arrayBufferToHASH(arrayBuffer)
+
+        // let html = mammoth.convertToHtml(buffer)
+        data = text?.value || "";
+        // 正则匹配所有 多个\n换行的字符 替换成一次换行
+        data = data.replaceAll(/\n+/g,"\n") // 剔除多余换行
+        return {data}
+    }
+    async splitter(options?:{
+        chunkSize:number,
+        chunkOverlap:number
+    }){
+        if(!this.content) return
+        // 默认:递归字符文本分割器
+        let splitter = new RecursiveCharacterTextSplitter({
+            chunkSize: options?.chunkSize || 500,
+            chunkOverlap: options?.chunkOverlap || 150,
+        });
+          
+        let docOutput = await splitter.splitDocuments([
+            new Document({ pageContent: this.content }),
+        ]);
+        console.log(docOutput)
+        this.docList = docOutput
+        return this.docList
+    }
+
+    /**
+     * 文本向量提取
+     * @see
+     * https://js.langchain.com/docs/integrations/text_embedding/tensorflow/
+     * @returns 
+     */
+    //  TensorFlow embedding vector(512) NOT NULL -- NOTE: 512 for Tensorflow
+    //  OpenAI embedding vector(1536) NOT NULL -- NOTE: 1536 for ChatGPT
+    async embedings(){
+        if(!this.docList?.length){return}
+        const embeddings = new TensorFlowEmbeddings();
+        let documentRes = await embeddings.embedDocuments(this.docList?.map(item=>item.pageContent));
+        console.log(documentRes);
+
+        // 向量持久化
+        documentRes.forEach(async (vector512:any,index)=>{
+            /**
+             * metadata
+             * pageContent
+             */
+            let document = this.docList[index]
+            this.docList[index].vector512 = vector512
+            let hash = await arrayBufferToHASH(stringToArrayBuffer(document?.pageContent))
+            let query = new CloudQuery("Document");
+            query.equalTo("hash",hash);
+            let docObj = await query.first()
+            if(!docObj?.id){
+                docObj = new CloudObject("Document");
+            }
+            docObj.set({
+                metadata:document?.metadata,
+                pageContent:document?.pageContent,
+                vector512:vector512,
+                hash:hash,
+                story:this.story?.toPointer(),
+            })
+            docObj.save();
+        })
+        return documentRes;
+    }
+    async destoryAllDocument(){
+        if(this.story?.id){
+            let query = new CloudQuery("Document");
+            query.equalTo("story",this.story?.id);
+            let docList = await query.find();
+            docList.forEach(doc=>{
+                doc.destroy();
+            })
+        }
+        
+    }
+}
+
+export async function fetchFileBuffer(url: string): Promise<Buffer> {
+    const response = await fetch(url);
+
+    if (!response.ok) {
+        throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`);
+    }
+
+    const arrayBuffer = await response.arrayBuffer();
+    return Buffer.from(arrayBuffer);
+}
+
+async function setBackend(){
+
+        let backend
+        let WebGPU = (navigator as any).gpu
+        if (WebGPU) {
+          // WebGPU is supported
+          // console.log(WebGPU)
+          backend = "webgpu"
+        } else {
+          // WebGPU is not supported
+        }
+        let glcanvas = document.createElement('canvas');
+        let WebGL = glcanvas.getContext('webgl') || glcanvas.getContext('experimental-webgl');
+        if (WebGL) {
+          // console.log(WebGL)
+          // WebGL is supported
+          if(!backend) backend = "webgl"
+        } else {
+          // WebGL is not supported
+        }
+
+        if (typeof WebAssembly === 'object' && typeof WebAssembly.instantiate === 'function') {
+          // WebAssembly is supported
+          // console.log(WebAssembly)
+          if(!backend) backend = "wasm"
+        } else {
+          // WebAssembly is not supported
+        }
+
+        backend&&await tf.setBackend(backend);
+        await tf.ready();
+        return
+  }
+
+  export async function arrayBufferToHASH(arrayBuffer:any) {
+    // 使用 SubtleCrypto API 计算哈希
+    const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer); // 使用 SHA-256 代替 MD5
+    const hashArray = Array.from(new Uint8Array(hashBuffer)); // 将缓冲区转换为字节数组
+    const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join(''); // 转换为十六进制字符串
+    return hashHex;
+}
+export function stringToArrayBuffer(str:string) {
+    // 创建一个与字符串长度相同的Uint8Array
+    const encoder = new TextEncoder();
+    return encoder.encode(str).buffer;
+}
+export async function EmbedQuery(str:any):Promise<Array<number>>{
+    const embeddings = new TensorFlowEmbeddings();
+    let documentRes = await embeddings.embedQuery(str);
+    return documentRes
+}
+
+/** 向量余弦相似度计算 */
+export function RetriveAllDocument(vector1: Array<number>, docList: Array<any>): Array<any> {
+    docList.forEach(doc => {
+        const vector512 = doc.vector512;
+        doc.similarity = cosineSimilarity(vector1, vector512); // 计算余弦相似度并存储
+    });
+
+    // 按照相似度排序,降序排列
+    docList.sort((a, b) => b.similarity - a.similarity);
+
+    return docList; // 返回排序后的docList
+}
+function dotProduct(vectorA: number[], vectorB: number[]): number {
+    return vectorA.reduce((sum, value, index) => sum + value * vectorB[index], 0);
+}
+
+function magnitude(vector: number[]): number {
+    return Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
+}
+
+function cosineSimilarity(vectorA: number[], vectorB: number[]): number {
+    const dotProd = dotProduct(vectorA, vectorB);
+    const magnitudeA = magnitude(vectorA);
+    const magnitudeB = magnitude(vectorB);
+
+    if (magnitudeA === 0 || magnitudeB === 0) {
+        throw new Error("One or both vectors are zero vectors, cannot compute cosine similarity.");
+    }
+
+    return dotProd / (magnitudeA * magnitudeB);
+}

+ 3 - 1
heartvoice-app/src/lib/user/modal-user-edit/modal-user-edit.component.html

@@ -17,7 +17,9 @@
   <ion-item>
      <ion-input [value]="userData['gender']" (ionChange)="userDataChange('gender',$event)" label="性别" placeholder="请您输入男/女"></ion-input>
     </ion-item>
-  
+    <ion-item>
+      <ion-input [value]="userData['avatar']" (ionChange)="userDataChange('avatar',$event)" label="头像" placeholder="请您输入头像地址"></ion-input>
+     </ion-item>
 
    <ion-button expand="block" (click)="save()">保存</ion-button>
    <ion-button expand="block" (click)="cancel()">取消</ion-button>

+ 30 - 46
heartvoice-app/src/lib/user/modal-user-edit/modal-user-edit.component.ts

@@ -8,67 +8,51 @@ import { CloudUser } from 'src/lib/ncloud';
   templateUrl: './modal-user-edit.component.html',
   styleUrls: ['./modal-user-edit.component.scss'],
   standalone: true,
-  imports: [
-    IonHeader, IonToolbar, IonTitle, IonContent, 
-    IonCard, IonCardContent, IonButton, IonCardHeader, IonCardTitle, IonCardSubtitle,
-    IonInput, IonItem,
-    IonSegment, IonSegmentButton, IonLabel
+  imports: [IonHeader, IonToolbar, IonTitle, IonContent, 
+    IonCard,IonCardContent,IonButton,IonCardHeader,IonCardTitle,IonCardSubtitle,
+    IonInput,IonItem,
+    IonSegment,IonSegmentButton,IonLabel
   ],
 })
-export class ModalUserEditComponent implements OnInit {
-  currentUser: CloudUser | undefined;
-  
-  userData: any = {};
+export class ModalUserEditComponent  implements OnInit {
 
-   
-  constructor(private modalCtrl: ModalController) { 
-    this.currentUser = new CloudUser(); // 假设您有当前用户的信息
+  currentUser:CloudUser|undefined
+  userData:any = {}
+  userDataChange(key:string,ev:any){
+    let value = ev?.detail?.value
+    if(value){
+      this.userData[key] = value
+    }
+  }
+  constructor(private modalCtrl:ModalController) { 
+    this.currentUser = new CloudUser();
     this.userData = this.currentUser.data;
   }
 
   ngOnInit() {}
 
-  userDataChange(key: string, ev: any) {
-    let value = ev?.detail?.value;
-    if (value) {
-      this.userData[key] = value;
-    }
-  }
-
-async save() {
-  // 确保年龄字段为数字
-  Object.keys(this.userData).forEach(key => {
-    if (key === "age") {
-      this.userData[key] = Number(this.userData[key]); // 确保年龄为数字
-    }
-  });
-  
-
-  // 删除不需要的字段
-  delete this.userData.createdAt; // 删除 createdAt 字段
-  delete this.userData.updatedAt; // 删除 updatedAt 字段
+  async save(){
+    Object.keys(this.userData).forEach(key=>{
+      if(key=="age"){
+        this.userData[key] = Number(this.userData[key])
+      }
+    })
 
-  this.currentUser?.set(this.userData); // 设置用户数据
-
-  try {
-    await this.currentUser?.save(); // 保存用户数据
-    this.modalCtrl.dismiss(this.currentUser, "confirm");
-  } catch (error) {
-    console.error('保存用户时出错:', error);
+    this.currentUser?.set(this.userData)
+    await this.currentUser?.save()
+    this.modalCtrl.dismiss(this.currentUser,"confirm")
   }
-}
+  cancel(){
+    this.modalCtrl.dismiss(null,"cancel")
 
-  cancel() {
-    this.modalCtrl.dismiss(null, "cancel");
   }
 }
 
-// 打开用户编辑模态框的函数
-export async function openUserEditModal(modalCtrl: ModalController): Promise<CloudUser | null> {
+export async function openUserEditModal(modalCtrl:ModalController):Promise<CloudUser|null>{
   const modal = await modalCtrl.create({
     component: ModalUserEditComponent,
-    breakpoints: [0.7, 1.0],
-    initialBreakpoint: 0.7
+    breakpoints:[0.7,1.0],
+    initialBreakpoint:0.7
   });
   modal.present();
 
@@ -77,5 +61,5 @@ export async function openUserEditModal(modalCtrl: ModalController): Promise<Clo
   if (role === 'confirm') {
     return data;
   }
-  return null;
+  return null
 }

+ 1 - 1
heartvoice-app/src/lib/user/modal-user-login/modal-user-login.component.html

@@ -23,7 +23,7 @@
 
     @if(type=="signup"){
       <ion-item>
-        <ion-input [value]="password2" (ionChange)="password2Change($event)" label="再次输入" type="password" value="password"></ion-input>
+        <ion-input [value]="password2" (ionChange)="password2Change($event)" label="密码二次" type="password" value="password"></ion-input>
       </ion-item>
     }
     @if(type=="login"){

+ 2 - 5
heartvoice-app/src/lib/user/modal-user-login/modal-user-login.component.ts

@@ -33,9 +33,7 @@ export class ModalUserLoginComponent  implements OnInit {
   password2Change(ev:any){
     this.password2 = ev?.detail?.value
   }
-  constructor(private modalCtrl:ModalController) {
-    console.log(this.type)
-   }
+  constructor(private modalCtrl:ModalController) { }
 
   ngOnInit() {}
 
@@ -47,8 +45,7 @@ export class ModalUserLoginComponent  implements OnInit {
     let user:any = new CloudUser();
     user = await user.login(this.username,this.password);
     if(user?.id){
-       this.modalCtrl.dismiss(user,"confirm") // 
-       console.log("登录成功")
+       this.modalCtrl.dismiss(user,"confirm")
     }else{
       console.log("登录失败")
     }