@hecom/codearts 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -168
- package/dist/bin/cli.js +46 -28
- package/dist/commands/bug.command.d.ts +0 -2
- package/dist/commands/bug.command.js +105 -61
- package/dist/commands/config.command.d.ts +25 -0
- package/dist/commands/config.command.js +425 -78
- package/dist/commands/daily.command.d.ts +0 -5
- package/dist/commands/daily.command.js +265 -99
- package/dist/commands/work-hour.command.js +189 -131
- package/dist/services/api.service.d.ts +9 -2
- package/dist/services/api.service.js +12 -4
- package/dist/services/business.service.d.ts +21 -1
- package/dist/services/business.service.js +66 -1
- package/dist/types/index.d.ts +47 -0
- package/dist/types/index.js +34 -1
- package/dist/utils/config-loader.d.ts +31 -9
- package/dist/utils/config-loader.js +201 -32
- package/dist/utils/csv-writer.d.ts +12 -0
- package/dist/utils/csv-writer.js +60 -0
- package/dist/utils/logger.d.ts +62 -0
- package/dist/utils/logger.js +98 -0
- package/package.json +1 -3
- package/.env.example +0 -20
- package/dist/utils/global-config.d.ts +0 -24
- package/dist/utils/global-config.js +0 -153
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hecom/codearts
|
|
2
2
|
|
|
3
|
-
基于华为云 CodeArts API
|
|
3
|
+
基于华为云 CodeArts API 的统计分析工具。
|
|
4
4
|
|
|
5
5
|
## 快速开始
|
|
6
6
|
|
|
@@ -24,16 +24,29 @@ npx @hecom/codearts daily
|
|
|
24
24
|
# 生成当年工时统计
|
|
25
25
|
npx @hecom/codearts work-hour
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
npx @hecom/codearts
|
|
29
|
-
|
|
27
|
+
# 按迭代统计产品缺陷率
|
|
28
|
+
npx @hecom/codearts bug-rate "迭代1,迭代2"
|
|
29
|
+
```
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
npx @hecom/codearts daily 2026-01-15 --project-id abc123 --role-id 1,2
|
|
33
|
-
npx @hecom/codearts work-hour 2025 --role-id 1,2,3
|
|
31
|
+
### 3. 更新配置
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
```bash
|
|
34
|
+
# 更新全局配置
|
|
36
35
|
npx @hecom/codearts config
|
|
36
|
+
|
|
37
|
+
# 单独更新角色配置
|
|
38
|
+
npx @hecom/codearts config role-id
|
|
39
|
+
|
|
40
|
+
# 查看当前配置
|
|
41
|
+
npx @hecom/codearts config show
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 4. 升级
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# 更新最新版本
|
|
48
|
+
npx @hecom/codearts@latest
|
|
49
|
+
|
|
37
50
|
```
|
|
38
51
|
|
|
39
52
|
---
|
|
@@ -42,7 +55,7 @@ npx @hecom/codearts config
|
|
|
42
55
|
|
|
43
56
|
### 环境要求
|
|
44
57
|
|
|
45
|
-
- Node.js >=
|
|
58
|
+
- Node.js >= 23.0.0
|
|
46
59
|
- npm >= 7.0.0
|
|
47
60
|
|
|
48
61
|
### 安装依赖
|
|
@@ -51,175 +64,39 @@ npx @hecom/codearts config
|
|
|
51
64
|
npm install
|
|
52
65
|
```
|
|
53
66
|
|
|
54
|
-
###
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
npm run build
|
|
58
|
-
npm link
|
|
59
|
-
codearts --help
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### 开发调试
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
# 编译 TypeScript
|
|
66
|
-
npm run build
|
|
67
|
-
|
|
68
|
-
# 运行测试
|
|
69
|
-
npm test
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### 高级配置
|
|
67
|
+
### 本地运行
|
|
73
68
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
#### 项目级配置
|
|
77
|
-
|
|
78
|
-
在项目目录创建 `.env` 文件(优先级高于全局配置):
|
|
69
|
+
> 本地运行命令时,注意使用 `--` 分隔 npm 参数和 CLI 参数,否则 CLI 参数可能无法正确传递。
|
|
79
70
|
|
|
80
71
|
```bash
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
修改 `.env` 文件中的配置:
|
|
72
|
+
# 运行命令
|
|
73
|
+
npm run dev
|
|
85
74
|
|
|
86
|
-
|
|
87
|
-
# 华为云IAM认证端点(根据区域调整)
|
|
88
|
-
HUAWEI_CLOUD_IAM_ENDPOINT=https://iam.cn-north-4.myhuaweicloud.com
|
|
89
|
-
HUAWEI_CLOUD_REGION=cn-north-4
|
|
75
|
+
npm run dev -- daily
|
|
90
76
|
|
|
91
|
-
|
|
92
|
-
HUAWEI_CLOUD_USERNAME=your-iam-username
|
|
93
|
-
HUAWEI_CLOUD_PASSWORD=your-iam-password
|
|
94
|
-
HUAWEI_CLOUD_DOMAIN=your-domain-name
|
|
95
|
-
|
|
96
|
-
# 项目配置
|
|
97
|
-
CODEARTS_BASE_URL=https://projectman-ext.cn-north-4.myhuaweicloud.cn
|
|
98
|
-
PROJECT_ID=your-project-id
|
|
99
|
-
ROLE_ID=1,2,3 # 支持逗号分隔的多个角色ID
|
|
77
|
+
npm run dev -- work-hour
|
|
100
78
|
```
|
|
101
79
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
#### 环境变量说明
|
|
105
|
-
|
|
106
|
-
| 变量名 | 说明 | 必填 | 默认值 |
|
|
107
|
-
| --------------------------- | ---------------------------------- | ---- | ---------------------------------------------------- |
|
|
108
|
-
| `HUAWEI_CLOUD_IAM_ENDPOINT` | IAM 认证端点 | 否 | `https://iam.cn-north-4.myhuaweicloud.com` |
|
|
109
|
-
| `HUAWEI_CLOUD_REGION` | 华为云区域 | 否 | `cn-north-4` |
|
|
110
|
-
| `HUAWEI_CLOUD_USERNAME` | IAM 用户名 | 是 | - |
|
|
111
|
-
| `HUAWEI_CLOUD_PASSWORD` | IAM 密码 | 是 | - |
|
|
112
|
-
| `HUAWEI_CLOUD_DOMAIN` | 华为云账号名 | 是 | - |
|
|
113
|
-
| `CODEARTS_BASE_URL` | CodeArts API 地址 | 否 | `https://projectman-ext.cn-north-4.myhuaweicloud.cn` |
|
|
114
|
-
| `PROJECT_ID` | 项目 ID | 是 | - |
|
|
115
|
-
| `ROLE_ID` | 角色 ID(支持逗号分隔,如: 1,2,3) | 是 | - |
|
|
116
|
-
| `TARGET_DATE` | 目标日期(YYYY-MM-DD) | 否 | 当天日期 |
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## 项目信息
|
|
121
|
-
|
|
122
|
-
### 项目描述
|
|
123
|
-
|
|
124
|
-
这是一个使用 TypeScript/Node.js 构建的工时统计分析项目,通过华为云 CodeArts API 获取 issue、人员、工时等数据,并生成日报和年度工时统计报表。
|
|
125
|
-
|
|
126
|
-
### 技术栈
|
|
127
|
-
|
|
128
|
-
- **语言**: TypeScript
|
|
129
|
-
- **运行环境**: Node.js
|
|
130
|
-
- **HTTP客户端**: Axios
|
|
131
|
-
- **测试框架**: Jest
|
|
132
|
-
|
|
133
|
-
### 项目结构
|
|
80
|
+
### 本地链接 CLI 工具
|
|
134
81
|
|
|
82
|
+
```bash
|
|
83
|
+
npm run build
|
|
84
|
+
npm link
|
|
85
|
+
codearts --help
|
|
135
86
|
```
|
|
136
|
-
@hecom/codearts/
|
|
137
|
-
├── src/
|
|
138
|
-
│ ├── bin/ # CLI 入口
|
|
139
|
-
│ │ └── cli.ts # Commander.js CLI 定义
|
|
140
|
-
│ ├── commands/ # 命令实现
|
|
141
|
-
│ │ ├── config.command.ts # 配置命令逻辑
|
|
142
|
-
│ │ ├── daily.command.ts # 日报命令逻辑
|
|
143
|
-
│ │ ├── work-hour.command.ts# 工时统计命令逻辑
|
|
144
|
-
│ │ └── index.ts # 命令导出
|
|
145
|
-
│ ├── services/ # API服务类
|
|
146
|
-
│ │ ├── api.service.ts # 华为云基础 API 封装
|
|
147
|
-
│ │ └── business.service.ts # 业务场景 API 封装
|
|
148
|
-
│ ├── utils/ # 工具函数
|
|
149
|
-
│ │ ├── global-config.ts # 全局配置查找
|
|
150
|
-
│ │ └── config-loader.ts # 配置加载器(CLI参数 > 环境变量)
|
|
151
|
-
│ ├── config/ # 配置文件
|
|
152
|
-
│ │ └── holidays.ts # 节假日配置与工作日计算
|
|
153
|
-
│ ├── types/ # TypeScript 类型定义
|
|
154
|
-
│ │ └── index.ts # API 契约与数据结构定义
|
|
155
|
-
│ └── index.ts # 模块导出
|
|
156
|
-
├── bin/
|
|
157
|
-
│ └── codearts # CLI 可执行文件
|
|
158
|
-
├── __tests__/ # 测试文件
|
|
159
|
-
├── dist/ # 编译输出目录
|
|
160
|
-
├── .env # 环境变量配置
|
|
161
|
-
├── .env.example # 环境变量配置模板
|
|
162
|
-
├── tsconfig.json # TypeScript配置
|
|
163
|
-
├── jest.config.js # Jest测试配置
|
|
164
|
-
└── package.json # 项目配置
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### 核心功能
|
|
168
|
-
|
|
169
|
-
#### 日报统计 (daily)
|
|
170
|
-
|
|
171
|
-
生成指定日期的工时日报,包括:
|
|
172
|
-
|
|
173
|
-
- 按人员统计当日工时
|
|
174
|
-
- 统计 Bug 修复工时(自动合并同一 Bug 的多人工时)
|
|
175
|
-
- 统计活跃的工作项数量
|
|
176
|
-
- 计算工时完成率(基于角色成员数)
|
|
177
|
-
- 生成可读的日报格式输出
|
|
178
|
-
|
|
179
|
-
#### 年度工时统计 (work-hour)
|
|
180
|
-
|
|
181
|
-
生成指定年份的工时统计报表,包括:
|
|
182
|
-
|
|
183
|
-
- 计算年度应计工作日(自动排除法定节假日和周末)
|
|
184
|
-
- 统计实际工时与应计工时的完成率
|
|
185
|
-
- 按人员生成工时明细表
|
|
186
|
-
- 按领域类型(需求、缺陷、任务等)统计工时分布
|
|
187
|
-
- 支持多角色统计(按角色分组,显示小计和总计)
|
|
188
|
-
- 生成 Markdown 格式的统计表格
|
|
189
|
-
|
|
190
|
-
### API服务层
|
|
191
|
-
|
|
192
|
-
#### ApiService (api.service.ts)
|
|
193
|
-
|
|
194
|
-
华为云 CodeArts 基础 API 封装:
|
|
195
|
-
|
|
196
|
-
- IAM Token 认证管理(自动获取和缓存)
|
|
197
|
-
- 项目管理 API(项目列表、成员查询)
|
|
198
|
-
- 迭代管理 API(迭代列表、迭代详情)
|
|
199
|
-
- 工作项 API(Issue 列表、详情、创建、更新)
|
|
200
|
-
- 工时管理 API(工时查询、提交)
|
|
201
|
-
- 请求日志(可选的 curl 风格日志输出)
|
|
202
|
-
- 完善的错误处理
|
|
203
|
-
|
|
204
|
-
#### BusinessService (business.service.ts)
|
|
205
|
-
|
|
206
|
-
面向业务场景的高级 API 封装:
|
|
207
|
-
|
|
208
|
-
- `getMembersByRoleId()`: 通过角色获取人员列表
|
|
209
|
-
- `getActiveIterationsOnDate()`: 查询指定日期的活跃迭代
|
|
210
|
-
- `getWorkloadByIterationsAndUsers()`: 批量查询工作量
|
|
211
|
-
- `getAllIssuesByIteration()`: 查询迭代内所有 Issue(自动分页)
|
|
212
|
-
- `getDailyWorkHourStats()`: 获取日报工时统计
|
|
213
|
-
- `getAllWorkHourStats()`: 获取年度工时统计
|
|
214
|
-
- `addIssueNote()`: 添加工作项备注
|
|
215
|
-
|
|
216
|
-
### 节假日配置 (config/holidays.ts)
|
|
217
87
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
88
|
+
### 配置项
|
|
89
|
+
|
|
90
|
+
| 配置项 | 说明 | 必填 |
|
|
91
|
+
| --------------------------- | ---------------------------------- | ---- |
|
|
92
|
+
| `HUAWEI_CLOUD_IAM_ENDPOINT` | IAM 认证端点 | 是 |
|
|
93
|
+
| `HUAWEI_CLOUD_REGION` | 华为云区域 | 是 |
|
|
94
|
+
| `CODEARTS_BASE_URL` | CodeArts API 地址 | 是 |
|
|
95
|
+
| `HUAWEI_CLOUD_DOMAIN` | 华为云账号名 | 是 |
|
|
96
|
+
| `HUAWEI_CLOUD_USERNAME` | IAM 用户名 | 是 |
|
|
97
|
+
| `HUAWEI_CLOUD_PASSWORD` | IAM 密码 | 是 |
|
|
98
|
+
| `PROJECT_ID` | 项目 ID | 是 |
|
|
99
|
+
| `ROLE_ID` | 角色 ID(支持逗号分隔,如: 1,2,3) | 是 |
|
|
223
100
|
|
|
224
101
|
---
|
|
225
102
|
|
package/dist/bin/cli.js
CHANGED
|
@@ -33,19 +33,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
return result;
|
|
34
34
|
};
|
|
35
35
|
})();
|
|
36
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
-
};
|
|
39
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
37
|
const commander_1 = require("commander");
|
|
41
|
-
const dotenv_1 = __importDefault(require("dotenv"));
|
|
42
38
|
const fs = __importStar(require("fs"));
|
|
43
39
|
const path = __importStar(require("path"));
|
|
44
40
|
const bug_command_1 = require("../commands/bug.command");
|
|
45
41
|
const config_command_1 = require("../commands/config.command");
|
|
46
42
|
const daily_command_1 = require("../commands/daily.command");
|
|
47
43
|
const work_hour_command_1 = require("../commands/work-hour.command");
|
|
48
|
-
const
|
|
44
|
+
const config_loader_1 = require("../utils/config-loader");
|
|
45
|
+
const logger_1 = require("../utils/logger");
|
|
49
46
|
// 读取 package.json 中的版本号
|
|
50
47
|
const packageJsonPath = path.join(__dirname, '../../package.json');
|
|
51
48
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
@@ -53,51 +50,72 @@ const version = packageJson.version;
|
|
|
53
50
|
const program = new commander_1.Command();
|
|
54
51
|
program.name('codearts').description('华为云 CodeArts 统计分析工具').version(version);
|
|
55
52
|
// 全局选项(环境变量覆盖)
|
|
56
|
-
program.option('--role-id <ids>', '角色 ID(支持逗号分隔,如: 1,2,3),优先级高于环境变量 ROLE_ID');
|
|
57
|
-
// config 命令 - 交互式配置向导
|
|
58
53
|
program
|
|
54
|
+
.option('--role-id <ids>', '角色 ID(支持逗号分隔,如: 1,2)')
|
|
55
|
+
.option('--output <format>', '输出格式:console、csv、json', 'console');
|
|
56
|
+
// config 命令 - 交互式配置向导
|
|
57
|
+
const configCmd = program
|
|
59
58
|
.command('config')
|
|
60
|
-
.description('
|
|
59
|
+
.description('交互式配置向导,引导用户创建或更新全局配置文件')
|
|
61
60
|
.action(async () => {
|
|
62
61
|
await (0, config_command_1.configCommand)();
|
|
63
62
|
});
|
|
63
|
+
// config show 子命令 - 显示当前配置
|
|
64
|
+
configCmd
|
|
65
|
+
.command('show')
|
|
66
|
+
.description('显示当前配置信息')
|
|
67
|
+
.action(async () => {
|
|
68
|
+
await (0, config_command_1.showConfigCommand)();
|
|
69
|
+
});
|
|
70
|
+
// 为每个项目配置项添加子命令
|
|
71
|
+
const availableConfigs = (0, config_command_1.getAvailableProjectConfigs)();
|
|
72
|
+
availableConfigs.forEach((configItem) => {
|
|
73
|
+
const subCommandName = configItem.key.toLowerCase().replace(/_/g, '-');
|
|
74
|
+
configCmd
|
|
75
|
+
.command(subCommandName)
|
|
76
|
+
.description(`更新${configItem.label}`)
|
|
77
|
+
.action(async () => {
|
|
78
|
+
await (0, config_command_1.updateProjectConfigCommand)(configItem.key);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
64
81
|
// daily 命令
|
|
65
82
|
program
|
|
66
83
|
.command('daily [date]')
|
|
67
|
-
.description('
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
.description('每日工时统计(默认日期为当天)')
|
|
85
|
+
.option('-r, --report', '显示总结报告', false)
|
|
86
|
+
.action(async (date, options, command) => {
|
|
87
|
+
const cliOptions = { ...command.parent.opts(), report: options.report };
|
|
88
|
+
logger_1.logger.setOutputFormat(cliOptions.output);
|
|
89
|
+
await (0, daily_command_1.dailyCommand)(date, cliOptions);
|
|
71
90
|
});
|
|
72
91
|
// work-hour 命令
|
|
73
92
|
program
|
|
74
93
|
.command('work-hour [year]')
|
|
75
|
-
.description('
|
|
76
|
-
.action(async (year) => {
|
|
77
|
-
const
|
|
78
|
-
|
|
94
|
+
.description('年度工时统计(默认当前年份)')
|
|
95
|
+
.action(async (year, options, command) => {
|
|
96
|
+
const cliOptions = command.parent.opts();
|
|
97
|
+
logger_1.logger.setOutputFormat(cliOptions.output);
|
|
98
|
+
await (0, work_hour_command_1.workHourCommand)(year, cliOptions);
|
|
79
99
|
});
|
|
80
100
|
// bug-rate 命令
|
|
81
101
|
program
|
|
82
102
|
.command('bug-rate <iterations>')
|
|
83
|
-
.description('
|
|
84
|
-
.action(async (iterations) => {
|
|
85
|
-
const
|
|
86
|
-
|
|
103
|
+
.description('产品缺陷率统计,支持多个迭代')
|
|
104
|
+
.action(async (iterations, options, command) => {
|
|
105
|
+
const cliOptions = command.parent.opts();
|
|
106
|
+
logger_1.logger.setOutputFormat(cliOptions.output);
|
|
107
|
+
await (0, bug_command_1.bugCommand)(iterations, cliOptions);
|
|
87
108
|
});
|
|
88
109
|
// 检查配置并自动执行 config 命令
|
|
89
110
|
async function checkConfigAndRun() {
|
|
90
111
|
const args = process.argv.slice(2);
|
|
91
112
|
// 如果没有参数(直接执行 codearts),检测配置
|
|
92
113
|
if (args.length === 0) {
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const hasGlobalConfig = (0, global_config_1.globalConfigExists)();
|
|
97
|
-
const hasEnvConfig = process.env.PROJECT_ID && process.env.ROLE_ID;
|
|
98
|
-
if (!hasGlobalConfig && !hasEnvConfig) {
|
|
114
|
+
// 检查是否有全局配置
|
|
115
|
+
const hasConfig = (0, config_loader_1.configExists)();
|
|
116
|
+
if (!hasConfig) {
|
|
99
117
|
// 没有配置,自动执行 config 命令
|
|
100
|
-
|
|
118
|
+
logger_1.logger.info('未检测到配置文件,启动配置向导...\n');
|
|
101
119
|
await (0, config_command_1.configCommand)();
|
|
102
120
|
return;
|
|
103
121
|
}
|
|
@@ -108,6 +126,6 @@ async function checkConfigAndRun() {
|
|
|
108
126
|
program.parse();
|
|
109
127
|
}
|
|
110
128
|
checkConfigAndRun().catch((error) => {
|
|
111
|
-
|
|
129
|
+
logger_1.logger.error('执行失败: ', error);
|
|
112
130
|
process.exit(1);
|
|
113
131
|
});
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { CliOptions } from '../utils/config-loader';
|
|
2
2
|
/**
|
|
3
3
|
* bug 命令入口
|
|
4
|
-
* @param iterationTitlesStr 迭代标题(支持逗号、分号、空格、竖线、顿号等分隔符)
|
|
5
|
-
* @param cliOptions CLI 选项
|
|
6
4
|
*/
|
|
7
5
|
export declare function bugCommand(iterationTitlesStr: string, cliOptions?: CliOptions): Promise<void>;
|
|
@@ -4,13 +4,12 @@ exports.bugCommand = bugCommand;
|
|
|
4
4
|
const business_service_1 = require("../services/business.service");
|
|
5
5
|
const types_1 = require("../types");
|
|
6
6
|
const config_loader_1 = require("../utils/config-loader");
|
|
7
|
+
const csv_writer_1 = require("../utils/csv-writer");
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
7
9
|
/**
|
|
8
10
|
* 判断 Bug 是否属于需求变更或产品设计问题
|
|
9
|
-
* @param bug Bug 工作项
|
|
10
|
-
* @returns 是否属于需求变更或产品设计问题
|
|
11
11
|
*/
|
|
12
12
|
function isRequirementOrDesignBug(bug) {
|
|
13
|
-
// 检查 customValueNew(v2 版本的自定义字段,是对象格式)
|
|
14
13
|
const defectAnalysisValue = bug.customValueNew?.custom_field32;
|
|
15
14
|
if (defectAnalysisValue) {
|
|
16
15
|
return (defectAnalysisValue === types_1.DefectAnalysisType.REQUIREMENT_CHANGE ||
|
|
@@ -19,32 +18,35 @@ function isRequirementOrDesignBug(bug) {
|
|
|
19
18
|
return false;
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
|
-
*
|
|
21
|
+
* 查询 Bug 统计数据
|
|
23
22
|
*/
|
|
24
|
-
async function
|
|
25
|
-
|
|
26
|
-
console.log(`目标迭代: ${iterationTitles.join(', ')}`);
|
|
27
|
-
console.log(`角色过滤: ${roleIds.length > 0 ? roleIds.join(', ') : '无(保留所有人)'}`);
|
|
28
|
-
console.log(`统计规则: 统计所有 Bug,同时标记"需求变更问题"和"产品设计问题"`);
|
|
29
|
-
// Step 1: 获取指定角色的成员列表(如果启用了角色过滤)
|
|
23
|
+
async function queryBugReportData(businessService, projectId, roleIds, iterationTitles) {
|
|
24
|
+
const roleNames = new Set();
|
|
30
25
|
const targetMemberIds = new Set();
|
|
31
26
|
if (roleIds.length > 0) {
|
|
32
27
|
for (const roleId of roleIds) {
|
|
33
28
|
const members = await businessService.getMembersByRoleId(projectId, roleId);
|
|
34
|
-
members.forEach((member) =>
|
|
29
|
+
members.forEach((member) => {
|
|
30
|
+
targetMemberIds.add(member.user_num_id);
|
|
31
|
+
roleNames.add(member.role_name);
|
|
32
|
+
});
|
|
35
33
|
}
|
|
36
|
-
console.log(`\n目标角色成员数: ${targetMemberIds.size} 人`);
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
console.log(`\n角色过滤: 已禁用,将统计所有人`);
|
|
40
34
|
}
|
|
41
|
-
// Step 2: 获取所有 Story
|
|
42
35
|
const stories = await businessService.getStoriesByIterationTitles(projectId, iterationTitles);
|
|
43
36
|
if (stories.length === 0) {
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
return {
|
|
38
|
+
iterations: iterationTitles,
|
|
39
|
+
roleIds,
|
|
40
|
+
roleNames: Array.from(roleNames),
|
|
41
|
+
userStats: [],
|
|
42
|
+
summary: {
|
|
43
|
+
totalBugs: 0,
|
|
44
|
+
totalProductBugs: 0,
|
|
45
|
+
overallProductDefectRate: 0,
|
|
46
|
+
userCount: 0,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
46
49
|
}
|
|
47
|
-
// Step 3: 过滤出目标角色成员处理的 Story(如果启用了角色过滤)
|
|
48
50
|
const filteredStories = roleIds.length > 0
|
|
49
51
|
? stories.filter((story) => {
|
|
50
52
|
const assignedUserId = story.assigned_user?.id;
|
|
@@ -52,27 +54,27 @@ async function generateBugReport(businessService, projectId, roleIds, iterationT
|
|
|
52
54
|
})
|
|
53
55
|
: stories;
|
|
54
56
|
if (filteredStories.length === 0) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
return {
|
|
58
|
+
iterations: iterationTitles,
|
|
59
|
+
roleIds,
|
|
60
|
+
roleNames: Array.from(roleNames),
|
|
61
|
+
userStats: [],
|
|
62
|
+
summary: {
|
|
63
|
+
totalBugs: 0,
|
|
64
|
+
totalProductBugs: 0,
|
|
65
|
+
overallProductDefectRate: 0,
|
|
66
|
+
userCount: 0,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
60
69
|
}
|
|
61
|
-
else {
|
|
62
|
-
console.log(`\n找到 ${filteredStories.length} 个 Story,正在统计 Bug...`);
|
|
63
|
-
}
|
|
64
|
-
// Step 4: 获取每个 Story 的子工作项(Bug)
|
|
65
70
|
const bugStatsMap = new Map();
|
|
66
71
|
for (const story of filteredStories) {
|
|
67
72
|
const childIssues = await businessService.getChildIssues(projectId, String(story.id));
|
|
68
|
-
// 过滤出所有 Bug 类型的子工作项(tracker_id = 3)
|
|
69
73
|
const allBugs = childIssues.filter((issue) => issue.tracker.id === 3);
|
|
70
74
|
if (allBugs.length === 0) {
|
|
71
75
|
continue;
|
|
72
76
|
}
|
|
73
|
-
// 统计产品问题 Bug(需求变更 + 产品设计)
|
|
74
77
|
const productBugs = allBugs.filter((bug) => isRequirementOrDesignBug(bug));
|
|
75
|
-
// Step 3: 按 Story 的处理人统计 Bug
|
|
76
78
|
const assignedUserId = story.assigned_user?.id;
|
|
77
79
|
const assignedUserName = story.assigned_user?.nick_name;
|
|
78
80
|
if (!assignedUserId) {
|
|
@@ -98,61 +100,103 @@ async function generateBugReport(businessService, projectId, roleIds, iterationT
|
|
|
98
100
|
productBugCount: productBugs.length,
|
|
99
101
|
});
|
|
100
102
|
}
|
|
101
|
-
// 计算每个人的产品缺陷率
|
|
102
103
|
bugStatsMap.forEach((stats) => {
|
|
103
104
|
stats.productDefectRate =
|
|
104
|
-
stats.totalBugCount > 0
|
|
105
|
+
stats.totalBugCount > 0
|
|
106
|
+
? Math.round((stats.productBugCount / stats.totalBugCount) * 100 * 100) / 100
|
|
107
|
+
: 0;
|
|
105
108
|
});
|
|
106
|
-
// Step 4: 输出统计结果
|
|
107
|
-
console.log('\n');
|
|
108
|
-
console.log('='.repeat(80));
|
|
109
|
-
console.log(`Bug 统计报告 [${iterationTitles.join(', ')}]`);
|
|
110
|
-
console.log('='.repeat(80));
|
|
111
|
-
console.log('');
|
|
112
|
-
if (bugStatsMap.size === 0) {
|
|
113
|
-
console.log('未找到任何 Bug');
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
// 按总 Bug 数量降序排列
|
|
117
109
|
const sortedStats = Array.from(bugStatsMap.values()).sort((a, b) => b.totalBugCount - a.totalBugCount);
|
|
118
110
|
let totalBugs = 0;
|
|
119
111
|
let totalProductBugs = 0;
|
|
120
112
|
sortedStats.forEach((stats) => {
|
|
121
|
-
console.log(`\x1b[31m${stats.assignedUser}: 总 Bug ${stats.totalBugCount} 个 | 产品问题 ${stats.productBugCount} 个 | 产品缺陷率 ${stats.productDefectRate.toFixed(1)}%\x1b[0m`);
|
|
122
|
-
// 输出该用户的 Story 及其 Bug 数量
|
|
123
|
-
stats.stories.forEach((story) => {
|
|
124
|
-
console.log(` - ${story.storyName} (总 ${story.totalBugCount} 个 Bug,其中产品问题 ${story.productBugCount} 个)`);
|
|
125
|
-
});
|
|
126
113
|
totalBugs += stats.totalBugCount;
|
|
127
114
|
totalProductBugs += stats.productBugCount;
|
|
128
|
-
console.log('');
|
|
129
115
|
});
|
|
130
|
-
const overallProductDefectRate = totalBugs > 0 ? (totalProductBugs / totalBugs) * 100 : 0;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
116
|
+
const overallProductDefectRate = totalBugs > 0 ? Math.round((totalProductBugs / totalBugs) * 100 * 100) / 100 : 0;
|
|
117
|
+
return {
|
|
118
|
+
iterations: iterationTitles,
|
|
119
|
+
roleIds,
|
|
120
|
+
roleNames: Array.from(roleNames),
|
|
121
|
+
userStats: sortedStats,
|
|
122
|
+
summary: {
|
|
123
|
+
totalBugs,
|
|
124
|
+
totalProductBugs,
|
|
125
|
+
overallProductDefectRate,
|
|
126
|
+
userCount: sortedStats.length,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 控制台输出产品缺陷率统计
|
|
132
|
+
*/
|
|
133
|
+
function outputConsole(data) {
|
|
134
|
+
logger_1.logger.info(`产品缺陷率统计 [${data.roleNames.join(', ')}]`);
|
|
135
|
+
logger_1.logger.info('='.repeat(80));
|
|
136
|
+
logger_1.logger.info(`产品缺陷率: ${data.summary.overallProductDefectRate.toFixed(2)}% `);
|
|
137
|
+
logger_1.logger.info(`总 Bug 数: ${data.summary.totalBugs}个`);
|
|
138
|
+
logger_1.logger.info(`产品问题: ${data.summary.totalProductBugs}个`);
|
|
139
|
+
logger_1.logger.info(`统计人数: ${data.summary.userCount}人`);
|
|
140
|
+
logger_1.logger.info(`统计迭代: ${data.iterations.join(', ')}`);
|
|
141
|
+
logger_1.logger.info('='.repeat(80));
|
|
142
|
+
data.userStats.forEach((stats) => {
|
|
143
|
+
logger_1.logger.info(`\x1b[31m${stats.assignedUser}: ${stats.productDefectRate.toFixed(2)}% (${stats.productBugCount}/${stats.totalBugCount})\x1b[0m`);
|
|
144
|
+
stats.stories.forEach((story) => {
|
|
145
|
+
logger_1.logger.info(` ${story.storyName} (${story.productBugCount}/${story.totalBugCount})`);
|
|
146
|
+
});
|
|
147
|
+
logger_1.logger.info('');
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* CSV 文件输出
|
|
152
|
+
*/
|
|
153
|
+
function outputCsv(data, iterationTitles) {
|
|
154
|
+
const csvLines = [];
|
|
155
|
+
csvLines.push('迭代,处理人,Story名称,StoryID,总Bug数,产品问题Bug数,产品缺陷率');
|
|
156
|
+
data.userStats.forEach((userStat) => {
|
|
157
|
+
userStat.stories.forEach((story) => {
|
|
158
|
+
const defectRate = story.totalBugCount > 0
|
|
159
|
+
? ((story.productBugCount / story.totalBugCount) * 100).toFixed(2)
|
|
160
|
+
: '0.00';
|
|
161
|
+
csvLines.push(`"${(0, csv_writer_1.escapeCsv)(data.iterations.join(', '))}",${userStat.assignedUser ?? ''},"${(0, csv_writer_1.escapeCsv)(story.storyName ?? '')}",${story.storyId ?? ''},${story.totalBugCount ?? ''},${story.productBugCount ?? ''},${defectRate}%`);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
const filename = `bug-rate-${iterationTitles.join('-')}.csv`;
|
|
165
|
+
(0, csv_writer_1.writeCsvFile)(filename, csvLines, logger_1.logger);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* JSON 输出(直接打印到控制台,供编程调用)
|
|
169
|
+
*/
|
|
170
|
+
function outputJson(data) {
|
|
171
|
+
logger_1.logger.json(data);
|
|
134
172
|
}
|
|
135
173
|
/**
|
|
136
174
|
* bug 命令入口
|
|
137
|
-
* @param iterationTitlesStr 迭代标题(支持逗号、分号、空格、竖线、顿号等分隔符)
|
|
138
|
-
* @param cliOptions CLI 选项
|
|
139
175
|
*/
|
|
140
176
|
async function bugCommand(iterationTitlesStr, cliOptions = {}) {
|
|
141
177
|
try {
|
|
142
178
|
if (!iterationTitlesStr || iterationTitlesStr.trim() === '') {
|
|
143
|
-
throw new Error('
|
|
179
|
+
throw new Error('请指定至少一个迭代标题');
|
|
144
180
|
}
|
|
145
|
-
// 解析迭代标题(支持多种分隔符:逗号、分号、空格、竖线、顿号等)
|
|
146
181
|
const iterationTitles = iterationTitlesStr
|
|
147
182
|
.split(/[,,;;|\s、]+/)
|
|
148
183
|
.map((title) => title.trim())
|
|
149
184
|
.filter((title) => title.length > 0);
|
|
150
|
-
const { projectId, roleIds, config } = (0, config_loader_1.loadConfig)(cliOptions);
|
|
185
|
+
const { projectId, roleIds, config, outputFormat } = (0, config_loader_1.loadConfig)(cliOptions);
|
|
151
186
|
const businessService = new business_service_1.BusinessService(config);
|
|
152
|
-
await
|
|
187
|
+
const reportData = await queryBugReportData(businessService, projectId, roleIds, iterationTitles);
|
|
188
|
+
if (outputFormat === 'console') {
|
|
189
|
+
outputConsole(reportData);
|
|
190
|
+
}
|
|
191
|
+
else if (outputFormat === 'csv') {
|
|
192
|
+
outputCsv(reportData, iterationTitles);
|
|
193
|
+
}
|
|
194
|
+
else if (outputFormat === 'json') {
|
|
195
|
+
outputJson(reportData);
|
|
196
|
+
}
|
|
153
197
|
}
|
|
154
198
|
catch (error) {
|
|
155
|
-
|
|
199
|
+
logger_1.logger.error(`执行过程中发生错误: `, error);
|
|
156
200
|
process.exit(1);
|
|
157
201
|
}
|
|
158
202
|
}
|
|
@@ -1,5 +1,30 @@
|
|
|
1
|
+
import { BusinessService } from '../services/business.service';
|
|
2
|
+
import { ConfigKey } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* 第三阶段项目配置的配置项定义
|
|
5
|
+
*/
|
|
6
|
+
interface ProjectConfigItem {
|
|
7
|
+
key: ConfigKey;
|
|
8
|
+
label: string;
|
|
9
|
+
configure: (businessService: BusinessService, projectId: string, existingValue?: string) => Promise<string>;
|
|
10
|
+
}
|
|
1
11
|
/**
|
|
2
12
|
* 交互式配置向导命令
|
|
3
13
|
* 引导用户创建或更新全局配置文件
|
|
4
14
|
*/
|
|
5
15
|
export declare function configCommand(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* 单独更新某个项目配置项
|
|
18
|
+
* @param configKey 配置项的键名(例如 ConfigKey.ROLE_ID)
|
|
19
|
+
*/
|
|
20
|
+
export declare function updateProjectConfigCommand(configKey: ConfigKey): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* 获取所有可用的项目配置项
|
|
23
|
+
*/
|
|
24
|
+
export declare function getAvailableProjectConfigs(): ProjectConfigItem[];
|
|
25
|
+
/**
|
|
26
|
+
* 显示当前配置
|
|
27
|
+
*
|
|
28
|
+
*/
|
|
29
|
+
export declare function showConfigCommand(): Promise<void>;
|
|
30
|
+
export {};
|