@mison/wecom-cleaner 1.1.0 → 1.2.1

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.
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ 用法:
7
+ recycle_maintain_report.sh [--execute true|false]
8
+ [--retention-enabled true|false]
9
+ [--retention-max-age-days <int>]
10
+ [--retention-min-keep-batches <int>]
11
+ [--retention-size-threshold-gb <int>]
12
+ [--root <path>] [--state-root <path>]
13
+
14
+ 说明:
15
+ - 默认只做预演(--execute false)。
16
+ - --execute true 且候选批次>0 时,执行真实回收区治理。
17
+ EOF
18
+ }
19
+
20
+ if ! command -v jq >/dev/null 2>&1; then
21
+ echo "错误:缺少 jq,请先安装(brew install jq)。" >&2
22
+ exit 2
23
+ fi
24
+
25
+ if ! command -v wecom-cleaner >/dev/null 2>&1; then
26
+ echo "错误:未找到 wecom-cleaner 命令,请先安装 @mison/wecom-cleaner。" >&2
27
+ exit 2
28
+ fi
29
+
30
+ EXECUTE="false"
31
+ RETENTION_ENABLED=""
32
+ RETENTION_MAX_AGE_DAYS=""
33
+ RETENTION_MIN_KEEP_BATCHES=""
34
+ RETENTION_SIZE_THRESHOLD_GB=""
35
+ ROOT=""
36
+ STATE_ROOT=""
37
+
38
+ while [[ $# -gt 0 ]]; do
39
+ case "$1" in
40
+ --execute)
41
+ EXECUTE="${2:-false}"
42
+ shift 2
43
+ ;;
44
+ --retention-enabled)
45
+ RETENTION_ENABLED="${2:-}"
46
+ shift 2
47
+ ;;
48
+ --retention-max-age-days)
49
+ RETENTION_MAX_AGE_DAYS="${2:-}"
50
+ shift 2
51
+ ;;
52
+ --retention-min-keep-batches)
53
+ RETENTION_MIN_KEEP_BATCHES="${2:-}"
54
+ shift 2
55
+ ;;
56
+ --retention-size-threshold-gb)
57
+ RETENTION_SIZE_THRESHOLD_GB="${2:-}"
58
+ shift 2
59
+ ;;
60
+ --root)
61
+ ROOT="${2:-}"
62
+ shift 2
63
+ ;;
64
+ --state-root)
65
+ STATE_ROOT="${2:-}"
66
+ shift 2
67
+ ;;
68
+ -h | --help)
69
+ usage
70
+ exit 0
71
+ ;;
72
+ *)
73
+ echo "错误:未知参数 $1" >&2
74
+ usage
75
+ exit 2
76
+ ;;
77
+ esac
78
+ done
79
+
80
+ case "$EXECUTE" in
81
+ true | false) ;;
82
+ *)
83
+ echo "错误:--execute 只能是 true 或 false" >&2
84
+ exit 2
85
+ ;;
86
+ esac
87
+
88
+ human_bytes() {
89
+ local bytes="${1:-0}"
90
+ awk -v b="$bytes" '
91
+ BEGIN {
92
+ split("B KB MB GB TB", u, " ");
93
+ i=1;
94
+ while (b>=1024 && i<5) { b=b/1024; i++; }
95
+ if (i==1) printf "%d %s", b, u[i];
96
+ else printf "%.2f %s", b, u[i];
97
+ }
98
+ '
99
+ }
100
+
101
+ local_time() {
102
+ local ts="${1:-0}"
103
+ if [[ "$ts" -le 0 ]]; then
104
+ printf '%s' '-'
105
+ return
106
+ fi
107
+ date -r "$((ts / 1000))" '+%Y-%m-%d %H:%M'
108
+ }
109
+
110
+ PREVIEW_JSON="$(mktemp -t wecom-recycle-preview.XXXX.json)"
111
+ EXEC_JSON="$(mktemp -t wecom-recycle-exec.XXXX.json)"
112
+ VERIFY_JSON="$(mktemp -t wecom-recycle-verify.XXXX.json)"
113
+ PREVIEW_ERR="$(mktemp -t wecom-recycle-preview.XXXX.err)"
114
+ EXEC_ERR="$(mktemp -t wecom-recycle-exec.XXXX.err)"
115
+ VERIFY_ERR="$(mktemp -t wecom-recycle-verify.XXXX.err)"
116
+ trap 'rm -f "$PREVIEW_JSON" "$EXEC_JSON" "$VERIFY_JSON" "$PREVIEW_ERR" "$EXEC_ERR" "$VERIFY_ERR"' EXIT
117
+
118
+ run_cmd_to_file() {
119
+ local dry_run="$1"
120
+ local output_file="$2"
121
+ local err_file="$3"
122
+ local cmd_parts=(--recycle-maintain --output json --dry-run "$dry_run")
123
+ if [[ -n "$RETENTION_ENABLED" ]]; then
124
+ cmd_parts+=(--retention-enabled "$RETENTION_ENABLED")
125
+ fi
126
+ if [[ -n "$RETENTION_MAX_AGE_DAYS" ]]; then
127
+ cmd_parts+=(--retention-max-age-days "$RETENTION_MAX_AGE_DAYS")
128
+ fi
129
+ if [[ -n "$RETENTION_MIN_KEEP_BATCHES" ]]; then
130
+ cmd_parts+=(--retention-min-keep-batches "$RETENTION_MIN_KEEP_BATCHES")
131
+ fi
132
+ if [[ -n "$RETENTION_SIZE_THRESHOLD_GB" ]]; then
133
+ cmd_parts+=(--retention-size-threshold-gb "$RETENTION_SIZE_THRESHOLD_GB")
134
+ fi
135
+ if [[ -n "$ROOT" ]]; then
136
+ cmd_parts+=(--root "$ROOT")
137
+ fi
138
+ if [[ -n "$STATE_ROOT" ]]; then
139
+ cmd_parts+=(--state-root "$STATE_ROOT")
140
+ fi
141
+ if [[ "$dry_run" == "false" ]]; then
142
+ cmd_parts+=(--yes)
143
+ fi
144
+ if ! wecom-cleaner "${cmd_parts[@]}" >"$output_file" 2>"$err_file"; then
145
+ err_head="$(head -n 3 "$err_file" 2>/dev/null || true)"
146
+ echo "执行失败(dry-run=${dry_run}):${err_head:-未知错误}" >&2
147
+ return 1
148
+ fi
149
+ }
150
+
151
+ run_cmd_to_file true "$PREVIEW_JSON" "$PREVIEW_ERR"
152
+
153
+ candidate_count="$(jq -r '.summary.candidateCount // 0' "$PREVIEW_JSON")"
154
+ deleted_batches_preview="$(jq -r '.summary.deletedBatches // 0' "$PREVIEW_JSON")"
155
+ deleted_bytes_preview="$(jq -r '.summary.deletedBytes // 0' "$PREVIEW_JSON")"
156
+ failed_batches_preview="$(jq -r '.summary.failedBatches // 0' "$PREVIEW_JSON")"
157
+ selected_by_age="$(jq -r '.summary.selectedByAge // 0' "$PREVIEW_JSON")"
158
+ selected_by_size="$(jq -r '.summary.selectedBySize // 0' "$PREVIEW_JSON")"
159
+ before_batches="$(jq -r '.data.report.before.totalBatches // 0' "$PREVIEW_JSON")"
160
+ before_bytes="$(jq -r '.data.report.before.totalBytes // 0' "$PREVIEW_JSON")"
161
+ after_batches_preview="$(jq -r '.summary.remainingBatches // 0' "$PREVIEW_JSON")"
162
+ after_bytes_preview="$(jq -r '.summary.remainingBytes // 0' "$PREVIEW_JSON")"
163
+ threshold_bytes="$(jq -r '.data.report.thresholdBytes // 0' "$PREVIEW_JSON")"
164
+ over_threshold="$(jq -r '.data.report.overThreshold // false' "$PREVIEW_JSON")"
165
+ engine="$(jq -r '.meta.engine // "unknown"' "$PREVIEW_JSON")"
166
+ duration_preview="$(jq -r '.meta.durationMs // 0' "$PREVIEW_JSON")"
167
+ warnings_preview="$(jq -r '(.warnings // []) | length' "$PREVIEW_JSON")"
168
+ errors_preview="$(jq -r '(.errors // []) | length' "$PREVIEW_JSON")"
169
+
170
+ executed="false"
171
+ deleted_batches_exec=0
172
+ deleted_bytes_exec=0
173
+ failed_batches_exec=0
174
+ after_batches_exec="$after_batches_preview"
175
+ after_bytes_exec="$after_bytes_preview"
176
+ duration_exec=0
177
+ duration_verify=0
178
+ warnings_exec=0
179
+ warnings_verify=0
180
+ errors_exec=0
181
+ errors_verify=0
182
+ candidate_count_verify="$candidate_count"
183
+
184
+ if [[ "$candidate_count" -gt 0 && "$EXECUTE" == "true" ]]; then
185
+ run_cmd_to_file false "$EXEC_JSON" "$EXEC_ERR"
186
+ executed="true"
187
+ deleted_batches_exec="$(jq -r '.summary.deletedBatches // 0' "$EXEC_JSON")"
188
+ deleted_bytes_exec="$(jq -r '.summary.deletedBytes // 0' "$EXEC_JSON")"
189
+ failed_batches_exec="$(jq -r '.summary.failedBatches // 0' "$EXEC_JSON")"
190
+ after_batches_exec="$(jq -r '.summary.remainingBatches // 0' "$EXEC_JSON")"
191
+ after_bytes_exec="$(jq -r '.summary.remainingBytes // 0' "$EXEC_JSON")"
192
+ duration_exec="$(jq -r '.meta.durationMs // 0' "$EXEC_JSON")"
193
+ warnings_exec="$(jq -r '(.warnings // []) | length' "$EXEC_JSON")"
194
+ errors_exec="$(jq -r '(.errors // []) | length' "$EXEC_JSON")"
195
+
196
+ run_cmd_to_file true "$VERIFY_JSON" "$VERIFY_ERR"
197
+ candidate_count_verify="$(jq -r '.summary.candidateCount // 0' "$VERIFY_JSON")"
198
+ duration_verify="$(jq -r '.meta.durationMs // 0' "$VERIFY_JSON")"
199
+ warnings_verify="$(jq -r '(.warnings // []) | length' "$VERIFY_JSON")"
200
+ errors_verify="$(jq -r '(.errors // []) | length' "$VERIFY_JSON")"
201
+ fi
202
+
203
+ duration_total=$((duration_preview + duration_exec + duration_verify))
204
+ warnings_total=$((warnings_preview + warnings_exec + warnings_verify))
205
+ errors_total=$((errors_preview + errors_exec + errors_verify))
206
+
207
+ printf '\n=== 回收区治理结果(给用户)===\n'
208
+ if [[ "$executed" == "true" ]]; then
209
+ printf -- '- 已完成:已处理 %s 个候选批次,释放 %s。\n' "$deleted_batches_exec" "$(human_bytes "$deleted_bytes_exec")"
210
+ else
211
+ printf -- '- 已完成预演:发现 %s 个候选批次,预计可释放 %s。\n' "$candidate_count" "$(human_bytes "$deleted_bytes_preview")"
212
+ fi
213
+ printf -- '- 目标:回收区瘦身,释放历史批次占用空间。\n'
214
+
215
+ printf '\n你关心的范围\n'
216
+ printf -- '- 预演前:%s 个批次,%s\n' "$before_batches" "$(human_bytes "$before_bytes")"
217
+ printf -- '- 阈值:%s(当前%s阈值)\n' "$(human_bytes "$threshold_bytes")" "$( [[ "$over_threshold" == "true" ]] && printf '高于' || printf '未高于' )"
218
+ printf -- '- 候选批次:%s(按年龄 %s,按容量 %s)\n' "$candidate_count" "$selected_by_age" "$selected_by_size"
219
+
220
+ printf '\n治理结果总览\n'
221
+ if [[ "$executed" == "true" ]]; then
222
+ printf -- '- 实际删除批次:%s,释放 %s,失败 %s\n' \
223
+ "$deleted_batches_exec" "$(human_bytes "$deleted_bytes_exec")" "$failed_batches_exec"
224
+ printf -- '- 治理后:%s 个批次,%s\n' "$after_batches_exec" "$(human_bytes "$after_bytes_exec")"
225
+ printf -- '- 复核:当前候选批次剩余 %s 个\n' "$candidate_count_verify"
226
+ else
227
+ printf -- '- 预演可删批次:%s,预计释放 %s,失败 %s\n' \
228
+ "$deleted_batches_preview" "$(human_bytes "$deleted_bytes_preview")" "$failed_batches_preview"
229
+ printf -- '- 执行状态:未执行真实治理(仅预演)。\n'
230
+ printf -- '- 预演后估计:%s 个批次,%s\n' "$after_batches_preview" "$(human_bytes "$after_bytes_preview")"
231
+ fi
232
+
233
+ printf '\n候选批次清单(Top 20)\n'
234
+ candidate_rows=0
235
+ while IFS=$'\t' read -r bid first_time age_days bytes selected_by; do
236
+ [[ -z "${bid:-}" ]] && continue
237
+ printf -- '- 批次 %s | 时间 %s | 年龄 %s 天 | %s | 来源 %s\n' \
238
+ "$bid" "$(local_time "$first_time")" "$age_days" "$(human_bytes "$bytes")" "$selected_by"
239
+ candidate_rows=$((candidate_rows + 1))
240
+ if [[ "$candidate_rows" -ge 20 ]]; then
241
+ break
242
+ fi
243
+ done < <(
244
+ jq -r '.data.report.selectedCandidates // [] | sort_by(.totalBytes) | reverse | .[] | [(.batchId // "-"), ((.firstTime // 0)|tostring), ((.ageDays // 0)|tostring), ((.totalBytes // 0)|tostring), (.selectedBy // "-")] | @tsv' \
245
+ "$PREVIEW_JSON"
246
+ )
247
+ if [[ "$candidate_rows" -eq 0 ]]; then
248
+ printf -- '- 无候选批次。\n'
249
+ fi
250
+
251
+ if [[ "$executed" == "true" ]]; then
252
+ printf '\n实际执行明细(Top 20)\n'
253
+ op_rows=0
254
+ while IFS=$'\t' read -r bid status selected_by bytes batch_root; do
255
+ [[ -z "${bid:-}" ]] && continue
256
+ printf -- '- 批次 %s | 状态 %s | 来源 %s | %s | %s\n' \
257
+ "$bid" "$status" "$selected_by" "$(human_bytes "$bytes")" "${batch_root:--}"
258
+ op_rows=$((op_rows + 1))
259
+ if [[ "$op_rows" -ge 20 ]]; then
260
+ break
261
+ fi
262
+ done < <(
263
+ jq -r '.data.report.operations // [] | .[] | [(.batchId // "-"), (.status // "-"), (.selectedBy // "-"), ((.totalBytes // 0)|tostring), (.batchRoot // "-")] | @tsv' \
264
+ "$EXEC_JSON"
265
+ )
266
+ if [[ "$op_rows" -eq 0 ]]; then
267
+ printf -- '- 无执行明细。\n'
268
+ fi
269
+ fi
270
+
271
+ printf '\n运行状态\n'
272
+ printf -- '- 扫描引擎:%s\n' "$engine"
273
+ printf -- '- 总耗时:%s ms\n' "$duration_total"
274
+ printf -- '- 告警:%s\n' "$warnings_total"
275
+ printf -- '- 错误:%s\n' "$errors_total"
276
+
277
+ printf '\n指标释义\n'
278
+ printf -- '- 候选批次:按“保留最近N批 + 最大保留天数 + 容量阈值”筛出的可治理批次。\n'
279
+ printf -- '- 按年龄/按容量:分别表示由时间策略和空间策略选中的批次数。\n'
280
+ printf -- '- 预计释放:预演阶段估算可释放空间。\n'
281
+ printf -- '- 实际删除批次:真实执行时已删除的回收区批次数。\n'
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ 用法:
7
+ restore_batch_report.sh --batch-id <batchId> [--conflict skip|rename|overwrite]
8
+ [--execute true|false] [--root <path>] [--state-root <path>]
9
+ [--external-roots <path1,path2>]
10
+
11
+ 说明:
12
+ - 默认只做预演(--execute false)。
13
+ - --execute true 时执行真实恢复(带 --yes)。
14
+ EOF
15
+ }
16
+
17
+ if ! command -v jq >/dev/null 2>&1; then
18
+ echo "错误:缺少 jq,请先安装(brew install jq)。" >&2
19
+ exit 2
20
+ fi
21
+
22
+ if ! command -v wecom-cleaner >/dev/null 2>&1; then
23
+ echo "错误:未找到 wecom-cleaner 命令,请先安装 @mison/wecom-cleaner。" >&2
24
+ exit 2
25
+ fi
26
+
27
+ BATCH_ID=""
28
+ CONFLICT="rename"
29
+ EXECUTE="false"
30
+ ROOT=""
31
+ STATE_ROOT=""
32
+ EXTERNAL_ROOTS=""
33
+
34
+ while [[ $# -gt 0 ]]; do
35
+ case "$1" in
36
+ --batch-id | --restore-batch)
37
+ BATCH_ID="${2:-}"
38
+ shift 2
39
+ ;;
40
+ --conflict)
41
+ CONFLICT="${2:-rename}"
42
+ shift 2
43
+ ;;
44
+ --execute)
45
+ EXECUTE="${2:-false}"
46
+ shift 2
47
+ ;;
48
+ --root)
49
+ ROOT="${2:-}"
50
+ shift 2
51
+ ;;
52
+ --state-root)
53
+ STATE_ROOT="${2:-}"
54
+ shift 2
55
+ ;;
56
+ --external-roots)
57
+ EXTERNAL_ROOTS="${2:-}"
58
+ shift 2
59
+ ;;
60
+ -h | --help)
61
+ usage
62
+ exit 0
63
+ ;;
64
+ *)
65
+ echo "错误:未知参数 $1" >&2
66
+ usage
67
+ exit 2
68
+ ;;
69
+ esac
70
+ done
71
+
72
+ if [[ -z "$BATCH_ID" ]]; then
73
+ echo "错误:必须提供 --batch-id" >&2
74
+ usage
75
+ exit 2
76
+ fi
77
+
78
+ case "$EXECUTE" in
79
+ true | false) ;;
80
+ *)
81
+ echo "错误:--execute 只能是 true 或 false" >&2
82
+ exit 2
83
+ ;;
84
+ esac
85
+
86
+ case "$CONFLICT" in
87
+ skip | rename | overwrite) ;;
88
+ *)
89
+ echo "错误:--conflict 只能是 skip/rename/overwrite" >&2
90
+ exit 2
91
+ ;;
92
+ esac
93
+
94
+ human_bytes() {
95
+ local bytes="${1:-0}"
96
+ awk -v b="$bytes" '
97
+ BEGIN {
98
+ split("B KB MB GB TB", u, " ");
99
+ i=1;
100
+ while (b>=1024 && i<5) { b=b/1024; i++; }
101
+ if (i==1) printf "%d %s", b, u[i];
102
+ else printf "%.2f %s", b, u[i];
103
+ }
104
+ '
105
+ }
106
+
107
+ short_path() {
108
+ local raw="${1:-}"
109
+ local max_len="${2:-90}"
110
+ if [[ "${#raw}" -le "$max_len" ]]; then
111
+ printf '%s' "$raw"
112
+ return
113
+ fi
114
+ local keep=$((max_len - 3))
115
+ printf '...%s' "${raw: -$keep}"
116
+ }
117
+
118
+ PREVIEW_JSON="$(mktemp -t wecom-restore-preview.XXXX.json)"
119
+ EXEC_JSON="$(mktemp -t wecom-restore-exec.XXXX.json)"
120
+ PREVIEW_ERR="$(mktemp -t wecom-restore-preview.XXXX.err)"
121
+ EXEC_ERR="$(mktemp -t wecom-restore-exec.XXXX.err)"
122
+ trap 'rm -f "$PREVIEW_JSON" "$EXEC_JSON" "$PREVIEW_ERR" "$EXEC_ERR"' EXIT
123
+
124
+ run_cmd_to_file() {
125
+ local dry_run="$1"
126
+ local output_file="$2"
127
+ local err_file="$3"
128
+ local cmd_parts=(
129
+ --restore-batch "$BATCH_ID"
130
+ --conflict "$CONFLICT"
131
+ --output json
132
+ --dry-run "$dry_run"
133
+ )
134
+ if [[ -n "$ROOT" ]]; then
135
+ cmd_parts+=(--root "$ROOT")
136
+ fi
137
+ if [[ -n "$STATE_ROOT" ]]; then
138
+ cmd_parts+=(--state-root "$STATE_ROOT")
139
+ fi
140
+ if [[ -n "$EXTERNAL_ROOTS" ]]; then
141
+ cmd_parts+=(--external-roots "$EXTERNAL_ROOTS")
142
+ fi
143
+ if [[ "$dry_run" == "false" ]]; then
144
+ cmd_parts+=(--yes)
145
+ fi
146
+ if ! wecom-cleaner "${cmd_parts[@]}" >"$output_file" 2>"$err_file"; then
147
+ err_head="$(head -n 3 "$err_file" 2>/dev/null || true)"
148
+ echo "执行失败(dry-run=${dry_run}):${err_head:-未知错误}" >&2
149
+ return 1
150
+ fi
151
+ }
152
+
153
+ run_cmd_to_file true "$PREVIEW_JSON" "$PREVIEW_ERR"
154
+
155
+ preview_success="$(jq -r '.summary.successCount // 0' "$PREVIEW_JSON")"
156
+ preview_skipped="$(jq -r '.summary.skippedCount // 0' "$PREVIEW_JSON")"
157
+ preview_failed="$(jq -r '.summary.failedCount // 0' "$PREVIEW_JSON")"
158
+ preview_restored="$(jq -r '.summary.restoredBytes // 0' "$PREVIEW_JSON")"
159
+ entry_count="$(jq -r '.summary.entryCount // (.data.report.matched.totalEntries // 0)' "$PREVIEW_JSON")"
160
+ matched_bytes="$(jq -r '.summary.matchedBytes // (.data.report.matched.totalBytes // 0)' "$PREVIEW_JSON")"
161
+ scope_count="$(jq -r '.summary.scopeCount // (.data.report.matched.byScope // [] | length)' "$PREVIEW_JSON")"
162
+ category_count="$(jq -r '.summary.categoryCount // (.data.report.matched.byCategory // [] | length)' "$PREVIEW_JSON")"
163
+ root_path_count="$(jq -r '.summary.rootPathCount // (.data.report.matched.byRoot // [] | length)' "$PREVIEW_JSON")"
164
+ engine="$(jq -r '.meta.engine // "unknown"' "$PREVIEW_JSON")"
165
+ duration_preview="$(jq -r '.meta.durationMs // 0' "$PREVIEW_JSON")"
166
+ warnings_preview="$(jq -r '(.warnings // []) | length' "$PREVIEW_JSON")"
167
+ errors_preview="$(jq -r '(.errors // []) | length' "$PREVIEW_JSON")"
168
+
169
+ executed="false"
170
+ execute_success=0
171
+ execute_skipped=0
172
+ execute_failed=0
173
+ execute_restored=0
174
+ duration_exec=0
175
+ warnings_exec=0
176
+ errors_exec=0
177
+
178
+ if [[ "$EXECUTE" == "true" ]]; then
179
+ run_cmd_to_file false "$EXEC_JSON" "$EXEC_ERR"
180
+ executed="true"
181
+ execute_success="$(jq -r '.summary.successCount // 0' "$EXEC_JSON")"
182
+ execute_skipped="$(jq -r '.summary.skippedCount // 0' "$EXEC_JSON")"
183
+ execute_failed="$(jq -r '.summary.failedCount // 0' "$EXEC_JSON")"
184
+ execute_restored="$(jq -r '.summary.restoredBytes // 0' "$EXEC_JSON")"
185
+ duration_exec="$(jq -r '.meta.durationMs // 0' "$EXEC_JSON")"
186
+ warnings_exec="$(jq -r '(.warnings // []) | length' "$EXEC_JSON")"
187
+ errors_exec="$(jq -r '(.errors // []) | length' "$EXEC_JSON")"
188
+ fi
189
+
190
+ duration_total=$((duration_preview + duration_exec))
191
+ warnings_total=$((warnings_preview + warnings_exec))
192
+ errors_total=$((errors_preview + errors_exec))
193
+
194
+ printf '\n=== 批次恢复结果(给用户)===\n'
195
+ if [[ "$executed" == "true" ]]; then
196
+ printf -- '- 已完成:批次 %s 恢复成功 %s 项,恢复体积 %s。\n' "$BATCH_ID" "$execute_success" "$(human_bytes "$execute_restored")"
197
+ else
198
+ printf -- '- 已完成预演:批次 %s 预计可恢复 %s 项(体积 %s)。\n' "$BATCH_ID" "$preview_success" "$(human_bytes "$preview_restored")"
199
+ fi
200
+ printf -- '- 冲突策略:%s。\n' "$CONFLICT"
201
+
202
+ printf '\n你关心的范围\n'
203
+ printf -- '- 批次号:%s\n' "$BATCH_ID"
204
+ printf -- '- 批次条目:%s 项\n' "$entry_count"
205
+ printf -- '- 涉及作用域:%s 类\n' "$scope_count"
206
+ printf -- '- 涉及类别:%s 类\n' "$category_count"
207
+ printf -- '- 涉及目录根:%s 个\n' "$root_path_count"
208
+
209
+ printf '\n恢复结果总览\n'
210
+ printf -- '- 预演成功:%s,跳过:%s,失败:%s,预计恢复:%s\n' \
211
+ "$preview_success" "$preview_skipped" "$preview_failed" "$(human_bytes "$preview_restored")"
212
+ if [[ "$executed" == "true" ]]; then
213
+ printf -- '- 实际执行:成功 %s / 跳过 %s / 失败 %s,实际恢复 %s\n' \
214
+ "$execute_success" "$execute_skipped" "$execute_failed" "$(human_bytes "$execute_restored")"
215
+ else
216
+ printf -- '- 执行状态:未执行真实恢复(仅预演)。\n'
217
+ fi
218
+ printf -- '- 批次总量:%s(按批次记录统计)\n' "$(human_bytes "$matched_bytes")"
219
+
220
+ printf '\n按作用域统计\n'
221
+ scope_rows=0
222
+ while IFS=$'\t' read -r scope count bytes; do
223
+ [[ -z "${scope:-}" ]] && continue
224
+ printf -- '- %s:%s 项,%s\n' "$scope" "$count" "$(human_bytes "$bytes")"
225
+ scope_rows=$((scope_rows + 1))
226
+ done < <(
227
+ jq -r '.data.report.matched.byScope // [] | .[] | [(.scope // "-"), ((.targetCount // 0)|tostring), ((.sizeBytes // 0)|tostring)] | @tsv' \
228
+ "$PREVIEW_JSON"
229
+ )
230
+ if [[ "$scope_rows" -eq 0 ]]; then
231
+ printf -- '- 无作用域数据。\n'
232
+ fi
233
+
234
+ printf '\n按类别统计\n'
235
+ cat_rows=0
236
+ while IFS=$'\t' read -r label count bytes; do
237
+ [[ -z "${label:-}" ]] && continue
238
+ printf -- '- %s:%s 项,%s\n' "$label" "$count" "$(human_bytes "$bytes")"
239
+ cat_rows=$((cat_rows + 1))
240
+ if [[ "$cat_rows" -ge 20 ]]; then
241
+ break
242
+ fi
243
+ done < <(
244
+ jq -r '.data.report.matched.byCategory // [] | .[] | [(.categoryLabel // .categoryKey // "-"), ((.targetCount // 0)|tostring), ((.sizeBytes // 0)|tostring)] | @tsv' \
245
+ "$PREVIEW_JSON"
246
+ )
247
+ if [[ "$cat_rows" -eq 0 ]]; then
248
+ printf -- '- 无类别数据。\n'
249
+ fi
250
+
251
+ printf '\n按月份统计\n'
252
+ month_rows=0
253
+ while IFS=$'\t' read -r month count bytes; do
254
+ [[ -z "${month:-}" ]] && continue
255
+ printf -- '- %s:%s 项,%s\n' "$month" "$count" "$(human_bytes "$bytes")"
256
+ month_rows=$((month_rows + 1))
257
+ if [[ "$month_rows" -ge 24 ]]; then
258
+ break
259
+ fi
260
+ done < <(
261
+ jq -r '.data.report.matched.byMonth // [] | .[] | [(.monthKey // "非月份目录"), ((.targetCount // 0)|tostring), ((.sizeBytes // 0)|tostring)] | @tsv' \
262
+ "$PREVIEW_JSON"
263
+ )
264
+ if [[ "$month_rows" -eq 0 ]]; then
265
+ printf -- '- 无月份数据。\n'
266
+ fi
267
+
268
+ printf '\n路径范围(按体积Top 8根目录)\n'
269
+ root_rows=0
270
+ while IFS=$'\t' read -r root count bytes; do
271
+ [[ -z "${root:-}" ]] && continue
272
+ printf -- '- %s:%s 项,%s\n' "$(short_path "$root" 86)" "$count" "$(human_bytes "$bytes")"
273
+ root_rows=$((root_rows + 1))
274
+ if [[ "$root_rows" -ge 8 ]]; then
275
+ break
276
+ fi
277
+ done < <(
278
+ jq -r '.data.report.matched.byRoot // [] | .[] | [(.rootPath // "-"), ((.targetCount // 0)|tostring), ((.sizeBytes // 0)|tostring)] | @tsv' \
279
+ "$PREVIEW_JSON"
280
+ )
281
+ if [[ "$root_rows" -eq 0 ]]; then
282
+ printf -- '- 无路径数据。\n'
283
+ fi
284
+
285
+ printf '\n路径样例(按体积Top 10)\n'
286
+ top_rows=0
287
+ while IFS=$'\t' read -r source recycle label month bytes; do
288
+ [[ -z "${source:-}" ]] && continue
289
+ printf -- '- %s | %s | %s | 原路径:%s | 回收路径:%s\n' \
290
+ "$label" "${month:-非月份目录}" "$(human_bytes "$bytes")" "$(short_path "$source" 56)" "$(short_path "$recycle" 56)"
291
+ top_rows=$((top_rows + 1))
292
+ if [[ "$top_rows" -ge 10 ]]; then
293
+ break
294
+ fi
295
+ done < <(
296
+ jq -r '.data.report.matched.topEntries // [] | .[] | [(.sourcePath // "-"), (.recyclePath // "-"), (.categoryLabel // .categoryKey // "-"), (.monthKey // "非月份目录"), ((.sizeBytes // 0)|tostring)] | @tsv' \
297
+ "$PREVIEW_JSON"
298
+ )
299
+ if [[ "$top_rows" -eq 0 ]]; then
300
+ printf -- '- 无路径样例。\n'
301
+ fi
302
+
303
+ if [[ "$executed" == "true" ]]; then
304
+ printf '\n实际执行明细(按类别)\n'
305
+ exec_rows=0
306
+ while IFS=$'\t' read -r label s k f sbytes; do
307
+ [[ -z "${label:-}" ]] && continue
308
+ printf -- '- %s:成功 %s / 跳过 %s / 失败 %s,恢复 %s\n' "$label" "$s" "$k" "$f" "$(human_bytes "$sbytes")"
309
+ exec_rows=$((exec_rows + 1))
310
+ if [[ "$exec_rows" -ge 20 ]]; then
311
+ break
312
+ fi
313
+ done < <(
314
+ jq -r '.data.report.executed.byCategory // [] | .[] | [(.categoryLabel // .categoryKey // "-"), ((.successCount // 0)|tostring), ((.skippedCount // 0)|tostring), ((.failedCount // 0)|tostring), ((.successBytes // 0)|tostring)] | @tsv' \
315
+ "$EXEC_JSON"
316
+ )
317
+ if [[ "$exec_rows" -eq 0 ]]; then
318
+ printf -- '- 当前未返回执行落地明细。\n'
319
+ fi
320
+
321
+ printf '\n实际执行明细(按月份)\n'
322
+ exec_month_rows=0
323
+ while IFS=$'\t' read -r month s k f sbytes; do
324
+ [[ -z "${month:-}" ]] && continue
325
+ printf -- '- %s:成功 %s / 跳过 %s / 失败 %s,恢复 %s\n' "$month" "$s" "$k" "$f" "$(human_bytes "$sbytes")"
326
+ exec_month_rows=$((exec_month_rows + 1))
327
+ if [[ "$exec_month_rows" -ge 24 ]]; then
328
+ break
329
+ fi
330
+ done < <(
331
+ jq -r '.data.report.executed.byMonth // [] | .[] | [(.monthKey // "非月份目录"), ((.successCount // 0)|tostring), ((.skippedCount // 0)|tostring), ((.failedCount // 0)|tostring), ((.successBytes // 0)|tostring)] | @tsv' \
332
+ "$EXEC_JSON"
333
+ )
334
+ if [[ "$exec_month_rows" -eq 0 ]]; then
335
+ printf -- '- 当前未返回按月份执行明细。\n'
336
+ fi
337
+ fi
338
+
339
+ printf '\n运行状态\n'
340
+ printf -- '- 扫描引擎:%s\n' "$engine"
341
+ printf -- '- 总耗时:%s ms\n' "$duration_total"
342
+ printf -- '- 告警:%s\n' "$warnings_total"
343
+ printf -- '- 错误:%s\n' "$errors_total"
344
+
345
+ printf '\n指标释义\n'
346
+ printf -- '- 批次条目:该批次里可恢复记录的总数。\n'
347
+ printf -- '- 预计恢复:预演阶段判断可恢复的体积。\n'
348
+ printf -- '- 冲突策略:目标路径已存在时的处理方式(跳过/重命名/覆盖)。\n'
349
+ printf -- '- 实际恢复:仅真实执行时生效,表示已成功回放到原目录的数据量。\n'