bfi-cli 0.1.107 → 0.1.109

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/cli.mjs CHANGED
@@ -8714,6 +8714,31 @@ async function runInstall(args, globalOpts) {
8714
8714
  console.log();
8715
8715
  }
8716
8716
  const YMC_DEFAULT_BASE = process.env.BFI_YMC_BASE || "https://bfi.yyuap.com/bfi-api/bficli/api/v2";
8717
+ function messageOf(parsed) {
8718
+ const raw = parsed.msg ?? parsed.message;
8719
+ if (typeof raw === "string") return raw;
8720
+ if (raw === void 0 || raw === null) return "";
8721
+ try {
8722
+ return JSON.stringify(raw);
8723
+ } catch {
8724
+ return String(raw);
8725
+ }
8726
+ }
8727
+ function describeApiError(parsed, status, raw = "") {
8728
+ const prefix = status >= 400 ? `HTTP ${status} ` : "";
8729
+ const detail = parsed.detail;
8730
+ if (Array.isArray(detail) && detail.length > 0) return `${prefix}${detail.map((item) => {
8731
+ if (!item || typeof item !== "object") return String(item);
8732
+ const o = item;
8733
+ const loc = Array.isArray(o.loc) ? o.loc.map(String).join(".") : "";
8734
+ const msg = o.msg ?? o.message ?? o.type ?? "参数错误";
8735
+ return loc ? `${loc}: ${String(msg)}` : String(msg);
8736
+ }).join("; ")}`;
8737
+ if (detail !== void 0 && detail !== null) return `${prefix}${typeof detail === "string" ? detail : JSON.stringify(detail)}`;
8738
+ const msg = messageOf(parsed);
8739
+ if (msg.trim()) return `${prefix}${msg}`;
8740
+ return `${prefix}${raw.trim().slice(0, 200) || "服务端未返回错误信息"}`;
8741
+ }
8717
8742
  var YmcApi = class {
8718
8743
  baseUrl;
8719
8744
  token;
@@ -8732,12 +8757,12 @@ var YmcApi = class {
8732
8757
  if (body !== void 0 && Object.keys(body).length > 0) console.log(JSON.stringify(body, null, 2));
8733
8758
  process.exit(0);
8734
8759
  }
8735
- async call(path, body = {}, timeoutMs = 9e4) {
8760
+ async call(path, body = {}, timeoutMs = 12e4) {
8736
8761
  if (this.dryRun) this._dryRunExit("POST", path, body);
8737
8762
  const url = `${this.baseUrl}/${path.replace(/^\//, "")}`;
8738
8763
  const payload = JSON.stringify(body);
8739
8764
  const mod = url.startsWith("https://") ? https : http;
8740
- const raw = await new Promise((resolve, reject) => {
8765
+ const { status, text: raw } = await new Promise((resolve, reject) => {
8741
8766
  const req = mod.request(url, {
8742
8767
  method: "POST",
8743
8768
  headers: {
@@ -8750,7 +8775,10 @@ var YmcApi = class {
8750
8775
  }, (res) => {
8751
8776
  const chunks = [];
8752
8777
  res.on("data", (c) => chunks.push(c));
8753
- res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
8778
+ res.on("end", () => resolve({
8779
+ status: res.statusCode ?? 0,
8780
+ text: Buffer.concat(chunks).toString("utf-8")
8781
+ }));
8754
8782
  });
8755
8783
  req.on("timeout", () => {
8756
8784
  req.destroy(/* @__PURE__ */ new Error(`请求超时(${timeoutMs}ms)`));
@@ -8763,12 +8791,13 @@ var YmcApi = class {
8763
8791
  try {
8764
8792
  parsed = JSON.parse(raw);
8765
8793
  } catch {
8766
- throw new Error(`非 JSON 响应(HTTP):${raw.slice(0, 200)}`);
8794
+ throw new Error(`HTTP ${status} 非 JSON 响应:${raw.slice(0, 200)}`);
8767
8795
  }
8768
- if (parsed.detail && parsed.code === void 0) throw new Error(String(parsed.detail));
8796
+ if (parsed.detail !== void 0 && parsed.code === void 0) throw new Error(describeApiError(parsed, status, raw));
8797
+ if (status >= 400 && parsed.code === void 0 && parsed.msg === void 0) throw new Error(describeApiError(parsed, status, raw));
8769
8798
  return {
8770
8799
  code: typeof parsed.code === "number" ? parsed.code : -1,
8771
- msg: String(parsed.msg ?? ""),
8800
+ msg: messageOf(parsed),
8772
8801
  data: parsed.data ?? null,
8773
8802
  raw: parsed
8774
8803
  };
@@ -8816,7 +8845,7 @@ var YmcApi = class {
8816
8845
  if (this.dryRun) this._dryRunExit("GET", path);
8817
8846
  const url = `${this.baseUrl}/${path.replace(/^\//, "")}`;
8818
8847
  const mod = url.startsWith("https://") ? https : http;
8819
- const raw = await new Promise((resolve, reject) => {
8848
+ const { status, text: raw } = await new Promise((resolve, reject) => {
8820
8849
  const req = mod.request(url, {
8821
8850
  method: "GET",
8822
8851
  headers: {
@@ -8827,7 +8856,10 @@ var YmcApi = class {
8827
8856
  }, (res) => {
8828
8857
  const chunks = [];
8829
8858
  res.on("data", (c) => chunks.push(c));
8830
- res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
8859
+ res.on("end", () => resolve({
8860
+ status: res.statusCode ?? 0,
8861
+ text: Buffer.concat(chunks).toString("utf-8")
8862
+ }));
8831
8863
  });
8832
8864
  req.on("timeout", () => {
8833
8865
  req.destroy(/* @__PURE__ */ new Error(`请求超时(${timeoutMs}ms)`));
@@ -8835,11 +8867,16 @@ var YmcApi = class {
8835
8867
  req.on("error", reject);
8836
8868
  req.end();
8837
8869
  });
8838
- const parsed = JSON.parse(raw);
8839
- if (parsed.detail && parsed.code === void 0) throw new Error(String(parsed.detail));
8870
+ let parsed;
8871
+ try {
8872
+ parsed = JSON.parse(raw);
8873
+ } catch {
8874
+ throw new Error(`HTTP ${status} 非 JSON 响应:${raw.slice(0, 200)}`);
8875
+ }
8876
+ if (parsed.detail !== void 0 && parsed.code === void 0) throw new Error(describeApiError(parsed, status, raw));
8840
8877
  return {
8841
8878
  code: typeof parsed.code === "number" ? parsed.code : -1,
8842
- msg: String(parsed.msg ?? ""),
8879
+ msg: messageOf(parsed),
8843
8880
  data: parsed.data ?? null,
8844
8881
  raw: parsed
8845
8882
  };
@@ -8921,7 +8958,6 @@ const F_ENV = [
8921
8958
  "--env",
8922
8959
  "--env-name"
8923
8960
  ];
8924
- const F_TIME = ["-t", "--time"];
8925
8961
  const F_RANGE = [
8926
8962
  "-t",
8927
8963
  "--time",
@@ -8941,7 +8977,7 @@ const ALLOWED_FLAGS = {
8941
8977
  ],
8942
8978
  "sls-grep": [
8943
8979
  ...F_ENV,
8944
- ...F_TIME,
8980
+ ...F_RANGE,
8945
8981
  "--group-by",
8946
8982
  "--line",
8947
8983
  "--app"
@@ -9091,15 +9127,6 @@ function requireEnv(args) {
9091
9127
  }
9092
9128
  return { env_name: env };
9093
9129
  }
9094
- function timeOf(args, required = true) {
9095
- const t = opt(args, "-t", "--time");
9096
- const err = timeError(t);
9097
- if (err && required) {
9098
- console.error(err);
9099
- process.exit(1);
9100
- }
9101
- return t ? { time: t } : {};
9102
- }
9103
9130
  function timeOpts(args) {
9104
9131
  const out = {};
9105
9132
  const t = opt(args, "-t", "--time");
@@ -9192,7 +9219,7 @@ async function runYmc(args, globalOpts) {
9192
9219
  trace_id: id,
9193
9220
  ...requireEnv(rest),
9194
9221
  ...opt(rest, "--ts") ? { ts: Number(opt(rest, "--ts")) } : {}
9195
- }), jsonMode, (data) => {
9222
+ }, 18e4), jsonMode, (data) => {
9196
9223
  const d = data;
9197
9224
  const lines = [];
9198
9225
  const wc = d.warning_chain;
@@ -9277,7 +9304,7 @@ async function runYmc(args, globalOpts) {
9277
9304
  case "sls-grep": {
9278
9305
  const kw = positionals(rest)[0];
9279
9306
  if (!kw) {
9280
- console.error("用法:bfi ymc sls-grep \"<关键词>\" -e <env> [-t time] [--app <appCode>] [--group-by applicationName|message|requestURL]");
9307
+ console.error("用法:bfi ymc sls-grep \"<关键词>\" -e <env> [-t <time> | --start-time <s> --end-time <e>] [--app <appCode>] [--group-by applicationName|message|requestURL]");
9281
9308
  console.error("例:bfi ymc sls-grep \"ClassCastException\" -e daily -t 3d");
9282
9309
  console.error("⚠️ 关键词按「整值」匹配:类名/全限定异常名必须整串加引号");
9283
9310
  console.error(" (\"java.lang.OutOfMemoryError\" 命中;简写 \"OutOfMemoryError\" 恒 0 且不报错)——sls-discipline §3");
@@ -9292,7 +9319,7 @@ async function runYmc(args, globalOpts) {
9292
9319
  emit(await api.call("sls/raw", {
9293
9320
  query,
9294
9321
  ...requireEnv(rest),
9295
- ...timeOf(rest, false),
9322
+ ...timeOpts(rest),
9296
9323
  line: Number(opt(rest, "--line") || 50)
9297
9324
  }), jsonMode, aggSummary(`SLS 全文检索 "${kw}"`));
9298
9325
  break;
@@ -9404,7 +9431,7 @@ async function runYmc(args, globalOpts) {
9404
9431
  };
9405
9432
  const app = opt(rest, "--app");
9406
9433
  if (app) body.filters = { service_name: app };
9407
- emit(await api.call("kibana/stats", body), jsonMode, (data) => {
9434
+ emit(await api.call("kibana/stats", body, 18e4), jsonMode, (data) => {
9408
9435
  const d = data;
9409
9436
  const a = d.aggs;
9410
9437
  return `✓ URL 统计${app ? ` [${app}]` : "(⚠️ 未限定服务,共享路径为全平台混合)"}: QPS=${a.qps} avg=${a.avg_rt?.value ?? "-"} 慢请求Top=${Array.isArray(d.slow_top) ? d.slow_top.length : 0}`;
@@ -9429,7 +9456,7 @@ async function runYmc(args, globalOpts) {
9429
9456
  console.error("用法:bfi ymc ingress --trace-id <id> | --uri </path> [-app <service_name>] -e <env> [-t time]");
9430
9457
  process.exit(1);
9431
9458
  }
9432
- emit(await api.call("kibana/search", body), jsonMode, rowsSummary("ingress 日志"));
9459
+ emit(await api.call("kibana/search", body, 18e4), jsonMode, rowsSummary("ingress 日志"));
9433
9460
  break;
9434
9461
  }
9435
9462
  case "dump-list": {
@@ -9721,7 +9748,7 @@ const HELP = {
9721
9748
  bfi ymc sls-raw 'applicationname: yonbip-scm-scmmp AND "unaudit" | SELECT traceid, time LIMIT 10' -e core3 -t 1d
9722
9749
  注意:业务关键词检索必须 applicationname: 限定目标服务(共享路径 URL 全平台都有);
9723
9750
  traceid/spanid 检索例外(全局唯一)。非索引字段(logger/requesturl)不能进检索语法,只能在 SELECT 里引用。`,
9724
- "sls-grep": `bfi ymc sls-grep "<关键词>" -e <env> -t <time> [--app <appCode>] [--group-by <字段>] [--json]
9751
+ "sls-grep": `bfi ymc sls-grep "<关键词>" -e <env> [-t <time> | --start-time <s> --end-time <e>] [--app <appCode>] [--group-by <字段>] [--json]
9725
9752
  SLS 全文检索聚合(每组计数精确不封顶;组数上限 500——实测 2026-09-17:计数不受限,仅组数受 SLS 默认 LIMIT 约束,服务端已放宽)。
9726
9753
  ⚠️ 关键词按「整值」匹配:类名/全限定异常名必须整串加引号
9727
9754
  ("java.lang.OutOfMemoryError" 命中;简写 "OutOfMemoryError" 恒 0 命中且不报错)——sls-discipline §3
@@ -3,6 +3,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { CallToolRequestSchema, CallToolResultSchema, ListToolsRequestSchema, ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
5
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
+ import { openAsBlob } from "node:fs";
6
7
  import { readFile, realpath, stat } from "node:fs/promises";
7
8
  import path from "node:path";
8
9
  const PM_ORIGIN = "https://bfi.yyuap.com";
@@ -42,27 +43,50 @@ var PmRemote = class {
42
43
  }
43
44
  async listTools() {
44
45
  try {
45
- return (await (await this.ensure()).request({ method: "tools/list" }, ListToolsResultSchema)).tools;
46
+ return await this.listToolsOnce();
46
47
  } catch (e) {
48
+ if (isSessionInvalid(e)) {
49
+ this.client = null;
50
+ try {
51
+ return await this.listToolsOnce();
52
+ } catch (e2) {
53
+ console.error(`[bfi-pm-mcp] 远端工具列表获取失败: ${messageOf(e2)}`);
54
+ this.client = null;
55
+ return [];
56
+ }
57
+ }
47
58
  console.error(`[bfi-pm-mcp] 远端工具列表获取失败: ${messageOf(e)}`);
48
59
  this.client = null;
49
60
  return [];
50
61
  }
51
62
  }
52
63
  async callTool(name, args) {
53
- const client = await this.ensure();
54
64
  try {
55
- return await client.request({
56
- method: "tools/call",
57
- params: {
58
- name,
59
- arguments: args ?? {}
60
- }
61
- }, CallToolResultSchema);
65
+ return await this.callToolOnce(name, args);
62
66
  } catch (e) {
67
+ if (isSessionInvalid(e)) {
68
+ this.client = null;
69
+ try {
70
+ return await this.callToolOnce(name, args);
71
+ } catch (e2) {
72
+ return this.handleRemoteFailure(e2);
73
+ }
74
+ }
63
75
  return this.handleRemoteFailure(e);
64
76
  }
65
77
  }
78
+ async listToolsOnce() {
79
+ return (await (await this.ensure()).request({ method: "tools/list" }, ListToolsResultSchema)).tools;
80
+ }
81
+ async callToolOnce(name, args) {
82
+ return (await this.ensure()).request({
83
+ method: "tools/call",
84
+ params: {
85
+ name,
86
+ arguments: args ?? {}
87
+ }
88
+ }, CallToolResultSchema);
89
+ }
66
90
  async ensure() {
67
91
  if (this.client) return this.client;
68
92
  if (!this.credentials) throw new PmError("unauthorized", credentialHint());
@@ -97,6 +121,9 @@ var PmRemote = class {
97
121
  function messageOf(e) {
98
122
  return e instanceof Error ? e.message : String(e);
99
123
  }
124
+ function isSessionInvalid(e) {
125
+ return /session not found|session expired|\b404\b/i.test(messageOf(e));
126
+ }
100
127
  const DEFAULT_MAX_MB = 50;
101
128
  function resolveUploadRoots() {
102
129
  return [...(process.env.BFI_UPLOAD_ROOTS || "").split(path.delimiter).map((s) => s.trim()).filter(Boolean), process.cwd()].map((p) => path.resolve(p));
@@ -135,20 +162,27 @@ function absoluteUploadUrl(uploadUrl, origin = PM_ORIGIN, pmUrl = process.env.BF
135
162
  return url.toString();
136
163
  }
137
164
  async function uploadToTicket(url, file, fetchImpl = fetch) {
138
- const data = await readFile(file.realPath);
165
+ if (file.size === 0) throw new Error("文件为空,无法上传");
139
166
  const form = new FormData();
140
- form.append("file", new Blob([new Uint8Array(data)]), file.name);
167
+ const blob = typeof openAsBlob === "function" ? await openAsBlob(file.realPath) : new Blob([new Uint8Array(await readFile(file.realPath))]);
168
+ form.append("file", blob, file.name);
169
+ const timeoutMs = Number(process.env.BFI_UPLOAD_TIMEOUT_MS || 12e4);
141
170
  const resp = await fetchImpl(url, {
142
171
  method: "POST",
143
- body: form
172
+ body: form,
173
+ signal: AbortSignal.timeout(Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 12e4)
144
174
  });
145
175
  const text = await resp.text();
146
176
  if (!resp.ok) throw new Error(`附件上传失败 HTTP ${resp.status}: ${text.slice(0, 200)}`);
177
+ let parsed;
147
178
  try {
148
- return JSON.parse(text);
179
+ parsed = JSON.parse(text);
149
180
  } catch {
150
181
  throw new Error(`附件上传响应非 JSON: ${text.slice(0, 200)}`);
151
182
  }
183
+ const code = parsed.code;
184
+ if (code !== void 0 && String(code) !== "0" && String(code) !== "200") throw new Error(`附件上传被拒绝(code=${String(code)}): ${String(parsed.msg || text.slice(0, 200))}`);
185
+ return parsed;
152
186
  }
153
187
  const LOCAL_TOOLS = [{
154
188
  name: "attachment_upload",
@@ -181,6 +215,15 @@ const LOCAL_TOOLS = [{
181
215
  type: "string",
182
216
  description: "评论正文"
183
217
  },
218
+ entity_type: {
219
+ type: "string",
220
+ enum: [
221
+ "DEFECT",
222
+ "FEATURE",
223
+ "SUBTASK"
224
+ ],
225
+ description: "单据类型,默认 DEFECT"
226
+ },
184
227
  file_paths: {
185
228
  type: "array",
186
229
  items: { type: "string" },
@@ -238,8 +281,9 @@ function buildServer(remote, fetchImpl) {
238
281
  const ticket = extractJson(await pm.callTool("get_upload_ticket", { file_name: (fileName || file.name).trim() }));
239
282
  const uploadUrl = String(ticket.upload_url || "");
240
283
  if (!uploadUrl) throw new PmError("ticket_failed", `签发 ticket 失败: ${JSON.stringify(ticket).slice(0, 200)}`);
241
- const uploaded = await uploadToTicket(absoluteUploadUrl(uploadUrl), file, fetchImpl);
242
- if (!uploaded.aid) throw new PmError("upload_failed", `上传响应缺少 aid: ${JSON.stringify(uploaded).slice(0, 200)}`);
284
+ const response = await uploadToTicket(absoluteUploadUrl(uploadUrl), file, fetchImpl);
285
+ const uploaded = response.data && typeof response.data === "object" ? response.data : response;
286
+ if (!uploaded.aid) throw new PmError("upload_failed", `上传响应缺少 aid: ${JSON.stringify(response).slice(0, 200)}`);
243
287
  return {
244
288
  ...uploaded,
245
289
  fileName: uploaded.fileName || file.name,
@@ -256,17 +300,36 @@ function buildServer(remote, fetchImpl) {
256
300
  if (!input.object_id) throw new PmError("invalid_params", "object_id 不能为空");
257
301
  if (!paths.length) throw new PmError("invalid_params", "file_paths 不能为空");
258
302
  const uploads = [];
259
- for (const p of paths) uploads.push(await uploadOne(p));
303
+ for (const p of paths) try {
304
+ uploads.push(await uploadOne(p));
305
+ } catch (e) {
306
+ return textResult({
307
+ ok: false,
308
+ error_kind: "upload_failed",
309
+ message: `第 ${uploads.length + 1} 个文件上传失败: ${e instanceof Error ? e.message : String(e)}`,
310
+ uploaded: uploads
311
+ }, true);
312
+ }
260
313
  const aids = uploads.map((u) => String(u.aid));
261
- return textResult({
262
- ok: true,
263
- uploads,
264
- comment: extractJson(await pm.callTool("add_comment", {
265
- object_id: String(input.object_id),
266
- content: input.content ? String(input.content) : "",
267
- attache_ids: aids
268
- }))
269
- });
314
+ try {
315
+ return textResult({
316
+ ok: true,
317
+ uploads,
318
+ comment: extractJson(await pm.callTool("add_comment", {
319
+ object_id: String(input.object_id),
320
+ content: input.content ? String(input.content) : "",
321
+ entity_type: input.entity_type ? String(input.entity_type) : "DEFECT",
322
+ attache_ids: aids
323
+ }))
324
+ });
325
+ } catch (e) {
326
+ return textResult({
327
+ ok: false,
328
+ error_kind: e instanceof PmError ? e.errorKind : "internal_error",
329
+ message: `附件已上传,但评论失败: ${e instanceof Error ? e.message : String(e)}`,
330
+ uploaded: uploads
331
+ }, true);
332
+ }
270
333
  }
271
334
  return await pm.callTool(name, input);
272
335
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bfi-cli",
3
- "version": "0.1.107",
3
+ "version": "0.1.109",
4
4
  "description": "用友大业财BFI统一命令行工具,支持技能,技能包,元数据,UI元数据等",
5
5
  "type": "module",
6
6
  "bin": {