xiongzhu 2 years ago
parent
commit
792fb1e2d6

+ 1 - 1
.vscode/settings.json

@@ -1,4 +1,4 @@
 {
 
-    "debug.javascript.autoAttachFilter": "smart",
+    "debug.javascript.autoAttachFilter": "disabled",
 }

+ 5 - 0
nest-cli.json

@@ -16,6 +16,11 @@
                 "include": "../views",
                 "outDir": "dist/views",
                 "watchAssets": true
+            },
+            {
+                "include": "../res",
+                "outDir": "dist/res",
+                "watchAssets": true
             }
         ],
         "watchAssets": true

+ 6 - 1
package.json

@@ -23,6 +23,8 @@
   },
   "dependencies": {
     "@alicloud/dysmsapi20170525": "2.0.23",
+    "@aws-sdk/client-s3": "^3.441.0",
+    "@aws-sdk/lib-storage": "^3.441.0",
     "@cyber2024/pdf-parse-fixed": "^1.2.5",
     "@dqbd/tiktoken": "^1.0.6",
     "@esm2cjs/p-timeout": "^6.0.0",
@@ -58,6 +60,7 @@
     "dedent": "^0.7.0",
     "dotenv": "^16.3.1",
     "eventsource-parser": "^1.0.0",
+    "execa": "^8.0.1",
     "express-basic-auth": "^1.2.1",
     "express-handlebars": "^7.0.6",
     "fastq": "^1.15.0",
@@ -67,7 +70,7 @@
     "ioredis": "^5.3.2",
     "isomorphic-fetch": "^3.0.0",
     "keyv": "^4.5.2",
-    "langchain": "^0.0.117",
+    "langchain": "^0.0.174",
     "mime": "^3.0.0",
     "mongodb": "^5.2.0",
     "mongoose": "^7.0.4",
@@ -75,6 +78,7 @@
     "nestjs-typeorm-paginate": "^4.0.3",
     "node-xlsx": "^0.23.0",
     "nodemailer": "^6.9.1",
+    "p-retry": "^6.1.0",
     "p-timeout": "^6.1.1",
     "passport": "^0.6.0",
     "passport-http-bearer": "^1.0.1",
@@ -88,6 +92,7 @@
     "rimraf": "^4.1.2",
     "rxjs": "^7.8.0",
     "sequelize": "^6.31.1",
+    "stream-buffers": "^3.0.2",
     "tnwx": "^2.5.6",
     "typeorm": "^0.3.12",
     "util": "^0.12.5",

BIN
res/plantuml-1.2023.12.jar


+ 3 - 0
res/plantuml.cfg

@@ -0,0 +1,3 @@
+!theme materia
+skinparam dpi 300
+skinparam backgroundColor #FFFFFF

+ 3 - 1
src/app.module.ts

@@ -32,6 +32,7 @@ import { OrgModule } from './org/org.module';
 import { KnowledgeBaseModule } from './knowledge-base/knowledge-base.module';
 import { FormModule } from './form/form.module'
 import { WebModule } from './web/web.module'
+import { PaperModule } from './paper/paper.module';
 @Module({
     imports: [
         DevtoolsModule.register({
@@ -116,7 +117,8 @@ import { WebModule } from './web/web.module'
         OrgModule,
         KnowledgeBaseModule,
         FormModule,
-        WebModule
+        WebModule,
+        PaperModule
     ],
     providers: [
         {

+ 10 - 0
src/paper/dto/create-order.dto.ts

@@ -0,0 +1,10 @@
+import { IsString } from "class-validator";
+
+export class CreatePaperOrderDto {
+
+    @IsString()
+    title: string
+
+    @IsString()
+    description: string
+}

+ 26 - 0
src/paper/entities/paper-gen-result.entity.ts

@@ -0,0 +1,26 @@
+import { JsonTransformer } from '../../transformers/json.transformer'
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+@Entity()
+export class PaperGenResult {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column()
+    orderId: number
+
+    @Column({ type: 'longtext' })
+    content: string
+
+    @Column()
+    fileUrl: string
+
+    @Column({ type: 'text', transformer: new JsonTransformer() })
+    tokenUsage: any
+
+    @Column()
+    duration: number
+}

+ 28 - 0
src/paper/entities/paper-order.entity.ts

@@ -0,0 +1,28 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
+
+export enum PaperOrderStatus {
+    Pending = 'pending',
+    Generating = 'generating',
+    Complete = 'complete'
+}
+
+@Entity()
+export class PaperOrder {
+    @PrimaryGeneratedColumn()
+    id: number
+
+    @CreateDateColumn()
+    createdAt: Date
+
+    @Column()
+    userId: number
+
+    @Column()
+    title: string
+
+    @Column({ type: 'text' })
+    description: string
+
+    @Column({ type: 'enum', enum: PaperOrderStatus, default: PaperOrderStatus.Pending })
+    status: PaperOrderStatus
+}

+ 73 - 0
src/paper/paper-gen/er.ts

@@ -0,0 +1,73 @@
+import { HumanMessage } from 'langchain/schema'
+import { StructuredOutputParser } from 'langchain/output_parsers'
+import { z } from 'zod'
+import { uploadUml } from './upload'
+
+async function genErNames(tools, title, desc) {
+    const { llm, usage, conversation } = tools
+    const scheme = StructuredOutputParser.fromZodSchema(
+        z.array(
+            z.object({
+                title: z.string().describe('E-R图名称')
+            })
+        )
+    )
+
+    const res = await llm.call([
+        new HumanMessage(`${scheme.getFormatInstructions()}
+------
+你是一个计算机专业擅长写毕业论文的专家,你的任务是帮我的毕业设计撰写一篇论文
+我的毕业设计项目是${title}
+这个项目包含以下功能:
+${desc}
+现在我们需要开始为这个系统设计数据库,首先我们分析一下系统中有哪些实体,我们需要绘制以下五个主要E-R图:`)
+    ])
+    return await scheme.parse(res.content)
+}
+
+async function plotER(tools, title, desc, name) {
+    const { llm, usage, conversation } = tools
+    const { chain } = conversation(`你是一个计算机专业擅长写毕业论文的专家,你的任务是帮我的毕业设计撰写一篇论文
+我的毕业设计项目是${title}
+这个项目包含以下功能:
+${desc}`)
+    const { response: descr } = await chain.call({
+        input: `现在我们需要在论文中附上一个E-R图:${name},请你先用文字描述一下这个实体关系`
+    })
+    let { response: code } = await chain.call({
+        input: `现在根据文字描述将它转换为plantuml语言
+这是一个plantuml描述E-R图的示例:
+\`\`\`
+@startuml 示例E-R图
+entity 客户  {
+name
+}
+
+entity 订单  {
+date
+}
+
+entity 订单详情 {
+price
+}
+
+客户 -right-o{ 订单
+订单 ||-right-|{ 订单详情
+
+@enduml
+\`\`\``
+    })
+    code = /\`\`\`(?:plantuml)?([\s\S]+)\`\`\`/.exec(code)[1]
+    const url = await uploadUml(code)
+    return {
+        name,
+        desc: descr,
+        url
+    }
+}
+
+export async function createER(tools, title, desc) {
+    const pRetry = (await eval("import('p-retry')")).default
+    const list = await pRetry(() => genErNames(tools, title, desc), { retries: 5 })
+    return await Promise.all(list.map((item) => pRetry(() => plotER(tools, title, desc, item.title), { retries: 5 })))
+}

+ 70 - 0
src/paper/paper-gen/flow.ts

@@ -0,0 +1,70 @@
+import { HumanMessage } from 'langchain/schema'
+import { StructuredOutputParser } from 'langchain/output_parsers'
+import { z } from 'zod'
+import { uploadUml } from './upload'
+
+async function genFlowNames(tools, title, desc) {
+    const { llm, usage, conversation } = tools
+    const scheme = StructuredOutputParser.fromZodSchema(
+        z.array(
+            z.object({
+                title: z.string().describe('流程图名称')
+            })
+        )
+    )
+    const { content } = await llm.call([
+        new HumanMessage(`${scheme.getFormatInstructions()}
+------
+你是一个计算机专业擅长写毕业论文的专家,你的任务是帮我的毕业设计撰写一篇论文
+我的毕业设计项目是${title}
+这个项目包含以下功能:
+${desc}
+现在我们分析一下论文中需要用到的流程图,我们需要绘制以下五个主要流程图:`)
+    ])
+    return await scheme.parse(content)
+}
+
+async function plotFlow(tools, title, desc, name) {
+    const { llm, usage, conversation } = tools
+    const { chain } = conversation(`你是一个计算机专业擅长写毕业论文的专家,你的任务是帮我的毕业设计撰写一篇论文
+我的毕业设计项目是${title}
+这个项目包含以下功能:
+${desc}`)
+    const { response: descr } = await chain.call({
+        input: `现在我们需要在论文中附上${name},请你先用文字描述一下这个流程`
+    })
+    let { response: code } = await chain.call({
+        input: `现在根据文字描述将它转换为plantuml语言
+这是一个plantuml流程图的示例:
+\`\`\`
+@startuml 名称
+
+start
+
+if (Graphviz installed?) then (yes)
+:process all\ndiagrams;
+else (no)
+:process only
+__sequence__ and __activity__ diagrams;
+endif
+
+stop
+
+@enduml
+
+\`\`\``
+    })
+    code = /\`\`\`(?:plantuml)?([\s\S]+)\`\`\`/.exec(code)[1]
+    const url = await uploadUml(code)
+    return {
+        name,
+        desc: descr,
+        url
+    }
+}
+
+export async function createFlow(tools, title, desc) {
+    const pRetry = (await eval("import('p-retry')")).default
+    const list = await pRetry(() => genFlowNames(tools, title, desc), { retries: 5 })
+    return await Promise.all(list.map((item) => pRetry(() => plotFlow(tools, title, desc, item.title), { retries: 5 })))
+}

+ 166 - 0
src/paper/paper-gen/index.ts

@@ -0,0 +1,166 @@
+import { WritableStreamBuffer } from 'stream-buffers'
+import { createLLM } from './llm'
+import { createFlow } from './flow'
+import { createER } from './er'
+import { createTable } from './table'
+import { uploadDoc } from './upload'
+
+export async function genPaper(title: string, desc: string) {
+    const tools = createLLM()
+    const { llm, usage, conversation } = tools
+    const pRetry = (await eval("import('p-retry')")).default
+
+    const chapters = [
+        {
+            title: '第一章概述'
+        },
+        {
+            title: '第二章关键技术介绍'
+        },
+        {
+            title: '第三章系统分析',
+            sections: [
+                {
+                    title: '3.1 系统概述'
+                },
+                {
+                    title: '3.2 需求分析'
+                },
+                {
+                    title: '3.3 可行性分析'
+                },
+                {
+                    title: '3.4 系统流程分析'
+                },
+                {
+                    title: '3.5 本章小节'
+                }
+            ]
+        },
+        {
+            title: '第四章系统设计',
+            sections: [
+                { title: '4.1 系统基本结构设计' },
+                {
+                    title: '4.2 数据库设计',
+                    sections: [{ title: '4.2.1 数据库E-R图设计' }, { title: '4.2.2 数据库表设计' }]
+                },
+                { title: '4.3 本章小节' }
+            ]
+        },
+        {
+            title: '第五章系统实现'
+        },
+        {
+            title: '第六章系统测试'
+        },
+        {
+            title: '结论'
+        },
+        {
+            title: '参考文献'
+        },
+        {
+            title: '致谢'
+        }
+    ]
+    let paper = new WritableStreamBuffer()
+    const startTime = Date.now()
+    const { chain, memory } = conversation(`你是一个计算机专业擅长写毕业论文的专家,你的任务是帮我的毕业设计撰写一篇论文
+我的毕业设计项目是${title}
+这个项目包含以下功能:${desc}
+现在我们已经为论文拟好了一个大纲:
+${chapters.map((i) => `- ${i.title}`).join('\n')}
+现在我们来一步一步的编写这篇论文
+请严格按照markdown格式返回内容`)
+
+    for (let i = 0; i < chapters.length; i++) {
+        if (i === 2) {
+            for (let j = 0; j < chapters[i].sections.length; j++) {
+                let input = ''
+                if (j === 0) {
+                    input += `现在我们来编写第三章系统分析,这一章节内容较多,我们可以分成几个小节来写:
+${chapters[i].sections.map((i) => i.title).join('\n')}\n`
+                }
+                input += `现在我们来编写${chapters[i].title}的第${j + 1}小节: ${chapters[i].sections[j].title}`
+
+                if (j === 3) {
+                    const flows = await createFlow(tools, title, desc)
+                    input += `\n我已为你绘制好了以下几个流程图       
+${flows
+    .map((i) => {
+        return `- ![${i.name}](${i.url})
+${i.desc}`
+    })
+    .join('\n\n')}
+请你帮我完成第${j + 1}小节: ${chapters[i].sections[j].title}的内容,请确保附上我提供的图片`
+                }
+                let { response } = await chain.call({
+                    input: input
+                })
+                if (j !== 0) {
+                    response = response.replace(/# ?第三章 ?系统分析\n/, '')
+                }
+                paper.write('\n\n' + response)
+            }
+        } else if (i === 3) {
+            for (let j = 0; j < chapters[i].sections.length; j++) {
+                let input = ''
+                if (j === 0) {
+                    input += `现在我们来编写第四章系统设计,这一章节内容较多,我们可以分成几个小节来写:
+${chapters[i].sections.map((i) => i.title).join('\n')}\n`
+                }
+                input += `现在我们来编写${chapters[i].title}的第${j + 1}小节: ${chapters[i].sections[j].title}`
+
+                if (j === 1) {
+                    const er = await createER(tools, title, desc)
+                    const table = await createTable(tools, title, desc)
+                    input += `\n本小节分为以下几个部分:
+${chapters[i].sections[j].sections.map((i) => `- ${i.title}`).join('\n')}
+
+我已为你绘制好了以下几个E-R图图
+${er
+    .map((i) => {
+        return `- ![${i.name}E-R图](${i.url})
+${i.desc}`
+    })
+    .join('\n')}
+
+这是我为你提供的数据库表:
+
+${table
+    .map((i) => {
+        return `- ${i.name}表:
+${i.desc}
+`
+    })
+    .join('\n\n')}
+
+请你帮我编写第${j + 1}小节: ${chapters[i].sections[j].title}的内容, 请确保附上我提供的E-R图和数据库表`
+                }
+                let { response } = await chain.call({
+                    input: input
+                })
+                if (j !== 0) {
+                    response = response.replace(/# ?第四章 ?系统设计\n/, '')
+                }
+                paper.write('\n\n' + response)
+            }
+        } else {
+            const { response } = await chain.call({
+                input: `现在我们来编写${chapters[i].title}`
+            })
+            paper.write('\n\n' + response)
+        }
+    }
+
+    const content = paper.getContentsAsString('utf8')
+    const duration = (Date.now() - startTime) / 1000
+    const fileUrl = await uploadDoc(title, content)
+    return {
+        content,
+        duration,
+        tokenUsage: usage,
+        fileUrl
+    }
+}

+ 64 - 0
src/paper/paper-gen/llm.ts

@@ -0,0 +1,64 @@
+import { ChatOpenAI } from 'langchain/chat_models/openai'
+import { CallbackManager } from 'langchain/callbacks'
+import { ChatPromptTemplate, MessagesPlaceholder } from 'langchain/prompts'
+import dedent from 'dedent'
+import { ConversationChain } from 'langchain/chains'
+import { BufferMemory, BufferWindowMemory } from 'langchain/memory'
+import { Logger } from '@nestjs/common'
+
+export function createLLM() {
+    const usage = { completionTokens: 0, promptTokens: 0, totalTokens: 0 }
+    const llm = new ChatOpenAI({
+        openAIApiKey: process.env.OPENAI_API_KEY,
+        modelName: 'gpt-3.5-turbo-16k',
+        timeout: 1000 * 60 * 5,
+        callbackManager: CallbackManager.fromHandlers({
+            async handleLLMStart(llm, prompts) {
+                Logger.log(`[LLM Start]LLM: ${JSON.stringify(llm)}`)
+                Logger.log(`['LLM Start]Prompts: ${prompts.join('\n')}`)
+            },
+            async handleLLMEnd(output) {
+                Logger.log(
+                    `[LLM End]${output.generations
+                        .reduce((acc, cur) => acc.concat(cur), [])
+                        .map((i) => i.text)
+                        .join('\n')}`
+                )
+                Logger.log(`[LLM End]${JSON.stringify(output.llmOutput)}`)
+                usage.completionTokens += output.llmOutput.tokenUsage.completionTokens
+                usage.promptTokens += output.llmOutput.tokenUsage.promptTokens
+                usage.totalTokens += output.llmOutput.tokenUsage.totalTokens
+            },
+            async handleLLMError(error) {
+                Logger.error(error)
+            }
+        }),
+        onFailedAttempt(error) {
+            Logger.error(error)
+        }
+
+        // configuration: {
+        //     baseURL: "https://openai.c8c.top/v1",
+        // },
+    })
+
+    function conversation(system) {
+        const chatPrompt = ChatPromptTemplate.fromMessages([
+            ['system', system],
+            new MessagesPlaceholder('history'),
+            ['human', '{input}']
+        ])
+        const memory = new BufferWindowMemory({
+            k: 3,
+            memoryKey: 'history',
+            returnMessages: true
+        })
+        const chain = new ConversationChain({
+            memory: memory,
+            prompt: chatPrompt,
+            llm: llm
+        })
+        return { memory, chain }
+    }
+    return { llm, usage, conversation }
+}

+ 49 - 0
src/paper/paper-gen/table.ts

@@ -0,0 +1,49 @@
+import { HumanMessage } from 'langchain/schema'
+import { StructuredOutputParser } from 'langchain/output_parsers'
+import { z } from 'zod'
+import pRetry from 'p-retry'
+
+async function listTables(tools, title, desc) {
+    const { llm, usage, conversation } = tools
+    const scheme = StructuredOutputParser.fromZodSchema(z.array(z.string().describe('表名')))
+    const res = await llm.call([
+        new HumanMessage(`${scheme.getFormatInstructions()}
+------
+你是一个计算机专业擅长写毕业论文的专家,你的任务是帮我的毕业设计撰写一篇论文
+我的毕业设计项目是${title}
+这个项目包含以下功能:
+${desc}
+现在我们需要开始为这个系统设计数据库,首先请帮我列出我们需要哪些表:`)
+    ])
+    return await scheme.parse(res.content)
+}
+
+async function describeTable(tools, title, desc, list, name) {
+    const { llm, usage, conversation } = tools
+    const res = await llm.call([
+        new HumanMessage(`
+你是一个计算机专业擅长写毕业论文的专家,你的任务是帮我的毕业设计撰写一篇论文
+我的毕业设计项目是${title}
+这个项目包含以下功能:
+${desc}
+现在我们需要开始为这个系统设计数据库,我们需要以下这些数据库表:
+${list.map((i) => `- ${i}`).join('\n')}
+现在我们一步一步设计这些表的具体字段,首先我们需要设计${name}表的字段,请你帮我设计一下这个表的字段,以表格的形式返回,包含字段名称、类型、长度、字段说明、主键、默认值`)
+    ])
+
+    return {
+        name,
+        desc: '\n' + new RegExp('\\|[\\s\\S]+\\|').exec(res.content)[0] + '\n'
+    }
+}
+
+export async function createTable(tools, title, desc) {
+    const pRetry = (await eval("import('p-retry')")).default
+    const list = await pRetry(() => listTables(tools, title, desc), { retries: 5 })
+
+    const tables = []
+    for (const item of list) {
+        tables.push(await pRetry(() => describeTable(tools, title, desc, list, item), { retries: 5 }))
+    }
+    return tables
+}

+ 93 - 0
src/paper/paper-gen/upload.ts

@@ -0,0 +1,93 @@
+import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
+import { Upload } from '@aws-sdk/lib-storage'
+import { PassThrough, Readable } from 'stream'
+import { WritableStreamBuffer } from 'stream-buffers'
+import * as path from 'path'
+
+export async function uploadUml(plantUml) {
+    const s3 = new S3Client({
+        region: process.env.ALIYUN_OSS_REGION,
+        endpoint: `https://${process.env.ALIYUN_OSS_ENDPOINT}`,
+        credentials: {
+            accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
+            secretAccessKey: process.env.ALIYUN_ACCESS_KEY_SECRET
+        }
+    })
+    const stream = new PassThrough()
+    const key = `uml/${Date.now()}.png`
+
+    const upload = new Upload({
+        client: s3,
+        params: {
+            ACL: 'public-read',
+            Bucket: process.env.ALIYUN_OSS_BUCKET,
+            Key: key,
+            Body: stream
+        }
+    })
+    const { execa } = await (eval('import("execa")') as Promise<typeof import('execa')>)
+    const puml = execa('java', [
+        '-jar',
+        '-Djava.awt.headless=true',
+        '-DPLANTUML_LIMIT_SIZE=8192',
+        `${path.join(__dirname, '../../../res/plantuml-1.2023.12.jar')}`,
+        '-config',
+        `${path.join(__dirname, '../../../res/plantuml.cfg')}`,
+        '-tpng',
+        '-pipe',
+        '-fastfail',
+        '-noerror'
+    ])
+    // convert -units PixelsPerInch - -density 300 -
+    const convert = execa('convert', ['-units', 'PixelsPerInch', '-', '-density', '300', '-'])
+    Readable.from(plantUml).pipe(puml.stdin)
+    puml.pipeStdout(convert.stdin)
+    convert.pipeStdout(stream)
+
+    const err = new WritableStreamBuffer()
+    puml.pipeStderr(err)
+    try {
+        await puml
+    } catch (error) {
+        throw new Error(err.getContents().toString())
+    }
+    await upload.done()
+    return `https://${process.env.ALIYUN_OSS_BUCKET}.${process.env.ALIYUN_OSS_ENDPOINT}/${key}`
+}
+
+export async function uploadDoc(title, content) {
+    const s3 = new S3Client({
+        region: process.env.ALIYUN_OSS_REGION,
+        endpoint: `https://${process.env.ALIYUN_OSS_ENDPOINT}`,
+        credentials: {
+            accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
+            secretAccessKey: process.env.ALIYUN_ACCESS_KEY_SECRET
+        }
+    })
+    const stream = new PassThrough()
+    const key = `doc/${title}_${Date.now()}.docx`
+
+    const upload = new Upload({
+        client: s3,
+        params: {
+            ACL: 'public-read',
+            Bucket: process.env.ALIYUN_OSS_BUCKET,
+            Key: key,
+            Body: stream
+        }
+    })
+
+    const { execa } = await (eval('import("execa")') as Promise<typeof import('execa')>)
+    const p = execa('pandoc', ['-f', 'markdown', '-t', 'docx'])
+    Readable.from(content).pipe(p.stdin)
+    const err = new WritableStreamBuffer()
+    p.pipeStdout(stream)
+    p.pipeStderr(err)
+    try {
+        await p
+    } catch (error) {
+        throw new Error(err.getContents().toString())
+    }
+    await upload.done()
+    return `https://${process.env.ALIYUN_OSS_BUCKET}.${process.env.ALIYUN_OSS_ENDPOINT}/${key}`
+}

+ 7 - 0
src/paper/paper.admin.controller.ts

@@ -0,0 +1,7 @@
+import { Controller } from '@nestjs/common'
+import { PaperService } from './paper.service'
+
+@Controller('paper')
+export class PaperController {
+    constructor(private readonly paperService: PaperService) {}
+}

+ 36 - 0
src/paper/paper.controller.ts

@@ -0,0 +1,36 @@
+import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'
+import { PaperService } from './paper.service'
+import { PageRequest } from '../common/dto/page-request'
+import { PaperOrder } from './entities/paper-order.entity'
+import { PaperGenResult } from './entities/paper-gen-result.entity'
+import { CreatePaperOrderDto } from './dto/create-order.dto'
+
+@Controller('paper')
+export class PaperController {
+    constructor(private readonly paperService: PaperService) {}
+
+    @Post('/orders')
+    async orders(@Body() page: PageRequest<PaperOrder>) {
+        return await this.paperService.findAllOrders(page)
+    }
+
+    @Post('/results')
+    async results(@Body() page: PageRequest<PaperGenResult>) {
+        return await this.paperService.findAllResults(page)
+    }
+
+    @Put('/orders')
+    async createOrder(@Body() dto: CreatePaperOrderDto) {
+        return await this.paperService.createOrder(dto)
+    }
+
+    @Get('/orders/:id')
+    async getOrder(@Param('id') id: string) {
+        return await this.paperService.findOrderById(Number(id))
+    }
+
+    @Post('/orders/:id/gen')
+    async gen(@Param('id') id: string) {
+        return await this.paperService.gen(Number(id))
+    }
+}

+ 13 - 0
src/paper/paper.module.ts

@@ -0,0 +1,13 @@
+import { Module } from '@nestjs/common'
+import { PaperController } from './paper.controller'
+import { PaperService } from './paper.service'
+import { TypeOrmModule } from '@nestjs/typeorm'
+import { PaperOrder } from './entities/paper-order.entity'
+import { PaperGenResult } from './entities/paper-gen-result.entity'
+
+@Module({
+    imports: [TypeOrmModule.forFeature([PaperOrder, PaperGenResult])],
+    controllers: [PaperController],
+    providers: [PaperService]
+})
+export class PaperModule {}

+ 83 - 0
src/paper/paper.service.ts

@@ -0,0 +1,83 @@
+import { Injectable, InternalServerErrorException, Logger, OnModuleInit } from '@nestjs/common'
+import { InjectRepository } from '@nestjs/typeorm'
+import { PaperOrder, PaperOrderStatus } from './entities/paper-order.entity'
+import { Repository } from 'typeorm'
+import { PaperGenResult } from './entities/paper-gen-result.entity'
+import { Pagination, paginate } from 'nestjs-typeorm-paginate'
+import { PageRequest } from 'src/common/dto/page-request'
+import { CreatePaperOrderDto } from './dto/create-order.dto'
+import { genPaper } from './paper-gen'
+
+@Injectable()
+export class PaperService implements OnModuleInit {
+    constructor(
+        @InjectRepository(PaperOrder)
+        private readonly paperOrderRepository: Repository<PaperOrder>,
+        @InjectRepository(PaperGenResult)
+        private readonly paperGenResultRepository: Repository<PaperGenResult>
+    ) {}
+
+    async onModuleInit() {
+        for (const order of await this.paperOrderRepository.findBy({
+            status: PaperOrderStatus.Generating
+        })) {
+            if (
+                (await this.paperGenResultRepository.countBy({
+                    orderId: order.id
+                })) === 0
+            ) {
+                order.status = PaperOrderStatus.Pending
+            } else {
+                order.status = PaperOrderStatus.Complete
+            }
+            await this.paperOrderRepository.save(order)
+        }
+    }
+
+    async findAllOrders(req: PageRequest<PaperOrder>): Promise<Pagination<PaperOrder>> {
+        return await paginate<PaperOrder>(this.paperOrderRepository, req.page, req.search)
+    }
+
+    async findOrderById(id: number): Promise<PaperOrder> {
+        return await this.paperOrderRepository.findOneByOrFail({
+            id
+        })
+    }
+
+    async findAllResults(req: PageRequest<PaperGenResult>): Promise<Pagination<PaperGenResult>> {
+        return await paginate<PaperGenResult>(this.paperGenResultRepository, req.page, req.search)
+    }
+
+    async createOrder(dto: CreatePaperOrderDto): Promise<PaperOrder> {
+        return await this.paperOrderRepository.save(dto)
+    }
+
+    async gen(orderId: number) {
+        const order = await this.findOrderById(orderId)
+        if (order.status === PaperOrderStatus.Generating) {
+            throw new InternalServerErrorException('正在生成中,请稍后再试')
+        }
+        order.status = PaperOrderStatus.Generating
+        await this.paperOrderRepository.save(order)
+        this.genPaper(order)
+    }
+
+    async genPaper(order: PaperOrder) {
+        try {
+            const { content, duration, tokenUsage, fileUrl } = await genPaper(order.title, order.description)
+            await this.paperGenResultRepository.save({
+                orderId: order.id,
+                content,
+                duration,
+                tokenUsage,
+                fileUrl
+            })
+            order.status = PaperOrderStatus.Complete
+            await this.paperOrderRepository.save(order)
+        } catch (error) {
+            Logger.error(error, '生成论文失败')
+            order.status = PaperOrderStatus.Pending
+            await this.paperOrderRepository.save(order)
+        }
+    }
+}

+ 13 - 3
src/sys-config/sys-config.service.ts

@@ -1,12 +1,12 @@
-import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'
+import { Injectable, InternalServerErrorException, NotFoundException, OnModuleInit } from '@nestjs/common'
 import { InjectRepository } from '@nestjs/typeorm'
 import { Repository } from 'typeorm'
-import { SysConfig } from './entities/sys-config.entity'
+import { SysConfig, SysConfigType } from './entities/sys-config.entity'
 import { PageRequest } from '../common/dto/page-request'
 import { Pagination, paginate } from 'nestjs-typeorm-paginate'
 
 @Injectable()
-export class SysConfigService {
+export class SysConfigService implements OnModuleInit {
     constructor(
         @InjectRepository(SysConfig)
         private readonly sysConfigRepository: Repository<SysConfig>
@@ -26,6 +26,16 @@ export class SysConfigService {
             }
         })()
     }
+    async onModuleInit() {
+        if (!(await this.sysConfigRepository.findOneBy({ name: 'vote_delay' }))) {
+            await this.sysConfigRepository.save({
+                name: 'paper_gen_model',
+                value: '0',
+                type: SysConfigType.String,
+                remark: '生成论文模型'
+            })
+        }
+    }
 
     async findAll(req: PageRequest<SysConfig>) {
         try {

+ 10 - 0
src/transformers/json.transformer.ts

@@ -0,0 +1,10 @@
+import { ValueTransformer } from 'typeorm'
+
+export class JsonTransformer implements ValueTransformer {
+    to(value: any): string {
+        return value ? JSON.stringify(value) : null
+    }
+    from(value: string): string[] {
+        return value ? JSON.parse(value) : null
+    }
+}

File diff suppressed because it is too large
+ 1037 - 4
yarn.lock


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