bfi-cli 0.1.109 → 0.1.111

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.
@@ -7,8 +7,8 @@ import { randomUUID } from "node:crypto";
7
7
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
8
8
  import { lstat, mkdir, open, readFile, readdir, realpath, rename, writeFile } from "node:fs/promises";
9
9
  import { homedir } from "node:os";
10
- import { z } from "zod";
11
10
  import { execFile } from "node:child_process";
11
+ import { z } from "zod";
12
12
  const nativeManifestFormats = {
13
13
  "claude-code": ["CLAUDE"],
14
14
  codex: [
package/dist/cli.mjs CHANGED
@@ -30,6 +30,7 @@ import { promisify } from "util";
30
30
  import { execFile, spawn, spawnSync } from "child_process";
31
31
  import { access as access$1, chmod, cp as cp$1, lstat as lstat$1, mkdir as mkdir$1, mkdtemp, readFile as readFile$1, readdir as readdir$1, readlink, realpath as realpath$1, rename as rename$1, rm as rm$1, stat, symlink, writeFile as writeFile$1 } from "fs/promises";
32
32
  import { determineAgent } from "@vercel/detect-agent";
33
+ import { spawn as spawn$1 } from "node:child_process";
33
34
  import { applyEdits, getNodeValue, modify, parseTree } from "jsonc-parser";
34
35
  import { parse as parse$1, stringify } from "smol-toml";
35
36
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
@@ -5899,9 +5900,68 @@ function fail(message) {
5899
5900
  process.exit(1);
5900
5901
  }
5901
5902
  var UseCommandError = class extends Error {};
5903
+ function openBrowser(url) {
5904
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
5905
+ const args = process.platform === "win32" ? [
5906
+ "/c",
5907
+ "start",
5908
+ "",
5909
+ url
5910
+ ] : [url];
5911
+ try {
5912
+ const child = spawn$1(cmd, args, {
5913
+ detached: true,
5914
+ stdio: "ignore"
5915
+ });
5916
+ child.on("error", () => {});
5917
+ child.unref();
5918
+ } catch {}
5919
+ }
5920
+ var DeviceFlowUnavailableError = class extends Error {};
5921
+ var DeviceFlowDeniedError = class extends Error {};
5922
+ function dataOf(body) {
5923
+ if (body && typeof body === "object") {
5924
+ const data = body.data;
5925
+ if (data && typeof data === "object") return data;
5926
+ }
5927
+ return {};
5928
+ }
5929
+ async function requestDeviceCode(client) {
5930
+ const resp = await client.request("POST", "/auth/device/code");
5931
+ const data = dataOf(resp.body);
5932
+ const deviceCode = String(data.deviceCode || "");
5933
+ const userCode = String(data.userCode || "");
5934
+ const verificationUri = String(data.verificationUri || "");
5935
+ if (resp.status !== 200 || !deviceCode || !userCode || !verificationUri) throw new DeviceFlowUnavailableError(`设备授权接口不可用(HTTP ${resp.status})`);
5936
+ return {
5937
+ deviceCode,
5938
+ userCode,
5939
+ verificationUri,
5940
+ expiresIn: Number(data.expiresIn) > 0 ? Number(data.expiresIn) : 900,
5941
+ interval: Number(data.interval) > 0 ? Number(data.interval) : 5
5942
+ };
5943
+ }
5944
+ async function pollDeviceToken(client, deviceCode, opts) {
5945
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5946
+ const now = opts.now ?? (() => Date.now());
5947
+ const deadline = now() + opts.expiresInSec * 1e3;
5948
+ while (now() < deadline) {
5949
+ await sleep(opts.intervalSec * 1e3);
5950
+ const resp = await client.request("POST", "/auth/device/token", { body: { deviceCode } });
5951
+ const data = dataOf(resp.body);
5952
+ if (resp.status === 200 && data.accessToken) return String(data.accessToken);
5953
+ if (resp.status >= 500) throw new DeviceFlowUnavailableError(`设备授权轮询失败(HTTP ${resp.status})`);
5954
+ const error = String(data.error || "");
5955
+ if (error === "authorization_pending") continue;
5956
+ if (error) throw new DeviceFlowDeniedError(`设备授权失败:${error}`);
5957
+ }
5958
+ throw new DeviceFlowDeniedError("设备授权超时,请重试");
5959
+ }
5902
5960
  async function runLogin(args, globalOpts) {
5903
- const token = getOptionValue$3(args, "--token") || await promptForToken();
5904
5961
  const { registry } = resolveConfig(globalOpts);
5962
+ const explicitToken = getOptionValue$3(args, "--token");
5963
+ const noDevice = args.includes("--no-device");
5964
+ const token = explicitToken || (noDevice ? await promptForToken() : await loginViaDevice(registry));
5905
5965
  const api = new SkillHubAPI(registry, token);
5906
5966
  try {
5907
5967
  const response = await api.whoami();
@@ -5922,6 +5982,42 @@ async function runLogin(args, globalOpts) {
5922
5982
  process.exit(1);
5923
5983
  }
5924
5984
  }
5985
+ async function loginViaDevice(registry) {
5986
+ const tokenPage = `${registry.replace(/\/$/, "")}/dashboard/tokens`;
5987
+ const client = new SkillHubAPI(registry, null);
5988
+ let deviceCode;
5989
+ try {
5990
+ deviceCode = await requestDeviceCode(client);
5991
+ } catch (error) {
5992
+ return fallbackToTokenInput(error, tokenPage);
5993
+ }
5994
+ console.error("\n请在浏览器打开以下地址完成授权:");
5995
+ console.error(` ${deviceCode.verificationUri}`);
5996
+ console.error(`并输入设备代码:${deviceCode.userCode}(${Math.ceil(deviceCode.expiresIn / 60)} 分钟内有效)`);
5997
+ openBrowser(deviceCode.verificationUri);
5998
+ console.error("等待授权中…(Ctrl+C 取消)");
5999
+ try {
6000
+ const token = await pollDeviceToken(client, deviceCode.deviceCode, {
6001
+ intervalSec: deviceCode.interval,
6002
+ expiresInSec: deviceCode.expiresIn
6003
+ });
6004
+ console.error("✓ 设备授权成功");
6005
+ return token;
6006
+ } catch (error) {
6007
+ if (error instanceof DeviceFlowUnavailableError) return fallbackToTokenInput(error, tokenPage);
6008
+ if (error instanceof DeviceFlowDeniedError) {
6009
+ console.error(`登录失败:${error.message}`);
6010
+ process.exit(1);
6011
+ }
6012
+ throw error;
6013
+ }
6014
+ }
6015
+ async function fallbackToTokenInput(error, tokenPage) {
6016
+ console.error(`⚠️ ${error?.message || "设备授权不可用"},改用网页 Token。`);
6017
+ console.error(`Token 页面:${tokenPage}`);
6018
+ openBrowser(tokenPage);
6019
+ return promptForToken();
6020
+ }
5925
6021
  async function runLogout() {
5926
6022
  const config = loadGlobalConfig();
5927
6023
  if (config?.token) {
@@ -5934,7 +6030,7 @@ async function runLogout() {
5934
6030
  async function runWhoami(globalOpts) {
5935
6031
  const { registry, token } = resolveConfig(globalOpts);
5936
6032
  if (!token) {
5937
- console.error("未认证。运行:bfi auth login(在隐藏提示中输入 Token)");
6033
+ console.error("未认证。运行:bfi auth login(浏览器设备授权;或 --token 直接指定)");
5938
6034
  process.exit(1);
5939
6035
  }
5940
6036
  const response = await new SkillHubAPI(registry, token).whoami();
@@ -8992,7 +9088,8 @@ const ALLOWED_FLAGS = {
8992
9088
  "--app",
8993
9089
  "--no-profiler",
8994
9090
  "--page",
8995
- "--page-size"
9091
+ "--page-size",
9092
+ "--timeout"
8996
9093
  ],
8997
9094
  slowcall: [
8998
9095
  ...F_ENV,
@@ -9003,7 +9100,8 @@ const ALLOWED_FLAGS = {
9003
9100
  "--profiler",
9004
9101
  "--min-cost",
9005
9102
  "--page",
9006
- "--page-size"
9103
+ "--page-size",
9104
+ "--timeout"
9007
9105
  ],
9008
9106
  exceptions: [
9009
9107
  ...F_ENV,
@@ -9144,6 +9242,43 @@ function timeOpts(args) {
9144
9242
  if (et) out.end_time = et;
9145
9243
  return out;
9146
9244
  }
9245
+ function slowQueryTimeoutMs(args) {
9246
+ const raw = opt(args, "--timeout");
9247
+ if (raw === void 0) return 15e4;
9248
+ const sec = Number(raw);
9249
+ if (!Number.isFinite(sec) || sec <= 0) {
9250
+ console.error(`--timeout 非法:${raw}(应为正数秒,如 180)`);
9251
+ process.exit(1);
9252
+ }
9253
+ return sec * 1e3;
9254
+ }
9255
+ function windowDays(args) {
9256
+ const t = opt(args, "-t", "--time");
9257
+ if (t) {
9258
+ const m = /^(\d+)([mhdw])$/.exec(t);
9259
+ if (!m) return 0;
9260
+ const n = Number(m[1]);
9261
+ const unit = m[2];
9262
+ if (unit === "w") return n * 7;
9263
+ if (unit === "d") return n;
9264
+ if (unit === "h") return n / 24;
9265
+ return n / 1440;
9266
+ }
9267
+ const st = opt(args, "--start-time");
9268
+ const et = opt(args, "--end-time");
9269
+ if (st && et) {
9270
+ const ms = Date.parse(et.replace(" ", "T")) - Date.parse(st.replace(" ", "T"));
9271
+ return Number.isFinite(ms) ? ms / 864e5 : 0;
9272
+ }
9273
+ return 0;
9274
+ }
9275
+ function warnWideQuery(label, args, pageSize, narrowFlags) {
9276
+ if (narrowFlags.some((f) => opt(args, f))) return;
9277
+ const days = windowDays(args);
9278
+ if (days < 1 && pageSize <= 200) return;
9279
+ const win = days ? `窗口 ${days.toFixed(1)} 天` : "窗口未识别";
9280
+ console.error(`⚠️ ${label} ${win} / pageSize ${pageSize} 且未限定服务:上游模糊匹配 + 全服务扫描,易触发 120s 超时;建议加 --app <appCode> 或缩短 -t(≤6h)`);
9281
+ }
9147
9282
  function requireToken(api) {
9148
9283
  if (!api.token) {
9149
9284
  console.error("未认证。先运行:bfi auth login --token <sk-token>(或传 --token)");
@@ -9354,7 +9489,12 @@ async function runYmc(args, globalOpts) {
9354
9489
  const v = opt(rest, name);
9355
9490
  if (v) body[k] = v;
9356
9491
  }
9357
- emit(await api.call("ymc/warningSql", body), jsonMode, (data) => {
9492
+ warnWideQuery("慢 SQL", rest, Number(body.pageSize), [
9493
+ "--app",
9494
+ "--trace-id",
9495
+ "--md5"
9496
+ ]);
9497
+ emit(await api.call("ymc/warningSql", body, slowQueryTimeoutMs(rest)), jsonMode, (data) => {
9358
9498
  const d = data;
9359
9499
  const rows = Array.isArray(d) ? d : d.rows ?? d.data ?? [];
9360
9500
  const n = rows.length;
@@ -9388,7 +9528,12 @@ async function runYmc(args, globalOpts) {
9388
9528
  const cost = opt(rest, "--min-cost");
9389
9529
  if (cost) body.costTime = [Number(cost), 999999];
9390
9530
  if (flag(rest, "--profiler")) console.error("⚠️ --profiler 是上游采样过滤器,实测常返回空集——常规取证请去掉(CPU 热点走 dump-list)");
9391
- emit(await api.call("ymc/warningCall", body), jsonMode, (data) => {
9531
+ warnWideQuery("慢调用", rest, Number(body.pageSize), [
9532
+ "--app",
9533
+ "--trace-id",
9534
+ "--method"
9535
+ ]);
9536
+ emit(await api.call("ymc/warningCall", body, slowQueryTimeoutMs(rest)), jsonMode, (data) => {
9392
9537
  const d = data;
9393
9538
  const rows = Array.isArray(d) ? d : d.rows ?? d.data ?? [];
9394
9539
  const n = rows.length;
@@ -9753,27 +9898,33 @@ const HELP = {
9753
9898
  ⚠️ 关键词按「整值」匹配:类名/全限定异常名必须整串加引号
9754
9899
  ("java.lang.OutOfMemoryError" 命中;简写 "OutOfMemoryError" 恒 0 命中且不报错)——sls-discipline §3
9755
9900
  --app <appCode> 裸微服务名,前置 applicationname 限定(不限服务的检索会命中不相干服务)`,
9756
- slowsql: `bfi ymc slowsql -t <time> -e <env> [--sql-str <表名或片段>] [--md5 <指纹>] [--trace-id <id>] [--app <appCode>] [--no-profiler] [--page N] [--page-size N] [--json]
9901
+ 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]
9757
9902
  慢 SQL 告警明细(耗时降序;摘要含 服务/耗时/行数/SQL 前段/指纹/调用栈首帧)。
9758
9903
  参数:
9759
9904
  -t <time> 时间窗(必填语义,服务端强校验)
9760
- --sql-str 按 SQL 片段过滤(表名/语句片段)
9761
- --md5 按 SQL MD5 指纹精确过滤(同类 SQL 归类)
9905
+ --sql-str 按 SQL 片段过滤(表名/语句片段;模糊匹配,开销大,见下方警示)
9906
+ --md5 按 SQL MD5 指纹精确过滤(同类 SQL 归类;有指纹时优先用,毫秒级)
9762
9907
  --trace-id 按链路过滤。⚠️ 按 trace 查询时不带 --app(traceId 已唯一限定,
9763
9908
  --app 会截断其他服务的告警行——慢 SQL 归属哪个服务事先未知)
9764
9909
  --app 服务维度过滤(无 traceId 时必带,裸 appCode)
9910
+ --page-size N 每页条数(服务端上限 200,超限直接报错并提示分页)
9911
+ --timeout <秒> 客户端超时(默认 150s;服务端上游超时 120s,留余量先拿服务端消息)
9765
9912
  --no-profiler 显式关闭 enableProfiler(默认自动带 'true'——不带该字段上游 500,
9766
- 带了才返回 sqlStack 调用栈)
9913
+ 带了才返回 sqlStack 调用栈;不需要调用栈时关闭约省 40% 耗时)
9914
+ ⚠️ --sql-str 是模糊匹配:大窗口(≥1d)必须配 --app,否则上游全服务扫描极易 120s 超时
9915
+ (实测 10d 无 --app 必超时;同条件加 --app 后 ~105s 成功)。窗口优先 ≤6h。
9767
9916
  示例:
9768
9917
  bfi ymc slowsql -t 1d --app yonbip-scm-scmmp -e core3 # 服务维度(SQL 画像)
9769
9918
  bfi ymc slowsql -t 1d --trace-id abc -e core3 # 链路维度(勿带 --app)`,
9770
- slowcall: `bfi ymc slowcall -t <time> -e <env> [--trace-id <id>] [--app <appCode>] [--method <方法>] [--min-cost <ms>] [--page N] [--page-size N] [--json]
9919
+ slowcall: `bfi ymc slowcall -t <time> -e <env> [--trace-id <id>] [--app <appCode>] [--method <方法>] [--min-cost <ms>] [--page N] [--page-size N] [--timeout <秒>] [--json]
9771
9920
  慢调用告警明细(耗时降序;摘要含 服务/耗时/sql/rpc/redis 分解/方法/pod)。
9772
9921
  参数:
9773
9922
  --trace-id 按链路过滤。⚠️ 不带 --app(同 slowsql,--app 会截断下游服务告警行)
9774
9923
  --app 服务维度过滤(裸 appCode)
9775
9924
  --method 按调用方法过滤
9776
9925
  --min-cost <ms> 最小耗时过滤(body.costTime=[ms, 999999])
9926
+ --page-size N 每页条数(服务端上限 200,超限直接报错并提示分页)
9927
+ --timeout <秒> 客户端超时(默认 150s)
9777
9928
  --profiler 透传上游 enableProfiler(仅返回有 CPU 采样记录的调用);
9778
9929
  该过滤器常返回空集,常规取证勿带,CPU 热点走 dump-list。
9779
9930
  示例:
@@ -10655,7 +10806,7 @@ ${BOLD}bfi — 用友大业财BFI统一命令行工具${RESET}
10655
10806
  ${BOLD}Usage:${RESET} bfi <command> [options]
10656
10807
 
10657
10808
  ${BOLD}━━━ 认证 ━━━${RESET}
10658
- auth login [--token <token>] 登录 SkillHub(不带参数时隐藏输入 Token)
10809
+ auth login [--token <token>] 登录 SkillHub(默认设备授权:浏览器打开并输入代码;--no-device 走隐藏输入)
10659
10810
  auth logout 登出
10660
10811
  auth whoami 查看当前用户
10661
10812
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bfi-cli",
3
- "version": "0.1.109",
3
+ "version": "0.1.111",
4
4
  "description": "用友大业财BFI统一命令行工具,支持技能,技能包,元数据,UI元数据等",
5
5
  "type": "module",
6
6
  "bin": {