pymecli 0.4.3__tar.gz → 0.4.5__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.
Files changed (46) hide show
  1. {pymecli-0.4.3 → pymecli-0.4.5}/PKG-INFO +14 -3
  2. pymecli-0.4.5/README.md +26 -0
  3. pymecli-0.4.5/api/v1/clash.py +113 -0
  4. pymecli-0.4.5/cli/dst.py +222 -0
  5. {pymecli-0.4.3 → pymecli-0.4.5}/cli/fast.py +2 -2
  6. {pymecli-0.4.3 → pymecli-0.4.5}/core/clash.py +181 -9
  7. {pymecli-0.4.3 → pymecli-0.4.5}/data/template.yaml +70 -70
  8. {pymecli-0.4.3 → pymecli-0.4.5}/pyproject.toml +4 -1
  9. pymecli-0.4.3/README.md +0 -16
  10. pymecli-0.4.3/api/v1/clash.py +0 -62
  11. {pymecli-0.4.3 → pymecli-0.4.5}/.gitignore +0 -0
  12. {pymecli-0.4.3 → pymecli-0.4.5}/api/__init__.py +0 -0
  13. {pymecli-0.4.3 → pymecli-0.4.5}/api/v1/__init__.py +0 -0
  14. {pymecli-0.4.3 → pymecli-0.4.5}/api/v1/redis.py +0 -0
  15. {pymecli-0.4.3 → pymecli-0.4.5}/cli/__init__.py +0 -0
  16. {pymecli-0.4.3 → pymecli-0.4.5}/cli/baidu.py +0 -0
  17. {pymecli-0.4.3 → pymecli-0.4.5}/cli/bitget.py +0 -0
  18. {pymecli-0.4.3 → pymecli-0.4.5}/cli/example.py +0 -0
  19. {pymecli-0.4.3 → pymecli-0.4.5}/cli/gate.py +0 -0
  20. {pymecli-0.4.3 → pymecli-0.4.5}/cli/redis_csv.py +0 -0
  21. {pymecli-0.4.3 → pymecli-0.4.5}/cli/util.py +0 -0
  22. {pymecli-0.4.3 → pymecli-0.4.5}/core/__init__.py +0 -0
  23. {pymecli-0.4.3 → pymecli-0.4.5}/core/config.py +0 -0
  24. {pymecli-0.4.3 → pymecli-0.4.5}/core/redis_client.py +0 -0
  25. {pymecli-0.4.3 → pymecli-0.4.5}/crypto/__init__.py +0 -0
  26. {pymecli-0.4.3 → pymecli-0.4.5}/crypto/bitget.py +0 -0
  27. {pymecli-0.4.3 → pymecli-0.4.5}/crypto/gate.py +0 -0
  28. {pymecli-0.4.3 → pymecli-0.4.5}/data/__init__.py +0 -0
  29. {pymecli-0.4.3 → pymecli-0.4.5}/data/dou_dict.py +0 -0
  30. {pymecli-0.4.3 → pymecli-0.4.5}/data/dou_list.py +0 -0
  31. {pymecli-0.4.3 → pymecli-0.4.5}/data/main.py +0 -0
  32. {pymecli-0.4.3 → pymecli-0.4.5}/models/__init__.py +0 -0
  33. {pymecli-0.4.3 → pymecli-0.4.5}/models/douzero_model.py +0 -0
  34. {pymecli-0.4.3 → pymecli-0.4.5}/models/ocr_model.py +0 -0
  35. {pymecli-0.4.3 → pymecli-0.4.5}/models/response.py +0 -0
  36. {pymecli-0.4.3 → pymecli-0.4.5}/utils/__init__.py +0 -0
  37. {pymecli-0.4.3 → pymecli-0.4.5}/utils/elapsed.py +0 -0
  38. {pymecli-0.4.3 → pymecli-0.4.5}/utils/helper.py +0 -0
  39. {pymecli-0.4.3 → pymecli-0.4.5}/utils/logger.py +0 -0
  40. {pymecli-0.4.3 → pymecli-0.4.5}/utils/mysql.py +0 -0
  41. {pymecli-0.4.3 → pymecli-0.4.5}/utils/path.py +0 -0
  42. {pymecli-0.4.3 → pymecli-0.4.5}/utils/pd.py +0 -0
  43. {pymecli-0.4.3 → pymecli-0.4.5}/utils/pyredis.py +0 -0
  44. {pymecli-0.4.3 → pymecli-0.4.5}/utils/sleep.py +0 -0
  45. {pymecli-0.4.3 → pymecli-0.4.5}/utils/text.py +0 -0
  46. {pymecli-0.4.3 → pymecli-0.4.5}/utils/toml.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymecli
3
- Version: 0.4.3
3
+ Version: 0.4.5
4
4
  Summary: My CLI
5
5
  Project-URL: Homepage, https://pypi.org/project/pymecli
6
6
  Project-URL: Repository, https://github.com/meme2046/pymecli
@@ -22,6 +22,7 @@ Requires-Dist: pyyaml>=6.0.3
22
22
  Requires-Dist: redis[hiredis]>=7.1.0
23
23
  Requires-Dist: requests[socks]>=2.32.5
24
24
  Requires-Dist: selenium>=4.40.0
25
+ Requires-Dist: slpp>=1.2.3
25
26
  Requires-Dist: sqlalchemy>=2.0.45
26
27
  Requires-Dist: tabulate>=0.9.0
27
28
  Requires-Dist: toml>=0.10.2
@@ -42,7 +43,17 @@ uv run example goodbye Xiaoming -f
42
43
  # fastapi server
43
44
 
44
45
  ```shell
45
- uv run fast --port 8877
46
+ uv run fast --port 8877 # local server
46
47
  uv build
47
- uv publish --token (TOKEN)
48
+ uv publish --token < TOKEN > # publish
49
+ ```
50
+
51
+ # dontstarvetogether
52
+
53
+ 1. 饥荒专服禁client mods使用, 更新client mods version, modoverrides.lua:
54
+
55
+ ```shell
56
+ uv run dst cmu d:/.backups/dontstarvetogether/modoverrides.lua -o c:/.dst/save/Cluster_1/Master/modoverrides.lua -o c:/.dst/save/Cluster_1/Caves/modoverrides.lua
57
+
58
+ uv run dst cml d:/.backups/dontstarvetogether/modoverrides.lua -m 3486375086
48
59
  ```
@@ -0,0 +1,26 @@
1
+ # typer example
2
+
3
+ ```shell
4
+ uv run example hello Xiaoming
5
+ uv run example hello Xiaoming --from Pymecli
6
+ uv run example goodbye Xiaoming
7
+ uv run example goodbye Xiaoming -f
8
+ ```
9
+
10
+ # fastapi server
11
+
12
+ ```shell
13
+ uv run fast --port 8877 # local server
14
+ uv build
15
+ uv publish --token < TOKEN > # publish
16
+ ```
17
+
18
+ # dontstarvetogether
19
+
20
+ 1. 饥荒专服禁client mods使用, 更新client mods version, modoverrides.lua:
21
+
22
+ ```shell
23
+ uv run dst cmu d:/.backups/dontstarvetogether/modoverrides.lua -o c:/.dst/save/Cluster_1/Master/modoverrides.lua -o c:/.dst/save/Cluster_1/Caves/modoverrides.lua
24
+
25
+ uv run dst cml d:/.backups/dontstarvetogether/modoverrides.lua -m 3486375086
26
+ ```
@@ -0,0 +1,113 @@
1
+ import yaml
2
+ from fastapi import APIRouter, Query, Response
3
+
4
+ from core.clash import ClashYamlGenerator, get_generator_dependency
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ class NoAnchorDumper(yaml.Dumper):
10
+ def ignore_aliases(self, data):
11
+ return True
12
+
13
+
14
+ @router.get(
15
+ "/subpw",
16
+ summary="转换订阅(可以转换多个订阅),proxy-providers方式合并多个订阅,白名单模式",
17
+ description="根据提供的URL、User-Agent和名称获取并处理订阅信息,返回YAML格式的Clash配置",
18
+ response_description="返回YAML格式的Clash配置文件",
19
+ )
20
+ async def subProviderWhite(
21
+ urls: str = Query(..., description="订阅URL,逗号分隔"),
22
+ agents: str = Query(
23
+ "clash-verge/v2.4.3",
24
+ description="User-Agent,逗号分隔(根据客户段选择)",
25
+ ),
26
+ names: str = Query(
27
+ "订阅1", description="订阅名称,逗号分隔(可选,在客户端中的显示名)"
28
+ ),
29
+ generator: ClashYamlGenerator = get_generator_dependency(),
30
+ ):
31
+ # 将JSON字符串解析为Python对象
32
+ sub_list = generator.query2sub(urls, agents, names)
33
+ yaml_content, userinfo = generator.genPW(sub_list)
34
+ yaml_string = yaml.dump(
35
+ yaml_content,
36
+ allow_unicode=True,
37
+ default_flow_style=False,
38
+ sort_keys=False,
39
+ # Dumper=NoAnchorDumper,
40
+ )
41
+ return Response(
42
+ headers={"Subscription-Userinfo": userinfo},
43
+ content=yaml_string,
44
+ media_type="text/yaml",
45
+ )
46
+
47
+
48
+ @router.get(
49
+ "/subpb",
50
+ summary="转换订阅(可以转换多个订阅),proxy-providers方式合并多个订阅, 黑名单模式",
51
+ description="根据提供的URL、User-Agent和名称获取并处理订阅信息,返回YAML格式的Clash配置",
52
+ response_description="返回YAML格式的Clash配置文件",
53
+ )
54
+ async def subProviderBlack(
55
+ urls: str = Query(..., description="订阅URL,逗号分隔"),
56
+ agents: str = Query(
57
+ "clash-verge/v2.4.3",
58
+ description="User-Agent,逗号分隔(根据客户段选择)",
59
+ ),
60
+ names: str = Query(
61
+ "订阅1", description="订阅名称,逗号分隔(可选,在客户端中的显示名)"
62
+ ),
63
+ generator: ClashYamlGenerator = get_generator_dependency(),
64
+ ):
65
+ # 将JSON字符串解析为Python对象
66
+ sub_list = generator.query2sub(urls, agents, names)
67
+ yaml_content, userinfo = generator.genPB(sub_list)
68
+ yaml_string = yaml.dump(
69
+ yaml_content,
70
+ allow_unicode=True,
71
+ default_flow_style=False,
72
+ sort_keys=False,
73
+ # Dumper=NoAnchorDumper,
74
+ )
75
+ return Response(
76
+ headers={"Subscription-Userinfo": userinfo},
77
+ content=yaml_string,
78
+ media_type="text/yaml",
79
+ )
80
+
81
+
82
+ @router.get(
83
+ "/subb",
84
+ summary="转换订阅(可以转换多个订阅),拼接proxies的方式合并多个订阅, 黑名单模式",
85
+ description="根据提供的URL、User-Agent和名称获取并处理订阅信息,返回YAML格式的Clash配置",
86
+ response_description="返回YAML格式的Clash配置文件",
87
+ )
88
+ async def subBlack(
89
+ urls: str = Query(..., description="订阅URL,逗号分隔"),
90
+ agents: str = Query(
91
+ "clash-verge/v2.4.3",
92
+ description="User-Agent,逗号分隔(根据客户段选择)",
93
+ ),
94
+ names: str = Query(
95
+ "订阅1", description="订阅名称,逗号分隔(可选,在客户端中的显示名)"
96
+ ),
97
+ generator: ClashYamlGenerator = get_generator_dependency(),
98
+ ):
99
+ # 将JSON字符串解析为Python对象
100
+ sub_list = generator.query2sub(urls, agents, names)
101
+ yaml_content, userinfo = generator.genB(sub_list)
102
+ yaml_string = yaml.dump(
103
+ yaml_content,
104
+ allow_unicode=True,
105
+ default_flow_style=False,
106
+ sort_keys=False,
107
+ # Dumper=NoAnchorDumper,
108
+ )
109
+ return Response(
110
+ headers={"Subscription-Userinfo": userinfo},
111
+ content=yaml_string,
112
+ media_type="text/yaml",
113
+ )
@@ -0,0 +1,222 @@
1
+ import re
2
+ from typing import Dict, List, Optional
3
+
4
+ import requests
5
+ import typer
6
+ from slpp import slpp as lua
7
+
8
+ app = typer.Typer()
9
+
10
+ STEAM_API_URL = (
11
+ "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v0001"
12
+ )
13
+
14
+
15
+ def get_mod_versions(workshop_ids: List[str]) -> Dict[str, Dict[str, Optional[str]]]:
16
+ if not workshop_ids:
17
+ return {}
18
+
19
+ data = {"itemcount": str(len(workshop_ids))}
20
+ for i, wid in enumerate(workshop_ids):
21
+ data[f"publishedfileids[{i}]"] = wid
22
+
23
+ try:
24
+ response = requests.post(STEAM_API_URL, data=data, timeout=15)
25
+ response.raise_for_status()
26
+ result = response.json()
27
+ except Exception as e:
28
+ typer.secho(f"❌ Steam API 请求失败: {e}", fg=typer.colors.RED)
29
+ return {}
30
+
31
+ versions: Dict[str, Dict[str, Optional[str]]] = {}
32
+ for detail in result["response"].get("publishedfiledetails", []):
33
+ publishedfileid = detail.get("publishedfileid")
34
+ title = detail.get("title")
35
+ tags = detail.get("tags", [])
36
+ version = None
37
+ for tag in tags:
38
+ tag_str = tag.get("tag", "")
39
+ if tag_str.startswith("version:"):
40
+ version = tag_str.replace("version:", "")
41
+ break
42
+ versions[publishedfileid] = {"version": version, "title": title}
43
+
44
+ return versions
45
+
46
+
47
+ def extract_workshop_id(key: str) -> Optional[str]:
48
+ match = re.match(r"workshop-(\d+)", key)
49
+ return match.group(1) if match else None
50
+
51
+
52
+ def parse_lua_file(file_path: str) -> dict:
53
+ with open(file_path, "r", encoding="utf-8") as f:
54
+ source = f.read()
55
+ source = source.strip()
56
+ if source.startswith("return"):
57
+ source = source[6:].strip()
58
+ result = lua.decode(source)
59
+ assert isinstance(result, dict)
60
+ return result
61
+
62
+
63
+ def write_lua_file(file_path: str, data: dict):
64
+ with open(file_path, "w", encoding="utf-8") as f:
65
+ f.write("return " + lua.encode(data) + "\n")
66
+
67
+
68
+ def get_client_mods_list(file_path: str, mod_id: str) -> Optional[tuple[dict, dict]]:
69
+ """
70
+ 解析 lua 文件,返回 (data, client_mods_list)。
71
+ 失败返回 None(错误信息已由函数自行打印)。
72
+ """
73
+ try:
74
+ data = parse_lua_file(file_path)
75
+ except FileNotFoundError:
76
+ typer.secho(f"❌ 文件不存在: {file_path}", fg=typer.colors.RED)
77
+ return None
78
+ except Exception as e:
79
+ typer.secho(f"❌ 解析 Lua 文件失败: {e}", fg=typer.colors.RED)
80
+ return None
81
+
82
+ main_mod_key = f"workshop-{mod_id}"
83
+ main_mod = data.get(main_mod_key)
84
+ if not main_mod:
85
+ typer.secho(f"❌ 未找到 {main_mod_key}", fg=typer.colors.RED)
86
+ return None
87
+
88
+ client_mods_list = main_mod.get("configuration_options", {}).get(
89
+ "client_mods_list", {}
90
+ )
91
+ if not client_mods_list:
92
+ typer.secho("❌ 未找到 client_mods_list", fg=typer.colors.RED)
93
+ return None
94
+
95
+ return data, client_mods_list
96
+
97
+
98
+ # update_client_mods
99
+ @app.command()
100
+ def cmu(
101
+ file_path: str = typer.Argument(
102
+ "d:/.backups/dontstarvetogether/modoverrides.lua",
103
+ help="modoverrides.lua 文件路径",
104
+ ),
105
+ mod_id: str = typer.Option(
106
+ "3486375086",
107
+ "--mod-id",
108
+ "-m",
109
+ help="主 mod 的 workshop ID",
110
+ ),
111
+ output: List[str] = typer.Option(
112
+ None,
113
+ "--output",
114
+ "-o",
115
+ help="额外的输出路径,可多次指定",
116
+ ),
117
+ ):
118
+ result = get_client_mods_list(file_path, mod_id)
119
+ if result is None:
120
+ raise typer.Exit(code=1)
121
+ data, client_mods_list = result
122
+
123
+ workshop_ids = []
124
+ for key in client_mods_list.keys():
125
+ wid = extract_workshop_id(key)
126
+ if wid:
127
+ workshop_ids.append(wid)
128
+
129
+ if not workshop_ids:
130
+ typer.secho("❌ 未找到任何 client_mods", fg=typer.colors.RED)
131
+ raise typer.Exit(code=1)
132
+
133
+ typer.secho(
134
+ f"🔍 发现 {len(workshop_ids)} 个 client mods,正在查询 Steam API...",
135
+ fg=typer.colors.BLUE,
136
+ )
137
+ versions = get_mod_versions(workshop_ids)
138
+
139
+ if not versions:
140
+ typer.secho("❌ 未能获取版本信息", fg=typer.colors.RED)
141
+ raise typer.Exit(code=1)
142
+
143
+ typer.secho("📝 正在更新版本号...", fg=typer.colors.BLUE)
144
+ updated_count = 0
145
+ for mod_key, mod_info in client_mods_list.items():
146
+ wid = extract_workshop_id(mod_key)
147
+ if not wid:
148
+ continue
149
+ mod_data = versions.get(wid)
150
+ if mod_data is None:
151
+ typer.secho(f"⚠️ {mod_key}: 未获取到版本信息", fg=typer.colors.YELLOW)
152
+ continue
153
+ new_version = mod_data.get("version")
154
+ title = mod_data.get("title", "")
155
+ if new_version is None:
156
+ typer.secho(
157
+ f"⚠️ {mod_key} ({title}): 未获取到版本信息", fg=typer.colors.YELLOW
158
+ )
159
+ continue
160
+ old_version = mod_info.get("version")
161
+ if old_version != new_version:
162
+ mod_info["version"] = new_version
163
+ updated_count += 1
164
+ typer.secho(
165
+ f"✅ {mod_key} ({title}): {old_version} -> {new_version}",
166
+ fg=typer.colors.GREEN,
167
+ )
168
+ else:
169
+ typer.secho(
170
+ f"ℹ️ {mod_key} ({title}): 已是最新版本 {old_version}",
171
+ fg=typer.colors.WHITE,
172
+ )
173
+
174
+ try:
175
+ if updated_count > 0:
176
+ write_lua_file(file_path, data)
177
+ typer.secho(f"💾 已保存到: {file_path}", fg=typer.colors.CYAN)
178
+ if output:
179
+ for p in output:
180
+ write_lua_file(p, data)
181
+ typer.secho(f"💾 已保存到: {p}", fg=typer.colors.CYAN)
182
+ except Exception as e:
183
+ typer.secho(f"❌ 写入文件失败: {e}", fg=typer.colors.RED)
184
+ raise typer.Exit(code=1)
185
+
186
+ if updated_count > 0:
187
+ typer.secho(f"\n🎉 成功更新 {updated_count} 个 mod 版本", fg=typer.colors.GREEN)
188
+ else:
189
+ typer.secho("\nℹ️ 所有 mod 已是最新版本", fg=typer.colors.YELLOW)
190
+
191
+
192
+ # list_client_mods
193
+ @app.command()
194
+ def cml(
195
+ file_path: str = typer.Argument(
196
+ "d:/.backups/dontstarvetogether/modoverrides.lua",
197
+ help="modoverrides.lua 文件路径",
198
+ ),
199
+ mod_id: str = typer.Option(
200
+ "3486375086",
201
+ "--mod-id",
202
+ "-m",
203
+ help="主 mod 的 workshop ID",
204
+ ),
205
+ ):
206
+ """
207
+ 列出 client_mods_list 中所有 mod 的当前版本
208
+ """
209
+ result = get_client_mods_list(file_path, mod_id)
210
+ if result is None:
211
+ raise typer.Exit(code=1)
212
+ _, client_mods_list = result
213
+
214
+ typer.secho("\n📋 client_mods_list 中的 mod 列表:", fg=typer.colors.BLUE)
215
+ for i, (mod_key, mod_info) in enumerate(client_mods_list.items(), 1):
216
+ version = mod_info.get("version", "unknown")
217
+ typer.secho(f"{i}. {mod_key}: {version}", fg=typer.colors.WHITE)
218
+
219
+
220
+ if __name__ == "__main__":
221
+ # cmu("d:/.backups/dontstarvetogether/modoverrides.lua", "3486375086")
222
+ cml("d:/.backups/dontstarvetogether/modoverrides.lua", "3486375086")
@@ -136,13 +136,13 @@ def run_app(
136
136
  help="ssl certfile",
137
137
  ),
138
138
  rule: str = typer.Option(
139
- "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release",
139
+ "https://fastly.jsdelivr.net/gh/Loyalsoldier/clash-rules@release",
140
140
  "--rule",
141
141
  "-r",
142
142
  help="clash Rule base URL",
143
143
  ),
144
144
  my_rule: str = typer.Option(
145
- "https://raw.githubusercontent.com/meme2046/data/refs/heads/main/clash",
145
+ "https://fastly.jsdelivr.net/gh/meme2046/data@main/clash",
146
146
  "--my-rule",
147
147
  "-mr",
148
148
  help="my clash rule base URL(自定义规则)",
@@ -22,7 +22,7 @@ class ClashYamlGenerator:
22
22
  self.request_proxy = config.request_proxy
23
23
 
24
24
  # 白名单模式 Rules 配置方式
25
- def genW(self, sub_list: list[dict]):
25
+ def genPW(self, sub_list: list[dict]):
26
26
  proxies = None
27
27
  if self.request_proxy:
28
28
  proxies = {
@@ -103,13 +103,6 @@ class ClashYamlGenerator:
103
103
  },
104
104
  )
105
105
 
106
- template["rules"].append("IP-CIDR,192.168.0.0/16,DIRECT,no-resolve")
107
- template["rules"].append("IP-CIDR,10.0.0.0/8,DIRECT,no-resolve")
108
- template["rules"].append("IP-CIDR,172.16.0.0/12,DIRECT,no-resolve")
109
- template["rules"].append("IP-CIDR,127.0.0.0/8,DIRECT,no-resolve")
110
-
111
- template["rules"].append("DOMAIN,clash.razord.top,DIRECT")
112
- template["rules"].append("DOMAIN,yacd.haishan.me,DIRECT")
113
106
  # 获取rules
114
107
  rule_list = [
115
108
  [f"{self.my_rule_base_url}/direct.yaml", "DIRECT"],
@@ -251,7 +244,7 @@ class ClashYamlGenerator:
251
244
  return template, userinfo
252
245
 
253
246
  # 黑名单模式 Rules 配置方式
254
- def genB(self, sub_list: list[dict]):
247
+ def genPB(self, sub_list: list[dict]):
255
248
  proxies = None
256
249
  if self.request_proxy:
257
250
  proxies = {
@@ -442,6 +435,185 @@ class ClashYamlGenerator:
442
435
 
443
436
  return template, userinfo
444
437
 
438
+ def genB(self, sub_list: list[dict]):
439
+ proxies = None
440
+ if self.request_proxy:
441
+ proxies = {
442
+ "http": self.request_proxy,
443
+ "https": self.request_proxy,
444
+ }
445
+
446
+ with open(str(module_dir / "data/template.yaml"), "r", encoding="utf-8") as f:
447
+ template = yaml.safe_load(f)
448
+
449
+ template["proxy-groups"].extend(
450
+ [
451
+ {
452
+ "name": "全局选择",
453
+ "type": "select",
454
+ "proxies": ["自动选择", "手动选择", "轮询"]
455
+ + [item["name"] for item in sub_list],
456
+ },
457
+ {
458
+ "name": "自动选择",
459
+ "type": "url-test",
460
+ "include-all": True,
461
+ },
462
+ {
463
+ "name": "手动选择",
464
+ "type": "select",
465
+ "include-all": True,
466
+ },
467
+ {
468
+ "name": "轮询",
469
+ "type": "load-balance",
470
+ "url": "https://api.bitget.com/api/v2/public/time",
471
+ "strategy": "round-robin",
472
+ "include-all": True,
473
+ },
474
+ ]
475
+ )
476
+
477
+ userinfo = ""
478
+ template["proxies"] = []
479
+ for item in sub_list:
480
+ headers = {"User-Agent": item["user_agent"]} if item["user_agent"] else {}
481
+
482
+ if not item["url"]:
483
+ raise ValueError("Invalid subscription URL.")
484
+ response = requests.get(item["url"], headers=headers, proxies=proxies)
485
+ response.raise_for_status()
486
+ if not userinfo:
487
+ userinfo = response.headers["Subscription-Userinfo"]
488
+ remote_config = yaml.safe_load(response.text)
489
+
490
+ # 检查 remote_config 是否为 None
491
+ if remote_config is None:
492
+ raise ValueError("Invalid subscription content: empty or invalid YAML.")
493
+
494
+ ps = remote_config.get("proxies", [])
495
+ if not ps:
496
+ raise ValueError("No proxies found in subscription.")
497
+
498
+ template["proxies"].extend(ps)
499
+
500
+ template["proxy-groups"].append(
501
+ {
502
+ "name": item["name"],
503
+ "type": "url-test",
504
+ "proxies": [item["name"] for item in ps],
505
+ },
506
+ )
507
+
508
+ template["rules"].append("IP-CIDR,192.168.0.0/16,DIRECT,no-resolve")
509
+ template["rules"].append("IP-CIDR,10.0.0.0/8,DIRECT,no-resolve")
510
+ template["rules"].append("IP-CIDR,172.16.0.0/12,DIRECT,no-resolve")
511
+ template["rules"].append("IP-CIDR,127.0.0.0/8,DIRECT,no-resolve")
512
+ # ⬇️饥荒服务器相关端口
513
+ template["rules"].append("DST-PORT,10999,DIRECT")
514
+ template["rules"].append("DST-PORT,10998,DIRECT")
515
+ template["rules"].append("DST-PORT,27016,DIRECT")
516
+ template["rules"].append("DST-PORT,27017,DIRECT")
517
+ template["rules"].append("DST-PORT,8766,DIRECT")
518
+ template["rules"].append("DST-PORT,8767,DIRECT")
519
+
520
+ template["rules"].append("DOMAIN,clash.razord.top,DIRECT")
521
+ template["rules"].append("DOMAIN,yacd.haishan.me,DIRECT")
522
+ # template["rules"].append("DOMAIN,accounts.klei.com,全局选择")
523
+
524
+ # 获取rules
525
+ rule_list = [
526
+ [f"{self.my_rule_base_url}/direct.yaml", "DIRECT"],
527
+ [f"{self.my_rule_base_url}/proxy.yaml", "全局选择"],
528
+ [
529
+ f"{self.my_rule_base_url}/round.yaml",
530
+ "轮询",
531
+ ],
532
+ [
533
+ f"{self.my_rule_base_url}/reject.yaml",
534
+ "REJECT",
535
+ ],
536
+ ]
537
+
538
+ for item in rule_list:
539
+ response = requests.get(item[0], proxies=proxies)
540
+ response.raise_for_status()
541
+ remote = yaml.safe_load(response.text)
542
+ template["rule-providers"][os.path.basename(item[0])] = {
543
+ "type": "inline",
544
+ "behavior": "classical",
545
+ "payload": remote["payload"],
546
+ }
547
+
548
+ template["rules"].append(f"RULE-SET,{os.path.basename(item[0])},{item[1]}")
549
+
550
+ template["rule-providers"].update(
551
+ {
552
+ "applications": {
553
+ "type": "http",
554
+ "format": "yaml",
555
+ "behavior": "classical",
556
+ "url": f"{self.rule_base_url}/applications.txt",
557
+ "path": "./ruleset/applications.yaml",
558
+ "interval": 86400,
559
+ },
560
+ "private": {
561
+ "type": "http",
562
+ "format": "yaml",
563
+ "behavior": "domain",
564
+ "url": f"{self.rule_base_url}/private.txt",
565
+ "path": "./ruleset/private.yaml",
566
+ "interval": 86400,
567
+ },
568
+ "tld-not-cn": {
569
+ "type": "http",
570
+ "format": "yaml",
571
+ "behavior": "domain",
572
+ "url": f"{self.rule_base_url}/tld-not-cn.txt",
573
+ "path": "./ruleset/tld-not-cn.yaml",
574
+ "interval": 86400,
575
+ },
576
+ "telegramcidr": {
577
+ "type": "http",
578
+ "format": "yaml",
579
+ "behavior": "ipcidr",
580
+ "url": f"{self.rule_base_url}/telegramcidr.txt",
581
+ "path": "./ruleset/telegramcidr.yaml",
582
+ "interval": 86400,
583
+ },
584
+ "gfw": {
585
+ "type": "http",
586
+ "format": "yaml",
587
+ "behavior": "domain",
588
+ "url": f"{self.rule_base_url}/gfw.txt",
589
+ "path": "./ruleset/gfw.yaml",
590
+ "interval": 86400,
591
+ },
592
+ # "reject": {
593
+ # "type": "http",
594
+ # "format": "yaml",
595
+ # "behavior": "domain",
596
+ # "url": f"{self.rule_base_url}/reject.txt",
597
+ # "path": "./ruleset/reject.yaml",
598
+ # "interval": 86400,
599
+ # },
600
+ },
601
+ )
602
+
603
+ template["rules"].extend(
604
+ [
605
+ "RULE-SET,applications,DIRECT",
606
+ "RULE-SET,private,DIRECT",
607
+ # "RULE-SET,reject,REJECT",
608
+ "RULE-SET,tld-not-cn,全局选择",
609
+ "RULE-SET,gfw,全局选择",
610
+ "RULE-SET,telegramcidr,全局选择",
611
+ "MATCH,DIRECT",
612
+ ]
613
+ )
614
+
615
+ return template, userinfo
616
+
445
617
  def query2sub(self, urls: str, agents: str, names: str):
446
618
  url_list = urls.split(",") if urls and urls.strip() else []
447
619
  agents_list = agents.split(",") if agents else []
@@ -1,70 +1,70 @@
1
- mixed-port: 7897
2
- allow-lan: false
3
- mode: rule
4
- log-level: warning
5
- external-controller: "127.0.0.1:9097"
6
- client-fingerprint: chrome
7
- unified-delay: true
8
- ipv6: false
9
- profile:
10
- store-selected: true
11
- tun:
12
- mtu: 1500
13
- dns:
14
- enable: true
15
- listen: ":53"
16
- enhanced-mode: "fake-ip"
17
- fake-ip-range: "198.18.0.1/16"
18
- fake-ip-filter-mode: "blacklist"
19
- prefer-h3: false
20
- respect-rules: false
21
- use-hosts: false
22
- use-system-hosts: false
23
- ipv6: true
24
- fake-ip-filter:
25
- - "*.lan"
26
- - "*.local"
27
- - "*.arpa"
28
- - "time.*.com"
29
- - "ntp.*.com"
30
- - "time.*.com"
31
- - "+.market.xiaomi.com"
32
- - "localhost.ptlogin2.qq.com"
33
- - "*.msftncsi.com"
34
- - "www.msftconnecttest.com"
35
- default-nameserver:
36
- - "system"
37
- - "223.6.6.6"
38
- - "8.8.8.8"
39
- - "2400:3200::1"
40
- - "2001:4860:4860::8888"
41
- nameserver:
42
- - "8.8.8.8"
43
- - "https://doh.pub/dns-query"
44
- - "https://dns.alidns.com/dns-query"
45
- direct-nameserver-follow-policy: false
46
- fallback-filter:
47
- geoip: true
48
- geoip-code: "CN"
49
- ipcidr:
50
- - "240.0.0.0/4"
51
- - "0.0.0.0/32"
52
- domain:
53
- - "+.google.com"
54
- - "+.facebook.com"
55
- - "+.youtube.com"
56
- fallback: []
57
- proxy-server-nameserver:
58
- - "https://doh.pub/dns-query"
59
- - "https://dns.alidns.com/dns-query"
60
- - "tls://223.5.5.5"
61
- direct-nameserver: []
62
- external-controller-cors:
63
- allow-private-network: true
64
- allow-origins:
65
- - "*"
66
- proxies: []
67
- proxy-providers: {}
68
- proxy-groups: []
69
- rule-providers: {}
70
- rules: []
1
+ mixed-port: 7897
2
+ allow-lan: false
3
+ mode: rule
4
+ log-level: warning
5
+ external-controller: "127.0.0.1:9097"
6
+ client-fingerprint: chrome
7
+ unified-delay: true
8
+ ipv6: false
9
+ profile:
10
+ store-selected: true
11
+ tun:
12
+ mtu: 1500
13
+ dns:
14
+ enable: true
15
+ listen: ":53"
16
+ enhanced-mode: "fake-ip"
17
+ fake-ip-range: "198.18.0.1/16"
18
+ fake-ip-filter-mode: "blacklist"
19
+ prefer-h3: false
20
+ respect-rules: false
21
+ use-hosts: false
22
+ use-system-hosts: false
23
+ ipv6: true
24
+ fake-ip-filter:
25
+ - "*.lan"
26
+ - "*.local"
27
+ - "*.arpa"
28
+ - "time.*.com"
29
+ - "ntp.*.com"
30
+ - "time.*.com"
31
+ - "+.market.xiaomi.com"
32
+ - "localhost.ptlogin2.qq.com"
33
+ - "*.msftncsi.com"
34
+ - "www.msftconnecttest.com"
35
+ default-nameserver:
36
+ - "system"
37
+ - "223.6.6.6"
38
+ - "8.8.8.8"
39
+ - "2400:3200::1"
40
+ - "2001:4860:4860::8888"
41
+ nameserver:
42
+ - "8.8.8.8"
43
+ - "https://doh.pub/dns-query"
44
+ - "https://dns.alidns.com/dns-query"
45
+ direct-nameserver-follow-policy: false
46
+ fallback-filter:
47
+ geoip: true
48
+ geoip-code: "CN"
49
+ ipcidr:
50
+ - "240.0.0.0/4"
51
+ - "0.0.0.0/32"
52
+ domain:
53
+ - "+.google.com"
54
+ - "+.facebook.com"
55
+ - "+.youtube.com"
56
+ fallback: []
57
+ proxy-server-nameserver:
58
+ - "https://doh.pub/dns-query"
59
+ - "https://dns.alidns.com/dns-query"
60
+ - "tls://223.5.5.5"
61
+ direct-nameserver: []
62
+ external-controller-cors:
63
+ allow-private-network: true
64
+ allow-origins:
65
+ - "*"
66
+ proxies: []
67
+ proxy-providers: {}
68
+ proxy-groups: []
69
+ rule-providers: {}
70
+ rules: []
@@ -10,6 +10,7 @@
10
10
  "cowsay>=6.1",
11
11
  "dotenv>=0.9.9",
12
12
  "fastapi>=0.127.0",
13
+ "SLPP>=1.2.3",
13
14
  "numpy<2.0.0",
14
15
  "pandas>=2.3.3",
15
16
  "pymysql>=1.1.2",
@@ -18,6 +19,7 @@
18
19
  "redis[hiredis]>=7.1.0",
19
20
  "requests[socks]>=2.32.5",
20
21
  "selenium>=4.40.0",
22
+ "slpp>=1.2.3",
21
23
  "sqlalchemy>=2.0.45",
22
24
  "tabulate>=0.9.0",
23
25
  "toml>=0.10.2",
@@ -30,7 +32,7 @@
30
32
  name = "pymecli"
31
33
  readme = "README.md"
32
34
  requires-python = ">=3.10"
33
- version = "0.4.3"
35
+ version = "0.4.5"
34
36
 
35
37
  [project.urls]
36
38
  Homepage = "https://pypi.org/project/pymecli"
@@ -39,6 +41,7 @@
39
41
  [project.scripts]
40
42
  baidu = "cli.baidu:app"
41
43
  bitget = "cli.bitget:app"
44
+ dst = "cli.dst:app"
42
45
  example = "cli.example:app"
43
46
  fast = "cli.fast:typer_app"
44
47
  gate = "cli.gate:app"
pymecli-0.4.3/README.md DELETED
@@ -1,16 +0,0 @@
1
- # typer example
2
-
3
- ```shell
4
- uv run example hello Xiaoming
5
- uv run example hello Xiaoming --from Pymecli
6
- uv run example goodbye Xiaoming
7
- uv run example goodbye Xiaoming -f
8
- ```
9
-
10
- # fastapi server
11
-
12
- ```shell
13
- uv run fast --port 8877
14
- uv build
15
- uv publish --token (TOKEN)
16
- ```
@@ -1,62 +0,0 @@
1
- import yaml
2
- from fastapi import APIRouter, Query, Response
3
-
4
- from core.clash import ClashYamlGenerator, get_generator_dependency
5
-
6
- router = APIRouter()
7
-
8
-
9
- @router.get(
10
- "/subwhite",
11
- summary="转换订阅(可以转换多个订阅)",
12
- description="根据提供的URL、User-Agent和名称获取并处理订阅信息,返回YAML格式的Clash配置",
13
- response_description="返回YAML格式的Clash配置文件",
14
- )
15
- async def subWhite(
16
- urls: str = Query(..., description="订阅URL,逗号分隔"),
17
- agents: str = Query(
18
- "clash-verge/v2.4.3",
19
- description="User-Agent,逗号分隔(根据客户段选择)",
20
- ),
21
- names: str = Query(
22
- "订阅1", description="订阅名称,逗号分隔(可选,在客户端中的显示名)"
23
- ),
24
- generator: ClashYamlGenerator = get_generator_dependency(),
25
- ):
26
- # 将JSON字符串解析为Python对象
27
- sub_list = generator.query2sub(urls, agents, names)
28
- yaml_content, userinfo = generator.genW(sub_list)
29
- yaml_string = yaml.dump(yaml_content, allow_unicode=True, default_flow_style=False)
30
- return Response(
31
- headers={"Subscription-Userinfo": userinfo},
32
- content=yaml_string,
33
- media_type="text/yaml",
34
- )
35
-
36
-
37
- @router.get(
38
- "/subblack",
39
- summary="转换订阅(可以转换多个订阅)",
40
- description="根据提供的URL、User-Agent和名称获取并处理订阅信息,返回YAML格式的Clash配置",
41
- response_description="返回YAML格式的Clash配置文件",
42
- )
43
- async def subBlack(
44
- urls: str = Query(..., description="订阅URL,逗号分隔"),
45
- agents: str = Query(
46
- "clash-verge/v2.4.3",
47
- description="User-Agent,逗号分隔(根据客户段选择)",
48
- ),
49
- names: str = Query(
50
- "订阅1", description="订阅名称,逗号分隔(可选,在客户端中的显示名)"
51
- ),
52
- generator: ClashYamlGenerator = get_generator_dependency(),
53
- ):
54
- # 将JSON字符串解析为Python对象
55
- sub_list = generator.query2sub(urls, agents, names)
56
- yaml_content, userinfo = generator.genB(sub_list)
57
- yaml_string = yaml.dump(yaml_content, allow_unicode=True, default_flow_style=False)
58
- return Response(
59
- headers={"Subscription-Userinfo": userinfo},
60
- content=yaml_string,
61
- media_type="text/yaml",
62
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes