@dsh-fly/dsh-plugin-fly-mode 0.1.7 → 0.1.9

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/README.md CHANGED
@@ -11,8 +11,8 @@
11
11
  - **Fly 标准模式** `preset-fly-standard`(行表 = **上游 `standard` 的行表** + 一行 smart,33 行)——
12
12
  由生成器从**引擎自己的组合结果**里取上游行表(不手抄),所以引擎升级后行表跟着走、漂了 `--check` 会红。
13
13
  其中
14
- **7 个 Fly 工具**(binary / reverse / pwn / diag / recon / blackbox / guard)的源码**内联在包内
15
- `tools/`**,行表用**本包子路径**引用它们 —— 装这个包**不会**在 npm 上再拉这 7 个包;
14
+ **8 个 Fly 工具**(binary / reverse / pwn / diag / recon / blackbox / artifact / guard)的源码**内联在包内
15
+ `tools/`**,行表用**本包子路径**引用它们 —— 装这个包**不会**在 npm 上再拉这 8 个包;
16
16
  **`fly-smart` 单独一个 plugin**:它是本包 `dependencies` 里**唯一**一项,行表保持裸名,
17
17
  由 npm 装进 profile 的 `node_modules`(它变化最快,且与工作台面板是一个整体,不适合内联)。
18
18
  - **不是**:不带 `dsh.client`(模式是 agent 平面,没有浏览器半边);不带 Fly 工作台
@@ -76,7 +76,7 @@ dsh plugin --profile=web add @dsh-fly/dsh-plugin-fly-mode@0.1.3 --registry=https
76
76
 
77
77
  ## 仓库状态(2026-10-11)
78
78
 
79
- - 形状定稿:一条 `- insert:` 声明 + 包内 `tools/`(7 个工具内联,行表用本包子路径引用)+
79
+ - 形状定稿:一条 `- insert:` 声明 + 包内 `tools/`(8 个工具内联,行表用本包子路径引用)+
80
80
  `dsh-plugin-fly-smart` 作为**唯一** npm 依赖。声明由 `dsh-fly` 仓的
81
81
  `scripts/build-fly-mode-bundle.mjs` 生成,门禁用 `--check` 断漂移(含多出来的文件)。
82
82
  - `.github/workflows/{ci,release}.yml` **已适配本仓**(纯声明包:不装依赖、直接跑
package/cordis.patch.yml CHANGED
@@ -5,7 +5,6 @@
5
5
  # 是同一条);8 个工具的行名被改写成**本包的子路径**(tools/**,随包一起发布)。
6
6
  # 门禁用 --check 断言本文件与 tools/** 都 == 生成器输出。
7
7
  # 不含三模型路由(用户机器状态)、不写 registry default(默认归上游 standard)。
8
- - insert:
9
8
  - insert:
10
9
  - id: preset-fly
11
10
  name: '@deepseek-ai/dsh-agent-preset'
@@ -177,12 +176,10 @@
177
176
 
178
177
  When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
179
178
 
180
- # 评审姿态(docs/fly-review-mode-plan.md):`/review` 开关一段只读引导(先讲阻塞问题、
181
179
  # 每条结论带 path:line 证据),收口工具 `exit_review_mode` 把判词交用户裁决。
182
180
  # 形状与上面的 plan-mode 同族:姿态的效果就是一段引导 ⇒ 进/出只改提示词段、不改工具目录;
183
181
  # 状态记会话日志 ⇒ resume / fork 不丢。**只读是引导、不是强制**(沙箱与审批策略独立执行)。
184
182
  # 行保持**裸名**(内联形状的契约):生成器认这个裸名,再把它改写成模式包内的
185
- # `dsh-plugin-fly-mode/tools/dsh-plugin-fly-review/index.js`(同那 8 个内联工具行);
186
183
  # 包内的源码布局则是 `tools/index.js`(内联器照这个路径拷贝)。
187
184
  # ── compaction ──────────────────────────────────────────────────────────────
188
185
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dsh-fly/dsh-plugin-fly-mode",
3
- "version": "0.1.7",
4
- "description": "Fly 模式作为一个可安装的包:**两个 preset** —— Fly 安全模式(preset-fly,7 个安全工具内联 + smart)与 Fly 标准模式(preset-fly-standard,上游 standard 的行表 + smart,无安全工具);依赖单独发布的 dsh-plugin-fly-smart。",
3
+ "version": "0.1.9",
4
+ "description": "Fly 模式作为一个可安装的包:**两个 preset** —— Fly 安全模式(preset-fly,8 个安全工具内联 + smart)与 Fly 标准模式(preset-fly-standard,上游 standard 的行表 + smart,无安全工具);依赖单独发布的 dsh-plugin-fly-smart。",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -39,6 +39,12 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
39
39
  *
40
40
  * 两个入口都在 P1 的"结论要能回指源码位置"那条要求下:清单的每条成员给**相对路径**,
41
41
  * 边界的每条命中给 **file:line**。
42
+ *
43
+ * **起因**(为什么这个包存在):`morluto/rea` 那边有 APK/JADX、固件/Binwalk、.NET、**JS/ASAR 与 HAR**
44
+ * 这些"看得懂打包产物"的能力,而 fly 这边此前只有原生二进制三件套 —— 一个 Electron 应用的
45
+ * 150MB `app.asar` 摆在面前,只能靠 `bin_strings` 碰运气。安全模式的定位是客户端黑盒排查,
46
+ * 所以这一面必须在拆仓之前补上(规划 §3.2):先把"清单 + 字面量边界"两层做出来,
47
+ * 再谈 AST 与 CDP(P2)。**不抄代码,只在设计上对齐**(分层、覆盖率、bounds 三件)。
42
48
  */
43
49
 
44
50
  export const name = 'fly-artifact'
@@ -59,6 +65,19 @@ function shq(value) {
59
65
  * version. `scripts/verify-security-tools.mjs` now fails loudly if a call site
60
66
  * names a seam method the executor does not have.
61
67
  */
68
+ /**
69
+ * 路径参数规范化:以 `-` 开头的路径加 `./` 前缀(2026-10-13)。
70
+ *
71
+ * 为什么不插 `--`:实测本机(macOS,发布目标平台)**objdump(-h/-d/-T)/ base64 -i / otool -hv 都不支持
72
+ * `--`**,插进去直接失败;而 `file -b --version` 会打印 `file-5.41`、`nm -u --version` 会打印
73
+ * `llvm-nm, …`,两者都 **exit 0** ⇒ 工具会把版本串当**数据**返回。加 `./` 之后它就是一个普通相对路径:
74
+ * 不存在就正常报错,存在就是那个文件。
75
+ */
76
+ function safePath(value) {
77
+ const text = String(value ?? '')
78
+ return text.startsWith('-') ? './' + text : text
79
+ }
80
+
62
81
  async function runCmd(shell, argv, exec, opts = {}) {
63
82
  const result = await (await shell.execute(shell.resolve({
64
83
  command: argv.map(shq).join(' '),
@@ -632,7 +651,7 @@ export function apply(ctx) {
632
651
  },
633
652
  output: stringOutput,
634
653
  async execute(args, exec) {
635
- const path = typeof args.path === 'string' ? args.path.trim() : ''
654
+ const path = safePath(typeof args.path === 'string' ? args.path.trim() : '')
636
655
  if (path === '') throw new Error('artifact_asar_inventory 需要 path(.asar 的路径)')
637
656
  const top = readTop(args.top, 'artifact_asar_inventory')
638
657
 
@@ -770,7 +789,7 @@ export function apply(ctx) {
770
789
  },
771
790
  output: stringOutput,
772
791
  async execute(args, exec) {
773
- const path = typeof args.path === 'string' ? args.path.trim() : ''
792
+ const path = safePath(typeof args.path === 'string' ? args.path.trim() : '')
774
793
  if (path === '') throw new Error('artifact_js_boundaries 需要 path(.asar 的路径,或解包后的 JS 目录)')
775
794
  const top = readTop(args.top, 'artifact_js_boundaries')
776
795
  const scope = args.scope === undefined ? 'app' : args.scope
@@ -5,6 +5,14 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
5
5
  * Registers model-facing analysis tools on ctx.tools. All command execution
6
6
  * goes through the host `shell` capability seam so the sandbox confines it;
7
7
  * a tool never shells out via node:child_process directly.
8
+ *
9
+ * **失败面的口径是"大声"**(Q5):缺 `readelf`/`objdump`/`strings`/`file`/`wc` 时**不静默降级** ——
10
+ * · 能在仓库内补的就补:没有 GNU readelf 就用**仓库内置的原生解析器**算头部/段/动态(并在
11
+ * 结果的 `limitations` 里明说"这是内置解析器算的,不是 readelf 的输出");
12
+ * · 补不了的就**抛**:`requireOk` 让缺命令以 `CommandError` 报出来(历史教训:`pwn_checksec`
13
+ * 曾在缺工具时静默回退,对**任何**二进制都报 `pie=no`);
14
+ * · "没有"要**说成没有**:`bin_strings` 一条都没匹配时返回"no printable strings matching …",
15
+ * 绝不返回空串(空串会被读成"这个文件没有字符串");节表读不到时同理。
8
16
  */
9
17
  export const name = 'fly-binary'
10
18
  export const inject = ['tools', 'shell']
@@ -24,6 +32,19 @@ function shq(value) {
24
32
  * version. `scripts/verify-security-tools.mjs` now fails loudly if a call site
25
33
  * names a seam method the executor does not have.
26
34
  */
35
+ /**
36
+ * 路径参数规范化:以 `-` 开头的路径加 `./` 前缀(2026-10-13)。
37
+ *
38
+ * 为什么不插 `--`:实测本机(macOS,发布目标平台)**objdump(-h/-d/-T)/ base64 -i / otool -hv 都不支持
39
+ * `--`**,插进去直接失败;而 `file -b --version` 会打印 `file-5.41`、`nm -u --version` 会打印
40
+ * `llvm-nm, …`,两者都 **exit 0** ⇒ 工具会把版本串当**数据**返回(`bin_sections { path: '--version' }`
41
+ * 真发生过)。加 `./` 之后它就是一个普通相对路径:不存在就正常报错,存在就是那个文件。
42
+ */
43
+ function safePath(value) {
44
+ const text = String(value ?? '')
45
+ return text.startsWith('-') ? './' + text : text
46
+ }
47
+
27
48
  async function runCmd(shell, argv, exec, opts = {}) {
28
49
  const result = await (await shell.execute(shell.resolve({
29
50
  command: argv.map(shq).join(' '),
@@ -54,9 +75,19 @@ async function runCmd(shell, argv, exec, opts = {}) {
54
75
  */
55
76
  function requireOk(run, what) {
56
77
  if (run.exitCode === 0) return run.stdout
57
- const detail = run.stderr.trim().split('\n').map(line => line.trim()).filter(Boolean).slice(0, 3).join(' | ')
58
- const missing = run.exitCode === 127 || /command not found/iu.test(run.stderr) ? ' — the command is not installed on this host' : ''
59
- const error = new Error(`${what} failed (exit ${run.exitCode})${missing}: ${run.command}${detail === '' ? '' : `\n${detail}`}`)
78
+ // 沙箱拒绝的形状是**结果正文里的标记**(`[sandbox: file access denied under <mode> mode]`,
79
+ // 抄自 @deepseek-ai/dsh-sandbox/index.js:65)—— 它落在 stdout 上;只读 stderr 就会把它丢掉,
80
+ // 于是模型读到的是「命令失败」,而不是「你没有被授权」。所以标记必须原样带进错误文案。
81
+ const sandboxMark = /^\[sandbox: .*\]$/mu.exec(run.stdout)
82
+ const detail = [sandboxMark?.[0], ...run.stderr.trim().split('\n').map(line => line.trim()).filter(Boolean)].slice(0, 3).join(' | ')
83
+ const notInstalled = run.exitCode === 127 || /command not found/iu.test(run.stderr)
84
+ const missing = notInstalled ? ' — the command is not installed on this host' : ''
85
+ // 失败文案要对**读它的模型**可行动(Q6):不是裸 stderr,但也不许臆断原因 ——
86
+ // 只给「这条 exit 属于哪一类」与「下一步可以试什么」,具体判断仍留给读的人。
87
+ const next = notInstalled
88
+ ? 'next: this host lacks that command — look for a built-in fallback in this package, or read the file another way'
89
+ : 'next: check the path, and that the file is what this tool expects (bin_file shows what it is); the line above is the command own reason'
90
+ const error = new Error(`${what} failed (exit ${run.exitCode})${missing}: ${run.command}${detail === '' ? '' : `\n${detail}\n${next}`}`)
60
91
  error.name = 'CommandError'
61
92
  throw error
62
93
  }
@@ -432,10 +463,23 @@ export function apply(ctx) {
432
463
  render: (_args, value) => [{ type: 'text', text: value }],
433
464
  },
434
465
  async execute(args, exec) {
435
- const wc = await runCmd(shell, ['wc', '-c', args.path], exec)
466
+ const path = safePath(args.path)
467
+ const wc = await runCmd(shell, ['wc', '-c', path], exec)
436
468
  const size = requireOk(wc, 'wc -c').trim().split(/\s+/)[0] ?? '0'
437
- const identify = await runCmd(shell, ['file', '-b', args.path], exec)
438
- return `size=${size}\n${requireOk(identify, 'file -b').trim()}`
469
+ const identify = await runCmd(shell, ['file', '-b', path], exec)
470
+ // 证据形状(规划 §5.1):信封 JSON,原文本原样进 observed.dump。
471
+ const text = `size=${size}\n${requireOk(identify, 'file -b').trim()}`
472
+ return JSON.stringify({
473
+ tool: 'bin_file',
474
+ target: args.path,
475
+ operation: 'one wc -c + one file -b (no parsing of the file content)',
476
+ observed: { bytes: Number(size), dump: text },
477
+ confidence: 'observed',
478
+ limitations: [
479
+ '只报 wc -c 的字节数与 file(1) 的判断:**不解析文件内部**(要字段另用 bin_readelf / bin_sections)',
480
+ 'file(1) 的判断来自魔数与片段匹配:损坏或被裁剪的文件也可能被认成某种格式',
481
+ ],
482
+ }, null, 2)
439
483
  },
440
484
  }))
441
485
 
@@ -453,18 +497,39 @@ export function apply(ctx) {
453
497
  },
454
498
  async execute(args, exec) {
455
499
  const min = String(args.min ?? 4)
456
- const run = await runCmd(shell, ['strings', '-n', min, args.path], exec, { stdoutMaxBytes: 1048576 })
500
+ const path = safePath(args.path)
501
+ const run = await runCmd(shell, ['strings', '-n', min, path], exec, { stdoutMaxBytes: 1048576 })
457
502
  let lines = requireOk(run, `strings -n ${min}`).split('\n').filter(Boolean)
458
503
  if (args.grep) {
459
504
  const g = args.grep.toLowerCase()
460
505
  lines = lines.filter((l) => l.toLowerCase().includes(g))
461
506
  }
462
507
  if (lines.length === 0) {
463
- return args.grep === undefined
464
- ? `no printable strings of length ${min} or more in ${args.path}`
465
- : `no printable strings matching ${JSON.stringify(args.grep)} (min length ${min}) in ${args.path}`
508
+ return JSON.stringify({
509
+ tool: 'bin_strings',
510
+ target: args.path,
511
+ operation: `strings -n ${min}` + (args.grep === undefined ? '' : ` filtered by ${JSON.stringify(args.grep)}`),
512
+ observed: { matched: 0, min: Number(min), grep: args.grep ?? null, dump: args.grep === undefined
513
+ ? `no printable strings of length ${min} or more in ${args.path}`
514
+ : `no printable strings matching ${JSON.stringify(args.grep)} (min length ${min}) in ${args.path}` },
515
+ confidence: 'observed',
516
+ limitations: [
517
+ '一条都没匹配上 ⇒ 这是「没有这样的字符串」,不是「这个文件里没有字符串」',
518
+ 'strings(1) 只看可打印字节序列:加密/压缩/UTF-16 里的字符串它看不到',
519
+ ],
520
+ }, null, 2)
466
521
  }
467
- return lines.slice(0, 400).join('\n')
522
+ return JSON.stringify({
523
+ tool: 'bin_strings',
524
+ target: args.path,
525
+ operation: `strings -n ${min}` + (args.grep === undefined ? '' : ` filtered by ${JSON.stringify(args.grep)}`),
526
+ observed: { matched: lines.length, shown: Math.min(lines.length, 400), min: Number(min), grep: args.grep ?? null, dump: lines.slice(0, 400).join('\n') },
527
+ confidence: 'observed',
528
+ limitations: [
529
+ lines.length > 400 ? '只给前 400 条(observed.matched 是匹配总数):要更多就加 grep 缩小范围' : '这次没有截断(匹配数 ≤ 400)',
530
+ 'strings(1) 只看可打印字节序列:加密/压缩/UTF-16 里的字符串它看不到',
531
+ ],
532
+ }, null, 2)
468
533
  },
469
534
  }))
470
535
 
@@ -480,26 +545,48 @@ export function apply(ctx) {
480
545
  render: (_args, value) => [{ type: 'text', text: value }],
481
546
  },
482
547
  async execute(args, exec) {
548
+ // 证据形状(规划 §5.1):四个分支都产出同一个信封;两条方法(GNU readelf / 仓库内置解析器)
549
+ // 必须写进 limitations,否则"本机没有 readelf"会被读成"这份 ELF 就是这样"。
550
+ const envelope = (observed, limitations) => JSON.stringify({
551
+ tool: 'bin_readelf',
552
+ target: args.path,
553
+ operation: `read ELF ${mode} (${flag}) and report it verbatim or via the built-in reader)`,
554
+ observed,
555
+ confidence: 'observed',
556
+ limitations,
557
+ }, null, 2)
483
558
  const mode = args.mode === 'segments' || args.mode === 'dynamic' ? args.mode : 'headers'
484
559
  const flag = mode === 'segments' ? '-l' : mode === 'dynamic' ? '-d' : '-h'
485
560
  // GNU binutils is not part of macOS. When readelf is absent (or refuses the
486
561
  // file) the ELF is parsed here instead: the previous revision threw
487
562
  // "command not found" on every Mac, which made four of the five binary
488
563
  // tools useless on the platform this app ships for.
489
- const real = await runCmd(shell, ['readelf', flag, args.path], exec)
490
- if (real.exitCode === 0 && real.stdout.trim() !== '') return real.stdout
491
- const elf = await elfProbe(rawRun(shell, exec), args.path)
564
+ const path = safePath(args.path)
565
+ const real = await runCmd(shell, ['readelf', flag, path], exec)
566
+ if (real.exitCode === 0 && real.stdout.trim() !== '') {
567
+ return envelope({ method: 'readelf', dump: real.stdout }, [
568
+ 'GNU readelf 的输出原样返回,本工具不改写它',
569
+ ])
570
+ }
571
+ const elf = await elfProbe(rawRun(shell, exec), path)
492
572
  if (elf === null) {
493
- const kind = requireOk(await runCmd(shell, ['file', '-b', args.path], exec), 'file -b').trim()
494
- return [
573
+ const kind = requireOk(await runCmd(shell, ['file', '-b', path], exec), 'file -b').trim()
574
+ return envelope({ method: null, dump: [
495
575
  `readelf ${flag} is unavailable on this host (${real.exitCode === 127 ? 'not installed' : `exit ${real.exitCode}`})`,
496
576
  `${args.path} is not ELF, so a native ELF reader has nothing to report either — file(1) says: ${kind}`,
497
577
  'for Mach-O use rev_disasm / rev_imports (objdump, otool and nm ship with macOS)',
498
- ].join('\n')
578
+ ].join('\n') }, [
579
+ '本机没有可用的 readelf,而这个文件也不是 ELF ⇒ 两条路都给不出读数(observed.dump 里是原因)',
580
+ ])
499
581
  }
500
- if (mode === 'segments') return renderElfSegments(elf)
501
- if (mode === 'dynamic') return await renderElfDynamic(elf)
502
- return renderElfHeader(elf)
582
+ // 本机没有 GNU readelf ⇒ 下面三支都是**仓库内置解析器**算的,limitations 要说清。
583
+ const native = [
584
+ '本机没有 GNU readelf(macOS 不带 binutils)⇒ 这份读数是仓库内置原生解析器算的,不是 readelf 的输出',
585
+ '内置解析器只覆盖本工具声明的字段;readelf 的其它开关(--notes/--relocs 等)它不做',
586
+ ]
587
+ if (mode === 'segments') return envelope({ method: 'native', dump: renderElfSegments(elf) }, native)
588
+ if (mode === 'dynamic') return envelope({ method: 'native', dump: await renderElfDynamic(elf) }, native)
589
+ return envelope({ method: 'native', dump: renderElfHeader(elf) }, native)
503
590
  },
504
591
  }))
505
592
 
@@ -517,11 +604,12 @@ export function apply(ctx) {
517
604
  // 证据形状(规划 §5.1 的第一个试点):返回值是**信封 JSON**,不再是裸表格文本。
518
605
  // 真值判据不变(observed.dump 里是 objdump 原文),新增 confidence 与 limitations ——
519
606
  // 逼这个工具回答"这个结论看不见什么"(本仓最贵的那条教训)。
520
- const run = await runCmd(shell, ['objdump', '-h', args.path], exec)
607
+ const path = safePath(args.path)
608
+ const run = await runCmd(shell, ['objdump', '-h', path], exec)
521
609
  const ok = run.exitCode === 0 && run.stdout.trim() !== ''
522
610
  // GNU binutils is not part of macOS. Instead of an opaque "command not found",
523
611
  // say which method failed and what to use instead (same shape as bin_readelf).
524
- const kind = ok ? null : await runCmd(shell, ['file', '-b', args.path], exec)
612
+ const kind = ok ? null : await runCmd(shell, ['file', '-b', path], exec)
525
613
  return JSON.stringify({
526
614
  tool: 'bin_sections',
527
615
  target: args.path,
@@ -306,6 +306,19 @@ function assertNotPrivileged(command) {
306
306
  * 这一点(本仓既有五个插件的 `runCmd` 正是这个写法;门禁桩现在按真接缝的形状发布
307
307
  * `result()`,所以这类"桩和真接缝不同形"的假绿会在这一层被抓住)。
308
308
  */
309
+ /**
310
+ * 路径参数规范化:以 `-` 开头的路径加 `./` 前缀(2026-10-13)。
311
+ *
312
+ * 为什么不插 `--`:实测本机(macOS,发布目标平台)**objdump(-h/-d/-T)/ base64 -i / otool -hv 都不支持
313
+ * `--`**,插进去直接失败;而 `file -b --version` 会打印 `file-5.41`、`nm -u --version` 会打印
314
+ * `llvm-nm, …`,两者都 **exit 0** ⇒ 工具会把版本串当**数据**返回。加 `./` 之后它就是一个普通相对路径:
315
+ * 不存在就正常报错,存在就是那个文件。
316
+ */
317
+ function safePath(value) {
318
+ const text = String(value ?? '')
319
+ return text.startsWith('-') ? './' + text : text
320
+ }
321
+
309
322
  async function runCmd(shell, argv, exec, opts = {}) {
310
323
  const command = typeof argv === 'string' ? argv : argv.map(shq).join(' ')
311
324
  assertNotPrivileged(command)
@@ -968,11 +981,17 @@ export function readHarDocument(buffer, path) {
968
981
  try {
969
982
  parsed = JSON.parse(payload.toString('utf8').replace(/^\uFEFF/u, ''))
970
983
  } catch (error) {
971
- throw new Error(`${path} 不是合法 JSON${gzip ? '(已按 gzip magic 解压)' : ''}:${error.message}`)
984
+ // Q6(2026-10-13):失败文案要对读它的模型可行动 —— what + why + **what to try next**,
985
+ // 且不臆断具体原因(写错的「为什么」比没有更糟)。
986
+ throw new Error(`${path} 不是合法 JSON${gzip ? '(已按 gzip magic 解压)' : ''}:${error.message}`
987
+ + ' —— 修法:确认这是 HAR 导出(浏览器 DevTools 的 Save all as HAR,或 mitmproxy 的硬拷贝转 HAR);'
988
+ + '只做普通 JSON 校验的话,先看它是不是别的格式。')
972
989
  }
973
990
  const entries = parsed?.log?.entries
974
991
  if (!Array.isArray(entries)) {
975
- throw new Error(`${path} 不像 HAR:log.entries 不是数组(HAR 1.2 的条目都在 log.entries 下)。`)
992
+ throw new Error(`${path} 不像 HAR:log.entries 不是数组(HAR 1.2 的条目都在 log.entries 下)。`
993
+ + ' —— 修法:重新导出一份 HAR,或确认导出的是 HAR 而不是本仓自己的 mitmproxy dump'
994
+ + '(`.mitm` 那种用 bx_flows 读)。')
976
995
  }
977
996
  const creator = parsed.log.creator
978
997
  return {
@@ -1696,7 +1715,7 @@ export function apply(ctx) {
1696
1715
  const root = workspaceRoot(exec)
1697
1716
  const state = readState(root)
1698
1717
  const action = String(args.action ?? '')
1699
- const dump = args.path ?? state?.dump ?? join(root, '.fly-blackbox', 'flows.mitm')
1718
+ const dump = safePath(args.path ?? state?.dump ?? join(root, '.fly-blackbox', 'flows.mitm'))
1700
1719
 
1701
1720
  const size = await statSizeVia(shell, exec, dump, 'dump')
1702
1721
  if (size > 32 * 1024 * 1024) {
@@ -1773,7 +1792,7 @@ export function apply(ctx) {
1773
1792
  },
1774
1793
  output: stringOutput,
1775
1794
  async execute(args, exec) {
1776
- const path = typeof args.path === 'string' ? args.path.trim() : ''
1795
+ const path = safePath(typeof args.path === 'string' ? args.path.trim() : '')
1777
1796
  if (path === '') throw new Error('bx_har 需要 path(.har 或 .har.gz 的路径)')
1778
1797
  const groupBy = args.groupBy ?? 'host'
1779
1798
  if (!HAR_GROUPINGS.includes(groupBy)) {