@mison/wecom-cleaner 1.0.0 → 1.2.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.
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ 用法:
7
+ space_governance_report.sh [--accounts all|current|id1,id2] [--tiers safe,caution,protected]
8
+ [--suggested-only true|false] [--allow-recent-active true|false]
9
+ [--targets id1,id2] [--execute true|false]
10
+ [--root <path>] [--state-root <path>]
11
+ [--external-roots-source preset|configured|auto|all]
12
+
13
+ 说明:
14
+ - 默认只做预演(--execute false)。
15
+ - --execute true 且预演命中>0 时,才执行真实治理。
16
+ EOF
17
+ }
18
+
19
+ if ! command -v jq >/dev/null 2>&1; then
20
+ echo "错误:缺少 jq,请先安装(brew install jq)。" >&2
21
+ exit 2
22
+ fi
23
+
24
+ if ! command -v wecom-cleaner >/dev/null 2>&1; then
25
+ echo "错误:未找到 wecom-cleaner 命令,请先安装 @mison/wecom-cleaner。" >&2
26
+ exit 2
27
+ fi
28
+
29
+ ACCOUNTS="all"
30
+ TIERS="safe,caution"
31
+ SUGGESTED_ONLY="true"
32
+ ALLOW_RECENT_ACTIVE="false"
33
+ TARGETS=""
34
+ EXECUTE="false"
35
+ ROOT=""
36
+ STATE_ROOT=""
37
+ EXTERNAL_ROOTS_SOURCE="all"
38
+
39
+ while [[ $# -gt 0 ]]; do
40
+ case "$1" in
41
+ --accounts)
42
+ ACCOUNTS="${2:-all}"
43
+ shift 2
44
+ ;;
45
+ --tiers)
46
+ TIERS="${2:-safe,caution}"
47
+ shift 2
48
+ ;;
49
+ --suggested-only)
50
+ SUGGESTED_ONLY="${2:-true}"
51
+ shift 2
52
+ ;;
53
+ --allow-recent-active)
54
+ ALLOW_RECENT_ACTIVE="${2:-false}"
55
+ shift 2
56
+ ;;
57
+ --targets)
58
+ TARGETS="${2:-}"
59
+ shift 2
60
+ ;;
61
+ --execute)
62
+ EXECUTE="${2:-false}"
63
+ shift 2
64
+ ;;
65
+ --root)
66
+ ROOT="${2:-}"
67
+ shift 2
68
+ ;;
69
+ --state-root)
70
+ STATE_ROOT="${2:-}"
71
+ shift 2
72
+ ;;
73
+ --external-roots-source)
74
+ EXTERNAL_ROOTS_SOURCE="${2:-all}"
75
+ shift 2
76
+ ;;
77
+ -h | --help)
78
+ usage
79
+ exit 0
80
+ ;;
81
+ *)
82
+ echo "错误:未知参数 $1" >&2
83
+ usage
84
+ exit 2
85
+ ;;
86
+ esac
87
+ done
88
+
89
+ case "$EXECUTE" in
90
+ true | false) ;;
91
+ *)
92
+ echo "错误:--execute 只能是 true 或 false" >&2
93
+ exit 2
94
+ ;;
95
+ esac
96
+
97
+ human_bytes() {
98
+ local bytes="${1:-0}"
99
+ awk -v b="$bytes" '
100
+ BEGIN {
101
+ split("B KB MB GB TB", u, " ");
102
+ i=1;
103
+ while (b>=1024 && i<5) { b=b/1024; i++; }
104
+ if (i==1) printf "%d %s", b, u[i];
105
+ else printf "%.2f %s", b, u[i];
106
+ }
107
+ '
108
+ }
109
+
110
+ short_path() {
111
+ local raw="${1:-}"
112
+ local max_len="${2:-90}"
113
+ if [[ "${#raw}" -le "$max_len" ]]; then
114
+ printf '%s' "$raw"
115
+ return
116
+ fi
117
+ local keep=$((max_len - 3))
118
+ printf '...%s' "${raw: -$keep}"
119
+ }
120
+
121
+ tier_label() {
122
+ local tier="${1:-}"
123
+ case "$tier" in
124
+ safe) printf '%s' '安全层' ;;
125
+ caution) printf '%s' '谨慎层' ;;
126
+ protected) printf '%s' '保护层' ;;
127
+ *) printf '%s' "$tier" ;;
128
+ esac
129
+ }
130
+
131
+ account_scope_label="$ACCOUNTS"
132
+ if [[ "$ACCOUNTS" == "all" ]]; then
133
+ account_scope_label="全部账号"
134
+ elif [[ "$ACCOUNTS" == "current" ]]; then
135
+ account_scope_label="当前账号"
136
+ fi
137
+
138
+ PREVIEW_JSON="$(mktemp -t wecom-space-preview.XXXX.json)"
139
+ EXEC_JSON="$(mktemp -t wecom-space-exec.XXXX.json)"
140
+ VERIFY_JSON="$(mktemp -t wecom-space-verify.XXXX.json)"
141
+ PREVIEW_ERR="$(mktemp -t wecom-space-preview.XXXX.err)"
142
+ EXEC_ERR="$(mktemp -t wecom-space-exec.XXXX.err)"
143
+ VERIFY_ERR="$(mktemp -t wecom-space-verify.XXXX.err)"
144
+ trap 'rm -f "$PREVIEW_JSON" "$EXEC_JSON" "$VERIFY_JSON" "$PREVIEW_ERR" "$EXEC_ERR" "$VERIFY_ERR"' EXIT
145
+
146
+ run_cmd_to_file() {
147
+ local dry_run="$1"
148
+ local output_file="$2"
149
+ local err_file="$3"
150
+ local cmd_parts=(
151
+ --space-governance
152
+ --accounts "$ACCOUNTS"
153
+ --tiers "$TIERS"
154
+ --suggested-only "$SUGGESTED_ONLY"
155
+ --allow-recent-active "$ALLOW_RECENT_ACTIVE"
156
+ --output json
157
+ --dry-run "$dry_run"
158
+ )
159
+ if [[ -n "$TARGETS" ]]; then
160
+ cmd_parts+=(--targets "$TARGETS")
161
+ fi
162
+ if [[ -n "$ROOT" ]]; then
163
+ cmd_parts+=(--root "$ROOT")
164
+ fi
165
+ if [[ -n "$STATE_ROOT" ]]; then
166
+ cmd_parts+=(--state-root "$STATE_ROOT")
167
+ fi
168
+ if [[ -n "$EXTERNAL_ROOTS_SOURCE" ]]; then
169
+ cmd_parts+=(--external-roots-source "$EXTERNAL_ROOTS_SOURCE")
170
+ fi
171
+ if [[ "$dry_run" == "false" ]]; then
172
+ cmd_parts+=(--yes)
173
+ fi
174
+ if ! wecom-cleaner "${cmd_parts[@]}" >"$output_file" 2>"$err_file"; then
175
+ err_head="$(head -n 3 "$err_file" 2>/dev/null || true)"
176
+ echo "执行失败(dry-run=$dry_run):${err_head:-未知错误}" >&2
177
+ return 1
178
+ fi
179
+ }
180
+
181
+ run_cmd_to_file true "$PREVIEW_JSON" "$PREVIEW_ERR"
182
+
183
+ matched_targets="$(jq -r '.summary.matchedTargets // 0' "$PREVIEW_JSON")"
184
+ matched_bytes="$(jq -r '.summary.matchedBytes // (.data.report.matched.totalBytes // 0)' "$PREVIEW_JSON")"
185
+ preview_reclaimed="$(jq -r '.summary.reclaimedBytes // 0' "$PREVIEW_JSON")"
186
+ preview_failed="$(jq -r '.summary.failedCount // 0' "$PREVIEW_JSON")"
187
+ tier_count="$(jq -r '.summary.tierCount // (.data.report.matched.byTier // [] | length)' "$PREVIEW_JSON")"
188
+ target_type_count="$(jq -r '.summary.targetTypeCount // (.data.report.matched.byTargetType // [] | length)' "$PREVIEW_JSON")"
189
+ root_path_count="$(jq -r '.summary.rootPathCount // (.data.report.matched.byRoot // [] | length)' "$PREVIEW_JSON")"
190
+ engine="$(jq -r '.data.engineUsed // .meta.engine // "unknown"' "$PREVIEW_JSON")"
191
+ duration_preview="$(jq -r '.meta.durationMs // 0' "$PREVIEW_JSON")"
192
+ warnings_preview="$(jq -r '(.warnings // []) | length' "$PREVIEW_JSON")"
193
+ errors_preview="$(jq -r '(.errors // []) | length' "$PREVIEW_JSON")"
194
+
195
+ executed="false"
196
+ execute_success=0
197
+ execute_skipped=0
198
+ execute_failed=0
199
+ execute_reclaimed=0
200
+ execute_batch="-"
201
+ verify_matched="$matched_targets"
202
+ duration_exec=0
203
+ duration_verify=0
204
+ warnings_exec=0
205
+ warnings_verify=0
206
+ errors_exec=0
207
+ errors_verify=0
208
+
209
+ if [[ "$matched_targets" -gt 0 && "$EXECUTE" == "true" ]]; then
210
+ run_cmd_to_file false "$EXEC_JSON" "$EXEC_ERR"
211
+ executed="true"
212
+ execute_success="$(jq -r '.summary.successCount // 0' "$EXEC_JSON")"
213
+ execute_skipped="$(jq -r '.summary.skippedCount // 0' "$EXEC_JSON")"
214
+ execute_failed="$(jq -r '.summary.failedCount // 0' "$EXEC_JSON")"
215
+ execute_reclaimed="$(jq -r '.summary.reclaimedBytes // 0' "$EXEC_JSON")"
216
+ execute_batch="$(jq -r '.summary.batchId // "-"' "$EXEC_JSON")"
217
+ duration_exec="$(jq -r '.meta.durationMs // 0' "$EXEC_JSON")"
218
+ warnings_exec="$(jq -r '(.warnings // []) | length' "$EXEC_JSON")"
219
+ errors_exec="$(jq -r '(.errors // []) | length' "$EXEC_JSON")"
220
+
221
+ run_cmd_to_file true "$VERIFY_JSON" "$VERIFY_ERR"
222
+ verify_matched="$(jq -r '.summary.matchedTargets // 0' "$VERIFY_JSON")"
223
+ duration_verify="$(jq -r '.meta.durationMs // 0' "$VERIFY_JSON")"
224
+ warnings_verify="$(jq -r '(.warnings // []) | length' "$VERIFY_JSON")"
225
+ errors_verify="$(jq -r '(.errors // []) | length' "$VERIFY_JSON")"
226
+ fi
227
+
228
+ if [[ "$executed" == "true" ]]; then
229
+ conclusion="已完成"
230
+ reason="已按授权执行全量空间治理,并完成同条件复核。"
231
+ elif [[ "$matched_targets" -eq 0 ]]; then
232
+ conclusion="无需执行"
233
+ reason="当前筛选条件下没有可治理目标,按安全规则未执行真实删除。"
234
+ else
235
+ conclusion="仅预演"
236
+ reason="已完成预演,等待你确认执行真实治理。"
237
+ fi
238
+
239
+ duration_total=$((duration_preview + duration_exec + duration_verify))
240
+ warnings_total=$((warnings_preview + warnings_exec + warnings_verify))
241
+ errors_total=$((errors_preview + errors_exec + errors_verify))
242
+
243
+ printf '\n=== 全量空间治理结果(给用户)===\n'
244
+ if [[ "$executed" == "true" ]]; then
245
+ printf -- '- 已完成:已治理 %s 项空间目标,释放 %s。\n' "$execute_success" "$(human_bytes "$execute_reclaimed")"
246
+ elif [[ "$matched_targets" -eq 0 ]]; then
247
+ printf -- '- 已完成检查:当前条件下未发现可治理目标,本次未执行删除。\n'
248
+ else
249
+ printf -- '- 已完成预演:预计可治理 %s 项、释放 %s;等待确认执行。\n' "$matched_targets" "$(human_bytes "$preview_reclaimed")"
250
+ fi
251
+ printf -- '- 你的目标:按“全量空间治理”规则清理低风险缓存目录。\n'
252
+
253
+ printf '\n你关心的范围\n'
254
+ printf -- '- 账号:%s\n' "$account_scope_label"
255
+ printf -- '- 层级:%s\n' "$TIERS"
256
+ printf -- '- 仅建议项:%s\n' "$SUGGESTED_ONLY"
257
+ printf -- '- 允许近期活跃:%s\n' "$ALLOW_RECENT_ACTIVE"
258
+ printf -- '- 命中路径根:%s 个\n' "$root_path_count"
259
+
260
+ printf '\n治理结果总览\n'
261
+ printf -- '- 命中治理项:%s 项\n' "$matched_targets"
262
+ printf -- '- 命中体积:%s\n' "$(human_bytes "$matched_bytes")"
263
+ printf -- '- 预计释放:%s\n' "$(human_bytes "$preview_reclaimed")"
264
+ if [[ "$executed" == "true" ]]; then
265
+ printf -- '- 实际释放:%s\n' "$(human_bytes "$execute_reclaimed")"
266
+ printf -- '- 清理批次:%s(可用于恢复)\n' "$execute_batch"
267
+ printf -- '- 复核结果:剩余可治理 %s 项\n' "$verify_matched"
268
+ else
269
+ printf -- '- 执行状态:未执行真实治理(%s)\n' "$reason"
270
+ printf -- '- 复核结果:沿用预演结论(剩余可治理 %s 项)\n' "$verify_matched"
271
+ fi
272
+
273
+ printf '\n按风险层级统计\n'
274
+ tier_rows=0
275
+ while IFS=$'\t' read -r tier tier_label_raw count bytes suggested_count active_count; do
276
+ [[ -z "${tier:-}" ]] && continue
277
+ if [[ -z "$tier_label_raw" || "$tier_label_raw" == "null" ]]; then
278
+ tier_label_raw="$(tier_label "$tier")"
279
+ fi
280
+ printf -- '- %s:%s 项,%s(建议 %s 项,近期活跃 %s 项)\n' \
281
+ "$tier_label_raw" "$count" "$(human_bytes "$bytes")" "$suggested_count" "$active_count"
282
+ tier_rows=$((tier_rows + 1))
283
+ done < <(
284
+ jq -r '.data.report.matched.byTier // [] | .[] | [(.tier // "-"), (.tierLabel // ""), ((.targetCount // 0)|tostring), ((.sizeBytes // 0)|tostring), ((.suggestedCount // 0)|tostring), ((.recentlyActiveCount // 0)|tostring)] | @tsv' \
285
+ "$PREVIEW_JSON"
286
+ )
287
+ if [[ "$tier_rows" -eq 0 ]]; then
288
+ printf -- '- 无层级数据。\n'
289
+ fi
290
+
291
+ printf '\n按目标类型统计(你清理了什么)\n'
292
+ target_rows=0
293
+ while IFS=$'\t' read -r label count bytes; do
294
+ [[ -z "${label:-}" ]] && continue
295
+ printf -- '- %s:%s 项,%s\n' "$label" "$count" "$(human_bytes "$bytes")"
296
+ target_rows=$((target_rows + 1))
297
+ if [[ "$target_rows" -ge 20 ]]; then
298
+ break
299
+ fi
300
+ done < <(
301
+ jq -r '.data.report.matched.byTargetType // [] | .[] | [(.targetLabel // .targetKey // "-"), ((.targetCount // 0)|tostring), ((.sizeBytes // 0)|tostring)] | @tsv' \
302
+ "$PREVIEW_JSON"
303
+ )
304
+ if [[ "$target_rows" -eq 0 ]]; then
305
+ printf -- '- 无命中目标类型。\n'
306
+ fi
307
+
308
+ printf '\n路径范围(主要治理目录)\n'
309
+ root_rows=0
310
+ while IFS=$'\t' read -r root_path count bytes root_type; do
311
+ [[ -z "${root_path:-}" ]] && continue
312
+ type_label="账号目录"
313
+ if [[ "$root_type" == "external" ]]; then
314
+ type_label="外部存储"
315
+ fi
316
+ printf -- '- [%s] %s:%s 项,%s\n' "$type_label" "$(short_path "$root_path" 88)" "$count" "$(human_bytes "$bytes")"
317
+ root_rows=$((root_rows + 1))
318
+ if [[ "$root_rows" -ge 10 ]]; then
319
+ break
320
+ fi
321
+ done < <(
322
+ jq -r '.data.report.matched.byRoot // [] | .[] | [(.rootPath // "-"), ((.targetCount // 0)|tostring), ((.sizeBytes // 0)|tostring), (.rootType // "profile")] | @tsv' \
323
+ "$PREVIEW_JSON"
324
+ )
325
+ if [[ "$root_rows" -eq 0 ]]; then
326
+ printf -- '- 无命中目录。\n'
327
+ fi
328
+
329
+ printf '\n路径样例(按体积Top 10)\n'
330
+ top_rows=0
331
+ while IFS=$'\t' read -r p label tier_label_text bytes acc suggested active; do
332
+ [[ -z "${p:-}" ]] && continue
333
+ tag=""
334
+ if [[ "$suggested" == "true" ]]; then
335
+ tag="${tag}建议 "
336
+ fi
337
+ if [[ "$active" == "true" ]]; then
338
+ tag="${tag}活跃 "
339
+ fi
340
+ printf -- '- %s | %s | %s | %s | %s%s\n' "$label" "$tier_label_text" "$acc" "$(human_bytes "$bytes")" "$tag" "$(short_path "$p" 80)"
341
+ top_rows=$((top_rows + 1))
342
+ if [[ "$top_rows" -ge 10 ]]; then
343
+ break
344
+ fi
345
+ done < <(
346
+ jq -r '.data.report.matched.topPaths // [] | .[] | [(.path // "-"), (.targetLabel // .targetKey // "-"), (.tierLabel // .tier // "-"), (.sizeBytes // 0 | tostring), (.accountShortId // "-"), ((.suggested // false)|tostring), ((.recentlyActive // false)|tostring)] | @tsv' \
347
+ "$PREVIEW_JSON"
348
+ )
349
+ if [[ "$top_rows" -eq 0 ]]; then
350
+ printf -- '- 无路径样例。\n'
351
+ fi
352
+
353
+ if [[ "$executed" == "true" ]]; then
354
+ printf '\n实际执行明细(按目标类型)\n'
355
+ exec_rows=0
356
+ while IFS=$'\t' read -r label s k f sbytes; do
357
+ [[ -z "${label:-}" ]] && continue
358
+ printf -- '- %s:成功 %s / 跳过 %s / 失败 %s,实际释放 %s\n' "$label" "$s" "$k" "$f" "$(human_bytes "$sbytes")"
359
+ exec_rows=$((exec_rows + 1))
360
+ if [[ "$exec_rows" -ge 20 ]]; then
361
+ break
362
+ fi
363
+ done < <(
364
+ jq -r '.data.report.executed.byCategory // [] | .[] | [(.categoryLabel // .categoryKey // "-"), ((.successCount // 0)|tostring), ((.skippedCount // 0)|tostring), ((.failedCount // 0)|tostring), ((.successBytes // 0)|tostring)] | @tsv' \
365
+ "$EXEC_JSON"
366
+ )
367
+ if [[ "$exec_rows" -eq 0 ]]; then
368
+ printf -- '- 当前未返回执行落地明细。\n'
369
+ fi
370
+
371
+ printf '\n实际执行明细(按月份)\n'
372
+ exec_month_rows=0
373
+ while IFS=$'\t' read -r month s k f sbytes; do
374
+ [[ -z "${month:-}" ]] && continue
375
+ printf -- '- %s:成功 %s / 跳过 %s / 失败 %s,实际释放 %s\n' "$month" "$s" "$k" "$f" "$(human_bytes "$sbytes")"
376
+ exec_month_rows=$((exec_month_rows + 1))
377
+ if [[ "$exec_month_rows" -ge 24 ]]; then
378
+ break
379
+ fi
380
+ done < <(
381
+ jq -r '.data.report.executed.byMonth // [] | .[] | [(.monthKey // "非月份目录"), ((.successCount // 0)|tostring), ((.skippedCount // 0)|tostring), ((.failedCount // 0)|tostring), ((.successBytes // 0)|tostring)] | @tsv' \
382
+ "$EXEC_JSON"
383
+ )
384
+ if [[ "$exec_month_rows" -eq 0 ]]; then
385
+ printf -- '- 当前未返回按月份执行明细。\n'
386
+ fi
387
+ fi
388
+
389
+ printf '\n运行状态\n'
390
+ printf -- '- 扫描引擎:%s\n' "$engine"
391
+ printf -- '- 耗时:%s ms\n' "$duration_total"
392
+ printf -- '- 告警:%s\n' "$warnings_total"
393
+ printf -- '- 错误:%s\n' "$errors_total"
394
+ printf -- '- 预演失败项:%s\n' "$preview_failed"
395
+ printf -- '- 风险层级数量:%s,目标类型数量:%s\n' "$tier_count" "$target_type_count"
396
+
397
+ printf '\n指标释义\n'
398
+ printf -- '- 命中治理项:本次筛选条件下可处理的目录目标数量。\n'
399
+ printf -- '- 预计释放:预演估算可回收空间,真实执行前不会实际删除。\n'
400
+ printf -- '- 建议项:由策略判断为优先治理的低风险目标。\n'
401
+ printf -- '- 近期活跃:最近仍有访问行为的目录,默认建议跳过。\n'