feat: 支持机器人
This commit is contained in:
parent
0fe1fdc695
commit
bc441827ec
121
controllers/manageRobot/index.ts
Normal file
121
controllers/manageRobot/index.ts
Normal 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;
|
@ -1,11 +1,13 @@
|
|||||||
import project from "./project";
|
import project from "./project";
|
||||||
import pipeline from "./pipeline";
|
import pipeline from "./pipeline";
|
||||||
import user from "./user";
|
import user from "./user";
|
||||||
|
import view from "./view";
|
||||||
|
|
||||||
const db = {
|
const db = {
|
||||||
project,
|
project,
|
||||||
pipeline,
|
pipeline,
|
||||||
user,
|
user,
|
||||||
|
view,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
|
44
db/view/index.ts
Normal file
44
db/view/index.ts
Normal 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;
|
@ -5,3 +5,5 @@ services:
|
|||||||
image: git.yingbo.im:333/zhaoyingbo/ci_monitor:sha
|
image: git.yingbo.im:333/zhaoyingbo/ci_monitor:sha
|
||||||
container_name: ci_monitor
|
container_name: ci_monitor
|
||||||
restart: always
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 3001:3000
|
||||||
|
11
index.ts
11
index.ts
@ -2,6 +2,7 @@ import { scheduleJob } from "node-schedule";
|
|||||||
import managePipeline from "./controllers/managePipeLine";
|
import managePipeline from "./controllers/managePipeLine";
|
||||||
import manageProject from "./controllers/manageProject";
|
import manageProject from "./controllers/manageProject";
|
||||||
import manageUser from "./controllers/manageUser";
|
import manageUser from "./controllers/manageUser";
|
||||||
|
import manageRobot from "./controllers/manageRobot";
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const fullProjList = await manageProject.getFullProjList();
|
const fullProjList = await manageProject.getFullProjList();
|
||||||
@ -20,3 +21,13 @@ const main = async () => {
|
|||||||
main();
|
main();
|
||||||
|
|
||||||
scheduleJob("*/15 * * * *", 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
128
readme.md
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
先从数据中获取用户信息填充,随后在根据填充完的用户信息获取全部的 userid 的列表,再写 pipeline 表
|
先从数据中获取用户信息填充,随后在根据填充完的用户信息获取全部的 userid 的列表,再写 pipeline 表
|
||||||
|
|
||||||
# 图表库
|
# 图表库(不用了)
|
||||||
|
|
||||||
https://g2plot.antv.antgroup.com/examples
|
https://g2plot.antv.antgroup.com/examples
|
||||||
|
|
||||||
@ -62,92 +62,33 @@ user 信息
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
date 视图 SQL
|
每周每个项目按分支的运行时长统计 SQL `statisticsPerProj`
|
||||||
|
|
||||||
```SQL
|
```SQL
|
||||||
SELECT
|
SELECT
|
||||||
(ROW_NUMBER() OVER()) as id,
|
(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,
|
strftime('%Y-%W', datetime(pip.started_at)) AS week,
|
||||||
pip.ref,
|
p.name AS name,
|
||||||
AVG(pip.duration) AS avg_duration
|
ROUND(AVG(pip.duration/60.0), 1) AS duration,
|
||||||
|
pip.ref
|
||||||
FROM project p
|
FROM project p
|
||||||
JOIN pipeline pip ON p.id = pip.project_id
|
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
|
```SQL
|
||||||
SELECT
|
SELECT
|
||||||
(ROW_NUMBER() OVER()) as id,
|
(ROW_NUMBER() OVER()) as id,
|
||||||
p.has_new_cicd,
|
strftime('%Y-%W', datetime(started_at)) AS week,
|
||||||
pip.ref,
|
COUNT(*) AS total_count,
|
||||||
AVG(pip.duration) AS avg_duration
|
SUM(CASE WHEN status = 'success' THEN 0 ELSE 1 END) AS failed_count,
|
||||||
FROM project p
|
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS success_count,
|
||||||
JOIN pipeline pip ON p.id = pip.project_id
|
ROUND(SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) AS success_rate,
|
||||||
WHERE strftime('%Y-%W', datetime(pip.started_at)) = strftime('%Y-%W', 'now')
|
ROUND(AVG(duration/60.0), 1) AS duration
|
||||||
GROUP BY p.has_new_cicd, pip.ref;
|
FROM pipeline
|
||||||
```
|
GROUP BY week;
|
||||||
|
|
||||||
上周平均用时视图 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;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# GPT
|
# GPT
|
||||||
@ -200,3 +141,42 @@ user 表
|
|||||||
```
|
```
|
||||||
|
|
||||||
我想按天展示每个项目的 pipline 按 ref 区分的平均 duration,如何创建视图
|
我想按天展示每个项目的 pipline 按 ref 区分的平均 duration,如何创建视图
|
||||||
|
|
||||||
|
# 机器人
|
||||||
|
|
||||||
|
卡片 ID:ctp_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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
@ -27,7 +27,7 @@ const fetchProjectDetails = async (id: number) => {
|
|||||||
const fetchPipelines = async (project_id: number, page = 1) => {
|
const fetchPipelines = async (project_id: number, page = 1) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
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
|
fetchGetParams
|
||||||
);
|
);
|
||||||
const body = (await response.json()) as GitlabPipeline[] & GitlabError;
|
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 = {
|
const service = {
|
||||||
fetchProjectDetails,
|
fetchProjectDetails,
|
||||||
fetchPipelines,
|
fetchPipelines,
|
||||||
fetchPipelineDetails,
|
fetchPipelineDetails,
|
||||||
|
sendMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default service;
|
export default service;
|
||||||
|
61
utils/robotTools.ts
Normal file
61
utils/robotTools.ts
Normal 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
22
utils/timeTools.ts
Normal 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);
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user