tyc-cli 0.2.5 → 0.3.0
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/CHANGELOG.md +28 -0
- package/README.md +52 -6
- package/dist/commands/category.js +29 -6
- package/dist/index.js +29 -6
- package/dist/utils/truncate.js +152 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 规范,版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-05-03
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- **输出格式三选一**(互斥优先级 `--md` > `--compact` > `--pretty` / 默认):
|
|
10
|
+
- `--compact`:紧凑单行 JSON(旧默认行为;管道 / `jq` 场景)
|
|
11
|
+
- 默认改为缩进 2 空格 JSON(pretty),人/Agent 通用
|
|
12
|
+
- `--pretty`:同默认;保留 flag 以保持向后兼容 / 显式声明意图
|
|
13
|
+
- `--md`:Markdown 表格(人类阅读 / Agent 上屏;自动渲染 `_summary` / 元数据 / `items` 表格)
|
|
14
|
+
- **截断与落盘**(与输出格式正交,可叠加任意子命令):
|
|
15
|
+
- `--head [N]`(默认 50):仅打印前 N 行;与 `--tail` 同时给则同时输出两端
|
|
16
|
+
- `--tail [M]`(默认 20):仅打印后 M 行
|
|
17
|
+
- `--full`:强制完整输出(最高优先级,覆盖 `--head/--tail/--threshold`)
|
|
18
|
+
- `--threshold <BYTES>`(默认 5000):**字节截断主开关**——超过该字节数从头按字节截断;**不传则永不截断**;与 `--head/--tail` 同时给则按字节截,行参数被忽略
|
|
19
|
+
- `--output-file <PATH>`:把完整结果写入指定路径;**必须显式指定才落盘**(不传则永不落盘)
|
|
20
|
+
- 截断/落盘提示打到 stderr,stdout 保持纯净数据流,便于管道与 `jq` 处理
|
|
21
|
+
|
|
22
|
+
### 变更
|
|
23
|
+
|
|
24
|
+
- 默认 stdout 由紧凑单行 JSON 改为缩进 2 空格 JSON。需要旧默认行为的脚本请显式加 `--compact`
|
|
25
|
+
|
|
26
|
+
## [0.3.0] - 2026-04-29
|
|
27
|
+
|
|
28
|
+
### 新增
|
|
29
|
+
|
|
30
|
+
- **分层发现入口**:`tyc layers` / `tyc L0 list` / `tyc L1 list` / `tyc L2 list`,支持 `--md` / `--json`
|
|
31
|
+
- 每个 tool 的 `tyc ... --help` 标题带 `[L0]/[L1]/[L2]` 徽章,让 Agent 先看结构、后发一次 `tools/call`
|
|
32
|
+
|
|
5
33
|
## [0.2.0] - 2026-04-27
|
|
6
34
|
|
|
7
35
|
### 架构重构
|
package/README.md
CHANGED
|
@@ -100,13 +100,58 @@ tyc executive personnel-dishonest "..." --humanName "张三"
|
|
|
100
100
|
|
|
101
101
|
### 全局选项
|
|
102
102
|
|
|
103
|
+
#### 输出格式(互斥优先级 `--md` > `--compact` > `--pretty` / 默认)
|
|
104
|
+
|
|
103
105
|
| 选项 | 说明 |
|
|
104
106
|
|------|------|
|
|
105
|
-
|
|
|
107
|
+
| _(默认)_ | 缩进 2 空格 JSON(pretty)—— 人/Agent 都好读,已成默认 |
|
|
108
|
+
| `--pretty` | 同默认;保留 flag 以保持向后兼容 / 显式声明意图 |
|
|
109
|
+
| `--compact` | 紧凑单行 JSON(旧默认行为;管道 / `jq` 场景) |
|
|
106
110
|
| `--md` | Markdown 表格化输出(人类阅读 / Agent 上屏) |
|
|
107
|
-
| `--verbose` | 打印 MCP 请求详情到 stderr
|
|
111
|
+
| `--verbose` | 打印 MCP 请求详情到 stderr(与上述输出格式正交) |
|
|
112
|
+
|
|
113
|
+
#### 输出截断 / 落盘(与上述输出格式正交,可叠加任意子命令)
|
|
114
|
+
|
|
115
|
+
| 选项 | 默认值 | 说明 |
|
|
116
|
+
|------|-------:|------|
|
|
117
|
+
| `--head [N]` | 50 | 仅打印前 N 行;与 `--tail` 同时给则同时输出两端,否则只输出 head |
|
|
118
|
+
| `--tail [M]` | 20 | 仅打印后 M 行;与 `--head` 同时给则同时输出两端 |
|
|
119
|
+
| `--full` | false | 强制完整输出(最高优先级,覆盖 `--head/--tail/--threshold`) |
|
|
120
|
+
| `--threshold <BYTES>` | 5000 | **字节截断主开关**:超过该字节数则从头按字节截断;**不传则永不截断** |
|
|
121
|
+
| `--output-file <PATH>` | — | 把完整结果写入指定路径;**不传则永不落盘**(无自动落盘) |
|
|
122
|
+
|
|
123
|
+
> ⚠️ **截断决策树(与 stdout / 落盘行为)**
|
|
124
|
+
>
|
|
125
|
+
> 1. `--output-file` 给了 → 落盘永远写**完整**结果(与 stdout 是否截断无关)
|
|
126
|
+
> 2. `--full` 最高优先级 → stdout 也输出完整
|
|
127
|
+
> 3. 否则若 `--threshold N` 给了:超过 N 字节从头按字节截;**此模式下 `--head/--tail` 被忽略**
|
|
128
|
+
> 4. 否则若 `--head/--tail` 任一给了 → 行级截断
|
|
129
|
+
> 5. 否则 → 无截断、无落盘
|
|
130
|
+
>
|
|
131
|
+
> 截断与落盘的提示信息("已写入 …"、"输出被 head=5 截断 …")打到 stderr,stdout 仍为可程序化解析的纯净数据流。
|
|
132
|
+
|
|
133
|
+
#### 用法示例
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# 调试时只看头部
|
|
137
|
+
tyc company registration-info "百度" --head # 默认 50 行
|
|
138
|
+
tyc company registration-info "百度" --head 10 # 自定义 10 行
|
|
139
|
+
|
|
140
|
+
# 看头看尾,确认数据起止
|
|
141
|
+
tyc company key-personnel "百度" --head 5 --tail 5
|
|
108
142
|
|
|
109
|
-
|
|
143
|
+
# 字节预算控制(适合塞进 LLM context)
|
|
144
|
+
tyc risk overview "百度" --threshold 4000
|
|
145
|
+
|
|
146
|
+
# stdout 简洁 + 完整结果落盘
|
|
147
|
+
tyc company equity-tree "百度" --head 30 --output-file ./equity-tree.json
|
|
148
|
+
|
|
149
|
+
# 强制全量(覆盖以上一切截断)
|
|
150
|
+
tyc operation bidding-info "百度" --full
|
|
151
|
+
|
|
152
|
+
# 管道场景沿用旧紧凑 JSON
|
|
153
|
+
tyc company registration-info "百度" --compact | jq .name
|
|
154
|
+
```
|
|
110
155
|
|
|
111
156
|
### 环境变量覆盖
|
|
112
157
|
|
|
@@ -201,7 +246,7 @@ tyc executive person-risk-overview "..." --humanName "张三"
|
|
|
201
246
|
│ │
|
|
202
247
|
│ └─ 多源并发聚合 · 时间戳格式化 · _summary 注入 · 空结果归一化
|
|
203
248
|
│
|
|
204
|
-
└─ 仅命令树 · 参数透传 · Session 管理 ·
|
|
249
|
+
└─ 仅命令树 · 参数透传 · Session 管理 · pretty/--md/--compact 呈现 · --head/--tail/--full/--threshold/--output-file 截断与落盘
|
|
205
250
|
```
|
|
206
251
|
|
|
207
252
|
**CLI 的职责**:
|
|
@@ -210,7 +255,7 @@ tyc executive person-risk-overview "..." --humanName "张三"
|
|
|
210
255
|
2. 组装 `tools/call` JSON-RPC 请求,透传 `Authorization` header
|
|
211
256
|
3. Session 管理(`initialize` + 24h 缓存 + 失效重建)
|
|
212
257
|
4. 解析 MCP Streamable HTTP 响应(纯 JSON 或 SSE)
|
|
213
|
-
5.
|
|
258
|
+
5. 格式化输出(默认 pretty / `--compact` / `--md`),并可叠加 `--head/--tail/--full/--threshold/--output-file` 做截断与落盘
|
|
214
259
|
|
|
215
260
|
**CLI 不做**:
|
|
216
261
|
|
|
@@ -243,7 +288,8 @@ tyc-cli/
|
|
|
243
288
|
│ ├── init.ts # tyc init
|
|
244
289
|
│ └── category.ts # 动态注册 6 分类 × N 方法
|
|
245
290
|
└── utils/
|
|
246
|
-
|
|
291
|
+
├── jsonToMarkdown.ts # --md 选项的 Markdown 渲染
|
|
292
|
+
└── truncate.ts # --head/--tail/--full/--threshold/--output-file 截断与落盘
|
|
247
293
|
```
|
|
248
294
|
|
|
249
295
|
---
|
|
@@ -2,6 +2,7 @@ import { getCategories, getToolsByGroup } from "../registry.js";
|
|
|
2
2
|
import { resolveConfig } from "../config.js";
|
|
3
3
|
import { callTool } from "../mcpClient.js";
|
|
4
4
|
import { jsonToMarkdown } from "../utils/jsonToMarkdown.js";
|
|
5
|
+
import { applyTruncation, resolveOptionalInt, TRUNCATE_DEFAULTS, } from "../utils/truncate.js";
|
|
5
6
|
export function registerCategoryCommands(program) {
|
|
6
7
|
for (const cat of getCategories()) {
|
|
7
8
|
const tools = getToolsByGroup(cat.group);
|
|
@@ -81,21 +82,43 @@ function emit(result, opts, toolName) {
|
|
|
81
82
|
payload = JSON.parse(text);
|
|
82
83
|
}
|
|
83
84
|
catch {
|
|
84
|
-
// 非 JSON
|
|
85
|
-
|
|
85
|
+
// 非 JSON 文本:直接原样输出(仍走截断 / 落盘)
|
|
86
|
+
emitWithTruncation(text.endsWith("\n") ? text.slice(0, -1) : text, opts);
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
89
|
+
// 输出模式选择(互斥优先级 --md > --compact > --pretty / 默认 = pretty)
|
|
88
90
|
let rendered;
|
|
89
91
|
if (opts.md) {
|
|
90
92
|
rendered = jsonToMarkdown(payload, toolName);
|
|
91
93
|
}
|
|
92
|
-
else if (opts.
|
|
93
|
-
rendered = JSON.stringify(payload
|
|
94
|
+
else if (opts.compact) {
|
|
95
|
+
rendered = JSON.stringify(payload);
|
|
94
96
|
}
|
|
95
97
|
else {
|
|
96
|
-
|
|
98
|
+
// 默认 = pretty(含 --pretty 显式情况)
|
|
99
|
+
rendered = JSON.stringify(payload, null, 2);
|
|
100
|
+
}
|
|
101
|
+
emitWithTruncation(rendered, opts);
|
|
102
|
+
}
|
|
103
|
+
function emitWithTruncation(rendered, opts) {
|
|
104
|
+
// 解析 head / tail / threshold:commander 对 `[n]` 在未传值时给 true
|
|
105
|
+
const head = resolveOptionalInt(opts.head, TRUNCATE_DEFAULTS.HEAD);
|
|
106
|
+
const tail = resolveOptionalInt(opts.tail, TRUNCATE_DEFAULTS.TAIL);
|
|
107
|
+
const threshold = resolveOptionalInt(opts.threshold, TRUNCATE_DEFAULTS.THRESHOLD);
|
|
108
|
+
const outputFile = typeof opts.outputFile === "string" && opts.outputFile.length > 0
|
|
109
|
+
? opts.outputFile
|
|
110
|
+
: undefined;
|
|
111
|
+
const r = applyTruncation(rendered, {
|
|
112
|
+
full: !!opts.full,
|
|
113
|
+
head,
|
|
114
|
+
tail,
|
|
115
|
+
threshold,
|
|
116
|
+
outputFile,
|
|
117
|
+
});
|
|
118
|
+
console.log(r.rendered);
|
|
119
|
+
for (const n of r.notices) {
|
|
120
|
+
process.stderr.write(n + "\n");
|
|
97
121
|
}
|
|
98
|
-
console.log(rendered);
|
|
99
122
|
}
|
|
100
123
|
function extractText(result) {
|
|
101
124
|
const content = result.content || [];
|
package/dist/index.js
CHANGED
|
@@ -59,12 +59,29 @@ DISCOVER BY CATEGORY (orthogonal to layers, 6 groups)
|
|
|
59
59
|
|
|
60
60
|
${renderCategoryLines()}
|
|
61
61
|
|
|
62
|
-
OUTPUT FORMATS (mutually exclusive, priority: --md > --
|
|
62
|
+
OUTPUT FORMATS (mutually exclusive, priority: --md > --compact > --pretty / default)
|
|
63
63
|
|
|
64
|
-
(default)
|
|
65
|
-
--pretty
|
|
64
|
+
(default) indented JSON (pretty) — readable for both humans and agents
|
|
65
|
+
--pretty same as default (kept for backward compatibility / explicit intent)
|
|
66
|
+
--compact compact single-line JSON (pipe / jq friendly; old default behavior)
|
|
66
67
|
--md Markdown tables (human + agent on-screen)
|
|
67
|
-
--verbose also print MCP request details to stderr
|
|
68
|
+
--verbose also print MCP request details to stderr (orthogonal to above)
|
|
69
|
+
|
|
70
|
+
OUTPUT TRUNCATION & DUMP (orthogonal — apply to any sub-command)
|
|
71
|
+
|
|
72
|
+
--head [N] print only the first N lines (N defaults to 50 if flag given alone)
|
|
73
|
+
--tail [M] print only the last M lines (M defaults to 20 if flag given alone)
|
|
74
|
+
· --head + --tail together: head + "... omitted ..." + tail
|
|
75
|
+
· either flag alone: that side only
|
|
76
|
+
--full force-print the FULL rendered content (no truncation; highest priority)
|
|
77
|
+
--threshold <BYTES> master switch for byte-truncation. Without --threshold there is
|
|
78
|
+
NO automatic truncation, regardless of output size.
|
|
79
|
+
With --threshold N: when rendered output > N bytes, byte-truncate
|
|
80
|
+
from the head; --head / --tail are IGNORED in this mode.
|
|
81
|
+
--output-file <PATH> write the FULL rendered content to PATH. **No file is written
|
|
82
|
+
unless --output-file is explicitly passed** (no auto-dump).
|
|
83
|
+
Combine with --head/--tail/--threshold to keep stdout terse
|
|
84
|
+
while preserving the full result on disk.
|
|
68
85
|
|
|
69
86
|
SETUP
|
|
70
87
|
|
|
@@ -97,10 +114,16 @@ function renderPriorityLines() {
|
|
|
97
114
|
const program = new Command()
|
|
98
115
|
.name("tyc")
|
|
99
116
|
.description(`Tianyan AI CLI — ${getTotalCount()} commercial-data tools · L0=${getLayerCount("L0")} L1=${getLayerCount("L1")} L2=${getLayerCount("L2")} L3=${getLayerCount("L3")} · the AI-native gateway (pair with the TA MCP Server)`)
|
|
100
|
-
.version("0.
|
|
101
|
-
.option("--pretty", "
|
|
117
|
+
.version("0.4.0")
|
|
118
|
+
.option("--pretty", "缩进 JSON 输出(默认行为,flag 保留以保持向后兼容)")
|
|
102
119
|
.option("--md", "Markdown 表格化输出(适合人类阅读 / Agent 上屏)")
|
|
120
|
+
.option("--compact", "紧凑单行 JSON(管道 / jq 场景;为旧默认行为)")
|
|
103
121
|
.option("--verbose", "打印 MCP 请求详情到 stderr")
|
|
122
|
+
.option("--head [n]", "仅输出前 N 行(不传值默认 50;与 --tail 同时给则同时输出两端)")
|
|
123
|
+
.option("--tail [m]", "仅输出后 M 行(不传值默认 20;与 --head 同时给则同时输出两端)")
|
|
124
|
+
.option("--full", "强制输出完整内容(最高优先级,覆盖 --head/--tail/--threshold)")
|
|
125
|
+
.option("--output-file <path>", "把完整结果写入指定路径(必须显式指定;不传则永不落盘)")
|
|
126
|
+
.option("--threshold <bytes>", "字节截断主开关:超过该字节数则从头按字节截断;不传则永不截断")
|
|
104
127
|
.addHelpText("after", helpText);
|
|
105
128
|
registerInitCommand(program);
|
|
106
129
|
registerLayerCommands(program);
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// 输出截断 / 落盘工具
|
|
2
|
+
//
|
|
3
|
+
// 设计原则(与用户对齐):
|
|
4
|
+
// 1. --threshold 是截断逻辑的"主开关"。**没有 --threshold 时永不按字节截断**。
|
|
5
|
+
// 2. --head N / --tail M 各自显式生效,互不依赖;同时给则同时输出两端。
|
|
6
|
+
// head/tail 是 line-based、对人类可读输出(pretty / md / compact)都适用。
|
|
7
|
+
// 3. --threshold 与 --head/--tail 同时出现时按"从头按字节截"语义优先(用户原话),
|
|
8
|
+
// 避免用户的字节预算被行截断绕过。
|
|
9
|
+
// 4. --full 最高优先级:永远输出完整内容;仍可与 --output-file 共存(落盘 + 全量打印)。
|
|
10
|
+
// 5. **不指定 --output-file 永不落盘**(用户明确要求)。
|
|
11
|
+
//
|
|
12
|
+
// 渲染顺序:先决定 mode(md / pretty / compact),渲染成字符串后送入 truncate;
|
|
13
|
+
// 落盘永远写"渲染后的字符串",与 stdout 看到的同源,便于 diff / 复盘。
|
|
14
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { dirname, resolve } from "node:path";
|
|
16
|
+
const HEAD_DEFAULT = 50;
|
|
17
|
+
const TAIL_DEFAULT = 20;
|
|
18
|
+
const THRESHOLD_DEFAULT = 5000;
|
|
19
|
+
/**
|
|
20
|
+
* 解析 commander 接收到的可选值参数:
|
|
21
|
+
* - undefined:用户未指定该 flag
|
|
22
|
+
* - true:用户指定了 flag 但未给值(commander `[n]` 语法)→ 用 fallback
|
|
23
|
+
* - string:用户指定了具体值 → parseInt
|
|
24
|
+
* - 非法值 → fallback(保持宽容;CLI 不该因为 --head abc 直接崩)
|
|
25
|
+
*/
|
|
26
|
+
export function resolveOptionalInt(raw, fallback) {
|
|
27
|
+
if (raw === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (raw === true || raw === "")
|
|
30
|
+
return fallback;
|
|
31
|
+
const n = parseInt(String(raw), 10);
|
|
32
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
33
|
+
}
|
|
34
|
+
/** Buffer 字节数(与 stdout 实际字节数一致;JS .length 是 UTF-16 code units,会高估纯 ASCII / 低估中文) */
|
|
35
|
+
function byteLen(s) {
|
|
36
|
+
return Buffer.byteLength(s, "utf8");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 按字节截到 ≤ maxBytes,且回退到 UTF-8 字符边界(避免半个汉字)。
|
|
40
|
+
* 我们只截 head 端(用户原话:"直接从头到尾按字节数量来截断")。
|
|
41
|
+
*/
|
|
42
|
+
function truncateByBytes(s, maxBytes) {
|
|
43
|
+
const buf = Buffer.from(s, "utf8");
|
|
44
|
+
if (buf.length <= maxBytes)
|
|
45
|
+
return s;
|
|
46
|
+
let end = maxBytes;
|
|
47
|
+
// UTF-8 续字节模式 10xxxxxx:往回退到上一个起始字节
|
|
48
|
+
while (end > 0 && (buf[end] & 0xc0) === 0x80)
|
|
49
|
+
end--;
|
|
50
|
+
return buf.subarray(0, end).toString("utf8");
|
|
51
|
+
}
|
|
52
|
+
function truncateByLines(s, head, tail) {
|
|
53
|
+
const lines = s.split("\n");
|
|
54
|
+
const total = lines.length;
|
|
55
|
+
if (head === undefined && tail === undefined) {
|
|
56
|
+
return { out: s, truncated: false, total };
|
|
57
|
+
}
|
|
58
|
+
const h = head ?? 0;
|
|
59
|
+
const t = tail ?? 0;
|
|
60
|
+
if (h + t >= total) {
|
|
61
|
+
return { out: s, truncated: false, total };
|
|
62
|
+
}
|
|
63
|
+
const omitted = total - h - t;
|
|
64
|
+
const parts = [];
|
|
65
|
+
if (head !== undefined)
|
|
66
|
+
parts.push(...lines.slice(0, h));
|
|
67
|
+
if (head !== undefined && tail !== undefined) {
|
|
68
|
+
parts.push(`... (省略中间 ${omitted} 行) ...`);
|
|
69
|
+
parts.push(...lines.slice(-t));
|
|
70
|
+
}
|
|
71
|
+
else if (head !== undefined) {
|
|
72
|
+
parts.push(`... (省略后续 ${omitted} 行) ...`);
|
|
73
|
+
}
|
|
74
|
+
else if (tail !== undefined) {
|
|
75
|
+
parts.push(`... (省略前 ${omitted} 行) ...`);
|
|
76
|
+
parts.push(...lines.slice(-t));
|
|
77
|
+
}
|
|
78
|
+
return { out: parts.join("\n"), truncated: true, total };
|
|
79
|
+
}
|
|
80
|
+
function writeDump(absPath, content) {
|
|
81
|
+
const dir = dirname(absPath);
|
|
82
|
+
mkdirSync(dir, { recursive: true });
|
|
83
|
+
writeFileSync(absPath, content, { encoding: "utf8" });
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 按用户语义对渲染好的字符串做截断 / 落盘。
|
|
87
|
+
*
|
|
88
|
+
* 决策树(每条分支互斥):
|
|
89
|
+
* 1. --output-file 给了 → 落盘(写"全量"原文,与 stdout 截断版本无关)
|
|
90
|
+
* 2. --full → stdout 全量
|
|
91
|
+
* 3. --threshold 给了:
|
|
92
|
+
* size > threshold → 字节截到 threshold(head 端)
|
|
93
|
+
* size ≤ threshold → 全量
|
|
94
|
+
* (此分支下 --head/--tail 被忽略,符合"从头按字节截断"语义)
|
|
95
|
+
* 4. --head 或 --tail 给了(且无 --threshold)→ line-based 头/尾/头+尾截断
|
|
96
|
+
* 5. 都没给 → 全量
|
|
97
|
+
*/
|
|
98
|
+
export function applyTruncation(rendered, opts) {
|
|
99
|
+
const notices = [];
|
|
100
|
+
let fileWritten;
|
|
101
|
+
// 1. 落盘永远写全量
|
|
102
|
+
if (opts.outputFile && opts.outputFile.length > 0) {
|
|
103
|
+
const abs = resolve(opts.outputFile);
|
|
104
|
+
try {
|
|
105
|
+
writeDump(abs, rendered);
|
|
106
|
+
fileWritten = abs;
|
|
107
|
+
notices.push(`已写入完整结果到 ${abs}(${byteLen(rendered)} 字节)`);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
111
|
+
notices.push(`⚠️ --output-file 写入失败:${msg}(继续打印 stdout)`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// 2. --full 最高优先级
|
|
115
|
+
if (opts.full) {
|
|
116
|
+
return { rendered, fileWritten, notices };
|
|
117
|
+
}
|
|
118
|
+
// 3. --threshold 主导:字节截
|
|
119
|
+
if (opts.threshold !== undefined) {
|
|
120
|
+
const size = byteLen(rendered);
|
|
121
|
+
if (size > opts.threshold) {
|
|
122
|
+
const cut = truncateByBytes(rendered, opts.threshold);
|
|
123
|
+
const omitted = size - byteLen(cut);
|
|
124
|
+
const tail = `\n... (已按 --threshold=${opts.threshold} 字节截断,省略尾部约 ${omitted} 字节) ...`;
|
|
125
|
+
notices.push(`输出被 --threshold=${opts.threshold} 截断(原始 ${size} 字节 → 输出 ${byteLen(cut)} 字节)` +
|
|
126
|
+
(fileWritten ? `;完整内容见 ${fileWritten}` : ";如需完整内容追加 --output-file <path>"));
|
|
127
|
+
return { rendered: cut + tail, fileWritten, notices };
|
|
128
|
+
}
|
|
129
|
+
return { rendered, fileWritten, notices };
|
|
130
|
+
}
|
|
131
|
+
// 4. line-based head/tail(无 --threshold)
|
|
132
|
+
if (opts.head !== undefined || opts.tail !== undefined) {
|
|
133
|
+
const r = truncateByLines(rendered, opts.head, opts.tail);
|
|
134
|
+
if (r.truncated) {
|
|
135
|
+
const segs = [];
|
|
136
|
+
if (opts.head !== undefined)
|
|
137
|
+
segs.push(`head=${opts.head}`);
|
|
138
|
+
if (opts.tail !== undefined)
|
|
139
|
+
segs.push(`tail=${opts.tail}`);
|
|
140
|
+
notices.push(`输出被 ${segs.join(" / ")} 截断(原始 ${r.total} 行)` +
|
|
141
|
+
(fileWritten ? `;完整内容见 ${fileWritten}` : ";如需完整内容追加 --output-file <path>"));
|
|
142
|
+
}
|
|
143
|
+
return { rendered: r.out, fileWritten, notices };
|
|
144
|
+
}
|
|
145
|
+
// 5. 无任何截断 flag
|
|
146
|
+
return { rendered, fileWritten, notices };
|
|
147
|
+
}
|
|
148
|
+
export const TRUNCATE_DEFAULTS = {
|
|
149
|
+
HEAD: HEAD_DEFAULT,
|
|
150
|
+
TAIL: TAIL_DEFAULT,
|
|
151
|
+
THRESHOLD: THRESHOLD_DEFAULT,
|
|
152
|
+
};
|