@dreamor/atlas-cli 0.7.15 → 0.7.17
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/dist/adapters/atlas/cli.js +2 -8
- package/dist/adapters/atlas/commands/actual/_logic.js +40 -0
- package/dist/adapters/atlas/commands/actual/index.js +13 -3
- package/dist/adapters/atlas/commands/compare/index.js +17 -3
- package/dist/adapters/atlas/commands/project/index.js +30 -0
- package/dist/adapters/atlas/http/client.js +25 -0
- package/dist/adapters/atlas/util/errors.js +0 -6
- package/dist/adapters/atlas/util/output.js +1 -4
- package/dist/adapters/atlas/util/version.js +1 -1
- package/package.json +3 -2
|
@@ -16,7 +16,7 @@ import { schemaCommandsCmd } from './commands/schema.js';
|
|
|
16
16
|
import { execCmd } from './commands/exec.js';
|
|
17
17
|
import { suggestCmd } from './commands/suggest.js';
|
|
18
18
|
import { updateCmd } from './commands/update.js';
|
|
19
|
-
import { BanmaApiError, ConfigError,
|
|
19
|
+
import { BanmaApiError, ConfigError, OutputTooLargeError, SessionExpiredError, isAtlasError, } from './util/errors.js';
|
|
20
20
|
import { isJsonMode, printError } from './util/output.js';
|
|
21
21
|
import { getOutputSchema } from './commands/_output_schema.js';
|
|
22
22
|
export function handleError(err) {
|
|
@@ -36,10 +36,6 @@ export function handleError(err) {
|
|
|
36
36
|
console.error(`Banma API error [${err.errCode}] ${err.errorMsg}`);
|
|
37
37
|
process.exit(3);
|
|
38
38
|
}
|
|
39
|
-
if (err instanceof NotImplementedError) {
|
|
40
|
-
console.error(err.message);
|
|
41
|
-
process.exit(64);
|
|
42
|
-
}
|
|
43
39
|
if (err instanceof OutputTooLargeError) {
|
|
44
40
|
const debug = process.env.DEBUG === '1';
|
|
45
41
|
console.error(debug ? err.stack ?? err.message : err.message);
|
|
@@ -56,8 +52,6 @@ export function exitCodeFor(err) {
|
|
|
56
52
|
return 3;
|
|
57
53
|
if (err instanceof OutputTooLargeError)
|
|
58
54
|
return 65;
|
|
59
|
-
if (err instanceof NotImplementedError)
|
|
60
|
-
return 64;
|
|
61
55
|
if (isAtlasError(err)) {
|
|
62
56
|
switch (err.code) {
|
|
63
57
|
case 'AMBIGUOUS_PROJECT': return 4;
|
|
@@ -476,7 +470,7 @@ export function buildProgram() {
|
|
|
476
470
|
6 API 限流(RateLimited)
|
|
477
471
|
7 网络错误(NetworkError)
|
|
478
472
|
8 版本更新异常(UpdateError)
|
|
479
|
-
64
|
|
473
|
+
64 配置错误(ConfigError)
|
|
480
474
|
65 输出超限(OutputTooLargeError)
|
|
481
475
|
`)
|
|
482
476
|
.showHelpAfterError()
|
|
@@ -39,3 +39,43 @@ export function aggregateByAxis(rows, axis) {
|
|
|
39
39
|
export function filterByStaff(entries, staffId) {
|
|
40
40
|
return entries.filter((p) => p.staffId === staffId || p.staffName?.includes(staffId));
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* 走查 summaryByTeam 返回的树结构,提取指定项目的实际工时条目。
|
|
44
|
+
*
|
|
45
|
+
* 树中同一员工可能出现在 manager 层(汇总下属)和 leaf 层(个人),
|
|
46
|
+
* 采取走最深层的策略:越深的节点 detail 越精确。
|
|
47
|
+
*/
|
|
48
|
+
export function flattenTree(nodes, targetPid) {
|
|
49
|
+
const byStaffId = new Map();
|
|
50
|
+
function walk(items, depth) {
|
|
51
|
+
for (const raw of items ?? []) {
|
|
52
|
+
const node = raw;
|
|
53
|
+
const details = (node.detail ?? []);
|
|
54
|
+
let projectManpower = 0;
|
|
55
|
+
for (const d of details) {
|
|
56
|
+
if (String(d.projectId) === targetPid) {
|
|
57
|
+
projectManpower += Number(d.manpower ?? 0);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (projectManpower > 0) {
|
|
61
|
+
const sid = String(node.staffId ?? '');
|
|
62
|
+
const existing = byStaffId.get(sid);
|
|
63
|
+
if (!existing || depth > existing.depth) {
|
|
64
|
+
byStaffId.set(sid, {
|
|
65
|
+
depth,
|
|
66
|
+
entry: {
|
|
67
|
+
staffId: sid,
|
|
68
|
+
staffName: String(node.realname ?? ''),
|
|
69
|
+
manpower: Math.round(projectManpower * 100) / 100,
|
|
70
|
+
departmentName: String(node.department ?? ''),
|
|
71
|
+
role: String(node.role ?? ''),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
walk(node.children, depth + 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
walk(nodes, 0);
|
|
80
|
+
return [...byStaffId.values()].map((v) => v.entry);
|
|
81
|
+
}
|
|
@@ -4,15 +4,25 @@ import { enforceOutputLimit } from '../../util/output-limit.js';
|
|
|
4
4
|
import { SessionExpiredError } from '../../util/errors.js';
|
|
5
5
|
import { expandMonths, expandMonthsDefault } from '../../util/months.js';
|
|
6
6
|
import { resolveSecureExportPath, secureWriteFile } from '../../util/secure-fs.js';
|
|
7
|
-
import { annotateWithMonth, aggregateByAxis, filterByStaff, } from './_logic.js';
|
|
7
|
+
import { annotateWithMonth, aggregateByAxis, filterByStaff, flattenTree, } from './_logic.js';
|
|
8
8
|
import { resolveProjectId } from '../../util/env.js';
|
|
9
|
+
import { readBanmaIdentity } from '../../auth/session.js';
|
|
9
10
|
function getProjectId(opts) {
|
|
10
11
|
return resolveProjectId(opts.projectId);
|
|
11
12
|
}
|
|
12
13
|
async function fetchActual(pid, month) {
|
|
13
14
|
const client = createClient();
|
|
14
|
-
const
|
|
15
|
-
|
|
15
|
+
const identity = await readBanmaIdentity();
|
|
16
|
+
const loginStaffId = identity?.staffId ?? '';
|
|
17
|
+
// B4: summaryByTeam 请求体需用 projectIds 数组 + 额外字段
|
|
18
|
+
// 使用 projectIds 时 detail 只返回该项目的数据
|
|
19
|
+
const raw = await client.post('/yuntu-service/manpower/weekly/summaryByTeam.json', { month, staffIds: [], projectIds: [pid], isConfirm: false, loginStaffId });
|
|
20
|
+
const tree = (Array.isArray(raw) ? raw : []);
|
|
21
|
+
// 根节点为当前用户,children 为团队成员
|
|
22
|
+
if (tree.length > 0 && tree[0].children) {
|
|
23
|
+
return flattenTree(tree[0].children, pid);
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
16
26
|
}
|
|
17
27
|
export async function showCmd(staffId, opts) {
|
|
18
28
|
const pid = getProjectId(opts);
|
|
@@ -5,6 +5,8 @@ import { expandMonths, expandMonthsDefault } from '../../util/months.js';
|
|
|
5
5
|
import { monthTsToKey } from '../../util/time.js';
|
|
6
6
|
import { groupByAxis, mergeBaselineActual, } from './_logic.js';
|
|
7
7
|
import { resolveProjectId, resolveProjectInfo } from '../../util/env.js';
|
|
8
|
+
import { readBanmaIdentity } from '../../auth/session.js';
|
|
9
|
+
import { flattenTree as flattenActualTree } from '../actual/_logic.js';
|
|
8
10
|
function getProjectId(opts) {
|
|
9
11
|
return resolveProjectId(opts.projectId);
|
|
10
12
|
}
|
|
@@ -38,9 +40,21 @@ export async function compareCmd(opts) {
|
|
|
38
40
|
}
|
|
39
41
|
// 指数退避:避免连续触发限流(B2)
|
|
40
42
|
try {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
const identity = await readBanmaIdentity();
|
|
44
|
+
const loginStaffId = identity?.staffId ?? '';
|
|
45
|
+
const raw = await client.post('/yuntu-service/manpower/weekly/summaryByTeam.json', {
|
|
46
|
+
month: m, staffIds: [], projectIds: [pid], isConfirm: false, loginStaffId,
|
|
47
|
+
});
|
|
48
|
+
const tree = (Array.isArray(raw) ? raw : []);
|
|
49
|
+
const results = [];
|
|
50
|
+
if (tree.length > 0 && tree[0].children) {
|
|
51
|
+
const entries = flattenActualTree(tree[0].children, pid);
|
|
52
|
+
for (const p of entries) {
|
|
53
|
+
results.push({ month: m, manpower: p.manpower ?? 0, role: p.role, departmentName: p.departmentName });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const ac of results) {
|
|
57
|
+
allAc.push(ac);
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
catch (e) {
|
|
@@ -71,6 +71,36 @@ export async function findCmd(kind, query, opts) {
|
|
|
71
71
|
results = projects
|
|
72
72
|
.filter((p) => getProjectId(p).includes(query) || getProjectName(p).includes(query))
|
|
73
73
|
.map((p) => ({ id: getProjectId(p), name: getProjectName(p), kind: 'project' }));
|
|
74
|
+
// B4 兜底:权限列表搜不到时尝试按 ID 或名称直接查项目
|
|
75
|
+
// 部分项目(如 status=2)不会出现在 selectHasPermisValidProject 中,
|
|
76
|
+
// 但 queryProjById / queryProjList 可返回完整信息
|
|
77
|
+
if (results.length === 0) {
|
|
78
|
+
// 兜底一:按 ID 查项目详情
|
|
79
|
+
try {
|
|
80
|
+
const detail = await client.get('/yuntu-service/projApi/queryProjById.json', { projId: query });
|
|
81
|
+
if (detail?.projInfo?.id && detail?.projInfo?.projName) {
|
|
82
|
+
results = [{ id: String(detail.projInfo.id), name: detail.projInfo.projName, kind: 'project' }];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// 静默忽略
|
|
87
|
+
}
|
|
88
|
+
// 兜底二:按名称模糊搜索项目列表
|
|
89
|
+
if (results.length === 0) {
|
|
90
|
+
try {
|
|
91
|
+
const list = await client.get('/yuntu-service/projApi/queryProjList.json', { projName: query, staffId: '' });
|
|
92
|
+
if (Array.isArray(list)) {
|
|
93
|
+
results = list
|
|
94
|
+
.filter((p) => p.id && p.projName)
|
|
95
|
+
.slice(0, parseInt(opts.limit ?? '20', 10))
|
|
96
|
+
.map((p) => ({ id: String(p.id), name: p.projName, kind: 'project' }));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// 静默忽略
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
74
104
|
}
|
|
75
105
|
else if (kind === 'department') {
|
|
76
106
|
const data = await client.post('/yuntu-service/department/tree/select.json', {});
|
|
@@ -63,6 +63,31 @@ export class HttpClient {
|
|
|
63
63
|
}
|
|
64
64
|
return headers;
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* GET 请求。
|
|
68
|
+
*
|
|
69
|
+
* query 参数以键值对形式传入,自动拼接到 URL 查询字符串。
|
|
70
|
+
* 可传可选 schema(ZodSchema),在 API 返回后对 json.data 做运行时校验。
|
|
71
|
+
*/
|
|
72
|
+
async get(path, query, schema) {
|
|
73
|
+
// A4: 校验 path 必须以 / 开头,防止 http://evil.example/x 绕过 baseUrl
|
|
74
|
+
if (!path.startsWith('/'))
|
|
75
|
+
throw new AtlasError('path must be relative (start with /)', 'CONFIG_ERROR');
|
|
76
|
+
const url = new URL(path, this.baseUrl + '/');
|
|
77
|
+
if (query) {
|
|
78
|
+
Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
|
|
79
|
+
}
|
|
80
|
+
if (url.origin !== this.baseOrigin)
|
|
81
|
+
throw new AtlasError('origin mismatch — possible SSRF', 'CONFIG_ERROR');
|
|
82
|
+
const headers = await this.buildHeaders();
|
|
83
|
+
const response = await request(url.toString(), {
|
|
84
|
+
method: 'GET',
|
|
85
|
+
headers,
|
|
86
|
+
headersTimeout: this.timeout,
|
|
87
|
+
bodyTimeout: this.timeout,
|
|
88
|
+
});
|
|
89
|
+
return this.parseResponse(response, schema);
|
|
90
|
+
}
|
|
66
91
|
/**
|
|
67
92
|
* POST 请求。
|
|
68
93
|
*
|
|
@@ -23,12 +23,6 @@ export class SessionExpiredError extends Error {
|
|
|
23
23
|
this.name = 'SessionExpiredError';
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
export class NotImplementedError extends Error {
|
|
27
|
-
constructor(message = '该功能尚未实现') {
|
|
28
|
-
super(message);
|
|
29
|
-
this.name = 'NotImplementedError';
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
26
|
export class OutputTooLargeError extends Error {
|
|
33
27
|
bytes;
|
|
34
28
|
limit;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BanmaApiError, AtlasError, ConfigError,
|
|
1
|
+
import { BanmaApiError, AtlasError, ConfigError, OutputTooLargeError, SessionExpiredError } from './errors.js';
|
|
2
2
|
export function isJsonMode() {
|
|
3
3
|
return process.env.ATLAS_OUTPUT === 'json';
|
|
4
4
|
}
|
|
@@ -44,9 +44,6 @@ export function printError(err, opts) {
|
|
|
44
44
|
else if (err instanceof ConfigError) {
|
|
45
45
|
console.error(`Config error: ${err.message}`);
|
|
46
46
|
}
|
|
47
|
-
else if (err instanceof NotImplementedError) {
|
|
48
|
-
console.error(err.message);
|
|
49
|
-
}
|
|
50
47
|
else if (err instanceof OutputTooLargeError) {
|
|
51
48
|
console.error(err.message);
|
|
52
49
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const ATLAS_VERSION = '0.7.
|
|
1
|
+
export const ATLAS_VERSION = '0.7.17';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreamor/atlas-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.17",
|
|
4
4
|
"description": "Atlas CLI - 斑马云图人力基线管理工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"test": "vitest run",
|
|
32
32
|
"test:watch": "vitest",
|
|
33
33
|
"verify": "node ./dist/adapters/atlas/cli.js --help",
|
|
34
|
-
"auth:login": "node --import tsx ./adapters/atlas/cli.ts auth login"
|
|
34
|
+
"auth:login": "node --import tsx ./adapters/atlas/cli.ts auth login",
|
|
35
|
+
"generate-skill-docs": "node scripts/generate-skill-docs.mjs"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"commander": "^14.0.3",
|