gitwebhooks 2.1.0__py3-none-any.whl

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 (47) hide show
  1. gitwebhooks/__init__.py +18 -0
  2. gitwebhooks/__main__.py +10 -0
  3. gitwebhooks/auth/__init__.py +20 -0
  4. gitwebhooks/auth/custom.py +40 -0
  5. gitwebhooks/auth/factory.py +73 -0
  6. gitwebhooks/auth/gitee.py +86 -0
  7. gitwebhooks/auth/github.py +52 -0
  8. gitwebhooks/auth/gitlab.py +29 -0
  9. gitwebhooks/auth/verifier.py +34 -0
  10. gitwebhooks/cli/__init__.py +107 -0
  11. gitwebhooks/cli/config.py +216 -0
  12. gitwebhooks/cli/init_wizard.py +701 -0
  13. gitwebhooks/cli/prompts.py +275 -0
  14. gitwebhooks/cli/service.py +374 -0
  15. gitwebhooks/config/__init__.py +17 -0
  16. gitwebhooks/config/loader.py +170 -0
  17. gitwebhooks/config/models.py +145 -0
  18. gitwebhooks/config/registry.py +104 -0
  19. gitwebhooks/config/server.py +85 -0
  20. gitwebhooks/handlers/__init__.py +22 -0
  21. gitwebhooks/handlers/base.py +107 -0
  22. gitwebhooks/handlers/custom.py +73 -0
  23. gitwebhooks/handlers/factory.py +83 -0
  24. gitwebhooks/handlers/gitee.py +113 -0
  25. gitwebhooks/handlers/github.py +43 -0
  26. gitwebhooks/handlers/gitlab.py +43 -0
  27. gitwebhooks/handlers/request.py +248 -0
  28. gitwebhooks/logging/__init__.py +10 -0
  29. gitwebhooks/logging/setup.py +57 -0
  30. gitwebhooks/main/__init__.py +117 -0
  31. gitwebhooks/main/__main__.py +12 -0
  32. gitwebhooks/models/__init__.py +14 -0
  33. gitwebhooks/models/provider.py +22 -0
  34. gitwebhooks/models/request.py +61 -0
  35. gitwebhooks/models/result.py +40 -0
  36. gitwebhooks/server.py +128 -0
  37. gitwebhooks/utils/__init__.py +14 -0
  38. gitwebhooks/utils/constants.py +55 -0
  39. gitwebhooks/utils/exceptions.py +34 -0
  40. gitwebhooks/utils/executor.py +34 -0
  41. gitwebhooks/utils/systemd.py +274 -0
  42. gitwebhooks-2.1.0.dist-info/METADATA +494 -0
  43. gitwebhooks-2.1.0.dist-info/RECORD +47 -0
  44. gitwebhooks-2.1.0.dist-info/WHEEL +5 -0
  45. gitwebhooks-2.1.0.dist-info/entry_points.txt +2 -0
  46. gitwebhooks-2.1.0.dist-info/licenses/LICENSE +21 -0
  47. gitwebhooks-2.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,701 @@
1
+ """
2
+ Interactive configuration initialization wizard.
3
+
4
+ This module provides a user-friendly wizard for creating git-webhooks-server
5
+ configuration files through an interactive command-line interface.
6
+ """
7
+
8
+ import os
9
+ import re
10
+ from dataclasses import dataclass
11
+ from typing import Optional, List, Dict
12
+ import configparser
13
+
14
+
15
+ # =============================================================================
16
+ # Module Constants
17
+ # =============================================================================
18
+
19
+ CONFIG_LEVELS = {
20
+ 'system': {
21
+ 'path': '/etc/gitwebhooks.ini',
22
+ 'requires_root': True
23
+ },
24
+ 'local': {
25
+ 'path': '/usr/local/etc/gitwebhooks.ini',
26
+ 'requires_root': True
27
+ },
28
+ 'user': {
29
+ 'path': '~/.gitwebhooks.ini',
30
+ 'requires_root': False
31
+ }
32
+ }
33
+
34
+ PLATFORMS = {
35
+ 'github': {
36
+ 'events': ['push', 'release', 'pull_request', 'tag'],
37
+ 'requires_secret': True
38
+ },
39
+ 'gitee': {
40
+ 'events': ['push', 'release', 'pull_request'],
41
+ 'requires_secret': True
42
+ },
43
+ 'gitlab': {
44
+ 'events': ['push', 'release', 'tag'],
45
+ 'requires_secret': True
46
+ },
47
+ 'custom': {
48
+ 'events': [],
49
+ 'requires_secret': False,
50
+ 'custom_fields': ['header_name', 'header_value', 'identifier_path', 'header_event']
51
+ }
52
+ }
53
+
54
+
55
+ # =============================================================================
56
+ # Data Classes
57
+ # =============================================================================
58
+
59
+ @dataclass
60
+ class ConfigLevel:
61
+ """Configuration level metadata."""
62
+ name: str
63
+ path: str
64
+ requires_root: bool
65
+
66
+
67
+ @dataclass
68
+ class ServerConfig:
69
+ """Server configuration parameters."""
70
+ address: str
71
+ port: int
72
+ log_file: str
73
+
74
+
75
+ @dataclass
76
+ class PlatformConfig:
77
+ """Platform configuration parameters."""
78
+ platform: str
79
+ handle_events: List[str]
80
+ verify: bool
81
+ secret: Optional[str] = None
82
+ custom_params: Optional[Dict[str, str]] = None
83
+
84
+
85
+ @dataclass
86
+ class RepositoryConfig:
87
+ """Repository configuration parameters."""
88
+ name: str
89
+ cwd: str
90
+ cmd: str
91
+
92
+
93
+ # =============================================================================
94
+ # Input Validation Functions
95
+ # =============================================================================
96
+
97
+ def validate_repo_name(value: str) -> bool:
98
+ """
99
+ Validate repository name format.
100
+
101
+ Supports:
102
+ - Two-level format: owner/repo (GitHub/Gitee)
103
+ - Multi-level format: group/subgroup/project (GitLab)
104
+
105
+ Args:
106
+ value: Repository name string
107
+
108
+ Returns:
109
+ True if valid, False otherwise
110
+ """
111
+ return bool(re.match(r'^[^/]+(/[^/]+)+$', value))
112
+
113
+
114
+ def validate_existing_path(value: str) -> bool:
115
+ """
116
+ Validate that a path exists and is a directory.
117
+
118
+ Args:
119
+ value: Path string to validate
120
+
121
+ Returns:
122
+ True if path exists and is a directory, False otherwise
123
+ """
124
+ return os.path.isdir(value)
125
+
126
+
127
+ def validate_non_empty(value: str) -> bool:
128
+ """
129
+ Validate that a string is not empty or whitespace-only.
130
+
131
+ Args:
132
+ value: String to validate
133
+
134
+ Returns:
135
+ True if string has non-whitespace content, False otherwise
136
+ """
137
+ return bool(value.strip())
138
+
139
+
140
+ def validate_port(value: str) -> bool:
141
+ """
142
+ Validate port number.
143
+
144
+ Args:
145
+ value: Port number as string
146
+
147
+ Returns:
148
+ True if valid port (1-65535), False otherwise
149
+ """
150
+ try:
151
+ port = int(value)
152
+ return 1 <= port <= 65535
153
+ except ValueError:
154
+ return False
155
+
156
+
157
+ # =============================================================================
158
+ # INI Generation Functions
159
+ # =============================================================================
160
+
161
+ def _generate_config(
162
+ server: ServerConfig,
163
+ platform: PlatformConfig,
164
+ repo: RepositoryConfig
165
+ ) -> configparser.ConfigParser:
166
+ """
167
+ Generate configuration object from collected data.
168
+
169
+ Args:
170
+ server: Server configuration
171
+ platform: Platform configuration
172
+ repo: Repository configuration
173
+
174
+ Returns:
175
+ ConfigParser object with all sections
176
+ """
177
+ config = configparser.ConfigParser()
178
+
179
+ # Server section
180
+ config['server'] = {
181
+ 'address': server.address,
182
+ 'port': str(server.port),
183
+ 'log_file': server.log_file
184
+ }
185
+
186
+ # Platform section
187
+ platform_section = platform.platform
188
+ platform_config = {
189
+ 'handle_events': ', '.join(platform.handle_events),
190
+ 'verify': 'true' if platform.verify else 'false'
191
+ }
192
+
193
+ if platform.verify and platform.secret:
194
+ platform_config['secret'] = platform.secret
195
+
196
+ # Add custom platform parameters if present
197
+ if platform.custom_params:
198
+ for key, value in platform.custom_params.items():
199
+ if value: # Only add non-empty values
200
+ platform_config[key] = value
201
+
202
+ config[platform_section] = platform_config
203
+
204
+ # Repository section
205
+ repo_section = f'repo/{repo.name}'
206
+ config[repo_section] = {
207
+ 'cwd': repo.cwd,
208
+ 'cmd': repo.cmd
209
+ }
210
+
211
+ return config
212
+
213
+
214
+ # =============================================================================
215
+ # Wizard Class
216
+ # =============================================================================
217
+
218
+ class Wizard:
219
+ """
220
+ Interactive configuration wizard.
221
+
222
+ Guides users through creating a git-webhooks-server configuration file
223
+ by collecting server, platform, and repository configuration parameters.
224
+ """
225
+
226
+ def __init__(self, level: Optional[str] = None):
227
+ """
228
+ Initialize the wizard.
229
+
230
+ Args:
231
+ level: Optional configuration level (system/local/user).
232
+ If None, user will be prompted to select.
233
+
234
+ Raises:
235
+ ValueError: If level is provided but invalid
236
+ """
237
+ if level is not None and level not in CONFIG_LEVELS:
238
+ raise ValueError(f"Invalid configuration level: {level}. "
239
+ f"Must be one of: {', '.join(CONFIG_LEVELS.keys())}")
240
+
241
+ self.level = level
242
+ self.config_level: Optional[ConfigLevel] = None
243
+
244
+ def run(self) -> str:
245
+ """
246
+ Run the wizard and generate configuration file.
247
+
248
+ Returns:
249
+ Absolute path to the generated configuration file
250
+
251
+ Raises:
252
+ KeyboardInterrupt: User presses Ctrl+C
253
+ PermissionError: Cannot write to target directory
254
+ FileExistsError: User cancels overwrite
255
+ """
256
+ try:
257
+ # Step 1: Determine configuration level
258
+ if self.level is None:
259
+ self._select_level()
260
+ else:
261
+ self._set_level(self.level)
262
+
263
+ # Step 2: Check permissions
264
+ self._check_permissions()
265
+
266
+ # Step 3: Collect server configuration
267
+ server_config = self._collect_server_config()
268
+
269
+ # Step 4: Select and configure platform
270
+ platform_name = self._select_platform()
271
+ platform_config = self._collect_platform_config(platform_name)
272
+
273
+ # Step 5: Collect repository configuration
274
+ repo_config = self._collect_repository_config()
275
+
276
+ # Step 6: Confirm overwrite if file exists
277
+ config_path = os.path.expanduser(self.config_level.path)
278
+ backup = False
279
+ if os.path.exists(config_path):
280
+ backup = self._confirm_overwrite(config_path)
281
+
282
+ # Step 7: Generate configuration
283
+ config = _generate_config(server_config, platform_config, repo_config)
284
+
285
+ # Step 8: Write configuration file
286
+ self._write_config(config, config_path, backup=backup)
287
+
288
+ # Step 9: Show completion message
289
+ self._show_completion(config_path)
290
+
291
+ return config_path
292
+
293
+ except KeyboardInterrupt:
294
+ print("\n操作已取消")
295
+ # Clean up partial files if any
296
+ self._cleanup()
297
+ raise
298
+
299
+ def _set_level(self, level_name: str) -> None:
300
+ """
301
+ Set configuration level from name.
302
+
303
+ Args:
304
+ level_name: Configuration level name (system/local/user)
305
+ """
306
+ level_config = CONFIG_LEVELS[level_name]
307
+ self.config_level = ConfigLevel(
308
+ name=level_name,
309
+ path=level_config['path'],
310
+ requires_root=level_config['requires_root']
311
+ )
312
+
313
+ def _select_level(self) -> None:
314
+ """
315
+ Interactively select configuration level.
316
+
317
+ Updates self.config_level with selected level.
318
+ """
319
+ print("选择配置级别:")
320
+ levels = list(CONFIG_LEVELS.items())
321
+ for idx, (name, config) in enumerate(levels, 1):
322
+ path = config['path']
323
+ print(f" {idx}. {name} ({path})")
324
+
325
+ while True:
326
+ try:
327
+ choice = input(f"输入选择 [1-{len(levels)}]: ").strip()
328
+ if not choice:
329
+ print("请选择一个配置级别")
330
+ continue
331
+
332
+ index = int(choice) - 1
333
+ if 0 <= index < len(levels):
334
+ level_name, level_config = levels[index]
335
+ self._set_level(level_name)
336
+ return
337
+ else:
338
+ print(f"无效选择,请输入 1-{len(levels)} 之间的数字")
339
+ except ValueError:
340
+ print("请输入有效的数字")
341
+ except KeyboardInterrupt:
342
+ raise
343
+
344
+ def _check_permissions(self) -> None:
345
+ """
346
+ Check if target directory is writable.
347
+
348
+ Raises:
349
+ PermissionError: If target directory is not writable
350
+ """
351
+ config_path = os.path.expanduser(self.config_level.path)
352
+ target_dir = os.path.dirname(config_path)
353
+
354
+ # Create directory if it doesn't exist (for user level)
355
+ if not os.path.exists(target_dir):
356
+ try:
357
+ os.makedirs(target_dir, exist_ok=True)
358
+ except OSError as e:
359
+ raise PermissionError(
360
+ f"无法创建目录 {target_dir}: {e}\n"
361
+ f"建议: 使用 sudo 或选择 user 级别"
362
+ )
363
+
364
+ # Check write permission
365
+ if not os.access(target_dir, os.W_OK):
366
+ raise PermissionError(
367
+ f"无法写入 {self.config_level.path}(权限不足)\n"
368
+ f"建议: 使用 sudo 或选择 user 级别"
369
+ )
370
+
371
+ def _collect_server_config(self) -> ServerConfig:
372
+ """
373
+ Collect server configuration parameters.
374
+
375
+ Returns:
376
+ ServerConfig object with collected parameters
377
+ """
378
+ print("\n--- 服务器配置 ---")
379
+
380
+ # Address
381
+ default_address = "0.0.0.0"
382
+ address = input(f"服务器监听地址 [默认: {default_address}]: ").strip()
383
+ if not address:
384
+ address = default_address
385
+
386
+ # Port
387
+ default_port = "6789"
388
+ while True:
389
+ port_str = input(f"服务器端口 [默认: {default_port}]: ").strip()
390
+ if not port_str:
391
+ port = int(default_port)
392
+ break
393
+ if validate_port(port_str):
394
+ port = int(port_str)
395
+ break
396
+ print("端口号必须在 1-65535 之间")
397
+
398
+ # Log file
399
+ default_log = "/var/log/git-webhooks-server.log"
400
+ log_file = input(f"日志文件路径 [默认: {default_log}]: ").strip()
401
+ if not log_file:
402
+ log_file = default_log
403
+
404
+ return ServerConfig(
405
+ address=address,
406
+ port=port,
407
+ log_file=log_file
408
+ )
409
+
410
+ def _select_platform(self) -> str:
411
+ """
412
+ Interactively select Git platform.
413
+
414
+ Returns:
415
+ Platform name (github/gitee/gitlab/custom)
416
+ """
417
+ print("\n选择 Git 平台:")
418
+ platforms = list(PLATFORMS.keys())
419
+ for idx, platform in enumerate(platforms, 1):
420
+ print(f" {idx}. {platform}")
421
+
422
+ while True:
423
+ try:
424
+ choice = input(f"输入选择 [1-{len(platforms)}]: ").strip()
425
+ if not choice:
426
+ print("请选择一个平台")
427
+ continue
428
+
429
+ index = int(choice) - 1
430
+ if 0 <= index < len(platforms):
431
+ return platforms[index]
432
+ else:
433
+ print(f"无效选择,请输入 1-{len(platforms)} 之间的数字")
434
+ except ValueError:
435
+ print("请输入有效的数字")
436
+ except KeyboardInterrupt:
437
+ raise
438
+
439
+ def _collect_platform_config(self, platform: str) -> PlatformConfig:
440
+ """
441
+ Collect platform configuration parameters.
442
+
443
+ Args:
444
+ platform: Platform name
445
+
446
+ Returns:
447
+ PlatformConfig object with collected parameters
448
+ """
449
+ print(f"\n--- {platform.upper()} 平台配置 ---")
450
+
451
+ platform_info = PLATFORMS[platform]
452
+
453
+ # Collect events
454
+ handle_events = self._collect_events(platform)
455
+
456
+ # Collect verification settings
457
+ verify = False
458
+ secret = None
459
+
460
+ if platform_info['requires_secret']:
461
+ while True:
462
+ verify_input = input("是否启用签名验证? [y/N]: ").strip().lower()
463
+ if verify_input in ('y', 'yes'):
464
+ verify = True
465
+ break
466
+ elif verify_input in ('n', 'no', ''):
467
+ verify = False
468
+ break
469
+ else:
470
+ print("请输入 y 或 n")
471
+
472
+ if verify:
473
+ while True:
474
+ secret = input("输入验证密钥: ").strip()
475
+ if validate_non_empty(secret):
476
+ break
477
+ print("验证密钥不能为空")
478
+
479
+ # Custom platform parameters
480
+ custom_params = None
481
+ if platform == 'custom':
482
+ custom_params = self._collect_custom_params()
483
+
484
+ return PlatformConfig(
485
+ platform=platform,
486
+ handle_events=handle_events,
487
+ verify=verify,
488
+ secret=secret,
489
+ custom_params=custom_params
490
+ )
491
+
492
+ def _collect_events(self, platform: str) -> List[str]:
493
+ """
494
+ Collect webhook events to handle.
495
+
496
+ Args:
497
+ platform: Platform name
498
+
499
+ Returns:
500
+ List of event names
501
+ """
502
+ platform_info = PLATFORMS[platform]
503
+
504
+ if platform == 'custom':
505
+ # Custom platform: ask for event name
506
+ print("自定义平台事件处理")
507
+ print("输入要处理的事件名称(直接回车跳过):")
508
+ events_str = input().strip()
509
+ return [e.strip() for e in events_str.split(',') if e.strip()] if events_str else ['webhook']
510
+
511
+ # Standard platforms: show event list
512
+ events = platform_info['events']
513
+ print("选择要处理的 webhook 事件(多个用逗号分隔,直接回车默认选 1):")
514
+ for idx, event in enumerate(events, 1):
515
+ print(f" {idx}. {event}")
516
+
517
+ while True:
518
+ try:
519
+ choice = input("输入选择: ").strip()
520
+ if not choice:
521
+ # Default to first event (push)
522
+ return [events[0]]
523
+
524
+ # Parse comma-separated indices
525
+ indices = [int(x.strip()) - 1 for x in choice.split(',')]
526
+
527
+ # Validate indices
528
+ if all(0 <= i < len(events) for i in indices):
529
+ return [events[i] for i in indices]
530
+ else:
531
+ print(f"无效选择,请输入 1-{len(events)} 之间的数字,多个用逗号分隔")
532
+
533
+ except ValueError:
534
+ print("请输入有效的数字,多个用逗号分隔")
535
+ except KeyboardInterrupt:
536
+ raise
537
+
538
+ def _collect_custom_params(self) -> Dict[str, str]:
539
+ """
540
+ Collect custom platform specific parameters.
541
+
542
+ Returns:
543
+ Dictionary of custom parameter names and values
544
+ """
545
+ print("\n--- 自定义平台参数 ---")
546
+
547
+ params = {}
548
+
549
+ # header_name
550
+ default_header_name = "X-Webhook-Token"
551
+ header_name = input(f"输入识别 Header 名称 [默认: {default_header_name}]: ").strip()
552
+ params['header_name'] = header_name if header_name else default_header_name
553
+
554
+ # header_value
555
+ while True:
556
+ header_value = input("输入验证 Header 值: ").strip()
557
+ if validate_non_empty(header_value):
558
+ params['header_value'] = header_value
559
+ break
560
+ print("Header 值不能为空")
561
+
562
+ # identifier_path
563
+ default_identifier = "project.path_with_namespace"
564
+ identifier_path = input(f"输入仓库标识 JSON 路径 [默认: {default_identifier}]: ").strip()
565
+ params['identifier_path'] = identifier_path if identifier_path else default_identifier
566
+
567
+ # header_event (optional)
568
+ header_event = input("输入事件类型 Header 名称 [可选,直接回车跳过]: ").strip()
569
+ if header_event:
570
+ params['header_event'] = header_event
571
+
572
+ return params
573
+
574
+ def _collect_repository_config(self) -> RepositoryConfig:
575
+ """
576
+ Collect repository configuration parameters.
577
+
578
+ Returns:
579
+ RepositoryConfig object with collected parameters
580
+ """
581
+ print("\n--- 仓库配置 ---")
582
+
583
+ # Repository name
584
+ while True:
585
+ repo_name = input("输入仓库名称 (格式: owner/repo): ").strip()
586
+ if validate_repo_name(repo_name):
587
+ break
588
+ print("仓库名称格式不正确,应为 owner/repo 格式")
589
+
590
+ # Working directory
591
+ while True:
592
+ cwd = input("输入工作目录路径 (必须已存在): ").strip()
593
+ if validate_existing_path(cwd):
594
+ break
595
+ print(f"错误: 目录不存在: {cwd}")
596
+
597
+ # Deploy command
598
+ while True:
599
+ cmd = input("输入部署命令: ").strip()
600
+ if validate_non_empty(cmd):
601
+ break
602
+ print("部署命令不能为空")
603
+
604
+ return RepositoryConfig(
605
+ name=repo_name,
606
+ cwd=cwd,
607
+ cmd=cmd
608
+ )
609
+
610
+ def _confirm_overwrite(self, path: str) -> bool:
611
+ """
612
+ Confirm whether to overwrite existing configuration file.
613
+
614
+ Args:
615
+ path: Path to existing file
616
+
617
+ Returns:
618
+ True to proceed with overwrite, False to cancel
619
+
620
+ Raises:
621
+ FileExistsError: User chooses to cancel
622
+ """
623
+ print(f"\n配置文件 {path} 已存在")
624
+ print("选择操作:")
625
+ print(" 1. 覆盖(会删除原文件)")
626
+ print(" 2. 备份后覆盖(保存为 .bak)")
627
+ print(" 3. 取消操作")
628
+
629
+ while True:
630
+ try:
631
+ choice = input("输入选择 [1-3]: ").strip()
632
+ if choice == '1':
633
+ return False # Overwrite without backup
634
+ elif choice == '2':
635
+ return True # Backup then overwrite
636
+ elif choice == '3':
637
+ raise FileExistsError("用户取消操作")
638
+ else:
639
+ print("请输入 1、2 或 3")
640
+ except KeyboardInterrupt:
641
+ raise
642
+
643
+ def _write_config(
644
+ self,
645
+ config: configparser.ConfigParser,
646
+ path: str,
647
+ backup: bool = False
648
+ ) -> None:
649
+ """
650
+ Write configuration file to disk.
651
+
652
+ Args:
653
+ config: ConfigParser object
654
+ path: Target file path
655
+ backup: Whether to backup existing file
656
+ """
657
+ try:
658
+ # Backup existing file if requested
659
+ if backup and os.path.exists(path):
660
+ backup_path = f"{path}.bak"
661
+ os.rename(path, backup_path)
662
+ print(f"已备份原文件到: {backup_path}")
663
+
664
+ # Write configuration
665
+ with open(path, 'w') as f:
666
+ config.write(f)
667
+
668
+ # Set secure permissions (user read/write only)
669
+ os.chmod(path, 0o600)
670
+
671
+ except OSError as e:
672
+ raise PermissionError(f"无法写入配置文件 {path}: {e}")
673
+
674
+ def _show_completion(self, path: str) -> None:
675
+ """
676
+ Display completion message and next steps.
677
+
678
+ Args:
679
+ path: Generated configuration file path
680
+ """
681
+ print(f"\n配置文件已生成: {path}")
682
+ print("后续步骤:")
683
+ print(f" 1. 检查配置文件内容: gitwebhooks-cli config view {path}")
684
+ print(f" 2. 启动服务器: gitwebhooks-cli -c {path}")
685
+ print(" 3. 或安装为服务: sudo gitwebhooks-cli service install")
686
+
687
+ def _cleanup(self) -> None:
688
+ """
689
+ Clean up partial files if wizard is interrupted.
690
+
691
+ This method is called when KeyboardInterrupt is caught.
692
+ """
693
+ # Remove partial configuration file if it exists
694
+ if self.config_level:
695
+ config_path = os.path.expanduser(self.config_level.path)
696
+ if os.path.exists(config_path):
697
+ try:
698
+ os.remove(config_path)
699
+ print(f"已清理部分文件: {config_path}")
700
+ except OSError:
701
+ pass