feat: 支持机器人
All checks were successful
CI Monitor CI/CD / build-image (push) Successful in 21s
CI Monitor CI/CD / deploy (push) Successful in 26s

This commit is contained in:
zhaoyingbo 2024-03-07 12:05:10 +00:00
parent 0fe1fdc695
commit bc441827ec
9 changed files with 339 additions and 75 deletions

View File

@ -0,0 +1,121 @@
import db from "../../db";
import service from "../../service";
import { calculateWeeklyRate } from "../../utils/robotTools";
import {
getPrevWeekWithYear,
getWeekTimeWithYear,
} from "../../utils/timeTools";
const getNewCicdStatus = async () => {
const fullProjList = await db.project.getFullList();
const has_new_cicd_count = String(
fullProjList.filter((item) => {
return item.has_new_cicd === true;
}).length
);
const without_new_cicd_count = String(
fullProjList.filter((item) => {
return item.has_new_cicd === false;
}).length
);
return {
has_new_cicd_count,
without_new_cicd_count,
};
};
const getStatisticsInfo = async () => {
const curWeekInfo = await db.view.getFullStatisticsByWeek(
getWeekTimeWithYear()
);
const prevWeekInfo = await db.view.getFullStatisticsByWeek(
getPrevWeekWithYear()
);
return {
total_count: String(curWeekInfo.total_count),
weekly_count_rate: calculateWeeklyRate(
curWeekInfo.total_count,
prevWeekInfo.total_count
).text,
duration: String(curWeekInfo.duration),
weekly_duration_rate: calculateWeeklyRate(
curWeekInfo.duration,
prevWeekInfo.duration
).text,
success_rate: String(curWeekInfo.success_rate),
weekly_success_rate: calculateWeeklyRate(
curWeekInfo.success_rate,
prevWeekInfo.success_rate
).text,
};
};
const getProjDiffInfo = async () => {
const curWeekInfo = await db.view.getProjStatisticsByWeek(
getWeekTimeWithYear()
);
const prevWeekInfo = await db.view.getProjStatisticsByWeek(
getPrevWeekWithYear()
);
const group: {
project_name: string;
project_ref: string;
project_duration: string;
project_duration_rate: string;
percentage: string;
}[] = [];
curWeekInfo.forEach((curWeekProjInfo) => {
const prevWeekProjInfo = prevWeekInfo.find(
(info) =>
info.ref === curWeekProjInfo.ref && info.name === curWeekProjInfo.name
);
if (!prevWeekProjInfo) return;
const { text: project_duration_rate, percentage } = calculateWeeklyRate(
curWeekProjInfo.duration,
prevWeekProjInfo.duration,
false
);
if (percentage === "0") return;
group.push({
project_name: curWeekProjInfo.name,
project_ref: curWeekProjInfo.ref,
project_duration: String(curWeekProjInfo.duration),
project_duration_rate,
percentage,
});
});
// 排序
group.sort((a, b) => Number(b.percentage) - Number(a.percentage));
// 取前五个
return group.slice(0, 5);
};
const sendRobotMsg = async () => {
const msgContent = {
type: "template",
data: {
template_id: "ctp_AAyVLS6Q37cL",
template_variable: {
...(await getNewCicdStatus()),
...(await getStatisticsInfo()),
group_table: await getProjDiffInfo(),
},
},
};
const res = await service.sendMessage(JSON.stringify(msgContent));
console.log(res);
};
const manageRobot = {
sendRobotMsg,
};
export default manageRobot;

View File

@ -1,11 +1,13 @@
import project from "./project";
import pipeline from "./pipeline";
import user from "./user";
import view from "./view";
const db = {
project,
pipeline,
user,
view,
};
export default db;

44
db/view/index.ts Normal file
View File

@ -0,0 +1,44 @@
import { RecordModel } from "pocketbase";
import { managePb404 } from "../../utils/pbTools";
import pbClient from "../pbClient";
export interface StatisticsPerWeekRecordModel extends RecordModel {
week: string;
total_count: number;
failed_count: number;
success_count: number;
success_rate: number;
duration: number;
}
export interface StatisticsPerProjRecordModel extends RecordModel {
week: string;
name: string;
duration: number;
ref: string;
}
const getFullStatisticsByWeek = (week: string) => {
return managePb404(
async () =>
await pbClient
.collection("statisticsPerWeek")
.getFirstListItem(`week="${week}"`)
) as Promise<StatisticsPerWeekRecordModel>;
};
const getProjStatisticsByWeek = (week: string) => {
return managePb404(
async () =>
await pbClient
.collection("statisticsPerProj")
.getFullList({ filter: `week="${week}"` })
) as Promise<StatisticsPerProjRecordModel[]>;
};
const view = {
getFullStatisticsByWeek,
getProjStatisticsByWeek,
};
export default view;

View File

@ -5,3 +5,5 @@ services:
image: git.yingbo.im:333/zhaoyingbo/ci_monitor:sha
container_name: ci_monitor
restart: always
ports:
- 3001:3000

View File

@ -2,6 +2,7 @@ import { scheduleJob } from "node-schedule";
import managePipeline from "./controllers/managePipeLine";
import manageProject from "./controllers/manageProject";
import manageUser from "./controllers/manageUser";
import manageRobot from "./controllers/manageRobot";
const main = async () => {
const fullProjList = await manageProject.getFullProjList();
@ -20,3 +21,13 @@ const main = async () => {
main();
scheduleJob("*/15 * * * *", main);
scheduleJob("0 10 * * 5", manageRobot.sendRobotMsg);
Bun.serve({
async fetch() {
await manageRobot.sendRobotMsg();
return new Response("OK");
},
port: 3000,
});

128
readme.md
View File

@ -10,7 +10,7 @@
先从数据中获取用户信息填充,随后在根据填充完的用户信息获取全部的 userid 的列表,再写 pipeline 表
# 图表库
# 图表库(不用了)
https://g2plot.antv.antgroup.com/examples
@ -62,92 +62,33 @@ user 信息
}
```
date 视图 SQL
每周每个项目按分支的运行时长统计 SQL `statisticsPerProj`
```SQL
SELECT
(ROW_NUMBER() OVER()) as id,
p.has_new_cicd,
p.name AS project_name,
strftime('%Y-%m-%d', datetime(pip.started_at)) AS date,
pip.ref,
AVG(pip.duration) AS avg_duration
FROM project p
JOIN pipeline pip ON p.id = pip.project_id
GROUP BY project_name, date, pip.ref;
```
week 视图 SQL
```SQL
SELECT
(ROW_NUMBER() OVER()) as id,
p.has_new_cicd,
p.name AS project_name,
strftime('%Y-%W', datetime(pip.started_at)) AS week,
pip.ref,
AVG(pip.duration) AS avg_duration
p.name AS name,
ROUND(AVG(pip.duration/60.0), 1) AS duration,
pip.ref
FROM project p
JOIN pipeline pip ON p.id = pip.project_id
GROUP BY project_name, week, pip.ref;
GROUP BY name, week, pip.ref;
```
本周平均用时视图 SQL
每周流水线运行统计 SQL `statisticsPerWeek`
```SQL
SELECT
(ROW_NUMBER() OVER()) as id,
p.has_new_cicd,
pip.ref,
AVG(pip.duration) AS avg_duration
FROM project p
JOIN pipeline pip ON p.id = pip.project_id
WHERE strftime('%Y-%W', datetime(pip.started_at)) = strftime('%Y-%W', 'now')
GROUP BY p.has_new_cicd, pip.ref;
```
上周平均用时视图 SQL
```SQL
SELECT
(ROW_NUMBER() OVER()) as id,
p.has_new_cicd,
pip.ref,
AVG(pip.duration) AS avg_duration
FROM project p
JOIN pipeline pip ON p.id = pip.project_id
WHERE strftime('%Y-%W', datetime(pip.started_at)) = strftime('%Y-%W', 'now', '-7 days')
GROUP BY p.has_new_cicd, pip.ref;
```
本周每个项目平均用时视图 SQL
```SQL
SELECT
(ROW_NUMBER() OVER()) as id,
p.has_new_cicd,
p.name AS project_name,
pip.ref,
AVG(pip.duration) AS avg_duration
FROM project p
JOIN pipeline pip ON p.id = pip.project_id
WHERE strftime('%Y-%W', datetime(pip.started_at)) = strftime('%Y-%W', 'now')
GROUP BY p.has_new_cicd, p.name, pip.ref;
```
上周每个项目平均用时视图 SQL
```SQL
SELECT
(ROW_NUMBER() OVER()) as id,
p.has_new_cicd,
p.name AS project_name,
pip.ref,
AVG(pip.duration) AS avg_duration
FROM project p
JOIN pipeline pip ON p.id = pip.project_id
WHERE strftime('%Y-%W', datetime(pip.started_at)) = strftime('%Y-%W', 'now', '-7 days')
GROUP BY p.has_new_cicd, p.name, pip.ref;
strftime('%Y-%W', datetime(started_at)) AS week,
COUNT(*) AS total_count,
SUM(CASE WHEN status = 'success' THEN 0 ELSE 1 END) AS failed_count,
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS success_count,
ROUND(SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) AS success_rate,
ROUND(AVG(duration/60.0), 1) AS duration
FROM pipeline
GROUP BY week;
```
# GPT
@ -200,3 +141,42 @@ user 表
```
我想按天展示每个项目的 pipline 按 ref 区分的平均 duration如何创建视图
# 机器人
卡片 IDctp_AAyVLS6Q37cL
JSON 示例
```json
{
"total_count": "29", // OK
"group_table": [
{
"project_name": "ai-ak-fe",
"project_ref": "master",
"project_duration": "1.4",
"project_duration_rate": "<font color='green'>↓12%</font>"
},
{
"project_name": "ai-class-schedule-fe",
"project_ref": "preview",
"project_duration": "3.2",
"project_duration_rate": "<font color='red'>↑5%</font>"
},
{
"project_name": "ai-scene-review-fe",
"project_ref": "staging",
"project_duration": "5.4",
"project_duration_rate": "<font color='green'>↓6%</font>"
}
],
"weekly_count_rate": "<font color='red'>较上周 ↑5%</font>", // OK
"weekly_duration_rate": "<font color='green'>较上周 ↓12%</font>", // OK
"weekly_success_rate": "<font color='red'>较上周 ↑12%</font>", // OK
"duration": "0.9", // OK
"success_rate": "100", // OK
"has_new_cicd_count": "15", // OK
"without_new_cicd_count": "20" // OK
}
```

View File

@ -27,7 +27,7 @@ const fetchProjectDetails = async (id: number) => {
const fetchPipelines = async (project_id: number, page = 1) => {
try {
const response = await fetch(
`https://git.n.xiaomi.com/api/v4/projects/${project_id}/pipelines?scope=finished&status=success&per_page=100&page=${page}`,
`https://git.n.xiaomi.com/api/v4/projects/${project_id}/pipelines?scope=finished&per_page=100&page=${page}`,
fetchGetParams
);
const body = (await response.json()) as GitlabPipeline[] & GitlabError;
@ -62,10 +62,31 @@ const fetchPipelineDetails = async (
}
};
const sendMessage = async (content: string) => {
try {
const response = await fetch("https://egg.imoaix.cn/message", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
group_id: "52usf3w8l6z4vs1",
msg_type: "interactive",
content,
}),
});
const body = await response.json();
return body;
} catch {
return null;
}
};
const service = {
fetchProjectDetails,
fetchPipelines,
fetchPipelineDetails,
sendMessage,
};
export default service;

61
utils/robotTools.ts Normal file
View File

@ -0,0 +1,61 @@
/**
*
* @param a
* @param b
*/
export const calculatePercentageChange = (cur: number, prev: number) => {
// 计算差值
const diff = cur - prev;
if (diff === 0)
return {
diff,
percentage: "0",
};
// 计算百分比
const percentage = Math.abs((diff / prev) * 100).toFixed(1);
return {
diff,
percentage,
};
};
/**
*
* @param cur
* @param prev
* @param needCN
*/
export const calculateWeeklyRate = (
cur: string | number,
prev: string | number,
needCN = true
) => {
const { diff, percentage } = calculatePercentageChange(
Number(cur),
Number(prev)
);
if (diff > 0)
return {
text: `<font color='red'>${
needCN ? "较上周 " : ""
}${percentage}%</font>`,
diff,
percentage,
};
if (diff < 0)
return {
text: `<font color='green'>${
needCN ? "较上周 " : ""
}${percentage}%</font>`,
diff,
percentage,
};
return {
text: `<font color='gray'>${needCN ? "较上周 " : ""}0%</font>`,
diff,
percentage,
};
};

22
utils/timeTools.ts Normal file
View File

@ -0,0 +1,22 @@
import moment from "moment";
/**
* like 2024-05
*/
export const getWeekTimeWithYear = () => {
return moment().format("YYYY-WW");
};
/**
* like 2024-04
*/
export const getPrevWeekWithYear = () => {
return moment().subtract(1, "weeks").format("YYYY-WW");
};
/**
*
*/
export const sec2min = (sec: number) => {
return (sec / 60).toFixed(1);
};