一、需求说明
我们在日常做后台类的前端开发通常会遇到很多重复性的工作,比如:
1.1 数据交互实体类的声明
一般来说,我们需要根据需求进行数据建模,比如编写一个企业管理的功能,我们需要对企业进行建模。
- 明确企业包含了哪些业务字段
- 明确字段的数据类型
- 一些字段的表单验证
1.2 API请求接口的声明
我们需要声明API调用的基础路径,比如用户模块应该是 /user,企业是 /company,同时也需要明确我们使用的数据模型
1.3 列表页和编辑页的编写
我们会使用封装的 Table Form 等自定义组件来完成列表页和编辑页面的开发。
二、需求分析
上述的一些日常操作中,目前大部分都是能被 AI 取代的。
比如我们可以为模型定义一些模板代码,然后通过与模型对话来调整输出的代码,最后直接通过模型分析 function calling 来实现函数调用直接保存文件。
思路已经有了,那我们来编写一个自定义的脚本服务来完成这些操作吧。
这里的前提是,你本地已经安装了 Ollama 和 qwen2.5 模型(你可以随意选择,我本地是 qwen2.5-7b 和 14b)
三、设计思路
我们的交互流程图如下:
好,有了这个流程,我们可以着手编码了。
四、编码实现
const readline = require('readline');
const path = require("path")
const fs = require("fs")
const prompt = '' // 篇幅太长,一会我会单独把我的 Prompt 放出来。
const history = []
const SYSTEM = 'system'
const ASSISTANT = 'assistant'
const USER = 'user'
const OLLAMA_URL = 'http://localhost:11434/api/chat'
const OLLAMA_REQUEST_OPTIONS = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}
const OLLAMA_REQUEST_BODY = {
model: 'qwen2.5:latest',
stream: true,
}
const command = readline.createInterface({
input: process.stdin,
output: process.stdout
});
command.on('close', () => {
process.exit(0);
});
const dir = process.cwd()
function requestCommand() {
command.question('请说: ', async (answer) => {
if (answer.toLowerCase() === '退出') {
command.close();
return
} else {
await request(answer)
requestCommand();
}
});
}
const tools = [{
type: 'function',
function: {
name: "saveCodeFile",
description: "从给定的JSON字符串中提取文件名和代码保存到文件",
parameters: {
type: "object",
properties: {
fileName: {
type: "string",
description: "保存的文件名,如 XXX.ts",
},
codes: {
type: "string",
description: "需要保存的代码文件内容",
},
},
required: ["fileName", "codes"],
},
}
}];
async function init() {
history.push({
role: SYSTEM,
content: prompt,
tools
})
await request('开始吧')
}
init()
async function saveFile() {
const message = history[history.length - 1]
const post = {
...OLLAMA_REQUEST_BODY,
messages: [{
role: USER,
content: message.content
}],
stream: false,
tools
}
const res = await fetch(OLLAMA_URL, {
...OLLAMA_REQUEST_OPTIONS,
body: JSON.stringify(post),
});
const data = await res.json()
if (data.message && data.message.tool_calls && data.message.tool_calls.length > 0) {
try {
const func = data.message.tool_calls[0].function.arguments
// 写到文件
const filePath = path.join(dir, func.fileName)
fs.writeFileSync(filePath, func.codes)
console.log('文件保存成功')
return
} catch (e) {
console.log(e)
}
} else {
console.log('文件保存失败')
}
}
async function request(message) {
if (message === "保存") {
await saveFile()
requestCommand()
return
}
const res = await fetch(OLLAMA_URL, {
...OLLAMA_REQUEST_OPTIONS,
body: JSON.stringify({
...OLLAMA_REQUEST_BODY,
messages: [
...history,
{
role: USER,
content: message
}
],
})
});
if (!res.ok) {
throw new Error(`Network Error: ${res.statusText}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
try {
let response = ''
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
try {
const obj = JSON.parse(chunk)
response += obj.message.content
process.stdout.write(obj.message.content);
} catch (e) {
console.log(e)
}
}
process.stdout.write("\n");
history.push({
role: ASSISTANT,
content: response
})
requestCommand()
} catch (err) {
console.error('Error reading stream:', err);
}
}
好的,代码编写完毕,那么接下来,我们把这个文件起名为 Assistant.js 并放到我的 Home 目录下,现在我可以在任意的文件夹内打开终端输入:
node ~/Assistant.js
即可开始:
- 生成实体类
- 生成列表页
更多的就不演示啦,欢迎大家自行体验~
五、我使用的Prompt
为了篇幅,这里单独附上:
const prompt = `
# 你是一个前端开发工程师,你擅长 **TypeScript** 和 **Vue3**
## 你的职责
理解我的意思,然后按我的要求为我生成 **模型** **Service** **列表页** **编辑页** **选择页** **索引文件** 这几个文件。
你只需要第一次询问我模型名称,第二次询问我哪些属性,然后生成文件,生成完毕后,请询问我是否保存。
## 代码模板示例
你通过理解 我提供的信息,不要询问我英文、数据类型、是否必填等,请你自行判断,并为我生成如下文件:
- **模型文件**
模型文件的文件名为 **UserEntity**,文件内容为:
\`\`\`ts
/**
* # 用户模型
* @author Hamm.cn
*/
export class UserEntity extends BaseEntity {
@Form({
requiredString: true,
})
@Table()
@Search()
@Field({
label: '真实姓名',
})
realname!: string
@Form({
requiredNumber: true,
min: 18,
max: 65
})
@Table()
@Search()
@Field({
label: '年龄',
})
age!: number
}
\`\`\`
- **Service 文件**
> 假设你已经知晓我需要你新建一个用户的API服务 **Service**
Service 文件的文件名为 **UserService**,文件内容为:
\`\`\`ts
/**
* # 用户 Service
* @author Hamm.cn
*/
export class UserService extends AbstractBaseService<UserEntity> {
baseUrl = 'user'
entityClass = UserEntity
}
\`\`\`
- **列表页文件**
列表页的文件名为 **list.vue**,文件内容为:
\`\`\`ts
<template>
<APanel>
<AToolBar
:loading="isLoading"
:entity="UserEntity"
:service="UserService"
@on-add="onAdd"
@on-search="onSearch"
/>
<ATable
v-loading="isLoading"
:data-list="response.list"
:entity="UserEntity"
:ctrl-width="150"
show-enable-and-disable
@on-edit="onEdit"
@on-delete="onDelete"
@on-enable="onEnable"
@on-disable="onDisable"
/>
<template #footerLeft>
<APage
:response="response"
@on-change="onPageChanged"
/>
</template>
</APanel>
</template>
<script lang="ts" setup>
import {
APage, APanel, ATable, AToolBar,
} from '@/airpower/component'
import { useAirTable } from '@/airpower/hook/useAirTable'
import { UserEditor } from './component'
const {
isLoading, response,
onPageChanged, onDelete, onEdit, onAdd, onSearch, onEnable, onDisable,
} = useAirTable(UserEntity, UserService, {
editView: UserEditor,
})
</script>
<style scoped lang="scss"></style>
\`\`\`
- **详情页文件**
详情页的文件名为 **editor.vue**,文件内容为:
\`\`\`html
<template>
<ADialog
:title="title"
:form-ref="formRef"
:loading="isLoading"
confirm-text="保存"
:fullable="false"
@on-confirm="onSubmit"
@on-cancel="onCancel"
>
<el-form
ref="formRef"
:model="formData"
label-width="120px"
:rules="rules"
@submit.prevent
>
<AFormField field="realname" />
<AFormField field="age" />
</el-form>
</ADialog>
</template>
<script lang="ts" setup>
import { ADialog, AFormField } from '@/airpower/component'
import { airPropsParam } from '@/airpower/config/AirProps'
import { useAirEditor } from '@/airpower/hook/useAirEditor'
const props = defineProps(airPropsParam(new UserEntity()))
const {
isLoading, formData, formRef, title, rules,
onSubmit,
} = useAirEditor(props, UserEntity, UserService)
</script>
<style scoped lang="scss">
</style>
\`\`\`
- **选择页面**
选择页面的文件名为 **selector.vue**,文件内容为:
\`\`\`html
<template>
<ASelector
:entity="UserEntity"
:service="UserService"
:props="props"
:editor="UserEditor"
/>
</template>
<script lang="ts" setup>
import { ASelector } from '@/airpower/component'
import { airPropsSelector } from '@/airpower/config/AirProps'
import { UserEditor } from '.'
const props = defineProps(airPropsSelector<UserEntity>(new UserEntity()))
</script>
<style scoped lang="scss"></style>
\`\`\`
- **索引文件**
索引文件的文件名为 **index.ts**,文件内容为:
\`\`\`ts
import UserSelector from './selector.vue'
import UserEditor from './editor.vue'
export {
UserSelector,
UserEditor,
}
\`\`\`
## 你的要求
请注意,我需要你引导我一步步提供信息。我提供完信息之后,你将记住我的信息,然后询问我下一步你需要的信息。
不要一次性生成所有文件,生成完一个文件之后询问我是否保存,然后再提供下一个文件。
询问我时请 **简短描述**(20字以内) 你需要的信息,不要举例,我会准确提供。
不要告诉我全流程,请一次会话引导我做一件事情。
## 回复格式
- 询问模型时:“请告诉我需要生成的模型名称?”
- 询问属性时:“请告诉我 {模型名称} 有哪些属性?”
- 输出生成的文件请使用JSON,如下:
\`\`\`json
{
"filaName": "xxx.ts",
"codes": "{CODES}"
}
\`\`\`
然后接着生成下一个文件。
`
这里我们提供了一些模板代码,于是 AI 就会按照我们的模板代码来生成啦。
六、总结和思考
我们今天用 Ollama + qwen2.5 来实现了代码的快速生成和保存,接下来我们可以基于生成的代码简单的做一些调整,并放到更合适的目录内,优化一下 import 的路径即可。
加上我们使用了很标准的前端基础框架,配合我们的开源项目 AirPower4T ,我们可以快速开发出我们想要的功能。
这大幅度提高了我们的工作效率,于是我们有更多的时间用来学习(摸鱼)了。
That's all~
Bye.
原文链接:https://juejin.cn/post/7439193362323783680