deepclean-mydisk 0.0.0-stage → 0.12.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,29 @@
1
+ import path from 'node:path'
2
+
3
+ // Protection is checked again on the host, independently of UI selection.
4
+ export function isProtected(target, whitelist, platform = process.platform) {
5
+ const paths = platform === 'win32' ? path.win32 : path.posix
6
+ if (typeof target !== 'string' || !paths.isAbsolute(target)) return false
7
+ return whitelist.some((root) => {
8
+ if (typeof root !== 'string' || !paths.isAbsolute(root)) return false
9
+ const contains = (parent, child) => {
10
+ const relative = paths.relative(paths.resolve(parent), paths.resolve(child))
11
+ return relative === '' || (relative !== '..' && !relative.startsWith('..' + paths.sep) && !paths.isAbsolute(relative))
12
+ }
13
+ // Removing an ancestor would also remove the protected descendant.
14
+ return contains(root, target) || contains(target, root)
15
+ })
16
+ }
17
+
18
+ export function parseTreeOutput(out, platform = process.platform) {
19
+ const children = []
20
+ for (const line of out.split(/\r?\n/)) {
21
+ const idx = line.indexOf('\t')
22
+ if (idx < 1) continue
23
+ const size = Number(line.slice(0, idx))
24
+ const target = line.slice(idx + 1)
25
+ if (!Number.isFinite(size) || size < 0 || !target) continue
26
+ children.push({ path: target, kb: platform === 'win32' ? size / 1024 : size })
27
+ }
28
+ return children
29
+ }
package/locale/en.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "title": "DeepClean MyDisk",
4
+ "description": "Read-only disk audit and cleanup that moves selected files to the Trash or quarantine, with undo.",
5
+ "detail": "docs/plugin/DETAIL.en.md"
6
+ }
7
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "title": "DeepClean MyDisk",
4
+ "description": "只读磁盘体检;清理把选中文件移入废纸篓或隔离区,可撤销。",
5
+ "detail": "docs/plugin/DETAIL.zh.md"
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "deepclean-mydisk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.12.0",
4
+ "description": "Read-only disk audit and cleanup for macOS and Windows: caches, leftovers, similar photos, space breakdown, and cross-disk file moves.",
5
+ "author": "huangxiaoliang",
6
+ "keywords": [
7
+ "dsh",
8
+ "dsh-plugin",
9
+ "disk",
10
+ "storage",
11
+ "cleanup",
12
+ "macos",
13
+ "windows"
14
+ ],
15
+ "icon": "./assets/icon-180.png",
16
+ "type": "module",
17
+ "main": "lib/index.js",
18
+ "engines": {
19
+ "dsh": ">=0.2.1-rc.5 <0.3.0-0"
20
+ },
21
+ "files": [
22
+ "lib",
23
+ "resources",
24
+ "cordis.patch.yml",
25
+ "README.md",
26
+ "LICENSE",
27
+ "locale/*.json",
28
+ "docs/plugin",
29
+ "assets/icon-180.png"
30
+ ],
31
+ "dsh": {
32
+ "bundle": {
33
+ "patch": "./cordis.patch.yml"
34
+ },
35
+ "client": {
36
+ "platform": "web"
37
+ }
38
+ },
39
+ "peerDependencies": {
40
+ "@deepseek-ai/dsh-tools": ">=0.2.1-rc.5 <0.3.0-0"
41
+ },
42
+ "license": "MIT",
43
+ "devDependencies": {
44
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.5",
45
+ "react": "^19.3.1",
46
+ "react-dom": "^19.3.1"
47
+ },
48
+ "exports": {
49
+ ".": {
50
+ "default": "./lib/index.js"
51
+ },
52
+ "./client": {
53
+ "default": "./lib/client.js"
54
+ },
55
+ "./package.json": "./package.json",
56
+ "./locale/*.json": "./locale/*.json"
57
+ },
58
+ "scripts": {
59
+ "test": "node --test"
60
+ },
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/huangxiaoliang/deepclean-mydisk-plugin.git"
64
+ },
65
+ "bugs": {
66
+ "url": "https://github.com/huangxiaoliang/deepclean-mydisk-plugin/issues"
67
+ },
68
+ "homepage": "https://github.com/huangxiaoliang/deepclean-mydisk-plugin#readme"
69
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Phosphor Icons
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
Binary file
@@ -0,0 +1,139 @@
1
+ {
2
+ "version": 1,
3
+ "description": "mac-disk-audit 分类规则。改这里不用改代码。",
4
+ "thresholds": {
5
+ "displayMinMb": 10,
6
+ "mediaConfirmMb": 50,
7
+ "commandMinMb": 100,
8
+ "bigFileMb": 300
9
+ },
10
+ "mediaHints": [
11
+ "video",
12
+ "player",
13
+ "tenvideo",
14
+ "iqiyi",
15
+ "youku",
16
+ "bilibili",
17
+ "tv.",
18
+ "music",
19
+ "netease",
20
+ "spotify",
21
+ "podcast",
22
+ "fm.",
23
+ "radio",
24
+ "movie",
25
+ "cinema",
26
+ "douyin",
27
+ "tiktok",
28
+ "kuaishou",
29
+ "xiaohongshu",
30
+ "zhihu",
31
+ "weibo",
32
+ "qqmusic"
33
+ ],
34
+ "meetingHints": [
35
+ "meeting",
36
+ "zoom",
37
+ "teams",
38
+ "webex",
39
+ "voov",
40
+ "wemeet",
41
+ "wwmapp"
42
+ ],
43
+ "userDataHints": [
44
+ "wechat",
45
+ "weixin",
46
+ "wxwork",
47
+ "wecom",
48
+ "feishu",
49
+ "lark",
50
+ "dingtalk",
51
+ "telegram",
52
+ "whatsapp",
53
+ "signal",
54
+ "messenger",
55
+ "qq.",
56
+ "qqexdoc",
57
+ "qqexbrowser",
58
+ "txdocs",
59
+ "notes",
60
+ "evernote",
61
+ "yinxiang",
62
+ "notion",
63
+ "docs",
64
+ "office",
65
+ "wps",
66
+ "pdf",
67
+ "mail",
68
+ "calendar",
69
+ "messages",
70
+ "photos",
71
+ "books",
72
+ "kindle",
73
+ "weread",
74
+ "portfolio",
75
+ "frodo",
76
+ "douban"
77
+ ],
78
+ "devHints": [
79
+ "xcode",
80
+ "simulator",
81
+ "docker",
82
+ "electron",
83
+ "cursor",
84
+ "trae",
85
+ "codex"
86
+ ],
87
+ "extSuffixRe": "(?i)\\.[A-Za-z0-9]*(widget|widgets|extension|ext|helper|share|shares|shareextension|notification|notificationservice|notificationserviceextension|notificationcontent|findersync|quicklook|quicklookpreview|quicklookthumbnail|intent|intents|intenthandler|intentui|intentsui|ddsintents|action|actionextension|siri|broadcast|replaykit|today|todaywidget|fileprovider|fileproviderextension|fileproviderui|xpcservice|launcher|recentfiles|menu|preview|thumbnail|ipchelper|bridgeservice|webapp|safariextension|sharingextension|serviceextension|widgetkit|docktileplugin|finderhelper)$",
88
+ "systemCacheNames": [
89
+ "CloudKit",
90
+ "GeoServices",
91
+ "com.apple.geod",
92
+ "com.apple.mediaanalysisd",
93
+ "com.apple.Safari",
94
+ "com.apple.QuickLook",
95
+ "com.apple.exchangesync"
96
+ ],
97
+ "knownNames": {
98
+ "com.tencent.txdocs": "腾讯文档",
99
+ "com.tencent.qqexdoc": "腾讯文档组件",
100
+ "com.tencent.wwmapp": "企业微信会议组件",
101
+ "com.tencent.qqexbrowser": "QQ 浏览器组件",
102
+ "com.bytedance.douyin.desktop": "抖音桌面端",
103
+ "com.douban.frodo": "豆瓣",
104
+ "com.tencent.QQMusicMac": "QQ 音乐",
105
+ "com.netease.163music": "网易云音乐",
106
+ "com.tencent.weread": "微信读书",
107
+ "com.tencent.xinWeChat": "微信",
108
+ "com.tencent.qq": "QQ",
109
+ "com.bytedance.macos.feishu": "飞书",
110
+ "com.yinxiang.Mac": "印象笔记",
111
+ "com.tencent.tenvideo": "腾讯视频",
112
+ "com.xunlei.mvideo": "迅雷影音",
113
+ "tv.danmaku.bilianime": "哔哩哔哩",
114
+ "com.youku.mac": "优酷",
115
+ "com.iqiyi.player": "爱奇艺",
116
+ "com.tencent.meeting": "腾讯会议",
117
+ "com.tencent.portfolio": "腾讯微云",
118
+ "net.whatsapp.WhatsApp": "WhatsApp",
119
+ "com.tencent.yuanbao": "腾讯元宝",
120
+ "com.google.drivefs": "Google Drive",
121
+ "com.tencent.wework": "企业微信"
122
+ },
123
+ "denyPaths": [
124
+ "~/Library/Mail",
125
+ "~/Library/Messages",
126
+ "~/Library/Photos",
127
+ "~/Library/Keychains",
128
+ "~/Library/CloudStorage",
129
+ "~/Library/Group Containers",
130
+ "~/Library/Application Support"
131
+ ],
132
+ "componentOf": {
133
+ "com.tencent.wwmapp": "com.tencent.WeWorkMac",
134
+ "com.tencent.wecom": "com.tencent.WeWorkMac",
135
+ "com.tencent.wework": "com.tencent.WeWorkMac",
136
+ "com.tencent.WeWork-Helper": "com.tencent.WeWorkMac",
137
+ "88L2Q4487U.com.tencent.WeWorkMac": "com.tencent.WeWorkMac"
138
+ }
139
+ }
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python3
2
+ """deepclean-mydisk 统一入口(v0.4.0)。
3
+
4
+ - macOS:调用 scripts/scan.py(成熟实现,行为与 v0.3.x 一致)
5
+ - Windows:调用 scripts/platforms/windows.py(只读,PowerShell 隔离区方案)
6
+
7
+ 用法:
8
+ python deepclean.py # 自动识别平台
9
+ python deepclean.py --quick --redact # 透传给平台脚本
10
+ python deepclean.py --platform windows --self-test
11
+ """
12
+ import argparse
13
+ import subprocess
14
+ import sys
15
+ import tempfile
16
+ from pathlib import Path
17
+
18
+ VERSION = "0.5.0"
19
+ HERE = Path(__file__).resolve().parent
20
+ MACOS = HERE / "scan.py"
21
+ WINDOWS = HERE / "platforms" / "windows.py"
22
+
23
+
24
+ def detect_platform():
25
+ if sys.platform.startswith("win"):
26
+ return "windows"
27
+ if sys.platform == "darwin":
28
+ return "macos"
29
+ return "macos"
30
+
31
+
32
+ def target(platform):
33
+ return WINDOWS if platform == "windows" else MACOS
34
+
35
+
36
+ def split_args(argv):
37
+ platform = None
38
+ self_test = False
39
+ rest = []
40
+ i = 0
41
+ while i < len(argv):
42
+ a = argv[i]
43
+ if a == "--platform":
44
+ platform = argv[i + 1] if i + 1 < len(argv) else None
45
+ i += 2
46
+ elif a.startswith("--platform="):
47
+ platform = a.split("=", 1)[1]
48
+ i += 1
49
+ elif a == "--self-test":
50
+ self_test = True
51
+ i += 1
52
+ else:
53
+ rest.append(a)
54
+ i += 1
55
+ return platform, self_test, rest
56
+
57
+
58
+ def macos_self_test():
59
+ """在临时 HOME 上跑一次 quick 扫描,验证只读链路与报告结构。"""
60
+ with tempfile.TemporaryDirectory(prefix="deepclean-mac-test-") as td:
61
+ home = Path(td) / "home"
62
+ (home / "Library" / "Containers").mkdir(parents=True)
63
+ (home / "Library" / "Caches").mkdir(parents=True)
64
+ report = Path(td) / "report.md"
65
+ proc = subprocess.run(
66
+ [sys.executable, str(MACOS), "--quick", "--home", str(home), "--md", str(report)],
67
+ capture_output=True, text=True)
68
+ if proc.returncode != 0:
69
+ print("FAIL macos 扫描退出码", proc.returncode)
70
+ print(proc.stderr[-800:])
71
+ return 1
72
+ text = report.read_text(encoding="utf-8")
73
+ checks = [
74
+ ("报告标题", "磁盘体检报告" in text),
75
+ ("预计可释放", "预计可释放" in text),
76
+ ("不建议清理", "不建议清理" in text),
77
+ ("扫描范围", "扫描范围" in text),
78
+ ]
79
+ ok = True
80
+ for name, cond in checks:
81
+ print(("PASS " if cond else "FAIL ") + name)
82
+ ok = ok and cond
83
+ print("\n自检结果:" + ("通过" if ok else "失败"))
84
+ return 0 if ok else 1
85
+
86
+
87
+ def main():
88
+ platform = None
89
+ if len(sys.argv) > 1:
90
+ platform, self_test, rest = split_args(sys.argv[1:])
91
+ else:
92
+ self_test, rest = False, []
93
+ if rest and rest[0] in ("--version", "-V"):
94
+ print(f"deepclean-mydisk {VERSION}")
95
+ return 0
96
+ platform = platform or detect_platform()
97
+ if platform not in ("macos", "windows"):
98
+ print(f"不支持的平台:{platform}", file=sys.stderr)
99
+ return 2
100
+ if self_test:
101
+ if platform == "windows":
102
+ return subprocess.call([sys.executable, str(WINDOWS), "--self-test"])
103
+ return macos_self_test()
104
+ cmd = [sys.executable, str(target(platform))] + rest
105
+ return subprocess.call(cmd)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ sys.exit(main())
@@ -0,0 +1,77 @@
1
+ # Read-only image decoding through the Windows .NET image codecs.
2
+ # Writes only explicitly requested temporary JPEG outputs (never input files).
3
+ param(
4
+ [Parameter(Mandatory=$true)][ValidateSet('hash','thumbnail')][string]$Mode,
5
+ [string]$InputPath,
6
+ [string]$OutputPath,
7
+ [ValidateRange(64,320)][int]$Size = 160
8
+ )
9
+ $ErrorActionPreference = 'Stop'
10
+ [Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
11
+ [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
12
+ try {
13
+ Add-Type -AssemblyName System.Drawing
14
+ function Get-PhotoHash([string]$File) {
15
+ $image = $null; $bitmap = $null; $graphics = $null
16
+ try {
17
+ $image = [System.Drawing.Image]::FromFile($File)
18
+ $bitmap = [System.Drawing.Bitmap]::new(9, 8)
19
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
20
+ $graphics.DrawImage($image, 0, 0, 9, 8)
21
+ $hex = [System.Text.StringBuilder]::new()
22
+ $nibble = 0; $count = 0
23
+ for ($y = 0; $y -lt 8; $y++) {
24
+ for ($x = 0; $x -lt 8; $x++) {
25
+ $a = $bitmap.GetPixel($x, $y); $b = $bitmap.GetPixel($x + 1, $y)
26
+ $left = [Math]::Floor(0.299 * $a.R + 0.587 * $a.G + 0.114 * $a.B)
27
+ $right = [Math]::Floor(0.299 * $b.R + 0.587 * $b.G + 0.114 * $b.B)
28
+ $nibble = $nibble -shl 1
29
+ if ($left -gt $right) { $nibble = $nibble -bor 1 }
30
+ $count++
31
+ if ($count -eq 4) {
32
+ [void]$hex.Append($nibble.ToString('x'))
33
+ $nibble = 0; $count = 0
34
+ }
35
+ }
36
+ }
37
+ return $hex.ToString()
38
+ } finally {
39
+ if ($graphics) { $graphics.Dispose() }
40
+ if ($bitmap) { $bitmap.Dispose() }
41
+ if ($image) { $image.Dispose() }
42
+ }
43
+ }
44
+ if ($Mode -eq 'hash') {
45
+ $files = ConvertFrom-Json -InputObject ([Console]::In.ReadToEnd())
46
+ $results = @(foreach ($file in $files) {
47
+ try { @{path=[string]$file; hash=(Get-PhotoHash ([string]$file))} }
48
+ catch { @{path=[string]$file; error=$_.Exception.Message} }
49
+ })
50
+ ConvertTo-Json -InputObject $results -Depth 4 -Compress
51
+ } else {
52
+ $job = ConvertFrom-Json -InputObject ([Console]::In.ReadToEnd())
53
+ $InputPath = [string]$job.path
54
+ $OutputPath = [string]$job.output
55
+ $Size = [int]$job.size
56
+ if ($Size -lt 64 -or $Size -gt 320) { throw 'Invalid thumbnail size' }
57
+ $image = $null; $bitmap = $null; $graphics = $null
58
+ try {
59
+ $image = [System.Drawing.Image]::FromFile($InputPath)
60
+ $scale = [Math]::Min(1.0, $Size / [double][Math]::Max($image.Width, $image.Height))
61
+ $width = [Math]::Max(1, [int]($image.Width * $scale))
62
+ $height = [Math]::Max(1, [int]($image.Height * $scale))
63
+ $bitmap = [System.Drawing.Bitmap]::new($width, $height)
64
+ $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
65
+ $graphics.Clear([System.Drawing.Color]::White)
66
+ $graphics.DrawImage($image, 0, 0, $width, $height)
67
+ $bitmap.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Jpeg)
68
+ } finally {
69
+ if ($graphics) { $graphics.Dispose() }
70
+ if ($bitmap) { $bitmap.Dispose() }
71
+ if ($image) { $image.Dispose() }
72
+ }
73
+ }
74
+ } catch {
75
+ [Console]::Error.WriteLine($_.Exception.Message)
76
+ exit 1
77
+ }