chandao4 0.1.0
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/.env.example +10 -0
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/dist/commands/bug.d.ts +3 -0
- package/dist/commands/bug.js +373 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +38 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.js +83 -0
- package/dist/commands/product.d.ts +3 -0
- package/dist/commands/product.js +41 -0
- package/dist/commands/project.d.ts +3 -0
- package/dist/commands/project.js +70 -0
- package/dist/commands/task.d.ts +3 -0
- package/dist/commands/task.js +445 -0
- package/dist/config/config.d.ts +17 -0
- package/dist/config/config.js +216 -0
- package/dist/config/defaults.d.ts +5 -0
- package/dist/config/defaults.js +23 -0
- package/dist/core/api-client.d.ts +20 -0
- package/dist/core/api-client.js +127 -0
- package/dist/core/auth.d.ts +44 -0
- package/dist/core/auth.js +244 -0
- package/dist/core/errors.d.ts +17 -0
- package/dist/core/errors.js +61 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +125 -0
- package/dist/services/bug.service.d.ts +59 -0
- package/dist/services/bug.service.js +232 -0
- package/dist/services/product.service.d.ts +12 -0
- package/dist/services/product.service.js +43 -0
- package/dist/services/project.service.d.ts +18 -0
- package/dist/services/project.service.js +35 -0
- package/dist/services/task.service.d.ts +55 -0
- package/dist/services/task.service.js +254 -0
- package/dist/types/api.d.ts +31 -0
- package/dist/types/api.js +3 -0
- package/dist/types/config.d.ts +18 -0
- package/dist/types/config.js +3 -0
- package/dist/types/models.d.ts +65 -0
- package/dist/types/models.js +33 -0
- package/dist/utils/format.d.ts +38 -0
- package/dist/utils/format.js +201 -0
- package/dist/utils/prompt.d.ts +12 -0
- package/dist/utils/prompt.js +154 -0
- package/dist/utils/validators.d.ts +14 -0
- package/dist/utils/validators.js +42 -0
- package/package.json +63 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ZentaoErrorResponse } from '../types/api';
|
|
2
|
+
/**
|
|
3
|
+
* API 错误类
|
|
4
|
+
*/
|
|
5
|
+
export declare class ZentaoApiError extends Error {
|
|
6
|
+
errcode: number;
|
|
7
|
+
errmsg: string;
|
|
8
|
+
constructor(errcode: number, errmsg: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 格式化 API 错误信息
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatApiError(err: unknown): string;
|
|
14
|
+
/**
|
|
15
|
+
* 判断是否为 Zentao API 错误响应
|
|
16
|
+
*/
|
|
17
|
+
export declare function isZentaoError(data: unknown): data is ZentaoErrorResponse;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// 错误处理
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.ZentaoApiError = void 0;
|
|
8
|
+
exports.formatApiError = formatApiError;
|
|
9
|
+
exports.isZentaoError = isZentaoError;
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
/**
|
|
12
|
+
* API 错误类
|
|
13
|
+
*/
|
|
14
|
+
class ZentaoApiError extends Error {
|
|
15
|
+
errcode;
|
|
16
|
+
errmsg;
|
|
17
|
+
constructor(errcode, errmsg) {
|
|
18
|
+
super(errmsg);
|
|
19
|
+
this.name = 'ZentaoApiError';
|
|
20
|
+
this.errcode = errcode;
|
|
21
|
+
this.errmsg = errmsg;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.ZentaoApiError = ZentaoApiError;
|
|
25
|
+
/**
|
|
26
|
+
* 格式化 API 错误信息
|
|
27
|
+
*/
|
|
28
|
+
function formatApiError(err) {
|
|
29
|
+
if (err instanceof ZentaoApiError) {
|
|
30
|
+
switch (err.errcode) {
|
|
31
|
+
case 401:
|
|
32
|
+
return chalk_1.default.red('认证失败: 请检查用户名/密码,或 code/token 是否正确');
|
|
33
|
+
case 403:
|
|
34
|
+
return chalk_1.default.red('没有权限执行此操作');
|
|
35
|
+
case 404:
|
|
36
|
+
return chalk_1.default.yellow('资源不存在');
|
|
37
|
+
default:
|
|
38
|
+
return chalk_1.default.red(`API 错误 [${err.errcode}]: ${err.errmsg}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (err instanceof Error) {
|
|
42
|
+
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
43
|
+
return chalk_1.default.red('无法连接到禅道服务器,请检查 URL 和网络');
|
|
44
|
+
}
|
|
45
|
+
if (err.code === 'ETIMEDOUT' || err.code === 'ECONNABORTED') {
|
|
46
|
+
return chalk_1.default.red('请求超时,请检查网络或服务器状态');
|
|
47
|
+
}
|
|
48
|
+
return chalk_1.default.red(`错误: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
return chalk_1.default.red('未知错误');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 判断是否为 Zentao API 错误响应
|
|
54
|
+
*/
|
|
55
|
+
function isZentaoError(data) {
|
|
56
|
+
if (typeof data === 'object' && data !== null) {
|
|
57
|
+
const obj = data;
|
|
58
|
+
return typeof obj.errcode === 'number' && typeof obj.errmsg === 'string';
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// 禅道 CLI - 主入口
|
|
4
|
+
// Web HTML 解析模式 - 无需 REST API 权限
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const commander_1 = require("commander");
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
12
|
+
const ora_1 = __importDefault(require("ora"));
|
|
13
|
+
const config_1 = require("./config/config");
|
|
14
|
+
const auth_1 = require("./core/auth");
|
|
15
|
+
const api_client_1 = require("./core/api-client");
|
|
16
|
+
const bug_service_1 = require("./services/bug.service");
|
|
17
|
+
const task_service_1 = require("./services/task.service");
|
|
18
|
+
const product_service_1 = require("./services/product.service");
|
|
19
|
+
const project_service_1 = require("./services/project.service");
|
|
20
|
+
const bug_1 = require("./commands/bug");
|
|
21
|
+
const task_1 = require("./commands/task");
|
|
22
|
+
const product_1 = require("./commands/product");
|
|
23
|
+
const project_1 = require("./commands/project");
|
|
24
|
+
const config_2 = require("./commands/config");
|
|
25
|
+
const login_1 = require("./commands/login");
|
|
26
|
+
const format_1 = require("./utils/format");
|
|
27
|
+
// 加载 .env 文件
|
|
28
|
+
dotenv_1.default.config();
|
|
29
|
+
// 懒初始化服务(只在需要网络请求时才校验配置)
|
|
30
|
+
let _services = null;
|
|
31
|
+
function createServices() {
|
|
32
|
+
const config = (0, config_1.loadConfig)();
|
|
33
|
+
const auth = new auth_1.AuthManager(config);
|
|
34
|
+
const apiClient = new api_client_1.ApiClient(config, auth);
|
|
35
|
+
return {
|
|
36
|
+
config,
|
|
37
|
+
auth,
|
|
38
|
+
apiClient,
|
|
39
|
+
bugService: new bug_service_1.BugService(apiClient),
|
|
40
|
+
taskService: new task_service_1.TaskService(apiClient),
|
|
41
|
+
productService: new product_service_1.ProductService(apiClient),
|
|
42
|
+
projectService: new project_service_1.ProjectService(apiClient),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function getServices() {
|
|
46
|
+
if (!_services) {
|
|
47
|
+
_services = createServices();
|
|
48
|
+
_services.apiClient.setDebug(getDebug());
|
|
49
|
+
}
|
|
50
|
+
return _services;
|
|
51
|
+
}
|
|
52
|
+
const program = new commander_1.Command();
|
|
53
|
+
program
|
|
54
|
+
.name('chandao4')
|
|
55
|
+
.description('禅道命令行工具 - Bug、任务、项目和产品管理')
|
|
56
|
+
.version('0.1.0')
|
|
57
|
+
.option('--json', '以 JSON 格式输出')
|
|
58
|
+
.option('--debug', '打印详细的请求和响应信息');
|
|
59
|
+
function getUseJson() {
|
|
60
|
+
return program.opts().json || false;
|
|
61
|
+
}
|
|
62
|
+
function getDebug() {
|
|
63
|
+
return program.opts().debug || false;
|
|
64
|
+
}
|
|
65
|
+
// ---- status 命令 ----
|
|
66
|
+
program.command('status')
|
|
67
|
+
.description('检查禅道服务器连接状态')
|
|
68
|
+
.action(async () => {
|
|
69
|
+
const spinner = (0, ora_1.default)('正在连接禅道服务器...').start();
|
|
70
|
+
try {
|
|
71
|
+
const { config, auth, productService, projectService } = getServices();
|
|
72
|
+
spinner.text = '正在登录...';
|
|
73
|
+
const authed = await auth.ensureAuth();
|
|
74
|
+
if (!authed) {
|
|
75
|
+
spinner.fail('连接失败');
|
|
76
|
+
console.log(chalk_1.default.red('\n认证失败。请检查:'));
|
|
77
|
+
console.log(' 1. 服务器 URL 是否正确');
|
|
78
|
+
console.log(' 2. 用户名密码是否正确');
|
|
79
|
+
console.log(' 3. 网络是否能访问禅道服务器');
|
|
80
|
+
console.log(chalk_1.default.gray('\n💡 运行 chandao4 login 重新配置'));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
spinner.text = '正在获取数据...';
|
|
84
|
+
const { products, total: pTotal } = await productService.getList();
|
|
85
|
+
const { projects, total: prjTotal } = await projectService.getList();
|
|
86
|
+
spinner.succeed('连接成功');
|
|
87
|
+
console.log(chalk_1.default.green(`\n✓ 禅道服务器: ${config.server.url}`));
|
|
88
|
+
console.log(chalk_1.default.green(`✓ 认证模式: ${auth.getAuthMode() === 'apikey' ? 'API Key' : 'Session (用户名/密码)'}`));
|
|
89
|
+
console.log(chalk_1.default.green(`✓ 账户: ${config.server.username}`));
|
|
90
|
+
if (!getUseJson()) {
|
|
91
|
+
if (projects.length > 0) {
|
|
92
|
+
console.log(chalk_1.default.bold(`\n项目 (${prjTotal}):\n`));
|
|
93
|
+
console.log((0, format_1.formatProjectTable)(projects.slice(0, 10)));
|
|
94
|
+
if (prjTotal > 10)
|
|
95
|
+
console.log(chalk_1.default.gray(` ... 还有 ${prjTotal - 10} 个项目,用 chandao4 project list 查看全部`));
|
|
96
|
+
}
|
|
97
|
+
if (products.length > 0) {
|
|
98
|
+
console.log(chalk_1.default.bold(`\n产品 (${pTotal}):\n`));
|
|
99
|
+
console.log((0, format_1.formatProductTable)(products.slice(0, 10)));
|
|
100
|
+
if (pTotal > 10)
|
|
101
|
+
console.log(chalk_1.default.gray(` ... 还有 ${pTotal - 10} 个产品,用 chandao4 product list 查看全部`));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
spinner.fail('连接失败');
|
|
107
|
+
console.error(String(err));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// 注册子命令
|
|
112
|
+
program.addCommand((0, login_1.createLoginCommand)());
|
|
113
|
+
program.addCommand((0, login_1.createLogoutCommand)());
|
|
114
|
+
program.addCommand((0, config_2.createConfigCommand)());
|
|
115
|
+
// 需要网络访问的命令通过 getter 懒加载
|
|
116
|
+
program.addCommand((0, project_1.createProjectCommand)(new Proxy({}, { get(_, p) { return getServices().projectService[p]; } }), getUseJson));
|
|
117
|
+
program.addCommand((0, product_1.createProductCommand)(new Proxy({}, { get(_, p) { return getServices().productService[p]; } }), getUseJson));
|
|
118
|
+
program.addCommand((0, bug_1.createBugCommand)(new Proxy({}, { get(_, p) { return getServices().bugService[p]; } }), getUseJson));
|
|
119
|
+
program.addCommand((0, task_1.createTaskCommand)(new Proxy({}, { get(_, p) { return getServices().taskService[p]; } }), getUseJson));
|
|
120
|
+
// 启动
|
|
121
|
+
program.parse(process.argv);
|
|
122
|
+
// 无参数时显示帮助
|
|
123
|
+
if (!process.argv.slice(2).length) {
|
|
124
|
+
program.outputHelp();
|
|
125
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ApiClient } from '../core/api-client';
|
|
2
|
+
import { Bug } from '../types/models';
|
|
3
|
+
export declare class BugService {
|
|
4
|
+
private client;
|
|
5
|
+
constructor(client: ApiClient);
|
|
6
|
+
/** 获取当前用户的 Bug 列表 */
|
|
7
|
+
getMyBugs(options?: {
|
|
8
|
+
limit?: number;
|
|
9
|
+
page?: number;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
bugs: Bug[];
|
|
12
|
+
total: number;
|
|
13
|
+
}>;
|
|
14
|
+
/** 获取 Bug 列表 */
|
|
15
|
+
getList(productId: number, options?: {
|
|
16
|
+
status?: string;
|
|
17
|
+
limit?: number;
|
|
18
|
+
page?: number;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
bugs: Bug[];
|
|
21
|
+
total: number;
|
|
22
|
+
}>;
|
|
23
|
+
private myBugPath;
|
|
24
|
+
private bugBrowsePath;
|
|
25
|
+
/** 获取 Bug 详情 */
|
|
26
|
+
getDetail(bugId: number): Promise<Bug | null>;
|
|
27
|
+
private mapBug;
|
|
28
|
+
private buildDetailFields;
|
|
29
|
+
create(productId: number, bug: {
|
|
30
|
+
title: string;
|
|
31
|
+
severity: number;
|
|
32
|
+
priority: number;
|
|
33
|
+
type?: string;
|
|
34
|
+
module?: number;
|
|
35
|
+
assignedTo?: string;
|
|
36
|
+
steps?: string;
|
|
37
|
+
openedBuild?: string;
|
|
38
|
+
deadline?: string;
|
|
39
|
+
}): Promise<Bug | null>;
|
|
40
|
+
update(bugId: number, updates: Record<string, unknown>): Promise<Bug | null>;
|
|
41
|
+
/**
|
|
42
|
+
* 解决 Bug(active/reopened → resolved)
|
|
43
|
+
* resolution: fixed | bydesign | duplicate | external | notrepro | postponed
|
|
44
|
+
*/
|
|
45
|
+
resolve(bugId: number, options: {
|
|
46
|
+
resolution: string;
|
|
47
|
+
resolvedBuild?: string;
|
|
48
|
+
comment?: string;
|
|
49
|
+
assignedTo?: string;
|
|
50
|
+
}): Promise<Bug | null>;
|
|
51
|
+
/** 关闭 Bug(resolved → closed) */
|
|
52
|
+
close(bugId: number, comment?: string): Promise<Bug | null>;
|
|
53
|
+
/** 激活 Bug(resolved/closed → active) */
|
|
54
|
+
activate(bugId: number, options?: {
|
|
55
|
+
assignedTo?: string;
|
|
56
|
+
comment?: string;
|
|
57
|
+
}): Promise<Bug | null>;
|
|
58
|
+
delete(bugId: number): Promise<void>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Bug 业务逻辑 - JSON API
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.BugService = void 0;
|
|
5
|
+
class BugService {
|
|
6
|
+
client;
|
|
7
|
+
constructor(client) {
|
|
8
|
+
this.client = client;
|
|
9
|
+
}
|
|
10
|
+
/** 获取当前用户的 Bug 列表 */
|
|
11
|
+
async getMyBugs(options) {
|
|
12
|
+
const data = await this.client.getJson(this.myBugPath(options));
|
|
13
|
+
const rawBugs = data.bugs || data;
|
|
14
|
+
const bugs = [];
|
|
15
|
+
if (Array.isArray(rawBugs)) {
|
|
16
|
+
for (const b of rawBugs)
|
|
17
|
+
bugs.push(this.mapBug(b));
|
|
18
|
+
}
|
|
19
|
+
else if (typeof rawBugs === 'object') {
|
|
20
|
+
for (const [id, b] of Object.entries(rawBugs)) {
|
|
21
|
+
if (b && typeof b === 'object')
|
|
22
|
+
bugs.push(this.mapBug({ ...b, id }));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
bugs.sort((a, b) => b.id - a.id);
|
|
26
|
+
let total = bugs.length;
|
|
27
|
+
if (data.summary) {
|
|
28
|
+
const m = data.summary.match(/共\s*(\d+)\s*个/);
|
|
29
|
+
if (m)
|
|
30
|
+
total = parseInt(m[1], 10);
|
|
31
|
+
}
|
|
32
|
+
return { bugs, total };
|
|
33
|
+
}
|
|
34
|
+
/** 获取 Bug 列表 */
|
|
35
|
+
async getList(productId, options) {
|
|
36
|
+
const data = await this.client.getJson(this.bugBrowsePath(productId, { limit: options?.limit, page: options?.page }));
|
|
37
|
+
const rawBugs = data.bugs || data;
|
|
38
|
+
const bugs = [];
|
|
39
|
+
if (Array.isArray(rawBugs)) {
|
|
40
|
+
for (const b of rawBugs)
|
|
41
|
+
bugs.push(this.mapBug(b));
|
|
42
|
+
}
|
|
43
|
+
else if (typeof rawBugs === 'object') {
|
|
44
|
+
for (const [id, b] of Object.entries(rawBugs)) {
|
|
45
|
+
if (b && typeof b === 'object')
|
|
46
|
+
bugs.push(this.mapBug({ ...b, id }));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
bugs.sort((a, b) => b.id - a.id);
|
|
50
|
+
let total = bugs.length;
|
|
51
|
+
if (data.summary) {
|
|
52
|
+
const m = data.summary.match(/共\s*(\d+)\s*个/);
|
|
53
|
+
if (m)
|
|
54
|
+
total = parseInt(m[1], 10);
|
|
55
|
+
}
|
|
56
|
+
return { bugs, total };
|
|
57
|
+
}
|
|
58
|
+
// 禅道 PATH_INFO 路由:要传 recPerPage 必须把前面参数段都补齐。query string 不被识别。
|
|
59
|
+
myBugPath(opts) {
|
|
60
|
+
if (!opts?.limit || opts.limit <= 0)
|
|
61
|
+
return '/my-bug.json';
|
|
62
|
+
const page = opts.page && opts.page > 0 ? opts.page : 1;
|
|
63
|
+
// my-bug-{type}-{orderBy}-{recTotal}-{recPerPage}-{pageID}
|
|
64
|
+
return `/my-bug-assignedTo-id_desc-0-${opts.limit}-${page}.json`;
|
|
65
|
+
}
|
|
66
|
+
bugBrowsePath(productId, opts) {
|
|
67
|
+
if (!opts?.limit || opts.limit <= 0)
|
|
68
|
+
return `/bug-browse-${productId}.json`;
|
|
69
|
+
const page = opts.page && opts.page > 0 ? opts.page : 1;
|
|
70
|
+
// bug-browse-{productID}-{branch}-{browseType}-{param}-{orderBy}-{recTotal}-{recPerPage}-{pageID}
|
|
71
|
+
return `/bug-browse-${productId}-0-all-0-id_desc-0-${opts.limit}-${page}.json`;
|
|
72
|
+
}
|
|
73
|
+
/** 获取 Bug 详情 */
|
|
74
|
+
async getDetail(bugId) {
|
|
75
|
+
const data = await this.client.getJson(`/bug-view-${bugId}.json`);
|
|
76
|
+
if (!data.bug)
|
|
77
|
+
return null;
|
|
78
|
+
return this.mapBug(data.bug);
|
|
79
|
+
}
|
|
80
|
+
mapBug(b) {
|
|
81
|
+
return {
|
|
82
|
+
id: parseInt(b.id, 10),
|
|
83
|
+
title: b.title || '',
|
|
84
|
+
product: parseInt(b.product, 10) || 0,
|
|
85
|
+
module: parseInt(b.module, 10) || 0,
|
|
86
|
+
severity: parseInt(b.severity, 10) || 3,
|
|
87
|
+
priority: parseInt(b.pri, 10) || 3,
|
|
88
|
+
status: b.status || 'active',
|
|
89
|
+
type: b.type || '',
|
|
90
|
+
assignedTo: b.assignedTo || '',
|
|
91
|
+
openedBy: b.openedBy || '',
|
|
92
|
+
resolvedBy: b.resolvedBy || undefined,
|
|
93
|
+
deadline: b.deadline !== '0000-00-00' ? b.deadline : undefined,
|
|
94
|
+
steps: b.steps || '',
|
|
95
|
+
openedDate: b.openedDate || '',
|
|
96
|
+
resolvedDate: b.resolvedDate || undefined,
|
|
97
|
+
detailFields: this.buildDetailFields(b),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
buildDetailFields(b) {
|
|
101
|
+
const f = {};
|
|
102
|
+
if (b.severity)
|
|
103
|
+
f['严重程度'] = ['', '致命', '严重', '一般', '建议'][parseInt(b.severity, 10)] || b.severity;
|
|
104
|
+
if (b.pri)
|
|
105
|
+
f['优先级'] = ['', '紧急', '高', '中', '低'][parseInt(b.pri, 10)] || b.pri;
|
|
106
|
+
if (b.status)
|
|
107
|
+
f['状态'] = b.status;
|
|
108
|
+
if (b.type)
|
|
109
|
+
f['类型'] = b.type;
|
|
110
|
+
if (b.assignedTo)
|
|
111
|
+
f['指派给'] = b.assignedTo;
|
|
112
|
+
if (b.openedBy)
|
|
113
|
+
f['创建者'] = b.openedBy;
|
|
114
|
+
if (b.openedDate)
|
|
115
|
+
f['创建日期'] = b.openedDate;
|
|
116
|
+
if (b.resolvedBy)
|
|
117
|
+
f['解决者'] = b.resolvedBy;
|
|
118
|
+
if (b.resolvedDate)
|
|
119
|
+
f['解决日期'] = b.resolvedDate;
|
|
120
|
+
if (b.deadline && b.deadline !== '0000-00-00')
|
|
121
|
+
f['截止日期'] = b.deadline;
|
|
122
|
+
if (b.steps)
|
|
123
|
+
f['重现步骤'] = b.steps.replace(/<[^>]+>/g, '\n').replace(/\n+/g, '\n').trim();
|
|
124
|
+
return f;
|
|
125
|
+
}
|
|
126
|
+
async create(productId, bug) {
|
|
127
|
+
const data = {
|
|
128
|
+
title: bug.title, severity: String(bug.severity),
|
|
129
|
+
pri: String(bug.priority), type: bug.type || 'codeerror',
|
|
130
|
+
product: String(productId), module: String(bug.module || 0),
|
|
131
|
+
'openedBuild[]': bug.openedBuild || 'trunk',
|
|
132
|
+
};
|
|
133
|
+
if (bug.assignedTo)
|
|
134
|
+
data.assignedTo = bug.assignedTo;
|
|
135
|
+
if (bug.steps)
|
|
136
|
+
data.steps = bug.steps;
|
|
137
|
+
data.deadline = bug.deadline || new Date().toISOString().split('T')[0];
|
|
138
|
+
const resp = await this.client.postJson(`/bug-create-${productId}.json`, data);
|
|
139
|
+
if (resp?.result === 'success' || resp?.status === 'success') {
|
|
140
|
+
const bugId = resp?.data ? parseInt(String(resp.data), 10) : 0;
|
|
141
|
+
if (bugId > 0) {
|
|
142
|
+
return this.getDetail(bugId);
|
|
143
|
+
}
|
|
144
|
+
const { bugs } = await this.getList(productId);
|
|
145
|
+
const created = bugs.find(b => b.title === bug.title) || (bugs.length > 0 ? bugs.reduce((a, b) => a.id > b.id ? a : b) : null);
|
|
146
|
+
if (created)
|
|
147
|
+
return created;
|
|
148
|
+
}
|
|
149
|
+
if (resp?.message) {
|
|
150
|
+
console.error('创建失败:', resp.message);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
async update(bugId, updates) {
|
|
156
|
+
const current = await this.getDetail(bugId);
|
|
157
|
+
if (!current) {
|
|
158
|
+
console.error('更新失败: Bug 不存在');
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const data = {};
|
|
162
|
+
if (updates.title)
|
|
163
|
+
data.title = String(updates.title);
|
|
164
|
+
if (updates.status)
|
|
165
|
+
data.status = String(updates.status);
|
|
166
|
+
if (updates.severity !== undefined)
|
|
167
|
+
data.severity = String(updates.severity);
|
|
168
|
+
if (updates.priority !== undefined)
|
|
169
|
+
data.pri = String(updates.priority);
|
|
170
|
+
if (updates.assignedTo)
|
|
171
|
+
data.assignedTo = String(updates.assignedTo);
|
|
172
|
+
if (updates.steps)
|
|
173
|
+
data.steps = String(updates.steps);
|
|
174
|
+
if (!data.title)
|
|
175
|
+
data.title = current.title;
|
|
176
|
+
data.deadline = String(updates.deadline || current.deadline || '');
|
|
177
|
+
await this.client.postJson(`/bug-edit-${bugId}.json`, data);
|
|
178
|
+
return this.getDetail(bugId);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 解决 Bug(active/reopened → resolved)
|
|
182
|
+
* resolution: fixed | bydesign | duplicate | external | notrepro | postponed
|
|
183
|
+
*/
|
|
184
|
+
async resolve(bugId, options) {
|
|
185
|
+
const data = {
|
|
186
|
+
resolution: options.resolution,
|
|
187
|
+
'resolvedBuild[]': options.resolvedBuild || 'trunk',
|
|
188
|
+
};
|
|
189
|
+
if (options.assignedTo)
|
|
190
|
+
data.assignedTo = options.assignedTo;
|
|
191
|
+
if (options.comment)
|
|
192
|
+
data.comment = options.comment;
|
|
193
|
+
await this.client.postJson(`/bug-resolve-${bugId}.json`, data);
|
|
194
|
+
const bug = await this.getDetail(bugId);
|
|
195
|
+
if (bug && bug.status !== 'resolved') {
|
|
196
|
+
console.error('解决失败: 服务端未能更新 Bug 状态,当前状态为', bug.status);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return bug;
|
|
200
|
+
}
|
|
201
|
+
/** 关闭 Bug(resolved → closed) */
|
|
202
|
+
async close(bugId, comment) {
|
|
203
|
+
const data = { comment: comment || '' };
|
|
204
|
+
await this.client.postJson(`/bug-close-${bugId}.json`, data);
|
|
205
|
+
const bug = await this.getDetail(bugId);
|
|
206
|
+
if (bug && bug.status !== 'closed') {
|
|
207
|
+
console.error('关闭失败: 服务端未能更新 Bug 状态,当前状态为', bug.status);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return bug;
|
|
211
|
+
}
|
|
212
|
+
/** 激活 Bug(resolved/closed → active) */
|
|
213
|
+
async activate(bugId, options) {
|
|
214
|
+
const data = { comment: options?.comment || '' };
|
|
215
|
+
if (options?.assignedTo)
|
|
216
|
+
data.assignedTo = options.assignedTo;
|
|
217
|
+
await this.client.postJson(`/bug-activate-${bugId}.json`, data);
|
|
218
|
+
const bug = await this.getDetail(bugId);
|
|
219
|
+
if (bug && bug.status !== 'active') {
|
|
220
|
+
console.error('激活失败: 服务端未能更新 Bug 状态,当前状态为', bug.status);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return bug;
|
|
224
|
+
}
|
|
225
|
+
async delete(bugId) {
|
|
226
|
+
const resp = await this.client.getJson(`/bug-delete-${bugId}-yes.json`);
|
|
227
|
+
if (resp?.result !== 'success' && resp?.message) {
|
|
228
|
+
console.error('删除失败:', resp.message);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
exports.BugService = BugService;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ApiClient } from '../core/api-client';
|
|
2
|
+
import { Product } from '../types/models';
|
|
3
|
+
export declare class ProductService {
|
|
4
|
+
private client;
|
|
5
|
+
constructor(client: ApiClient);
|
|
6
|
+
/** 获取产品列表 */
|
|
7
|
+
getList(): Promise<{
|
|
8
|
+
products: Product[];
|
|
9
|
+
total: number;
|
|
10
|
+
}>;
|
|
11
|
+
getDetail(id: number): Promise<Product | null>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Product 业务逻辑 - JSON API
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ProductService = void 0;
|
|
5
|
+
class ProductService {
|
|
6
|
+
client;
|
|
7
|
+
constructor(client) {
|
|
8
|
+
this.client = client;
|
|
9
|
+
}
|
|
10
|
+
/** 获取产品列表 */
|
|
11
|
+
async getList() {
|
|
12
|
+
const data = await this.client.getJson('/product-all.json');
|
|
13
|
+
const products = [];
|
|
14
|
+
if (data.products) {
|
|
15
|
+
for (const [id, name] of Object.entries(data.products)) {
|
|
16
|
+
products.push({ id: parseInt(id, 10), name: name, code: '', status: '' });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// productStats 有更详细的信息
|
|
20
|
+
if (data.productStats) {
|
|
21
|
+
for (const p of data.productStats) {
|
|
22
|
+
const existing = products.find(x => x.id === parseInt(p.id, 10));
|
|
23
|
+
if (existing) {
|
|
24
|
+
existing.code = p.code || '';
|
|
25
|
+
existing.status = p.status || '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { products, total: products.length };
|
|
30
|
+
}
|
|
31
|
+
async getDetail(id) {
|
|
32
|
+
const data = await this.client.getJson(`/product-view-${id}.json`);
|
|
33
|
+
if (!data.product)
|
|
34
|
+
return null;
|
|
35
|
+
return {
|
|
36
|
+
id: parseInt(data.product.id, 10),
|
|
37
|
+
name: data.product.name,
|
|
38
|
+
code: data.product.code || '',
|
|
39
|
+
status: data.product.status || '',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.ProductService = ProductService;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ApiClient } from '../core/api-client';
|
|
2
|
+
export interface Project {
|
|
3
|
+
id: number;
|
|
4
|
+
name: string;
|
|
5
|
+
code: string;
|
|
6
|
+
status: string;
|
|
7
|
+
begin: string;
|
|
8
|
+
end: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class ProjectService {
|
|
11
|
+
private client;
|
|
12
|
+
constructor(client: ApiClient);
|
|
13
|
+
getList(): Promise<{
|
|
14
|
+
projects: Project[];
|
|
15
|
+
total: number;
|
|
16
|
+
}>;
|
|
17
|
+
getDetail(id: number): Promise<Project | null>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Project 业务逻辑 - JSON API
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ProjectService = void 0;
|
|
5
|
+
class ProjectService {
|
|
6
|
+
client;
|
|
7
|
+
constructor(client) {
|
|
8
|
+
this.client = client;
|
|
9
|
+
}
|
|
10
|
+
async getList() {
|
|
11
|
+
const data = await this.client.getJson('/project-all.json');
|
|
12
|
+
const projects = [];
|
|
13
|
+
if (data.projects) {
|
|
14
|
+
for (const [id, name] of Object.entries(data.projects)) {
|
|
15
|
+
projects.push({ id: parseInt(id, 10), name: name, code: '', status: '', begin: '', end: '' });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { projects, total: projects.length };
|
|
19
|
+
}
|
|
20
|
+
async getDetail(id) {
|
|
21
|
+
const data = await this.client.getJson(`/project-view-${id}.json`);
|
|
22
|
+
if (!data.project)
|
|
23
|
+
return null;
|
|
24
|
+
const p = data.project;
|
|
25
|
+
return {
|
|
26
|
+
id: parseInt(p.id, 10),
|
|
27
|
+
name: p.name,
|
|
28
|
+
code: p.code || '',
|
|
29
|
+
status: p.status || '',
|
|
30
|
+
begin: p.begin || '',
|
|
31
|
+
end: p.end || '',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.ProjectService = ProjectService;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ApiClient } from '../core/api-client';
|
|
2
|
+
import { Task } from '../types/models';
|
|
3
|
+
export declare class TaskService {
|
|
4
|
+
private client;
|
|
5
|
+
constructor(client: ApiClient);
|
|
6
|
+
/** 获取当前用户的任务列表 */
|
|
7
|
+
getMyTasks(): Promise<{
|
|
8
|
+
tasks: Task[];
|
|
9
|
+
total: number;
|
|
10
|
+
}>;
|
|
11
|
+
/** 获取任务列表 */
|
|
12
|
+
getList(projectId: number): Promise<{
|
|
13
|
+
tasks: Task[];
|
|
14
|
+
total: number;
|
|
15
|
+
}>;
|
|
16
|
+
/** 获取任务详情 */
|
|
17
|
+
getDetail(taskId: number): Promise<Task | null>;
|
|
18
|
+
private mapTask;
|
|
19
|
+
private buildDetailFields;
|
|
20
|
+
/** 检查操作是否成功(通过 resp 或实际状态验证) */
|
|
21
|
+
private isSuccess;
|
|
22
|
+
create(projectId: number, task: {
|
|
23
|
+
name: string;
|
|
24
|
+
type?: string;
|
|
25
|
+
priority?: number;
|
|
26
|
+
estimate?: number;
|
|
27
|
+
deadline?: string;
|
|
28
|
+
estStarted?: string;
|
|
29
|
+
assignedTo?: string;
|
|
30
|
+
desc?: string;
|
|
31
|
+
}): Promise<Task | null>;
|
|
32
|
+
update(taskId: number, updates: Record<string, unknown>): Promise<Task | null>;
|
|
33
|
+
/** 取消任务 */
|
|
34
|
+
cancel(taskId: number, comment?: string): Promise<Task | null>;
|
|
35
|
+
/** 开始任务(wait/pause → doing) */
|
|
36
|
+
start(taskId: number, options?: {
|
|
37
|
+
consumed?: number;
|
|
38
|
+
left?: number;
|
|
39
|
+
assignedTo?: string;
|
|
40
|
+
comment?: string;
|
|
41
|
+
}): Promise<Task | null>;
|
|
42
|
+
/** 完成任务(doing → done) */
|
|
43
|
+
finish(taskId: number, options?: {
|
|
44
|
+
consumed?: number;
|
|
45
|
+
comment?: string;
|
|
46
|
+
}): Promise<Task | null>;
|
|
47
|
+
/** 关闭任务(done/cancel → closed) */
|
|
48
|
+
close(taskId: number, comment?: string): Promise<Task | null>;
|
|
49
|
+
/** 激活任务(cancel/closed → wait) */
|
|
50
|
+
activate(taskId: number, options?: {
|
|
51
|
+
left?: number;
|
|
52
|
+
comment?: string;
|
|
53
|
+
}): Promise<Task | null>;
|
|
54
|
+
delete(taskId: number, projectId?: number): Promise<void>;
|
|
55
|
+
}
|