bfi-cli 0.1.108 → 0.1.110

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.
Files changed (2) hide show
  1. package/dist/cli.mjs +119 -37
  2. package/package.json +1 -1
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"
@@ -8956,7 +8992,8 @@ const ALLOWED_FLAGS = {
8956
8992
  "--app",
8957
8993
  "--no-profiler",
8958
8994
  "--page",
8959
- "--page-size"
8995
+ "--page-size",
8996
+ "--timeout"
8960
8997
  ],
8961
8998
  slowcall: [
8962
8999
  ...F_ENV,
@@ -8967,7 +9004,8 @@ const ALLOWED_FLAGS = {
8967
9004
  "--profiler",
8968
9005
  "--min-cost",
8969
9006
  "--page",
8970
- "--page-size"
9007
+ "--page-size",
9008
+ "--timeout"
8971
9009
  ],
8972
9010
  exceptions: [
8973
9011
  ...F_ENV,
@@ -9091,15 +9129,6 @@ function requireEnv(args) {
9091
9129
  }
9092
9130
  return { env_name: env };
9093
9131
  }
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
9132
  function timeOpts(args) {
9104
9133
  const out = {};
9105
9134
  const t = opt(args, "-t", "--time");
@@ -9117,6 +9146,43 @@ function timeOpts(args) {
9117
9146
  if (et) out.end_time = et;
9118
9147
  return out;
9119
9148
  }
9149
+ function slowQueryTimeoutMs(args) {
9150
+ const raw = opt(args, "--timeout");
9151
+ if (raw === void 0) return 15e4;
9152
+ const sec = Number(raw);
9153
+ if (!Number.isFinite(sec) || sec <= 0) {
9154
+ console.error(`--timeout 非法:${raw}(应为正数秒,如 180)`);
9155
+ process.exit(1);
9156
+ }
9157
+ return sec * 1e3;
9158
+ }
9159
+ function windowDays(args) {
9160
+ const t = opt(args, "-t", "--time");
9161
+ if (t) {
9162
+ const m = /^(\d+)([mhdw])$/.exec(t);
9163
+ if (!m) return 0;
9164
+ const n = Number(m[1]);
9165
+ const unit = m[2];
9166
+ if (unit === "w") return n * 7;
9167
+ if (unit === "d") return n;
9168
+ if (unit === "h") return n / 24;
9169
+ return n / 1440;
9170
+ }
9171
+ const st = opt(args, "--start-time");
9172
+ const et = opt(args, "--end-time");
9173
+ if (st && et) {
9174
+ const ms = Date.parse(et.replace(" ", "T")) - Date.parse(st.replace(" ", "T"));
9175
+ return Number.isFinite(ms) ? ms / 864e5 : 0;
9176
+ }
9177
+ return 0;
9178
+ }
9179
+ function warnWideQuery(label, args, pageSize, narrowFlags) {
9180
+ if (narrowFlags.some((f) => opt(args, f))) return;
9181
+ const days = windowDays(args);
9182
+ if (days < 1 && pageSize <= 200) return;
9183
+ const win = days ? `窗口 ${days.toFixed(1)} 天` : "窗口未识别";
9184
+ console.error(`⚠️ ${label} ${win} / pageSize ${pageSize} 且未限定服务:上游模糊匹配 + 全服务扫描,易触发 120s 超时;建议加 --app <appCode> 或缩短 -t(≤6h)`);
9185
+ }
9120
9186
  function requireToken(api) {
9121
9187
  if (!api.token) {
9122
9188
  console.error("未认证。先运行:bfi auth login --token <sk-token>(或传 --token)");
@@ -9192,7 +9258,7 @@ async function runYmc(args, globalOpts) {
9192
9258
  trace_id: id,
9193
9259
  ...requireEnv(rest),
9194
9260
  ...opt(rest, "--ts") ? { ts: Number(opt(rest, "--ts")) } : {}
9195
- }), jsonMode, (data) => {
9261
+ }, 18e4), jsonMode, (data) => {
9196
9262
  const d = data;
9197
9263
  const lines = [];
9198
9264
  const wc = d.warning_chain;
@@ -9277,7 +9343,7 @@ async function runYmc(args, globalOpts) {
9277
9343
  case "sls-grep": {
9278
9344
  const kw = positionals(rest)[0];
9279
9345
  if (!kw) {
9280
- console.error("用法:bfi ymc sls-grep \"<关键词>\" -e <env> [-t time] [--app <appCode>] [--group-by applicationName|message|requestURL]");
9346
+ console.error("用法:bfi ymc sls-grep \"<关键词>\" -e <env> [-t <time> | --start-time <s> --end-time <e>] [--app <appCode>] [--group-by applicationName|message|requestURL]");
9281
9347
  console.error("例:bfi ymc sls-grep \"ClassCastException\" -e daily -t 3d");
9282
9348
  console.error("⚠️ 关键词按「整值」匹配:类名/全限定异常名必须整串加引号");
9283
9349
  console.error(" (\"java.lang.OutOfMemoryError\" 命中;简写 \"OutOfMemoryError\" 恒 0 且不报错)——sls-discipline §3");
@@ -9292,7 +9358,7 @@ async function runYmc(args, globalOpts) {
9292
9358
  emit(await api.call("sls/raw", {
9293
9359
  query,
9294
9360
  ...requireEnv(rest),
9295
- ...timeOf(rest, false),
9361
+ ...timeOpts(rest),
9296
9362
  line: Number(opt(rest, "--line") || 50)
9297
9363
  }), jsonMode, aggSummary(`SLS 全文检索 "${kw}"`));
9298
9364
  break;
@@ -9327,7 +9393,12 @@ async function runYmc(args, globalOpts) {
9327
9393
  const v = opt(rest, name);
9328
9394
  if (v) body[k] = v;
9329
9395
  }
9330
- emit(await api.call("ymc/warningSql", body), jsonMode, (data) => {
9396
+ warnWideQuery("慢 SQL", rest, Number(body.pageSize), [
9397
+ "--app",
9398
+ "--trace-id",
9399
+ "--md5"
9400
+ ]);
9401
+ emit(await api.call("ymc/warningSql", body, slowQueryTimeoutMs(rest)), jsonMode, (data) => {
9331
9402
  const d = data;
9332
9403
  const rows = Array.isArray(d) ? d : d.rows ?? d.data ?? [];
9333
9404
  const n = rows.length;
@@ -9361,7 +9432,12 @@ async function runYmc(args, globalOpts) {
9361
9432
  const cost = opt(rest, "--min-cost");
9362
9433
  if (cost) body.costTime = [Number(cost), 999999];
9363
9434
  if (flag(rest, "--profiler")) console.error("⚠️ --profiler 是上游采样过滤器,实测常返回空集——常规取证请去掉(CPU 热点走 dump-list)");
9364
- emit(await api.call("ymc/warningCall", body), jsonMode, (data) => {
9435
+ warnWideQuery("慢调用", rest, Number(body.pageSize), [
9436
+ "--app",
9437
+ "--trace-id",
9438
+ "--method"
9439
+ ]);
9440
+ emit(await api.call("ymc/warningCall", body, slowQueryTimeoutMs(rest)), jsonMode, (data) => {
9365
9441
  const d = data;
9366
9442
  const rows = Array.isArray(d) ? d : d.rows ?? d.data ?? [];
9367
9443
  const n = rows.length;
@@ -9404,7 +9480,7 @@ async function runYmc(args, globalOpts) {
9404
9480
  };
9405
9481
  const app = opt(rest, "--app");
9406
9482
  if (app) body.filters = { service_name: app };
9407
- emit(await api.call("kibana/stats", body), jsonMode, (data) => {
9483
+ emit(await api.call("kibana/stats", body, 18e4), jsonMode, (data) => {
9408
9484
  const d = data;
9409
9485
  const a = d.aggs;
9410
9486
  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 +9505,7 @@ async function runYmc(args, globalOpts) {
9429
9505
  console.error("用法:bfi ymc ingress --trace-id <id> | --uri </path> [-app <service_name>] -e <env> [-t time]");
9430
9506
  process.exit(1);
9431
9507
  }
9432
- emit(await api.call("kibana/search", body), jsonMode, rowsSummary("ingress 日志"));
9508
+ emit(await api.call("kibana/search", body, 18e4), jsonMode, rowsSummary("ingress 日志"));
9433
9509
  break;
9434
9510
  }
9435
9511
  case "dump-list": {
@@ -9721,32 +9797,38 @@ const HELP = {
9721
9797
  bfi ymc sls-raw 'applicationname: yonbip-scm-scmmp AND "unaudit" | SELECT traceid, time LIMIT 10' -e core3 -t 1d
9722
9798
  注意:业务关键词检索必须 applicationname: 限定目标服务(共享路径 URL 全平台都有);
9723
9799
  traceid/spanid 检索例外(全局唯一)。非索引字段(logger/requesturl)不能进检索语法,只能在 SELECT 里引用。`,
9724
- "sls-grep": `bfi ymc sls-grep "<关键词>" -e <env> -t <time> [--app <appCode>] [--group-by <字段>] [--json]
9800
+ "sls-grep": `bfi ymc sls-grep "<关键词>" -e <env> [-t <time> | --start-time <s> --end-time <e>] [--app <appCode>] [--group-by <字段>] [--json]
9725
9801
  SLS 全文检索聚合(每组计数精确不封顶;组数上限 500——实测 2026-09-17:计数不受限,仅组数受 SLS 默认 LIMIT 约束,服务端已放宽)。
9726
9802
  ⚠️ 关键词按「整值」匹配:类名/全限定异常名必须整串加引号
9727
9803
  ("java.lang.OutOfMemoryError" 命中;简写 "OutOfMemoryError" 恒 0 命中且不报错)——sls-discipline §3
9728
9804
  --app <appCode> 裸微服务名,前置 applicationname 限定(不限服务的检索会命中不相干服务)`,
9729
- slowsql: `bfi ymc slowsql -t <time> -e <env> [--sql-str <表名或片段>] [--md5 <指纹>] [--trace-id <id>] [--app <appCode>] [--no-profiler] [--page N] [--page-size N] [--json]
9805
+ slowsql: `bfi ymc slowsql -t <time> -e <env> [--sql-str <表名或片段>] [--md5 <指纹>] [--trace-id <id>] [--app <appCode>] [--no-profiler] [--page N] [--page-size N] [--timeout <秒>] [--json]
9730
9806
  慢 SQL 告警明细(耗时降序;摘要含 服务/耗时/行数/SQL 前段/指纹/调用栈首帧)。
9731
9807
  参数:
9732
9808
  -t <time> 时间窗(必填语义,服务端强校验)
9733
- --sql-str 按 SQL 片段过滤(表名/语句片段)
9734
- --md5 按 SQL MD5 指纹精确过滤(同类 SQL 归类)
9809
+ --sql-str 按 SQL 片段过滤(表名/语句片段;模糊匹配,开销大,见下方警示)
9810
+ --md5 按 SQL MD5 指纹精确过滤(同类 SQL 归类;有指纹时优先用,毫秒级)
9735
9811
  --trace-id 按链路过滤。⚠️ 按 trace 查询时不带 --app(traceId 已唯一限定,
9736
9812
  --app 会截断其他服务的告警行——慢 SQL 归属哪个服务事先未知)
9737
9813
  --app 服务维度过滤(无 traceId 时必带,裸 appCode)
9814
+ --page-size N 每页条数(服务端上限 200,超限直接报错并提示分页)
9815
+ --timeout <秒> 客户端超时(默认 150s;服务端上游超时 120s,留余量先拿服务端消息)
9738
9816
  --no-profiler 显式关闭 enableProfiler(默认自动带 'true'——不带该字段上游 500,
9739
- 带了才返回 sqlStack 调用栈)
9817
+ 带了才返回 sqlStack 调用栈;不需要调用栈时关闭约省 40% 耗时)
9818
+ ⚠️ --sql-str 是模糊匹配:大窗口(≥1d)必须配 --app,否则上游全服务扫描极易 120s 超时
9819
+ (实测 10d 无 --app 必超时;同条件加 --app 后 ~105s 成功)。窗口优先 ≤6h。
9740
9820
  示例:
9741
9821
  bfi ymc slowsql -t 1d --app yonbip-scm-scmmp -e core3 # 服务维度(SQL 画像)
9742
9822
  bfi ymc slowsql -t 1d --trace-id abc -e core3 # 链路维度(勿带 --app)`,
9743
- slowcall: `bfi ymc slowcall -t <time> -e <env> [--trace-id <id>] [--app <appCode>] [--method <方法>] [--min-cost <ms>] [--page N] [--page-size N] [--json]
9823
+ slowcall: `bfi ymc slowcall -t <time> -e <env> [--trace-id <id>] [--app <appCode>] [--method <方法>] [--min-cost <ms>] [--page N] [--page-size N] [--timeout <秒>] [--json]
9744
9824
  慢调用告警明细(耗时降序;摘要含 服务/耗时/sql/rpc/redis 分解/方法/pod)。
9745
9825
  参数:
9746
9826
  --trace-id 按链路过滤。⚠️ 不带 --app(同 slowsql,--app 会截断下游服务告警行)
9747
9827
  --app 服务维度过滤(裸 appCode)
9748
9828
  --method 按调用方法过滤
9749
9829
  --min-cost <ms> 最小耗时过滤(body.costTime=[ms, 999999])
9830
+ --page-size N 每页条数(服务端上限 200,超限直接报错并提示分页)
9831
+ --timeout <秒> 客户端超时(默认 150s)
9750
9832
  --profiler 透传上游 enableProfiler(仅返回有 CPU 采样记录的调用);
9751
9833
  该过滤器常返回空集,常规取证勿带,CPU 热点走 dump-list。
9752
9834
  示例:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bfi-cli",
3
- "version": "0.1.108",
3
+ "version": "0.1.110",
4
4
  "description": "用友大业财BFI统一命令行工具,支持技能,技能包,元数据,UI元数据等",
5
5
  "type": "module",
6
6
  "bin": {