当前位置: 首页 > news >正文

基于AIOHTTP、Websocket和Vue3一步步实现web部署平台,无延迟控制台输出,接近原生SSH连接

背景:笔者是一名Javaer,但是最近因为某些原因迷上了Python和它的Asyncio,至于什么原因?请往下看。在着迷”犯浑“的过程中,也接触到了一些高并发高性能的组件,通过简单的学习和了解,aiohttp这个组件引起了我极大的兴趣。

协程、异步非阻塞、”吓人“的性能,这些关键词让我不得不注意到它。

老样子,我们先看成品,再讲讲我曲折的过程。

读取实时日志

 构建+部署

技术痛点揭露

相信大家一定遇到过笔者这次的场景:

        疫情隔离,居家办公,这次我们做的是一个小程序,前端的小伙伴们要联调接口,可是不能用公司的资源,因为公司都断电了😭 ,于是乎我自己买(bai)了(piao)云服务器,自己搭建了一套环境,用自己的域名给他们架上了。本以为事情解决了,前后端可以愉快地调试接口了,但是想都别想,现实还是无情地用它宽大的手掌啪啪打我的三寸小脸。

        你看,后端的小伙伴写完代码,改完bug,提交了之后,一次又一次的让你部署,导致吃饭都想着部署,每次都是噼里啪啦一堆命令,脑瓜子嗡嗡的。(First Blood!)

        你再看,后端猿小帅和前端媛小美正在对接接口,小美说接口怎么一直报错?小帅眉头一皱,手一抖,微信窗口多出个小表情,一脸无辜:"我本地可以啊!"。对,就是这句话,“我本地可以啊,为啥线上不行”,成了接口对接中的口头禅。完了,肯定又是我的活,这不,咔咔一顿"艾特",让我帮忙看日志,啊啊啊啊啊啊,一天到晚登上服务器看了不下N次日志,我的头发在抗议。(Double Kill!)

        你再再再看,我们对接用的yapi,在接口未完成之前,前端调用的是mock,完成之后,得切换到真实接口。为了保证项目开发进度,让前后端的联调顺滑如丝,那付出的肯定是我了。一天下来,在忙上面事情的同时,我还在不断地调整Nginx反向代理配置,为他们放开一个个接口的代理。我内心只能说:mmp。(Triple Kill!)

        你再再再再看看,正常开发过程中总有些粗心捣蛋的人,提交的代码像一个炸弹。这不,刚刚这哥们还在小区楼底下蹦迪,下一秒回家晕乎乎地写了几个bug,潇洒提交,又蹦迪去了。这不提交不要紧,一提交之后,紧接着我习惯性的部署上去,一系列的连锁反应导致几个接口不能用了,兄弟们叫苦不迭,要不是居家,我真想上去抽那仁兄几个嘴巴子。这屁股还是得我擦,回退到上个版本,先凑合调试着。这种操作隔三差五在上演,也是麻烦的很。。。(Quadra kill!)

        最后脑补一个五杀(Penta kill!)🧠

尝试曲线救国

        上面列举了那么多痛点,是个人都被折磨的够呛吧,拜托🙏🏻,疫情即使在家办公也是要高效,更何况家人都在身边,不能焦躁,不能焦躁,不能焦躁!

        这个时候就有大佬说了,你搞这么多费力不讨好的事情,为啥不直接用CI/CD(持续集成)呢?我花费了5根头发想了想,我这1GB内存,1核CPU还能再战吗?再摸了摸我那比纸都薄的钱包,最后点了三炷香“祭奠”了一下我死去的5根头发,心里默默说了声,算了,忍忍,你可以的。

方案一   脚本大法 + 代理

        我开始尝试写脚本。我们的项目是微服务,正常部署都应该用docker-compose,或者直接上到k8s集群里,但是非常时期我们没有办法,只能人工部署。所以我写了一个又一个的脚本,然后写好备注,然后写了一个Low到爆的 HTML,写了超级烂的几行Java代码来调用这些脚本。最后通过Nginx给他们代理出去,把URL分发出去让他们自己点。

        我花费了几个小时完成了上述工作,就在我以为万事大吉的时候,我发现我服务器进不去了。。。WTF?登上控制台,看到CPU使用率125%?我就一个核怎么还超过100捏?虚机超频?呸呸呸,言归正传,排查了半天,我发现是因为多个人短时间内执行构建脚本和部署脚本,直接启动多个进程把机器“干”死了,我摇了摇头,方案1?去你的吧。

方案二  方案一的“进化”

        鉴于方案一存在的致命短板,我不得不针对这个问题进行优化,优化的手段嘛,不出大家所料,还是脚本,用low到爆的一个办法:每次运行构建,都通过 ps | grep | xargs kill -9 杀死之前的进程,再进行构建。运行结果也增加了反馈,用户执行结果会根据Sheel执行返回值进行判断,给出成功与否的响应。至于触发方法嘛,当然是老样子, 继续HTML点击,Java调用脚本。

        再次试验效果,我组织了一场视频会议,会议上我让小美和阿伟还有阿强分别点击部署,哇,效果嘎(chao)嘎(la)的(ji),小美先点击的居然部署成功了,阿伟和阿强后来的居然被杀了?awsl(阿伟死了)。后来排查了半天,发现小美家的wifi只有一格信号,请求发到后台慢了。总体来说方案二的可用度提高了,但是依然没什么卵用,小美提交的代码运行一会后报错了,原因是阿伟提交的一段代码引用了jdk中sun包内的东西,服务器openjdk没有相关类,服务压根没起来。还是很鸡肋。

方案三 另辟蹊径

        方案一和方案二都是短时间内拍脑门儿想出来的活,到现在为止我已经发现问题不能这么草率的解决了, 否则永远都是不断地返工。我深刻地分析了一下,作为一个完备的协同部署功能,至少需要满足以下几个条件:

1. 能够协同工作和实时交互。看了比较大的运维平台,基本上都具备实时的反馈,接近SSH会话级别的体验,能够确认当前的部署状态和部署进度,用户可以及时发现并避免和其他人的交叉使用。此外,如果可能的话,应该实现当前部署状态未完成,其他用户不可操作服务器。

2. 能够查看实时日志。系统运行的状况如何,应该具备日志查看的入口,这些入口开放给开发人员,才能够做到每个人都能及时处理自己的问题。此外,日志滚动频率过快,应该提供“暂停日志”和“恢复日志”的能力。

3. 实现用户身份标识。该功能也是必须的,因为通过HTML按钮点击部署出了问题,往往无法追溯是谁干的😭。后面,我设计为每个用户提供身份标识确认,通过线下发放key的方式提供服务的使用权限,每个key可以绑定到具体的用户,绑定key后,才可以正常使用运维能力。

4. 具有版本控制和一键回滚。一个合格的部署平台,必须具有防范风险的能力,体现在健壮性上来说,就是版本控制。利用shell脚本实现版本控制并不难,实现一键回滚也不难,难的是库表结构修改后产生的种种恩怨情仇。

        经过系统的分析之后,我们说干就干。

= 开始干活 =

        工欲善其事,必先利其器。干活前老样子,先做技术选型。为了一步到位,我直接选择了Vue3去写前端,后端压根没想着用Java去写,因为我写过很多pipleline的代码,java处理起来冗长又效率低下,果断选择了Python大法。事实证明,我的选择太明智了。

搭建Vue项目

技术栈

  • Vue 3
  • Ant Design Vue 3.1.1
  • Socket.io-client
  • CodeMirror Editor
  • Axios

我们使用最新的vue-cli搭建项目。

1. 环境准备

# 安装 Vue CLI
npm install -g @vue/cli# 创建项目
vue create web# 安装依赖
cd web
yarn add ant-design-vue @ant-design/icons-vue axios socket.io-client codemirror-editor-vue3

2. 项目配置

babel.config.js - 按需加载配置
module.exports = {presets: ['@vue/cli-plugin-babel/preset'],plugins: [["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": "css" }]]
}
vue.config.js - 开发服务器配置
module.exports = defineConfig({transpileDependencies: true,devServer: {port: 7777,proxy: {'/api': {target: 'http://localhost:8090',ws: false,changeOrigin: true,},}},
})

2. 核心功能实现

1. WebSocket 通信模块

项目使用 Socket.io 实现与后端的实时通信:

const wsConnect = (write, done) => {const handler = (data) => {// 处理管道数据const { success, end, content, msg = '' } = dataif (success) {content && write(content.replaceAll('\0', ' '))end && write('\n任务执行完毕', done)} else {write('\n管道读取失败!' + msg, done)}}return {io: null,async connect() {this.io = io(WS_URL, {transports: ['websocket'],query: { token }}).on('pipeline', this.handler.bind(this))},// 发送请求request(event, data = {}) {return new Promise((resolve, reject) => {const rid = Date.now()this.io.emit(event, { rid, ...data })this.pending[rid] = { resolve, reject }})}}
}
2. 部署控制台实现
<template><div class="deploy"><a-card :body-style="{padding: '10px 24px'}"><div class="opt-group">构建和部署:<a-button v-if="!deploying" :disabled="running" class="primary" type="primary" @click="deploy"><template #icon><build-filled /></template>构建并部署</a-button><a-button v-else :loading="stopping" class="primary" type="danger" @click="stop"><template #icon><close-circle-filled /></template>停止部署</a-button><a-button :disabled="deploying || running" class="primary" type="primary" @click="web"><template #icon><global-outlined /></template>构建部署前端</a-button><a-dropdown-button :disabled="deploying || running" type="danger" @click="restore" @visibleChange="loadHistories"><hourglass-filled />回滚版本<template #overlay><a-menu @click="editFile"><template v-if="histories.length"><a-menu-item :key="file" v-for="file in histories">{{file}}</a-menu-item></template><a-menu-item v-else disabled key="more">暂无可回滚版本</a-menu-item></a-menu></template></a-dropdown-button></div><a-divider class="divider" type="vertical" /><div class="opt-group">运行监控:<a-button v-if="!running" :disabled="deploying" class="primary" type="primary" @click="log"><template #icon><snippets-filled /></template>读取运行日志</a-button><a-button v-else :loading="stopping" class="primary" type="danger" @click="stop"><template #icon><close-circle-filled /></template>停止日志读取</a-button><a-button :disabled="deploying || stopping || running" :loading="restarting" class="primary" type="danger"@click="restart"><template #icon><appstore-filled /></template>重启项目</a-button><a-button v-if="!paused" :disabled="!running" @click="pause"><template #icon><pause-circle-filled /></template>暂停日志</a-button><a-button v-else :disabled="!running" type="primary" @click="play"><template #icon><play-circle-filled /></template>恢复日志</a-button></div><template v-if="admin"><a-divider class="divider" type="vertical" /><div class="opt-group">配置维护:<a-button class="primary" type="primary" @click="editFile"><template #icon><setting-filled /></template>修改配置文件</a-button><a-dropdown trigger="click"><template #overlay><a-menu @click="editFile"><a-menu-item :key="file" v-for="file in files">{{file}}</a-menu-item><a-menu-item key="more">创建脚本...</a-menu-item></a-menu></template><a-button @click="loadFiles">修改项目脚本<DownOutlined /></a-button></a-dropdown></div></template></a-card><a-card><template #extra><a href="#">当前版本v1.5.3</a></template><template #title><code-filled style="margin-right: 10px" />控制台<a-divider type="vertical" /><a v-if="current === 'deploy'">当前:部署日志</a><a v-else>当前:运行日志</a></template><code-mirror ref="editorRef" :height="350" :options="cmOptions" class="console" /></a-card><a-drawerwidth="1000":visible="!!editing.key"title="修改文件内容"placement="right"><code-mirror height="100%" :options="cmOptions" v-model:value="editing.content" class="console" /><template #footer><div style="text-align: center"><a-button style="margin-right: 8px" @click="editing = {content: ''}">取消</a-button><a-button type="primary" :loading="editing.loading" @click="saveFile">保存</a-button></div></template></a-drawer></div>
</template><script>
import { onMounted, ref } from 'vue';
import CodeMirror from 'codemirror-editor-vue3';
import { message, Modal } from 'ant-design-vue';
import {AppstoreFilled,BuildFilled,CloseCircleFilled,CodeFilled,DownOutlined,GlobalOutlined,HourglassFilled,PauseCircleFilled,PlayCircleFilled,SettingFilled,SnippetsFilled,
} from '@ant-design/icons-vue';
// import base style
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/material-darker.css'
// language
import 'codemirror/mode/javascript/javascript.js';
import createSocket from '@/api/pipeline';export default {name: 'DeployPage',components: {CodeMirror,CodeFilled,GlobalOutlined,BuildFilled,SettingFilled,HourglassFilled,SnippetsFilled,PauseCircleFilled,PlayCircleFilled,CloseCircleFilled,AppstoreFilled,DownOutlined},// 在我们的组件中setup() {// socketioconst socket = ref(null);// 编辑器实例const editorRef = ref(null);const editor = ref(null);// 活跃的回调const callback = ref(null);// 日志运行状态const running = ref(false);// 日志暂停状态const paused = ref(false);// 部署运行状态const deploying = ref(false);// 重启运行状态const restarting = ref(false);// 通用停止按钮状态const stopping = ref(false);// 配置文件列表const files = ref([]);// 历史版本列表const histories = ref([]);// 当前控制台视图,支持deploy部署、log日志const current = ref('deploy');// 运行管道信息const runningKey = ref('');// 缓存内容const cache = ref('');// 当前编辑文件const editing = ref({});// 目标对应字段const dicts = {set deploy(value) {deploying.value = value;},get deploy() {return deploying.value;},set web(value) {deploying.value = value;},get web() {return deploying.value;},set log(value) {running.value = value;},get log() {return running.value;},set restart(value) {restarting.value = value;running.value = value;if (!value) {appender('\n已完成重启,请读取日志查看')}},get restart() {return restarting.value;},set restore(value) {deploying.value = value;},get restore() {return deploying.value;},};// 日志追加器const appender = (text, end, clear) => {if (clear) {return editor.value?.setValue(text || '')}if (text) {// 如果暂停了,进缓存if (paused.value) {cache.value += text;} else {editor.value?.replaceRange(text, { line: Infinity });editor.value?.scrollTo(0, Infinity);}}// 具有回调,代表结束,做一些重置if (end) {end();cache.value = '';paused.value = false;restarting.value = false;runningKey.value = '';}};// 建立pipeline并读取const connector = async (target) => {current.value = target;callback.value = error => {if (error) appender('\n连接中断或异常,' + error);dicts[target] = false;};try {dicts[target] = true;runningKey.value = await socket.value.open(target);appender('\n管道建立成功!进程id:' + runningKey.value + '\n');} catch (e) {appender('\n无法建立管道连接,' + e.message, callback);}}// 挂载后获取实例onMounted(async () => {editor.value = editorRef.value?.cminstance;socket.value = await createSocket(appender, callback).connect();const instance = editor.value;if (instance) {instance.setValue('暂无运行日志\n\n\n\n\n\n\n\n\n\n\n\n\n');instance.focus();}});// 返回命名空间return {editorRef,running,stopping,current,deploying,restarting,paused,files,histories,editing,get admin() {return socket.value?.admin;},play: () => {paused.value = false;const cached = cache.value;cache.value = ''appender(cached);},pause: () => paused.value = true,deploy: async () => connector('deploy'),log: async () => connector('log'),restart: async () => connector('restart'),web: async () => connector('web'),// 停止pipeline并清理stop: async () => {try {stopping.value = true;await socket.value.kill(runningKey.value);appender('\n成功发送杀死指令')} catch (e) {appender('\n杀死作业失败!' + e.message)} finally {stopping.value = false;}},restore: () => {Modal.confirm({title: '请确认操作',content: '该操作会将上次运行的构建结果替换到当前环境运行,并且不可撤销,请确认操作',okText: '我确定',cancelText: '还是不了',onOk: async () => connector('restore'),})},loadFiles: async () => {files.value = await socket.value.listFile();},loadHistories: async visible => {try {histories.value = visible ? await socket.value.listHistory() : [];} catch (e) {message.error(e.message);}},editFile: async ({ key }) => {try {const body = { key };if (!key) {Object.assign(body, await socket.value.createFile('config.json'))} else if (key === 'more') {Object.assign(body, await socket.value.createFile())} else {body.content = await socket.value.getFile(key)}editing.value = body;} catch (e) {message.warn(e.message || e);}},saveFile: async () => {const close = message.loading('正在保存中...', 0);try {editing.value.loading = true;const { key, content } = editing.value;await socket.value.saveFile(key, content);editing.value = { content: '' };message.success('保存成功!')} catch (e) {message.error(e.message);} finally {close();editing.value.loading = false;}},cmOptions: {mode: "text/javascript", // Language modetheme: "material-darker", // ThemelineNumbers: true, // Show line numbersmartIndent: true, // Smart indentviewportMargin: 350,indentUnit: 2, // The smart indent unit is 2 spaces in lengthfoldGutter: true, // Code foldingstyleActiveLine: true, // Display the style of the selected row},}},
}
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {margin: 40px 0 0;
}ul {list-style-type: none;padding: 0;
}li {display: inline-block;margin: 0 10px;
}a {color: #42b983;
}.primary {margin-right: 20px
}.console {}.divider {margin: 0 15px;
}.opt-group {display: inline-block;line-height: 50px;
}@media screen and (max-width: 954px) {.opt-group {display: block;}.divider {display: none;}
}
</style>
3. 项目结构
web/
├── src/
│   ├── components/    # 组件
│   │   └── Deploy.vue # 部署控制台组件
│   ├── App.vue        # 根组件
│   └── main.js        # 入口文件
├── public/
│   └── banner.jpg     # 静态资源
├── babel.config.js    # babel配置
└── vue.config.js      # Vue CLI配置

至此,我们实现了:

  • 实时部署状态监控
  • 运行日志实时查看
  • 配置文件在线编辑
  • 版本回滚功能
  • 项目重启功能

总结一下,这波操作采用 Vue 3 + Ant Design Vue 的技术栈,实现了一个功能完整的智能部署控制台。通过 WebSocket 实现了与后端的实时通信,使用 CodeMirror 提供了良好的代码编辑体验。

Python异步部署服务端

经过技术的吸收,我实现了基于Python 3.9+的异步部署工具,主要特点:

  • 基于WebSocket的全双工实时通信
  • 支持自定义部署脚本
  • 支持配置热加载
  • 支持多用户管理
  • 支持部署历史版本管理

技术栈

  • Python 3.9+

  • aiohttp - 异步Web框架

  • python-socketio - WebSocket库

  • SQLite3 - 轻量级数据库

  • watchdog - 文件监控

核心实现

1. WebSocket服务器

使用python-socketio实现WebSocket服务器:

# 初始化socketio服务器
sio = socketio.AsyncServer(async_mode='aiohttp',cors_allowed_origins=['http://localhost:7777', 'http://deploy.flyfish.group'])
app = web.Application()
sio.attach(app)# 处理连接事件
@sio.event
async def connect(sid, environ):user = validate_token(environ['aiohttp.request'], sid)# 缓存客户端clients[sid] = {'process': None, 'killed': False, 'name': user['name']}await send(sid, {'success': True, 'user': {'name': user['name'], 'authority': user['authorities']}})# 处理断开事件
@sio.event 
async def disconnect(sid):if sid in clients:process = clients[sid]['process']if process:await kill_pipeline(sid, {'pid': process.pid})del clients[sid]
2. 异步管道实现

使用asyncio.create_subprocess_shell创建子进程,实现命令执行:

# 打开管道
@sio.event
async def open_pipeline(sid, message):# 取得类型和命令pipe_type = message['type']command = configs['scripts'][pipe_type]# 启动子进程proc = await asyncio.create_subprocess_shell(f'cd {configs["work_dir"]} && {command} {client["name"]}',stdout=asyncio.subprocess.PIPE,preexec_fn=os.setsid)# 返回成功await send(sid, {'success': True, 'pid': proc.pid, 'rid': message['rid']})# 等待提交await submit(sid, proc)# 异步读取输出
async def submit(sid, proc):item = clients[sid]item['process'] = procwhile True:# 异步读取输出line = await proc.stdout.read(BLOCK_SIZE)if not line:break# 实时推送到客户端    await send(sid, {'success': True, 'content': str(line, encoding='utf-8')})
3. 配置热加载

使用watchdog监控配置文件变化:

# 配置文件监听器
class ConfigFileHandler(FileSystemEventHandler):def on_modified(self, event):path = event.src_pathif path.endswith('config.json'):print("修改了配置文件,尝试加载...")if load_config():print('😊配置文件已经重载')# 初始化监听
async def init_app():observer = Observer()observer.schedule(ConfigFileHandler(), './')observer.start()load_config()return app
4. 数据库操作封装

使用上下文管理器封装SQLite操作:

class SqlSession:def execute(self, sql, param=()):with self.conn:cursor = self.conn.cursor()try:return cursor.execute(sql, param)except sqlite3.Error as e:cursor.close()raise eclass SqlOperation:# 插入操作def insert(self, data):if 'id' in data:del data['id']data['create_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')keys = data.keys()values = data.values()sql = f'insert into {self.table} ({",".join(keys)}) values ({",".join(["?"] * len(keys))})'self.session.execute(sql, tuple(values))
5. Webhook实现

实现Gitee的Webhook接收,能够自动部署,持续集成:

@app.route('/deploy', methods=['POST'])
def post_data():# 验证tokentoken = request.headers.get('X-Gitee-Token')if token != gitee_secret:return "token认证无效", 401# 获取推送信息data = json.loads(request.data)name = data['pusher']['name']# 执行部署脚本os.system(f'sh deploy.sh {name}')return jsonify({"status": 200})

项目结构

hooks/
├── bin/                # 核心代码
│   ├── app.py         # WebSocket服务器
│   ├── db.py          # 数据库操作
│   ├── hook.py        # Webhook接收器
│   └── post.py        # 消息推送
├── logs/              # 日志目录
└── requirements.txt   # 依赖配置

总结

到此,我们实现了以下所有能力

1. 全异步通信

  • 使用aiohttp和python-socketio实现全双工通信
  • 异步子进程管理,实时输出
  • 支持多用户并发操作
  • 实时配置
  • 配置文件热加载
  • 支持自定义部署脚本
  • 支持工作目录配置
  • 用户管理
  • 基于Token的认证
  • 会话管理
  • 权限控制

4. 部署管理

  • 支持部署历史
  • 支持版本回滚
  • 支持运行日志查看

通过以上努力,我采用Python异步编程实现了一个功能完整的部署工具,通过WebSocket实现了与前端的实时通信,支持多用户并发操作。

结束语 - 让部署不再是996的理由 🚀

写在最后

各位看官读到这里,相信你已经发现这不是一个普通的部署工具,而是一个能让你告别"部署恐惧症"的神器!

从此告别的场景 😂

  • 再也不用半夜被运维电话叫醒:"服务器挂了!"
  • 不用每次部署都像在玩俄罗斯轮盘赌
  • 告别"在我电脑上能运行"系列尴尬
  • 不用为搞错配置而痛哭流涕

你将收获的快乐 🎉

  • 一键部署,比订外卖还快
  • 实时日志,像看抖音一样上瘾
  • 版本回滚,时光机般的存在
  • 配置热加载,改完配置说走就走

写给犹豫的你 🤔

如果你还在为以下问题困扰:

  • 部署靠"祈祷"
  • 改配置要"跪求"
  • 看日志要"冥想"
  • 回滚要"许愿"

那么,来试试这个工具吧!它不仅能让你的部署工作变得轻松愉快,还能让你在同事面前装个小小的技术大佬。😎

彩蛋时间 🎮

知道为什么我们选择 WebSocket 吗?

  • 因为 HTTP 太慢了,慢得像极了周一的早晨
  • 因为实时通信,快得像极了发工资的瞬间
  • 因为全双工通信,比你谈恋爱还要双向奔赴

最后的最后 🌟

记住,这个工具的诞生不是为了让你加班,而是为了让你有更多时间:

  • 摸鱼 🐟
  • 追剧 📺
  • 打游戏 🎮
  • 谈恋爱 💑

如果这个项目帮你节省了时间,别忘了给我们点个星⭐️

如果没帮你节省时间...那一定是你还没用熟练 😅

愿你的每一次部署,都像喝可乐一样爽快!

愿你的每一次发版,都像春游一样愉快!

愿你的每一次回滚,都像退货一样简单!

结语中的结语 📝

记住我们的口号:

> 部署不再难,生活更自然!

>

> 配置不用愁,周末早回家!

>

> 日志一目了然,Bug无处遁藏!

最后送大家一句话:

> 工具再好,也补不了你的bug!

>

> 但至少...它能让你改bug的时候心情好一点!😊

好了,快去试试吧!让我们一起告别996,拥抱995.9!🎯


注:本项目副作用可能包括但不限于:让你对其他部署工具产生严重的依赖性鄙视,让你的同事对你投来羡慕的眼光,让你的老板觉得你太闲需要安排更多任务... 😜

代码下载

🎉 是的!我们开源啦!

💝 为什么要开源?

因为我们相信:

  • 好的代码应该像老婆的美貌一样,值得炫耀
  • 优秀的项目应该像奶茶一样,值得分享
  • 牛逼的工具应该像八卦一样,让更多人知道

下载地址奉上,希望大家支持!开发不易,请尊重博主的劳动成果!

https://download.csdn.net/download/wybaby168/90373568

相关文章:

基于AIOHTTP、Websocket和Vue3一步步实现web部署平台,无延迟控制台输出,接近原生SSH连接

背景&#xff1a;笔者是一名Javaer&#xff0c;但是最近因为某些原因迷上了Python和它的Asyncio&#xff0c;至于什么原因&#xff1f;请往下看。在着迷”犯浑“的过程中&#xff0c;也接触到了一些高并发高性能的组件&#xff0c;通过简单的学习和了解&#xff0c;aiohttp这个…...

如何在MacOS上查看edge/chrome的扩展源码

步骤 进入管理扩展页面点击详细信息复制对应id在命令行键入 open ~/Library/Application Support/Microsoft Edge/Default/Extensions/${你刚刚复制的id} 即可打开访达中对应的更目录 注意 由于原生命令行无法直接处理空格 ,所以需要加转义符\,即&#xff1a;open ~/Librar…...

【xdoj-离散线上练习H】T234(C++)

解题心得&#xff1a; 写递归函数的时候&#xff0c;首先写终止条件&#xff0c;这有助于对整个递归函数的把握。 题目&#xff1a;输入集合A和B&#xff0c;输出A到B上的所有函数。 问题描述 给定非空数字集合A和B&#xff0c;求出集合A到集合B上的所有函数。 输入格式 第一行…...

Docker Desktop Windows 安装

一、先下载Docker desktop WIndows 下载地址 二、安装 安装超简单 一路 下一步 三、安装之后&#xff0c;桌面会出现一个 小蓝鲸图标&#xff0c;打开它 》更新至最新版本&#xff0c;不然小蓝鲸打开&#xff0c;一会就退出了。 》wsl --update &#xff08;这个有时比较慢…...

springCloud-2021.0.9 之 GateWay 示例

文章目录 前言springCloud-2021.0.9 之 GateWay 示例1. GateWay 官网2. GateWay 三个关键名称3. GateWay 工作原理的高级概述4. 示例4.1. POM4.2. 启动类4.3. 过滤器4.4. 配置 5. 启动/测试 前言 如果您觉得有用的话&#xff0c;记得给博主点个赞&#xff0c;评论&#xff0c;收…...

JDK8 stream API用法汇总

目录 1.集合处理数据的弊端 2. Steam流式思想概述 3. Stream流的获取方式 3.1 根据Collection获取 3.1 通过Stream的of方法 4.Stream常用方法介绍 4.1 forEach 4.2 count 4.3 filter 4.4 limit 4.5 skip 4.6 map 4.7 sorted 4.8 distinct 4.9 match 4.10 find …...

windows生成SSL的PFX格式证书

生成crt证书: 安装openssl winget install -e --id FireDaemon.OpenSSL 生成cert openssl req -x509 -newkey rsa:2048 -keyout private.key -out certificate.crt -days 365 -nodes -subj "/CN=localhost" 转换pfx openssl pkcs12 -export -out certificate.pfx…...

玩转大语言模型——使用Kiln AI可视化环境进行大语言模型微调数据合成

系列文章目录 玩转大语言模型——使用langchain和Ollama本地部署大语言模型 玩转大语言模型——三分钟教你用langchain提示词工程获得猫娘女友 玩转大语言模型——ollama导入huggingface下载的模型 玩转大语言模型——langchain调用ollama视觉多模态语言模型 玩转大语言模型—…...

2025 西湖论剑wp

web Rank-l 打开题目环境&#xff1a; 发现一个输入框&#xff0c;看一下他是用上面语言写的 发现是python&#xff0c;很容易想到ssti 密码随便输&#xff0c;发现没有回显 但是输入其他字符会报错 确定为ssti注入 开始构造payload&#xff0c; {{(lipsum|attr(‘global…...

FPGA 28 ,基于 Vivado Verilog 的呼吸灯效果设计与实现( 使用 Vivado Verilog 实现呼吸灯效果 )

目录 前言 一. 设计流程 1.1 需求分析 1.2 方案设计 1.3 PWM解析 二. 实现流程 2.1 确定时间单位和精度 2.2 定义参数和寄存器 2.3 实现计数器逻辑 2.4 控制 LED 状态 三. 整体流程 3.1 全部代码 3.2 代码逻辑 1. 参数定义 2. 分级计数 3. 状态切换 4. LED 输…...

单片机简介

一、单片机简介 电脑和单片机性能对比 二、单片机发展历程 三、CISC VS RISC...

C++ 设计模式-桥接模式

C桥接模式的经典示例&#xff0c;包含测试代码&#xff1a; #include <iostream> #include <string>// 实现化接口 class Device { public:virtual ~Device() default;virtual bool isEnabled() const 0;virtual void enable() 0;virtual void disable() 0;vi…...

不小心删除服务[null]后,git bash出现错误

不小心删除服务[null]后&#xff0c;git bash出现错误&#xff0c;如何解决&#xff1f; 错误描述&#xff1a;打开 git bash、msys2都会出现错误「bash: /dev/null: No such device or address」 问题定位&#xff1a; 1.使用搜索引擎搜索「bash: /dev/null: No such device o…...

16.React学习笔记.React更新机制

一. 发生更新的时机以及顺序## image.png props/state改变render函数重新执行产生新的VDOM树新旧DOM树进行diff计算出差异进行更新更新到真实的DOM 二. React更新流程## React将最好的O(n^3)的tree比较算法优化为O(n)。 同层节点之间相互比较&#xff0c;不跨节点。不同类型的节…...

【Elasticsearch】词干提取(Stemming)

词干提取是将一个词还原为其词根形式的过程。这确保了在搜索过程中&#xff0c;一个词的不同变体能够匹配到彼此。 例如&#xff0c;walking&#xff08;行走&#xff09;和walked&#xff08;走过&#xff09;可以被还原到同一个词根walk&#xff08;走&#xff09;。一旦被还…...

【AI论文】10亿参数大语言模型能超越405亿参数大语言模型吗?重新思考测试时计算最优缩放

摘要&#xff1a;测试时缩放&#xff08;Test-Time Scaling&#xff0c;TTS&#xff09;是一种通过在推理阶段使用额外计算来提高大语言模型&#xff08;LLMs&#xff09;性能的重要方法。然而&#xff0c;目前的研究并未系统地分析策略模型、过程奖励模型&#xff08;Process …...

【设计模式】【行为型模式】状态模式(State)

&#x1f44b;hi&#xff0c;我不是一名外包公司的员工&#xff0c;也不会偷吃茶水间的零食&#xff0c;我的梦想是能写高端CRUD &#x1f525; 2025本人正在沉淀中… 博客更新速度 &#x1f4eb; 欢迎V&#xff1a; flzjcsg2&#xff0c;我们共同讨论Java深渊的奥秘 &#x1f…...

PostgreSQL错误: 编码“UTF8“的字符0x0xe9 0x94 0x99在编码“WIN1252“没有相对应值

错误介绍 今天遇到一个错误&#xff0c;记录一下 2025-02-10 17:04:35.264 HKT [28816] 错误: 编码"WIN1252"的字符0x0x81在编码"UTF8"没有相对应值 2025-02-10 17:04:35.264 HKT [28816] 错误: 编码"UTF8"的字符0x0xe9 0x94 0x99在编码&quo…...

Mac ARM 架构的命令行(终端)中,删除整行的快捷键是:Ctrl + U

在 Mac ARM 架构的命令行&#xff08;终端&#xff09;中&#xff0c;删除整行的快捷键是&#xff1a; Ctrl U这个快捷键会删除光标所在位置到行首之间的所有内容。如果你想删除光标后面的所有内容&#xff0c;可以使用&#xff1a; Ctrl K这两个快捷键可以帮助你快速清除当…...

Vue2下判断有新消息来时以站内信方式在页面右下角弹出

以下是完整的Vue2全局通知组件实现方案&#xff0c;包含自动挂载和全局调用方法&#xff1a; 第一步&#xff1a;创建通知组件 <!-- src/components/Notification/index.vue --> <template><div class"notification-container"><transition-g…...

【图像处理入门】6. 频域图像处理:傅里叶变换与滤波的奥秘

摘要 频域图像处理通过傅里叶变换将图像从空间域转换到频率域,为图像增强、去噪、压缩等任务提供全新视角。本文将深入解析傅里叶变换原理,介绍低通、高通滤波的实现方式,结合OpenCV和Python代码展示频域滤波在去除噪声、增强边缘中的应用,帮助读者掌握图像频域处理的核心…...

MySQL——视图 用户管理 语言访问

目录 视图 用户管理 数据库权限 访问 准备工作 使用函数 mysql界面级工具 连接池 视图 这里的视图与事务中的读视图是两个不同的概念&#xff1a;视图是一个虚拟表&#xff0c;其内容由查询定义。同真实的表一样&#xff0c;视图包含一系列带有名称的列和行数据。视图的…...

自由开发者计划 004:创建一个苹果手机长截屏小程序

一. 背景 年初&#xff0c;一个漂亮姐姐突然问我&#xff0c;iphone这么多年一直没法长截屏&#xff0c;你们程序员就没个办法把这个硬伤补上吗&#xff1f; 虎躯一震&#xff0c;脑瓜子嗡嗡的&#xff0c;这么多年的iphone资深用户&#xff0c;最初也不是没有想过这个问题&am…...

基于rpc框架Dubbo实现的微服务转发实战

目录 rpc微服务模块 导入依赖 配置dubbo 注解 开启Dubbo Dubbo的使用 特殊点 并没有使用 Reference 注入 微服务之间调用 可以选用Http 也可以Dubbo 我们 Dubbo 的实现需要一个注册中心 我作为一个服务的提供者 我需要把我的服务注册到注册中心去 调用方需要注册中心…...

MacOS解决局域网“没有到达主机的路由 no route to host“

可能原因&#xff1a;MacOS 15新增了"本地网络"访问权限&#xff0c;在 APP 第一次尝试访问本地网络的时候会请求权限&#xff0c;可能顺手选择了关闭。 解决办法&#xff1a;给想要访问本地网络的 APP &#xff08;例如 terminal、Navicat、Ftp&#xff09;添加访问…...

设计模式 - 模板方法模式

该模式将定义一个操作中的算法骨架&#xff0c;并将算法的一些步骤延迟到子类中实现&#xff0c;使得子类可以在不改变算法结构的情况下重定义算法的某些特定步骤。 例如&#xff0c;炒菜的步骤是固定的&#xff0c;具体可分为倒油、热油、倒蔬菜、倒调料品、翻炒等。通过模板…...

Dify 离线升级操作手册(适用于无外网企业内网环境)

一、准备工作 准备一台能访问互联网的外网机器 用于拉取最新的 Dify 镜像和代码建议使用 Linux 或 Windows Docker 环境 准备传输介质 U盘、移动硬盘&#xff0c;或企业内部网络共享路径 确认当前内网 Dify 版本和配置 确认版本号&#xff0c;备份配置文件和数据库 二、外…...

数论~~~

质数 质数Miller-Rabin算法质因子分解质数筛埃氏筛欧拉筛如果只是计数&#xff0c;埃氏筛改进 快速幂乘法快速幂矩阵快速幂1维k阶实战(提醒&#xff1a;最好在mul函数中作乘法时加上&#xff08;long long&#xff09;的强制类型转换 &#xff0c;或者全部数组换成long long&am…...

边缘计算网关赋能沸石转轮运行故障智能诊断的配置实例

一、项目背景 在环保行业&#xff0c;随着国家对大气污染治理要求的不断提高&#xff0c;VOCs废气处理成为了众多企业的重要任务。沸石转轮作为一种高效的VOCs治理设备&#xff0c;被广泛应用于石油化工、汽车制造、印刷包装等主流行业。这些行业生产规模大、废气排放量多&…...

CentOS 7 修改为静态 IP 地址完整指南

在企业网络环境中,服务器通常需要配置静态 IP 地址以确保网络连接的稳定性和可管理性。以下是使用 NetworkManager 工具在 CentOS 7 系统中将动态 IP 配置修改为静态 IP 的完整指南: 一、检查当前网络配置 查看网络连接状态: 使用 nmcli connection show 命令列出所有网络连…...