@dreamor/atlas-cli 0.7.14 → 0.7.16

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.
@@ -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, NotImplementedError, OutputTooLargeError, SessionExpiredError, isAtlasError, } from './util/errors.js';
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;
@@ -151,9 +145,9 @@ kind 说明:
151
145
 
152
146
  project 搜索有权限的项目列表(从 API 实时查询)
153
147
  department 搜索部门树中的部门名称(1500+ 部门节点)
154
- manpower-type 搜索人力类型(仅有: "斑马"、"智软" 两条,baseline --manpower-type 用)
155
- role 搜索人力基线角色(例: "开发"、"产品"、"测试"、"PM"、"设计" 等)
156
- area 搜索地域(仅有: "北上杭"、"合肥"、"武汉"、"西南" 四条)`)
148
+ manpower-type 搜索人力类型(例: "斑马"、"智软"baseline --manpower-type 用)
149
+ role 搜索人力基线角色(例: "开发"、"产品"、"测试"、"PM"、"设计" 等)
150
+ area 搜索地域(例: "北上杭"、"合肥"、"武汉"、"西南")`)
157
151
  .action(async (kind, query, opts) => {
158
152
  try {
159
153
  await findCmd(kind, query, opts);
@@ -476,7 +470,7 @@ export function buildProgram() {
476
470
  6 API 限流(RateLimited)
477
471
  7 网络错误(NetworkError)
478
472
  8 版本更新异常(UpdateError)
479
- 64 配置错误 / 未实现(ConfigError / NotImplementedError
473
+ 64 配置错误(ConfigError)
480
474
  65 输出超限(OutputTooLargeError)
481
475
  `)
482
476
  .showHelpAfterError()
@@ -26,15 +26,24 @@ function isMonthKey(v) {
26
26
  && typeof v.month === 'number'
27
27
  && Number.isFinite(v.month);
28
28
  }
29
+ /** mpType 值映射:0=斑马 1=智软 */
30
+ const MP_TYPE_MAP = { '0': '斑马', '1': '智软' };
31
+ function filterBaseline(details, opts) {
32
+ return details.filter(d => (!opts.department || (d.departmentName && d.departmentName.includes(opts.department)))
33
+ && (!opts.role || (d.role && d.role.includes(opts.role)))
34
+ && (!opts.area || (d.areaCode && d.areaCode.includes(opts.area)))
35
+ && (!opts.manpowerType || (d.mpType && MP_TYPE_MAP[d.mpType]?.includes(opts.manpowerType))));
36
+ }
29
37
  export async function monthCmd(opts) {
30
38
  const pid = getProjectId(opts);
31
39
  const months = opts.month ? [opts.month] : expandMonths(opts.from, opts.to);
32
40
  const allDetails = [];
33
41
  for (const m of months) {
34
42
  const data = await fetchBaselineMonth(pid, m);
35
- // data 是数组,每项含 linePlanMonthDetailList
36
43
  for (const item of (data ?? [])) {
37
44
  const details = (item.linePlanMonthDetailList ?? []);
45
+ const mpType = String(item.mpType ?? '');
46
+ const areaCode = item.areaCode;
38
47
  for (const d of details) {
39
48
  if (isMonthKey(d)) {
40
49
  allDetails.push({
@@ -42,14 +51,15 @@ export async function monthCmd(opts) {
42
51
  manpower: d.manpower ?? 0,
43
52
  role: item.role,
44
53
  departmentName: item.departmentName,
54
+ mpType,
55
+ areaCode,
45
56
  });
46
57
  }
47
58
  }
48
59
  }
49
60
  }
50
- // Apply --department / --role filter
51
- const filtered = allDetails.filter(d => (!opts.department || (d.departmentName && d.departmentName.includes(opts.department)))
52
- && (!opts.role || (d.role && d.role.includes(opts.role))));
61
+ // Apply filters
62
+ const filtered = filterBaseline(allDetails, opts);
53
63
  // 聚合
54
64
  const byMonth = new Map();
55
65
  for (const d of filtered) {
@@ -74,16 +84,17 @@ export async function summaryCmd(opts) {
74
84
  for (const m of months) {
75
85
  const data = await fetchBaselineMonth(pid, m);
76
86
  for (const item of (data ?? [])) {
87
+ const mpType = String(item.mpType ?? '');
88
+ const areaCode = item.areaCode;
77
89
  for (const d of (item.linePlanMonthDetailList ?? [])) {
78
90
  if (isMonthKey(d)) {
79
- allDetails.push({ month: monthTsToKey(d.month), manpower: d.manpower ?? 0, role: item.role, departmentName: item.departmentName });
91
+ allDetails.push({ month: monthTsToKey(d.month), manpower: d.manpower ?? 0, role: item.role, departmentName: item.departmentName, mpType, areaCode });
80
92
  }
81
93
  }
82
94
  }
83
95
  }
84
- // Apply --department / --role filter
85
- const filtered = allDetails.filter(d => (!opts.department || (d.departmentName && d.departmentName.includes(opts.department)))
86
- && (!opts.role || (d.role && d.role.includes(opts.role))));
96
+ // Apply filters
97
+ const filtered = filterBaseline(allDetails, opts);
87
98
  const axis = opts.by ?? 'month';
88
99
  const groups = new Map();
89
100
  for (const d of filtered) {
@@ -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, NotImplementedError, OutputTooLargeError, SessionExpiredError } from './errors.js';
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.14';
1
+ export const ATLAS_VERSION = '0.7.16';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamor/atlas-cli",
3
- "version": "0.7.14",
3
+ "version": "0.7.16",
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",