feat: 接口逻辑

This commit is contained in:
李如威 2025-09-10 10:45:56 +08:00
parent 8b043a58a1
commit c865132e1a
5 changed files with 217 additions and 90 deletions

View File

@ -1,9 +1,12 @@
import asyncio import asyncio
from hashlib import md5
import json
import uvicorn import uvicorn
import uuid import uuid
import time import time
import models.db as DB import models.db as DB
import nodes as F import nodes as F
from tortoise.transactions import in_transaction
from fastapi import FastAPI, Depends, HTTPException from fastapi import FastAPI, Depends, HTTPException
from models.schemas.system import InitStatusResponse, InitConfigRequest, BaseResponse from models.schemas.system import InitStatusResponse, InitConfigRequest, BaseResponse
from models.schemas.user import LoginRequest, LoginResponse from models.schemas.user import LoginRequest, LoginResponse
@ -46,10 +49,123 @@ async def is_init():
return init_status return init_status
async def _create_demo_records(app: FastAPI):
"""创建测试用的"""
if await DB.Work.exists():
return
async with in_transaction():
work = DB.Work(
uuid=uuid.uuid4().hex,
creator_id=1,
work_title="pai-beta",
work_desc="大白智护beta环境",
)
await work.save()
work_content = DB.WorkContent(
user_id=work.creator_id,
work_id=work.id,
content=[
{
"type": "git",
"title": "同步前端仓库",
"description": "clone 分支 pai-beta-build",
"uuid": uuid.uuid4().hex,
"detail": {
"host": "58.214.239.10",
"user": "dgx07",
"password": "f=bHt/.7kRo:KIXq",
"port": 6378,
"sudo": True,
"git_path": "/data/wrok-flow/dbpai-web",
"git_user": "liruwei",
"git_password": "Wsad.8246",
"git_url": "https://tencentgit.dabby.com.cn/dbpai/dbpai-web.git",
"git_branch": "pai-beta-build",
"git_clone_type": "http/https",
"git_clone_depth": 1,
},
},
{
"type": "git",
"title": "同步后端仓库",
"description": "clone 分支 v3.4.2",
"uuid": uuid.uuid4().hex,
"detail": {
"host": "58.214.239.10",
"user": "dgx07",
"password": "f=bHt/.7kRo:KIXq",
"port": 6378,
"sudo": True,
"git_path": "/data/wrok-flow/dbpai-server",
"git_user": "liruwei",
"git_password": "Wsad.8246",
"git_url": "https://tencentgit.dabby.com.cn/dbpai/dbpai-server.git",
"git_branch": "v3.4.2",
"git_clone_type": "http/https",
"git_clone_depth": 1,
},
},
{
"type": "git",
"title": "同步项目集合仓库",
"description": "clone 分支 v3.4.2",
"uuid": uuid.uuid4().hex,
"detail": {
"host": "58.214.239.10",
"user": "dgx07",
"password": "f=bHt/.7kRo:KIXq",
"port": 6378,
"sudo": True,
"git_path": "/data/wrok-flow/dbpai-jenkins",
"git_user": "liruwei",
"git_password": "Wsad.8246",
"git_url": "https://tencentgit.dabby.com.cn/dbpai/dbpai-jenkins.git",
"git_branch": "v3.4.2",
"git_clone_type": "http/https",
"git_clone_depth": 1,
},
},
{
"type": "cmd",
"title": "更新项目集合仓库",
"description": "更新 server/ html/ 内容",
"uuid": uuid.uuid4().hex,
"detail": {
"host": "58.214.239.10",
"user": "dgx07",
"password": "f=bHt/.7kRo:KIXq",
"port": 6378,
"sudo": True,
"cmd": ("cd /data/wrok-flow/dbpai-jenkins" " && rm -fr ./server" " && cp -r ../dbpai-server ./server" " && rm -fr ./html" " && cp -r ../dbpai-web/dist ./html" " && git add ." ' && git commit -m "build"' " && git push"),
},
},
{
"type": "cmd",
"title": "部署",
"description": "重新运行 pai-beta 分支 v3.4.2",
"uuid": uuid.uuid4().hex,
"detail": {
"host": "58.214.239.10",
"user": "dgx07",
"password": "f=bHt/.7kRo:KIXq",
"port": 6378,
"sudo": True,
"cmd": ("cd /data/PAI/dbpai-beta/dbpai-jenkins;" " git stash;" " git pull origin v3.4.2;" " git stash pop;" " docker-compose restart;"),
},
},
],
)
await work_content.save()
logger.debug("创建")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
tasks = {} tasks = {}
task_nodes = {}
app.state.tasks = tasks app.state.tasks = tasks
app.state.task_nodes = task_nodes
# 数据库 # 数据库
if await is_init(): if await is_init():
init_config = await load_init_config() init_config = await load_init_config()
@ -60,6 +176,8 @@ async def lifespan(app: FastAPI):
scheduler.start() scheduler.start()
logger.info("初始化定时器") logger.info("初始化定时器")
await _create_demo_records(app)
yield yield
for _, v in tasks.items(): for _, v in tasks.items():
@ -161,109 +279,94 @@ def create_app():
description="执行命令", description="执行命令",
response_model=BaseResponse, response_model=BaseResponse,
) )
async def flow_run(id: int): async def flow_run(work_uuid: str):
work = await DB.Work.filter(uuid=work_uuid).first()
if not work:
return BaseResponse(success=False, message="任务不存在")
task_id = uuid.uuid4().hex task_id = work_uuid
if task_id in app.state.tasks:
return BaseResponse(success=False, message="已有任务正在执行...")
async def _task(task_id): async def _update_node_status(task_id: str, node_id: str, status: str):
task_nodes = app.state.task_nodes
logger.info(f"task:{task_id} node:{node_id} status:{status}")
if task_id in task_nodes and node_id in task_nodes[task_id]:
task_nodes[task_id][node_id] = status
async def _task(task_id: str, work: DB.Work):
try: try:
work_content = await DB.WorkContent.filter(work_id=work.id).first()
if not work_content:
logger.warning(f"没有找到对应的内容: {work.uuid}")
return
nodes = [] nodes = []
node_0 = F.GitNode() params = {}
node_0.set_id("01K3JHSEGBHHAVQ92RAH8M6GPX") status = {}
nodes.append(node_0) for x in work_content.content:
node_1 = F.GitNode() node_id = x["uuid"]
node_1.set_id("01K3JHSHKESMWYEGSY4VNCJY4D") node_type = x["type"]
nodes.append(node_1) node_info = x["detail"]
node_2 = F.GitNode() params[node_id] = node_info
node_2.set_id("01K3T4GTWZJF3RT5FFH25D3T9C") status[node_id] = "wait"
nodes.append(node_2) if node_type == "git":
node_3 = F.CmdNode() node = F.GitNode()
node_3.set_id("01K3T4S2HN97T1B0TPFBCFSB00") node.set_info(node_id, lambda k, v: _update_node_status(task_id, k, v))
nodes.append(node_3) nodes.append(node)
elif node_type == "cmd":
node = F.CmdNode()
node.set_info(node_id, lambda k, v: _update_node_status(task_id, k, v))
nodes.append(node)
logger.debug(app.state.tasks) nodes.pop()
# 设置依赖关系: node0 - "true" >> node1 - "true" >> node2
for i in range(len(nodes) - 1): for i in range(len(nodes) - 1):
nodes[i] - "true" >> nodes[i + 1] nodes[i] - "true" >> nodes[i + 1]
flow = F.AsyncFlow(nodes[0])
flow = F.AsyncFlow(node_0) flow.set_params(params)
flow.set_params(
{
"01K3JHSEGBHHAVQ92RAH8M6GPX": {
"host": "58.214.239.10",
"user": "dgx07",
"password": "f=bHt/.7kRo:KIXq",
"port": 6378,
"sudo": True,
"git_path": "/data/wrok-flow/dbpai-web",
"git_user": "liruwei",
"git_password": "Wsad.8246",
"git_url": "https://tencentgit.dabby.com.cn/dbpai/dbpai-web.git",
"git_branch": "pai-beta-build",
"git_clone_type": "http/https",
"git_clone_depth": 1,
},
"01K3JHSHKESMWYEGSY4VNCJY4D": {
"host": "58.214.239.10",
"user": "dgx07",
"password": "f=bHt/.7kRo:KIXq",
"port": 6378,
"sudo": True,
"git_path": "/data/wrok-flow/dbpai-server",
"git_user": "liruwei",
"git_password": "Wsad.8246",
"git_url": "https://tencentgit.dabby.com.cn/dbpai/dbpai-server.git",
"git_branch": "v3.4.2",
"git_clone_type": "http/https",
"git_clone_depth": 1,
},
"01K3T4GTWZJF3RT5FFH25D3T9C": {
"host": "58.214.239.10",
"user": "dgx07",
"password": "f=bHt/.7kRo:KIXq",
"port": 6378,
"sudo": True,
"git_path": "/data/wrok-flow/dbpai-jenkins",
"git_user": "liruwei",
"git_password": "Wsad.8246",
"git_url": "https://tencentgit.dabby.com.cn/dbpai/dbpai-jenkins.git",
"git_branch": "v3.4.2",
"git_clone_type": "http/https",
"git_clone_depth": 1,
},
"01K3T4S2HN97T1B0TPFBCFSB00": {
"host": "58.214.239.10",
"user": "dgx07",
"password": "f=bHt/.7kRo:KIXq",
"port": 6378,
"sudo": True,
"cmd": "\n".join(
[
"cd /data/wrok-flow/dbpai-jenkins",
"git log",
]
),
},
}
)
shared = {} shared = {}
app.state.task_nodes[task_id] = status
await flow.run_async(shared) await flow.run_async(shared)
app.state.tasks.pop(task_id, None) app.state.tasks.pop(task_id, None)
app.state.task_nodes.pop(task_id, None)
except asyncio.CancelledError: except asyncio.CancelledError:
logger.debug(f"任务被取消: {task_id}") logger.debug(f"任务被取消: {task_id}")
app.state.tasks.pop(task_id, None) app.state.tasks.pop(task_id, None)
app.state.tasks[task_id] = asyncio.create_task(_task(task_id)) app.state.tasks[task_id] = asyncio.create_task(_task(task_id, work))
return BaseResponse(data=task_id) return BaseResponse(data=task_id)
@app.post("/api/stop", tags=["Flow"], description="停止执行指令", response_model=BaseResponse) @app.post("/api/stop", tags=["Flow"], description="停止执行指令", response_model=BaseResponse)
async def stop_run(task_id: str): async def stop_run(task_id: str):
task = app.state.tasks.pop(task_id, None) task = app.state.tasks.pop(task_id, None)
app.state.task_nodes.pop(task_id, None)
if task: if task:
task.cancel() task.cancel()
return BaseResponse() return BaseResponse()
@app.get(
"/api/tasks",
tags=["Flow"],
description="任务列表",
response_model=BaseResponse,
)
async def task_list():
global_tasks = app.state.tasks
global_status = app.state.task_nodes
work_list = await DB.Work.filter(is_deleted=0).order_by('-id')
work_content_list = await DB.WorkContent.filter(work_id__in=[x.id for x in work_list]).all()
res = []
for work in work_list:
obj = work.json()
content = next((x for x in work_content_list if x.work_id == work.id), None)
if content:
obj["content"] = [{k: v for k, v in _c.items() if k != "detail"} for _c in (content.content or [])]
obj["is_running"] = work.uuid in global_tasks
obj["status"] = global_status[work.uuid] if work.uuid in global_status else {}
res.append(obj)
return BaseResponse(data=res)
return app return app

View File

@ -1,3 +1,4 @@
from fastapi import FastAPI
from core.pocket_flow import AsyncNode from core.pocket_flow import AsyncNode
from core.logger import logger from core.logger import logger
import asyncio import asyncio
@ -107,10 +108,14 @@ async def async_run_cmd(
class CmdNode(AsyncNode): class CmdNode(AsyncNode):
def set_id(self, id): def set_info(self, id, callback):
self.id = id self.id = id
self.callback = callback
async def prep_async(self, shared): async def prep_async(self, shared):
if self.callback:
await self.callback(self.id, "exec")
return { return {
"cmd": self.params[self.id].get("cmd"), "cmd": self.params[self.id].get("cmd"),
"host": self.params[self.id].get("host"), "host": self.params[self.id].get("host"),
@ -121,6 +126,9 @@ class CmdNode(AsyncNode):
} }
async def exec_async(self, prep_res): async def exec_async(self, prep_res):
if self.callback:
await self.callback(self.id, "exec")
err = [] err = []
out = [] out = []
@ -144,6 +152,11 @@ class CmdNode(AsyncNode):
async def post_async(self, shared, prep_res, exec_res): async def post_async(self, shared, prep_res, exec_res):
shared[self.id] = exec_res shared[self.id] = exec_res
result = "true"
if exec_res.get("err"): if exec_res.get("err"):
return "false" result = "false"
return "true"
if self.callback:
await self.callback(self.id, "post")
return result

View File

@ -1,13 +1,18 @@
from fastapi import FastAPI
from core.pocket_flow import AsyncNode from core.pocket_flow import AsyncNode
from core.logger import logger from core.logger import logger
from .cmd_node import async_run_cmd from .cmd_node import async_run_cmd
from pathlib import Path from pathlib import Path
class GitNode(AsyncNode): class GitNode(AsyncNode):
def set_id(self, id): def set_info(self, id, callback):
self.id = id self.id = id
self.callback = callback
async def prep_async(self, shared): async def prep_async(self, shared):
if self.callback:
await self.callback(self.id, "prep")
return { return {
"client": { "client": {
"host": self.params[self.id].get("host"), "host": self.params[self.id].get("host"),
@ -28,6 +33,9 @@ class GitNode(AsyncNode):
} }
async def exec_async(self, prep_res): async def exec_async(self, prep_res):
if self.callback:
await self.callback(self.id, "exec")
client:dict = prep_res.get("client") client:dict = prep_res.get("client")
git:dict = prep_res.get("git") git:dict = prep_res.get("git")
@ -85,11 +93,14 @@ class GitNode(AsyncNode):
return {"err": err_str, "out": out_str} return {"err": err_str, "out": out_str}
return None
async def post_async(self, shared, prep_res, exec_res): async def post_async(self, shared, prep_res, exec_res):
shared[self.id] = exec_res shared[self.id] = exec_res
result = "true"
if exec_res.get("err"): if exec_res.get("err"):
return "false" result = "false"
return "true"
if self.callback:
await self.callback(self.id, "post")
return result

View File

@ -40,11 +40,11 @@ export default () => {
title: '操作', title: '操作',
valueType: 'option', valueType: 'option',
key: 'option', key: 'option',
width: user.level === 0 ? 120 : 60, width: user?.level === 0 ? 120 : 60,
fixed: 'right', fixed: 'right',
render: (_, record) => [ render: (_, record) => [
<Popconfirm key='1' title="运行工作" onConfirm={() => run(record)}> <Popconfirm key='1' title="运行工作" onConfirm={() => run(record)}>
<Button type="link" style={{ padding: '0 0', border: 'none' }}>运行</Button> <Button type="link" style={{ padding: '0 0', border: 'none' }}>进入</Button>
</Popconfirm>, </Popconfirm>,
user?.level === 0 && <Button key='2' type="link" style={{ padding: '0 0', border: 'none' }}>编辑</Button>, user?.level === 0 && <Button key='2' type="link" style={{ padding: '0 0', border: 'none' }}>编辑</Button>,
user?.level === 0 && <Button key='3' type="link" danger style={{ padding: '0 0', border: 'none' }}>删除</Button>, user?.level === 0 && <Button key='3' type="link" danger style={{ padding: '0 0', border: 'none' }}>删除</Button>,

View File

@ -100,7 +100,7 @@ const HeaderRight = ({ logout, navigate, user }) => {
style={{ border: 'none' }} style={{ border: 'none' }}
icon={<UserOutlined />} icon={<UserOutlined />}
> >
{user.username} {user?.username}
</Button> </Button>
</a> </a>
</Dropdown> </Dropdown>