damon-devtools 0.1.0__tar.gz
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.
- damon_devtools-0.1.0/PKG-INFO +46 -0
- damon_devtools-0.1.0/README.md +35 -0
- damon_devtools-0.1.0/damon_devtools.egg-info/PKG-INFO +46 -0
- damon_devtools-0.1.0/damon_devtools.egg-info/SOURCES.txt +27 -0
- damon_devtools-0.1.0/damon_devtools.egg-info/dependency_links.txt +1 -0
- damon_devtools-0.1.0/damon_devtools.egg-info/entry_points.txt +2 -0
- damon_devtools-0.1.0/damon_devtools.egg-info/requires.txt +1 -0
- damon_devtools-0.1.0/damon_devtools.egg-info/top_level.txt +1 -0
- damon_devtools-0.1.0/devtools/__init__.py +2 -0
- damon_devtools-0.1.0/devtools/__main__.py +53 -0
- damon_devtools-0.1.0/devtools/launcher.py +43 -0
- damon_devtools-0.1.0/devtools/tools/__init__.py +29 -0
- damon_devtools-0.1.0/devtools/tools/apkpull/__init__.py +1 -0
- damon_devtools-0.1.0/devtools/tools/apkpull/__main__.py +5 -0
- damon_devtools-0.1.0/devtools/tools/apkpull/core.py +123 -0
- damon_devtools-0.1.0/devtools/tools/apkpull/models.py +6 -0
- damon_devtools-0.1.0/devtools/tools/apkpull/ui.py +171 -0
- damon_devtools-0.1.0/devtools/tools/portkill/__init__.py +1 -0
- damon_devtools-0.1.0/devtools/tools/portkill/__main__.py +6 -0
- damon_devtools-0.1.0/devtools/tools/portkill/core.py +97 -0
- damon_devtools-0.1.0/devtools/tools/portkill/models.py +17 -0
- damon_devtools-0.1.0/devtools/tools/portkill/ui.py +203 -0
- damon_devtools-0.1.0/devtools/tools/removebg/__init__.py +0 -0
- damon_devtools-0.1.0/devtools/tools/removebg/__main__.py +5 -0
- damon_devtools-0.1.0/devtools/tools/removebg/cli.py +80 -0
- damon_devtools-0.1.0/devtools/tools/removebg/core.py +66 -0
- damon_devtools-0.1.0/devtools/types.py +9 -0
- damon_devtools-0.1.0/pyproject.toml +24 -0
- damon_devtools-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: damon-devtools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Developer CLI tools — portkill, removebg, apkpull
|
|
5
|
+
Author: damon
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: cli,devtools,android,apk,adb
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: psutil>=5.9
|
|
11
|
+
|
|
12
|
+
# devtools
|
|
13
|
+
|
|
14
|
+
Developer CLI tools: portkill, removebg, apkpull.
|
|
15
|
+
|
|
16
|
+
## Tools
|
|
17
|
+
|
|
18
|
+
### portkill
|
|
19
|
+
Kill Node.js processes by port or scan all node processes.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python -m devtools portkill
|
|
23
|
+
python -m devtools portkill --listen-only
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### apkpull
|
|
27
|
+
Pull APK files from Android device via ADB.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python -m devtools apkpull [target_dir]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### removebg
|
|
34
|
+
Remove image backgrounds using the remove.bg API.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python -m devtools removebg input.jpg
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install damon-devtools
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires: Python >=3.10
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# devtools
|
|
2
|
+
|
|
3
|
+
Developer CLI tools: portkill, removebg, apkpull.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
### portkill
|
|
8
|
+
Kill Node.js processes by port or scan all node processes.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
python -m devtools portkill
|
|
12
|
+
python -m devtools portkill --listen-only
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### apkpull
|
|
16
|
+
Pull APK files from Android device via ADB.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python -m devtools apkpull [target_dir]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### removebg
|
|
23
|
+
Remove image backgrounds using the remove.bg API.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python -m devtools removebg input.jpg
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install damon-devtools
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires: Python >=3.10
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: damon-devtools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Developer CLI tools — portkill, removebg, apkpull
|
|
5
|
+
Author: damon
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: cli,devtools,android,apk,adb
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: psutil>=5.9
|
|
11
|
+
|
|
12
|
+
# devtools
|
|
13
|
+
|
|
14
|
+
Developer CLI tools: portkill, removebg, apkpull.
|
|
15
|
+
|
|
16
|
+
## Tools
|
|
17
|
+
|
|
18
|
+
### portkill
|
|
19
|
+
Kill Node.js processes by port or scan all node processes.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python -m devtools portkill
|
|
23
|
+
python -m devtools portkill --listen-only
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### apkpull
|
|
27
|
+
Pull APK files from Android device via ADB.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python -m devtools apkpull [target_dir]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### removebg
|
|
34
|
+
Remove image backgrounds using the remove.bg API.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python -m devtools removebg input.jpg
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install damon-devtools
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires: Python >=3.10
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
damon_devtools.egg-info/PKG-INFO
|
|
4
|
+
damon_devtools.egg-info/SOURCES.txt
|
|
5
|
+
damon_devtools.egg-info/dependency_links.txt
|
|
6
|
+
damon_devtools.egg-info/entry_points.txt
|
|
7
|
+
damon_devtools.egg-info/requires.txt
|
|
8
|
+
damon_devtools.egg-info/top_level.txt
|
|
9
|
+
devtools/__init__.py
|
|
10
|
+
devtools/__main__.py
|
|
11
|
+
devtools/launcher.py
|
|
12
|
+
devtools/types.py
|
|
13
|
+
devtools/tools/__init__.py
|
|
14
|
+
devtools/tools/apkpull/__init__.py
|
|
15
|
+
devtools/tools/apkpull/__main__.py
|
|
16
|
+
devtools/tools/apkpull/core.py
|
|
17
|
+
devtools/tools/apkpull/models.py
|
|
18
|
+
devtools/tools/apkpull/ui.py
|
|
19
|
+
devtools/tools/portkill/__init__.py
|
|
20
|
+
devtools/tools/portkill/__main__.py
|
|
21
|
+
devtools/tools/portkill/core.py
|
|
22
|
+
devtools/tools/portkill/models.py
|
|
23
|
+
devtools/tools/portkill/ui.py
|
|
24
|
+
devtools/tools/removebg/__init__.py
|
|
25
|
+
devtools/tools/removebg/__main__.py
|
|
26
|
+
devtools/tools/removebg/cli.py
|
|
27
|
+
devtools/tools/removebg/core.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
psutil>=5.9
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
devtools
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from importlib import import_module
|
|
3
|
+
from devtools.tools import get_tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_tool(tool_name: str, args: list[str]):
|
|
7
|
+
tool = get_tool(tool_name)
|
|
8
|
+
if tool is None:
|
|
9
|
+
print(f"Unknown tool: {tool_name}")
|
|
10
|
+
print("Run `devtools` to see available tools.")
|
|
11
|
+
sys.exit(1)
|
|
12
|
+
|
|
13
|
+
module_name = tool.module
|
|
14
|
+
|
|
15
|
+
# Try module.run directly (if tool exposes run at module root)
|
|
16
|
+
mod = import_module(module_name)
|
|
17
|
+
if hasattr(mod, "run"):
|
|
18
|
+
mod.run(args)
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
# Try module.cli.run (removebg pattern)
|
|
22
|
+
try:
|
|
23
|
+
cli_mod = import_module(f"{module_name}.cli")
|
|
24
|
+
if hasattr(cli_mod, "run"):
|
|
25
|
+
cli_mod.run(args)
|
|
26
|
+
return
|
|
27
|
+
except ImportError:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
# Try module.ui.run (portkill pattern)
|
|
31
|
+
try:
|
|
32
|
+
ui_mod = import_module(f"{module_name}.ui")
|
|
33
|
+
if hasattr(ui_mod, "run"):
|
|
34
|
+
ui_mod.run(args)
|
|
35
|
+
return
|
|
36
|
+
except ImportError:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
print(f"Tool module {module_name} has no run() function")
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main():
|
|
44
|
+
if len(sys.argv) > 1:
|
|
45
|
+
run_tool(sys.argv[1], sys.argv[2:])
|
|
46
|
+
else:
|
|
47
|
+
from devtools.launcher import render_launcher
|
|
48
|
+
result = render_launcher()
|
|
49
|
+
if result:
|
|
50
|
+
run_tool(result, [])
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
if sys.platform == "win32":
|
|
3
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
4
|
+
|
|
5
|
+
from devtools.tools import TOOLS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def render_launcher():
|
|
9
|
+
"""
|
|
10
|
+
CLI launcher — list all tools with numbers, read selection.
|
|
11
|
+
Returns: tool name string to launch, or None to quit
|
|
12
|
+
"""
|
|
13
|
+
if not TOOLS:
|
|
14
|
+
print("No tools registered!")
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
print()
|
|
18
|
+
print("=" * 50)
|
|
19
|
+
print(" devtools — 开发者工具箱")
|
|
20
|
+
print("=" * 50)
|
|
21
|
+
print()
|
|
22
|
+
for i, tool in enumerate(TOOLS):
|
|
23
|
+
print(f" {i + 1}. {tool.name:15s} — {tool.description}")
|
|
24
|
+
print()
|
|
25
|
+
print(f" [1-{len(TOOLS)}] 选择工具 | [Q] 退出")
|
|
26
|
+
print()
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
choice = input("> ").strip()
|
|
30
|
+
except (EOFError, KeyboardInterrupt):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
if choice.lower() == "q":
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
idx = int(choice) - 1
|
|
38
|
+
if 0 <= idx < len(TOOLS):
|
|
39
|
+
return TOOLS[idx].name
|
|
40
|
+
except ValueError:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
return None
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Tools registry for devtools"""
|
|
2
|
+
from devtools.types import Tool
|
|
3
|
+
|
|
4
|
+
TOOLS = [
|
|
5
|
+
Tool(
|
|
6
|
+
name="portkill",
|
|
7
|
+
category="进程/端口",
|
|
8
|
+
description="按端口杀 Node.js 进程",
|
|
9
|
+
module="devtools.tools.portkill",
|
|
10
|
+
),
|
|
11
|
+
Tool(
|
|
12
|
+
name="removebg",
|
|
13
|
+
category="图像处理",
|
|
14
|
+
description="批量删除图片背景",
|
|
15
|
+
module="devtools.tools.removebg",
|
|
16
|
+
),
|
|
17
|
+
Tool(
|
|
18
|
+
name="apkpull",
|
|
19
|
+
category="设备管理",
|
|
20
|
+
description="提取手机 APK 文件",
|
|
21
|
+
module="devtools.tools.apkpull",
|
|
22
|
+
),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
def get_tool(name: str):
|
|
26
|
+
for tool in TOOLS:
|
|
27
|
+
if tool.name == name:
|
|
28
|
+
return tool
|
|
29
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""apkpull - Extract APK files from Android device via ADB"""
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import re
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
def _find_aapt() -> str | None:
|
|
7
|
+
"""Find aapt.exe — checks PATH first, then Android SDK build-tools"""
|
|
8
|
+
# 1. Check PATH
|
|
9
|
+
import shutil as _shutil
|
|
10
|
+
path_aapt = _shutil.which("aapt")
|
|
11
|
+
if path_aapt and os.path.isfile(path_aapt):
|
|
12
|
+
return path_aapt
|
|
13
|
+
path_aapt2 = _shutil.which("aapt2")
|
|
14
|
+
if path_aapt2 and os.path.isfile(path_aapt2):
|
|
15
|
+
return path_aapt2
|
|
16
|
+
# 2. Check Android SDK build-tools
|
|
17
|
+
sdk_base = os.environ.get("LOCALAPPDATA", "") + "/Android/Sdk"
|
|
18
|
+
build_tools = os.path.join(sdk_base, "build-tools")
|
|
19
|
+
if os.path.isdir(build_tools):
|
|
20
|
+
versions = sorted(os.listdir(build_tools), reverse=True)
|
|
21
|
+
for v in versions:
|
|
22
|
+
for binary in ("aapt.exe", "aapt2.exe"):
|
|
23
|
+
aapt = os.path.join(build_tools, v, binary)
|
|
24
|
+
if os.path.isfile(aapt):
|
|
25
|
+
return aapt
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
AAPT_PATH = _find_aapt()
|
|
29
|
+
|
|
30
|
+
def check_adb() -> bool:
|
|
31
|
+
"""检查 ADB 是否可用"""
|
|
32
|
+
try:
|
|
33
|
+
subprocess.run(["adb", "version"], capture_output=True, check=True)
|
|
34
|
+
return True
|
|
35
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def check_device() -> bool:
|
|
39
|
+
"""检查设备是否连接"""
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(["adb", "devices"], capture_output=True, text=True, encoding="utf-8", errors="replace", check=True)
|
|
42
|
+
lines = result.stdout.strip().split("\n")
|
|
43
|
+
for line in lines[1:]:
|
|
44
|
+
if line.strip() and not line.startswith("*"):
|
|
45
|
+
parts = line.split()
|
|
46
|
+
if len(parts) >= 2 and parts[1] == "device":
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
def scan_packages() -> list[str]:
|
|
53
|
+
"""执行 adb shell pm list packages -3,返回包名列表"""
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
["adb", "shell", "pm", "list", "packages", "-3"],
|
|
56
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
|
|
57
|
+
)
|
|
58
|
+
pkgs = []
|
|
59
|
+
for line in result.stdout.strip().split("\n"):
|
|
60
|
+
line = line.strip()
|
|
61
|
+
if line.startswith("package:"):
|
|
62
|
+
pkgs.append(line[8:])
|
|
63
|
+
return pkgs
|
|
64
|
+
|
|
65
|
+
def get_apk_path(pkg: str) -> str | None:
|
|
66
|
+
"""执行 pm path,返回 APK 物理路径"""
|
|
67
|
+
try:
|
|
68
|
+
result = subprocess.run(
|
|
69
|
+
["adb", "shell", "pm", "path", pkg],
|
|
70
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
|
|
71
|
+
)
|
|
72
|
+
for line in result.stdout.strip().split("\n"):
|
|
73
|
+
if line.startswith("package:"):
|
|
74
|
+
return line[8:].strip()
|
|
75
|
+
return None
|
|
76
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
def get_app_name(pkg: str) -> str:
|
|
80
|
+
"""通过 aapt 提取 APK 的 application label"""
|
|
81
|
+
if not AAPT_PATH:
|
|
82
|
+
return pkg
|
|
83
|
+
apk_path = get_apk_path(pkg)
|
|
84
|
+
if not apk_path:
|
|
85
|
+
return pkg
|
|
86
|
+
tmp = os.path.join(tempfile.gettempdir(), f"apkpull_{os.getpid()}_{pkg.replace('.', '_')}.apk")
|
|
87
|
+
try:
|
|
88
|
+
# pull APK to temp file
|
|
89
|
+
pull_ok = subprocess.run(
|
|
90
|
+
["adb", "pull", apk_path, tmp],
|
|
91
|
+
capture_output=True, encoding="utf-8", errors="replace"
|
|
92
|
+
).returncode == 0
|
|
93
|
+
if not pull_ok or not os.path.exists(tmp):
|
|
94
|
+
return pkg
|
|
95
|
+
# extract label via aapt
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
[AAPT_PATH, "dump", "badging", tmp],
|
|
98
|
+
capture_output=True, encoding="utf-8", errors="replace"
|
|
99
|
+
)
|
|
100
|
+
for line in result.stdout.split("\n"):
|
|
101
|
+
m = re.search(r"application-label:'(.+?)'", line)
|
|
102
|
+
if m:
|
|
103
|
+
return m.group(1)
|
|
104
|
+
return pkg
|
|
105
|
+
except Exception:
|
|
106
|
+
return pkg
|
|
107
|
+
finally:
|
|
108
|
+
if os.path.exists(tmp):
|
|
109
|
+
try:
|
|
110
|
+
os.remove(tmp)
|
|
111
|
+
except OSError:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
def pull_apk(apk_path: str, dest_dir: str) -> bool:
|
|
115
|
+
"""执行 adb pull,拉取 APK 到目标目录"""
|
|
116
|
+
try:
|
|
117
|
+
subprocess.run(
|
|
118
|
+
["adb", "pull", apk_path, dest_dir],
|
|
119
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace", check=True
|
|
120
|
+
)
|
|
121
|
+
return True
|
|
122
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
123
|
+
return False
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
if sys.platform == "win32":
|
|
3
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
from devtools.tools.apkpull.models import PackageInfo
|
|
8
|
+
|
|
9
|
+
def scan_packages_with_labels() -> list[PackageInfo]:
|
|
10
|
+
"""Scan packages and fetch labels in parallel threads (max 10 concurrent)"""
|
|
11
|
+
from devtools.tools.apkpull.core import scan_packages, check_adb, check_device, get_app_name
|
|
12
|
+
|
|
13
|
+
if not check_adb():
|
|
14
|
+
print("错误: 未找到 adb 命令,请确保 Android SDK platform-tools 已安装并加入 PATH")
|
|
15
|
+
return []
|
|
16
|
+
if not check_device():
|
|
17
|
+
print("错误: 未检测到设备,请确保手机已通过 USB 连接并开启了 USB 调试模式")
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
print("正在获取应用列表...")
|
|
21
|
+
pkgs = scan_packages()
|
|
22
|
+
if not pkgs:
|
|
23
|
+
print("未找到第三方应用")
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
print(f"共 {len(pkgs)} 个应用,正在获取应用名称...")
|
|
27
|
+
|
|
28
|
+
packages: list[PackageInfo] = []
|
|
29
|
+
lock = threading.Lock()
|
|
30
|
+
|
|
31
|
+
def fetch(pkg: str):
|
|
32
|
+
try:
|
|
33
|
+
label = get_app_name(pkg)
|
|
34
|
+
except Exception:
|
|
35
|
+
label = pkg # fallback on error
|
|
36
|
+
with lock:
|
|
37
|
+
packages.append(PackageInfo(pkg=pkg, label=label))
|
|
38
|
+
|
|
39
|
+
threads: list[threading.Thread] = []
|
|
40
|
+
for pkg in pkgs:
|
|
41
|
+
t = threading.Thread(target=fetch, args=(pkg,))
|
|
42
|
+
t.start()
|
|
43
|
+
threads.append(t)
|
|
44
|
+
if len(threads) >= 10:
|
|
45
|
+
threads[0].join()
|
|
46
|
+
threads = threads[1:]
|
|
47
|
+
|
|
48
|
+
for t in threads:
|
|
49
|
+
t.join()
|
|
50
|
+
|
|
51
|
+
packages.sort(key=lambda x: x.label.lower())
|
|
52
|
+
return packages
|
|
53
|
+
|
|
54
|
+
def show_packages(packages: list[PackageInfo], short: bool = True) -> None:
|
|
55
|
+
print()
|
|
56
|
+
print(f"{'#':<5} {'应用名':<30} {'包名'}")
|
|
57
|
+
print("-" * 90)
|
|
58
|
+
for i, p in enumerate(packages):
|
|
59
|
+
label = p.label[:28] + ".." if short and len(p.label) > 30 else p.label
|
|
60
|
+
pkg = p.pkg[:50] if short and len(p.pkg) > 50 else p.pkg
|
|
61
|
+
print(f"{i+1:<5} {label:<30} {pkg}")
|
|
62
|
+
|
|
63
|
+
def select_packages(packages: list[PackageInfo], user_input: str) -> list[PackageInfo]:
|
|
64
|
+
"""Parse user input: keyword search or numeric selection (1,3-5, 1,3,5)"""
|
|
65
|
+
if not user_input:
|
|
66
|
+
return []
|
|
67
|
+
# If input contains letters, treat as search keyword
|
|
68
|
+
if any(c.isalpha() for c in user_input):
|
|
69
|
+
keyword = user_input.lower()
|
|
70
|
+
return [p for p in packages if keyword in p.label.lower() or keyword in p.pkg.lower()]
|
|
71
|
+
# Numeric selection
|
|
72
|
+
indices: list[int] = []
|
|
73
|
+
for part in user_input.split(","):
|
|
74
|
+
part = part.strip()
|
|
75
|
+
if "-" in part:
|
|
76
|
+
start, end = part.split("-", 1)
|
|
77
|
+
try:
|
|
78
|
+
indices.extend(range(int(start) - 1, int(end) + 1)) # +1 because range end is exclusive
|
|
79
|
+
except ValueError:
|
|
80
|
+
print(f"无效输入: {part}")
|
|
81
|
+
return []
|
|
82
|
+
else:
|
|
83
|
+
try:
|
|
84
|
+
indices.append(int(part) - 1)
|
|
85
|
+
except ValueError:
|
|
86
|
+
print(f"无效输入: {part}")
|
|
87
|
+
return []
|
|
88
|
+
return [packages[i] for i in indices if 0 <= i < len(packages)]
|
|
89
|
+
|
|
90
|
+
def confirm_pull(packages: list[PackageInfo], dest_dir: str) -> bool:
|
|
91
|
+
print()
|
|
92
|
+
print("=" * 70)
|
|
93
|
+
for p in packages:
|
|
94
|
+
print(f" 应用名: {p.label}")
|
|
95
|
+
print(f" 包名: {p.pkg}")
|
|
96
|
+
print("-" * 70)
|
|
97
|
+
print(f"目标目录: {dest_dir}")
|
|
98
|
+
print(f"共 {len(packages)} 个 APK")
|
|
99
|
+
print()
|
|
100
|
+
try:
|
|
101
|
+
answer = input("确认拉取? (y/N): ").strip().lower()
|
|
102
|
+
except (EOFError, KeyboardInterrupt):
|
|
103
|
+
return False
|
|
104
|
+
return answer in ("y", "yes")
|
|
105
|
+
|
|
106
|
+
def run(args: list[str] | None = None):
|
|
107
|
+
"""Main entry point"""
|
|
108
|
+
if args is None:
|
|
109
|
+
args = []
|
|
110
|
+
|
|
111
|
+
# Determine destination directory
|
|
112
|
+
if args:
|
|
113
|
+
dest_dir = args[0]
|
|
114
|
+
else:
|
|
115
|
+
dest_dir = os.getcwd()
|
|
116
|
+
|
|
117
|
+
packages = scan_packages_with_labels()
|
|
118
|
+
if not packages:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
show_packages(packages)
|
|
122
|
+
|
|
123
|
+
while True:
|
|
124
|
+
print()
|
|
125
|
+
print("输入应用名/包名关键字搜索,或输入序号选择 (1,3-5, 1,3,5)")
|
|
126
|
+
print("回车退出")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
user_input = input("\n> ").strip()
|
|
130
|
+
except (EOFError, KeyboardInterrupt):
|
|
131
|
+
print()
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
if not user_input:
|
|
135
|
+
print("未选择,退出")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
selected = select_packages(packages, user_input)
|
|
139
|
+
if not selected:
|
|
140
|
+
print("未匹配到应用")
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
show_packages(selected, short=False)
|
|
144
|
+
|
|
145
|
+
if not confirm_pull(selected, dest_dir):
|
|
146
|
+
print("取消拉取")
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
# Pull APKs
|
|
150
|
+
from devtools.tools.apkpull.core import get_apk_path, pull_apk
|
|
151
|
+
succeeded: list[str] = []
|
|
152
|
+
failed: list[tuple] = []
|
|
153
|
+
|
|
154
|
+
for p in selected:
|
|
155
|
+
apk_path = get_apk_path(p.pkg)
|
|
156
|
+
if not apk_path:
|
|
157
|
+
failed.append((p.label, "未找到 APK 路径"))
|
|
158
|
+
continue
|
|
159
|
+
if pull_apk(apk_path, dest_dir):
|
|
160
|
+
succeeded.append(p.label)
|
|
161
|
+
else:
|
|
162
|
+
failed.append((p.label, "拉取失败"))
|
|
163
|
+
|
|
164
|
+
print()
|
|
165
|
+
if succeeded:
|
|
166
|
+
print(f"成功: {len(succeeded)} 个")
|
|
167
|
+
if failed:
|
|
168
|
+
print(f"失败: {len(failed)} 个")
|
|
169
|
+
for label, reason in failed:
|
|
170
|
+
print(f" {label}: {reason}")
|
|
171
|
+
return
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""portkill - kill Node.js processes by port"""
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import signal
|
|
3
|
+
import psutil
|
|
4
|
+
from typing import List, Dict
|
|
5
|
+
from devtools.tools.portkill.models import ProcessInfo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def scan_node_ports() -> List[ProcessInfo]:
|
|
9
|
+
"""扫描所有 Node.js 进程,返回按 PID 升序排列的列表"""
|
|
10
|
+
results: List[ProcessInfo] = []
|
|
11
|
+
seen_pids: Dict[int, ProcessInfo] = {}
|
|
12
|
+
|
|
13
|
+
# 首先找出所有 Node.js 进程
|
|
14
|
+
node_processes = []
|
|
15
|
+
for proc in psutil.process_iter():
|
|
16
|
+
try:
|
|
17
|
+
name = proc.name().lower()
|
|
18
|
+
if "node" not in name:
|
|
19
|
+
continue
|
|
20
|
+
node_processes.append(proc)
|
|
21
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
# 获取所有监听中的 TCP 连接(用于查端口)
|
|
25
|
+
connections_by_pid: Dict[int, List[int]] = {}
|
|
26
|
+
for conn in psutil.net_connections(kind="inet"):
|
|
27
|
+
if conn.status != "LISTEN":
|
|
28
|
+
continue
|
|
29
|
+
if not conn.laddr:
|
|
30
|
+
continue
|
|
31
|
+
pid = conn.pid
|
|
32
|
+
port = conn.laddr.port
|
|
33
|
+
if pid not in connections_by_pid:
|
|
34
|
+
connections_by_pid[pid] = []
|
|
35
|
+
connections_by_pid[pid].append(port)
|
|
36
|
+
|
|
37
|
+
# 为每个 Node.js 进程创建 ProcessInfo
|
|
38
|
+
for proc in node_processes:
|
|
39
|
+
pid = proc.pid
|
|
40
|
+
if pid in seen_pids:
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
ports = connections_by_pid.get(pid, [])
|
|
44
|
+
port = ports[0] if ports else 0
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
cmdline = " ".join(proc.cmdline())
|
|
48
|
+
cwd = proc.cwd()
|
|
49
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
50
|
+
cmdline = ""
|
|
51
|
+
cwd = ""
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
children = [child.pid for child in proc.children(recursive=True)]
|
|
55
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
56
|
+
children = []
|
|
57
|
+
|
|
58
|
+
seen_pids[pid] = ProcessInfo(
|
|
59
|
+
pid=pid,
|
|
60
|
+
port=port,
|
|
61
|
+
name=proc.name(),
|
|
62
|
+
cmdline=cmdline,
|
|
63
|
+
cwd=cwd,
|
|
64
|
+
children=children,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
results = list(seen_pids.values())
|
|
68
|
+
results.sort(key=lambda x: (-x.port, x.pid)) # 有端口优先,按端口降序;无端口按 PID 升序
|
|
69
|
+
return results
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_process_tree(pid: int) -> List[int]:
|
|
73
|
+
"""获取指定进程及其所有子进程的 PID 列表"""
|
|
74
|
+
try:
|
|
75
|
+
proc = psutil.Process(pid)
|
|
76
|
+
pids = [pid]
|
|
77
|
+
for child in proc.children(recursive=True):
|
|
78
|
+
pids.append(child.pid)
|
|
79
|
+
return pids
|
|
80
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
81
|
+
return [pid] # 进程不存在时只返回自身
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def kill_process(pid: int, kill_children: bool = False) -> bool:
|
|
85
|
+
"""向目标进程发送 SIGTERM 信号,成功返回 True"""
|
|
86
|
+
try:
|
|
87
|
+
proc = psutil.Process(pid)
|
|
88
|
+
if kill_children:
|
|
89
|
+
for child in proc.children(recursive=True):
|
|
90
|
+
try:
|
|
91
|
+
os.kill(child.pid, signal.SIGTERM)
|
|
92
|
+
except (OSError, psutil.NoSuchProcess):
|
|
93
|
+
pass
|
|
94
|
+
os.kill(pid, signal.SIGTERM)
|
|
95
|
+
return True
|
|
96
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, OSError):
|
|
97
|
+
return False
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class ProcessInfo:
|
|
7
|
+
"""Node.js 进程信息"""
|
|
8
|
+
pid: int
|
|
9
|
+
port: int
|
|
10
|
+
name: str
|
|
11
|
+
cmdline: str
|
|
12
|
+
cwd: str
|
|
13
|
+
children: list[int] = field(default_factory=list)
|
|
14
|
+
cmdline_short: Optional[str] = None # 截断后的命令摘要
|
|
15
|
+
|
|
16
|
+
def __post_init__(self):
|
|
17
|
+
self.cmdline_short = self.cmdline
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
if sys.platform == "win32":
|
|
3
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
4
|
+
|
|
5
|
+
from devtools.tools.portkill.models import ProcessInfo
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def show_processes(processes: list[ProcessInfo], short: bool = False) -> None:
|
|
9
|
+
"""Display processes in a simple table"""
|
|
10
|
+
print()
|
|
11
|
+
print(f"{'#':<4} {'PORT':<6} {'PID':<8} {'NAME':<20} {'CMDLINE'}")
|
|
12
|
+
print("-" * 120)
|
|
13
|
+
for i, p in enumerate(processes):
|
|
14
|
+
port_str = str(p.port) if p.port > 0 else "-"
|
|
15
|
+
cmdline = p.cmdline
|
|
16
|
+
if short:
|
|
17
|
+
cmdline = (p.cmdline[:50] + "...") if len(p.cmdline) > 50 else p.cmdline
|
|
18
|
+
print(f"{i+1:<4} {port_str:<6} {p.pid:<8} {p.name:<20} {cmdline}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def select_processes(processes: list[ProcessInfo], user_input: str) -> list[ProcessInfo]:
|
|
22
|
+
"""
|
|
23
|
+
Parse user selection from input string.
|
|
24
|
+
Supports: 1,3-5 or 1,3,5 or 1-3 style selection.
|
|
25
|
+
Returns selected ProcessInfo list.
|
|
26
|
+
"""
|
|
27
|
+
if not processes:
|
|
28
|
+
print("没有发现 Node.js 进程")
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
if not user_input:
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
# Parse selection
|
|
35
|
+
indices: list[int] = []
|
|
36
|
+
for part in user_input.split(","):
|
|
37
|
+
part = part.strip()
|
|
38
|
+
if not part:
|
|
39
|
+
continue
|
|
40
|
+
if "-" in part:
|
|
41
|
+
start, end = part.split("-", 1)
|
|
42
|
+
try:
|
|
43
|
+
indices.extend(range(int(start) - 1, int(end)))
|
|
44
|
+
except ValueError:
|
|
45
|
+
print(f"无效输入: {part}")
|
|
46
|
+
return []
|
|
47
|
+
else:
|
|
48
|
+
try:
|
|
49
|
+
indices.append(int(part) - 1)
|
|
50
|
+
except ValueError:
|
|
51
|
+
print(f"无效输入: {part}")
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
selected = [processes[i] for i in indices if 0 <= i < len(processes)]
|
|
55
|
+
return selected
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def confirm_kill(processes: list[ProcessInfo]) -> bool:
|
|
59
|
+
"""Show confirmation with process details"""
|
|
60
|
+
print()
|
|
61
|
+
print("=" * 60)
|
|
62
|
+
for p in processes:
|
|
63
|
+
print(f" 端口: {p.port} | PID: {p.pid} | 进程名: {p.name}")
|
|
64
|
+
print(f" 命令: {p.cmdline}")
|
|
65
|
+
print(f" 目录: {p.cwd}")
|
|
66
|
+
if p.children:
|
|
67
|
+
print(f" 子进程: {p.children}")
|
|
68
|
+
print("-" * 60)
|
|
69
|
+
print()
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
answer = input("确认关闭以上进程? (y/N): ").strip().lower()
|
|
73
|
+
except (EOFError, KeyboardInterrupt):
|
|
74
|
+
return False
|
|
75
|
+
return answer in ("y", "yes")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def execute_kill(pids: list[int], kill_children: bool = False) -> dict:
|
|
79
|
+
"""
|
|
80
|
+
Execute kill on given PIDs.
|
|
81
|
+
Returns {"killed": [...], "failed": [...]}
|
|
82
|
+
"""
|
|
83
|
+
from devtools.tools.portkill.core import kill_process, get_process_tree
|
|
84
|
+
|
|
85
|
+
killed: list[int] = []
|
|
86
|
+
failed: list[tuple] = []
|
|
87
|
+
|
|
88
|
+
all_pids: list[int] = []
|
|
89
|
+
for pid in pids:
|
|
90
|
+
if kill_children:
|
|
91
|
+
all_pids.extend(get_process_tree(pid))
|
|
92
|
+
else:
|
|
93
|
+
all_pids.append(pid)
|
|
94
|
+
|
|
95
|
+
seen = set()
|
|
96
|
+
unique_pids = []
|
|
97
|
+
for pid in all_pids:
|
|
98
|
+
if pid not in seen:
|
|
99
|
+
seen.add(pid)
|
|
100
|
+
unique_pids.append(pid)
|
|
101
|
+
|
|
102
|
+
for pid in unique_pids:
|
|
103
|
+
if kill_process(pid, kill_children=False):
|
|
104
|
+
killed.append(pid)
|
|
105
|
+
else:
|
|
106
|
+
failed.append((pid, "进程不存在或无权限"))
|
|
107
|
+
|
|
108
|
+
return {"killed": killed, "failed": failed}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def run(args: list[str] | None = None):
|
|
112
|
+
"""Main entry point for portkill CLI"""
|
|
113
|
+
from devtools.tools.portkill.core import scan_node_ports
|
|
114
|
+
|
|
115
|
+
if args is None:
|
|
116
|
+
args = []
|
|
117
|
+
|
|
118
|
+
short = "--long" not in args # short mode by default
|
|
119
|
+
listen_only = "--listen-only" in args
|
|
120
|
+
|
|
121
|
+
print("扫描 Node.js 进程...")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
all_processes = scan_node_ports()
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print(f"扫描失败: {e}")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
if not all_processes:
|
|
130
|
+
print("未发现 Node.js 进程")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
short_mode = short
|
|
134
|
+
listen_filter = listen_only
|
|
135
|
+
|
|
136
|
+
while True:
|
|
137
|
+
# Apply filters
|
|
138
|
+
processes = all_processes
|
|
139
|
+
if listen_filter:
|
|
140
|
+
processes = [p for p in processes if p.port > 0]
|
|
141
|
+
|
|
142
|
+
if not processes:
|
|
143
|
+
print("没有符合条件的进程")
|
|
144
|
+
print("提示: 按 l 切换显示模式,或 p 关闭端口筛选")
|
|
145
|
+
else:
|
|
146
|
+
show_processes(processes, short=short_mode)
|
|
147
|
+
|
|
148
|
+
print()
|
|
149
|
+
print("快捷: l=长短显示 p=端口筛选 r=重新扫描 回车=退出")
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
user_input = input("\n> ").strip()
|
|
153
|
+
except (EOFError, KeyboardInterrupt):
|
|
154
|
+
print()
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
if not user_input:
|
|
158
|
+
print("未选择任何进程,退出")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
# Interactive commands
|
|
162
|
+
if user_input == "l":
|
|
163
|
+
short_mode = not short_mode
|
|
164
|
+
continue
|
|
165
|
+
if user_input == "p":
|
|
166
|
+
listen_filter = not listen_filter
|
|
167
|
+
continue
|
|
168
|
+
if user_input == "r":
|
|
169
|
+
try:
|
|
170
|
+
all_processes = scan_node_ports()
|
|
171
|
+
except Exception as e:
|
|
172
|
+
print(f"扫描失败: {e}")
|
|
173
|
+
return
|
|
174
|
+
if not all_processes:
|
|
175
|
+
print("未发现 Node.js 进程")
|
|
176
|
+
return
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# Try to parse as selection
|
|
180
|
+
selected = select_processes(processes, user_input)
|
|
181
|
+
if not selected:
|
|
182
|
+
print("未选择任何进程,退出")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
if not confirm_kill(selected):
|
|
186
|
+
print("取消关闭")
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
child_answer = input("是否同时关闭子进程? (y/N): ").strip().lower()
|
|
191
|
+
except (EOFError, KeyboardInterrupt):
|
|
192
|
+
child_answer = "n"
|
|
193
|
+
kill_children = child_answer in ("y", "yes")
|
|
194
|
+
|
|
195
|
+
pids = [p.pid for p in selected]
|
|
196
|
+
result = execute_kill(pids, kill_children=kill_children)
|
|
197
|
+
|
|
198
|
+
print()
|
|
199
|
+
if result["killed"]:
|
|
200
|
+
print(f"已关闭: {result['killed']}")
|
|
201
|
+
if result["failed"]:
|
|
202
|
+
print(f"失败: {result['failed']}")
|
|
203
|
+
return
|
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""CLI entry point for removebg"""
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from devtools.tools.removebg.core import process_images
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
MODELS = ["u2net", "u2netp", "u2net_human_seg"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(args: list[str] | None = None):
|
|
13
|
+
if args is None:
|
|
14
|
+
args = sys.argv[1:]
|
|
15
|
+
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="python -m devtools removebg",
|
|
18
|
+
description="Remove background from images using rembg",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"-o", "--output",
|
|
22
|
+
type=Path,
|
|
23
|
+
default=None,
|
|
24
|
+
metavar="DIR",
|
|
25
|
+
help="Output directory (default: overwrite originals)",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"-m", "--model",
|
|
29
|
+
choices=MODELS,
|
|
30
|
+
default="u2net",
|
|
31
|
+
help="Model to use (default: u2net)",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"-a", "--alpha-matting",
|
|
35
|
+
action="store_true",
|
|
36
|
+
help="Enable alpha matting for better edge quality",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"files",
|
|
40
|
+
nargs="+",
|
|
41
|
+
type=Path,
|
|
42
|
+
help="Image files to process",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
ns = parser.parse_args(args)
|
|
46
|
+
|
|
47
|
+
# Validate files exist
|
|
48
|
+
missing = [f for f in ns.files if not f.exists()]
|
|
49
|
+
if missing:
|
|
50
|
+
for f in missing:
|
|
51
|
+
print(f"文件不存在: {f}", file=sys.stderr)
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
|
|
54
|
+
# Create output dir if specified
|
|
55
|
+
if ns.output is not None:
|
|
56
|
+
ns.output.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
print(f"开始处理 {len(ns.files)} 个文件...")
|
|
59
|
+
|
|
60
|
+
result = process_images(
|
|
61
|
+
input_paths=ns.files,
|
|
62
|
+
output_dir=ns.output,
|
|
63
|
+
model=ns.model,
|
|
64
|
+
alpha_matting=ns.alpha_matting,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Report results
|
|
68
|
+
print()
|
|
69
|
+
for path in result["succeeded"]:
|
|
70
|
+
print(f" OK: {path}")
|
|
71
|
+
|
|
72
|
+
for path, err in result["failed"]:
|
|
73
|
+
print(f" FAIL: {path} — {err}")
|
|
74
|
+
|
|
75
|
+
total = len(result["succeeded"]) + len(result["failed"])
|
|
76
|
+
print()
|
|
77
|
+
print(f"完成: {total} 文件, {len(result['succeeded'])} 成功, {len(result['failed'])} 失败")
|
|
78
|
+
|
|
79
|
+
if result["failed"]:
|
|
80
|
+
sys.exit(1)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""rembg core wrapper"""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from rembg import remove, new_session
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def process_images(
|
|
10
|
+
input_paths: list[Path],
|
|
11
|
+
output_dir: Optional[Path],
|
|
12
|
+
model: str = "u2net",
|
|
13
|
+
alpha_matting: bool = False,
|
|
14
|
+
alpha_matting_foreground_threshold: int = 240,
|
|
15
|
+
alpha_matting_background_threshold: int = 10,
|
|
16
|
+
alpha_matting_erode_size: int = 0,
|
|
17
|
+
) -> dict:
|
|
18
|
+
"""
|
|
19
|
+
Process a list of image files with rembg.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
input_paths: List of image file paths
|
|
23
|
+
output_dir: If set, write output files here. If None, overwrite originals.
|
|
24
|
+
model: u2net | u2netp | u2net_human_seg
|
|
25
|
+
alpha_matting: Enable alpha matting
|
|
26
|
+
alpha_matting_foreground_threshold: Foreground threshold for alpha matting
|
|
27
|
+
alpha_matting_background_threshold: Background threshold for alpha matting
|
|
28
|
+
alpha_matting_erode_size: Erode size for alpha matting
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
{"succeeded": [...], "failed": [(path, error_message), ...]}
|
|
32
|
+
"""
|
|
33
|
+
succeeded = []
|
|
34
|
+
failed = []
|
|
35
|
+
|
|
36
|
+
session = new_session(model_name=model)
|
|
37
|
+
|
|
38
|
+
for input_path in input_paths:
|
|
39
|
+
try:
|
|
40
|
+
with Image.open(input_path) as img:
|
|
41
|
+
# rembg returns RGBA
|
|
42
|
+
output = remove(
|
|
43
|
+
img,
|
|
44
|
+
session=session,
|
|
45
|
+
alpha_matting=alpha_matting,
|
|
46
|
+
alpha_matting_foreground_threshold=alpha_matting_foreground_threshold,
|
|
47
|
+
alpha_matting_background_threshold=alpha_matting_background_threshold,
|
|
48
|
+
alpha_matting_erode_size=alpha_matting_erode_size,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if output_dir is None:
|
|
52
|
+
# Overwrite original
|
|
53
|
+
output_path = input_path
|
|
54
|
+
else:
|
|
55
|
+
output_path = output_dir / input_path.name
|
|
56
|
+
|
|
57
|
+
# Ensure output directory exists
|
|
58
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
output.save(output_path, "PNG")
|
|
61
|
+
succeeded.append(str(output_path))
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
failed.append((str(input_path), str(e)))
|
|
65
|
+
|
|
66
|
+
return {"succeeded": succeeded, "failed": failed}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "damon-devtools"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Developer CLI tools — portkill, removebg, apkpull"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = ["psutil>=5.9"]
|
|
8
|
+
license = {text = "MIT"}
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "damon"},
|
|
11
|
+
]
|
|
12
|
+
keywords = ["cli", "devtools", "android", "apk", "adb"]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
devtools = "devtools.__main__:main"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["."]
|
|
19
|
+
include = ["devtools*"]
|
|
20
|
+
exclude = ["dotfiles*", "tests*", "build*", "dist*"]
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["setuptools>=61.0"]
|
|
24
|
+
build-backend = "setuptools.build_meta"
|