stigmergy 1.0.68 → 1.0.70
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.
- package/README.en.md +306 -300
- package/README.md +469 -301
- package/package.json +97 -81
- package/scripts/publish.js +268 -0
- package/scripts/simple-publish.js +59 -0
- package/src/index.js +12 -0
- package/test/enhanced-main-alignment.test.js +298 -0
- package/test/hook-system-integration-test.js +307 -0
- package/test/natural-language-skills-test.js +320 -0
- package/test/nl-integration-test.js +179 -0
- package/test/parameter-parsing-test.js +143 -0
- package/test/real-test.js +435 -0
- package/test/system-compatibility-test.js +447 -0
- package/test/tdd-fixes-test.js +211 -0
- package/test/third-party-skills-test.js +321 -0
- package/test/tool-selection-integration-test.js +157 -0
- package/test/unit/cli-scanner.test.js +291 -0
- package/test/unit/cross-cli-executor.test.js +399 -0
- package/src/adapters/claude/__init__.py +0 -13
- package/src/adapters/claude/claude_skills_integration.py +0 -609
- package/src/adapters/claude/hook_adapter.py +0 -663
- package/src/adapters/claude/install_claude_integration.py +0 -265
- package/src/adapters/claude/skills_hook_adapter.py +0 -841
- package/src/adapters/claude/standalone_claude_adapter.py +0 -384
- package/src/adapters/cline/__init__.py +0 -20
- package/src/adapters/cline/config.py +0 -108
- package/src/adapters/cline/install_cline_integration.py +0 -617
- package/src/adapters/cline/mcp_server.py +0 -713
- package/src/adapters/cline/standalone_cline_adapter.py +0 -459
- package/src/adapters/codebuddy/__init__.py +0 -13
- package/src/adapters/codebuddy/buddy_adapter.py +0 -1125
- package/src/adapters/codebuddy/install_codebuddy_integration.py +0 -279
- package/src/adapters/codebuddy/skills_hook_adapter.py +0 -672
- package/src/adapters/codebuddy/skills_integration.py +0 -395
- package/src/adapters/codebuddy/standalone_codebuddy_adapter.py +0 -403
- package/src/adapters/codex/__init__.py +0 -11
- package/src/adapters/codex/base.py +0 -46
- package/src/adapters/codex/install_codex_integration.py +0 -311
- package/src/adapters/codex/mcp_server.py +0 -493
- package/src/adapters/codex/natural_language_parser.py +0 -82
- package/src/adapters/codex/slash_command_adapter.py +0 -326
- package/src/adapters/codex/standalone_codex_adapter.py +0 -362
- package/src/adapters/copilot/__init__.py +0 -13
- package/src/adapters/copilot/install_copilot_integration.py +0 -564
- package/src/adapters/copilot/mcp_adapter.py +0 -772
- package/src/adapters/copilot/mcp_server.py +0 -168
- package/src/adapters/copilot/standalone_copilot_adapter.py +0 -114
- package/src/adapters/gemini/__init__.py +0 -13
- package/src/adapters/gemini/extension_adapter.py +0 -690
- package/src/adapters/gemini/install_gemini_integration.py +0 -257
- package/src/adapters/gemini/standalone_gemini_adapter.py +0 -366
- package/src/adapters/iflow/__init__.py +0 -7
- package/src/adapters/iflow/hook_adapter.py +0 -1038
- package/src/adapters/iflow/hook_installer.py +0 -536
- package/src/adapters/iflow/install_iflow_integration.py +0 -271
- package/src/adapters/iflow/official_hook_adapter.py +0 -1272
- package/src/adapters/iflow/standalone_iflow_adapter.py +0 -48
- package/src/adapters/iflow/workflow_adapter.py +0 -793
- package/src/adapters/qoder/hook_installer.py +0 -732
- package/src/adapters/qoder/install_qoder_integration.py +0 -265
- package/src/adapters/qoder/notification_hook_adapter.py +0 -863
- package/src/adapters/qoder/standalone_qoder_adapter.py +0 -48
- package/src/adapters/qwen/__init__.py +0 -17
- package/src/adapters/qwencode/__init__.py +0 -13
- package/src/adapters/qwencode/inheritance_adapter.py +0 -818
- package/src/adapters/qwencode/install_qwencode_integration.py +0 -276
- package/src/adapters/qwencode/standalone_qwencode_adapter.py +0 -399
- package/src/atomic_collaboration_handler.py +0 -461
- package/src/cli_collaboration_agent.py +0 -697
- package/src/collaboration/hooks.py +0 -315
- package/src/core/__init__.py +0 -21
- package/src/core/ai_environment_scanner.py +0 -331
- package/src/core/base_adapter.py +0 -220
- package/src/core/cli_hook_integration.py +0 -406
- package/src/core/cross_cli_executor.py +0 -713
- package/src/core/cross_cli_mapping.py +0 -1165
- package/src/core/cross_platform_encoding.py +0 -365
- package/src/core/cross_platform_safe_cli.py +0 -894
- package/src/core/direct_cli_executor.py +0 -805
- package/src/core/direct_cli_hook_system.py +0 -958
- package/src/core/enhanced_init_processor.py +0 -467
- package/src/core/graceful_cli_executor.py +0 -912
- package/src/core/md_enhancer.py +0 -342
- package/src/core/md_generator.py +0 -619
- package/src/core/models.py +0 -218
- package/src/core/parser.py +0 -108
- package/src/core/real_cli_hook_system.py +0 -852
- package/src/core/real_cross_cli_system.py +0 -925
- package/src/core/verified_cross_cli_system.py +0 -961
- package/src/deploy.js +0 -737
- package/src/enhanced-main.js +0 -626
- package/src/enhanced_deploy.js +0 -303
- package/src/enhanced_universal_cli_setup.py +0 -930
- package/src/kimi_wrapper.py +0 -104
- package/src/main.js +0 -1309
- package/src/shell_integration.py +0 -398
- package/src/simple-main.js +0 -315
- package/src/smart_router_creator.py +0 -323
- package/src/universal_cli_setup.py +0 -1289
- package/src/utils/__init__.py +0 -12
- package/src/utils/cli_detector.py +0 -445
- package/src/utils/file_utils.py +0 -246
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
跨平台编码安全工具库
|
|
5
|
-
解决Windows/Linux/macOS上GBK/UTF-8等编码冲突问题
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import os
|
|
9
|
-
import sys
|
|
10
|
-
import json
|
|
11
|
-
import yaml
|
|
12
|
-
import shutil
|
|
13
|
-
import locale
|
|
14
|
-
import platform
|
|
15
|
-
import codecs
|
|
16
|
-
import traceback
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from datetime import datetime
|
|
19
|
-
from typing import Dict, Any, Optional, Union, List
|
|
20
|
-
|
|
21
|
-
class EncodingError(Exception):
|
|
22
|
-
"""编码相关异常"""
|
|
23
|
-
pass
|
|
24
|
-
|
|
25
|
-
class CrossPlatformEncoding:
|
|
26
|
-
"""跨平台编码处理器"""
|
|
27
|
-
|
|
28
|
-
def __init__(self):
|
|
29
|
-
self.system = platform.system().lower()
|
|
30
|
-
self.default_encoding = self._detect_system_encoding()
|
|
31
|
-
self.fallback_encodings = self._get_fallback_encodings()
|
|
32
|
-
|
|
33
|
-
def _detect_system_encoding(self) -> str:
|
|
34
|
-
"""检测系统默认编码"""
|
|
35
|
-
try:
|
|
36
|
-
# 优先使用系统locale编码
|
|
37
|
-
system_encoding = locale.getpreferredencoding(False)
|
|
38
|
-
if system_encoding and system_encoding.lower() != 'ascii':
|
|
39
|
-
return system_encoding.lower()
|
|
40
|
-
except:
|
|
41
|
-
pass
|
|
42
|
-
|
|
43
|
-
try:
|
|
44
|
-
# 尝试Python默认编码
|
|
45
|
-
return sys.getdefaultencoding().lower()
|
|
46
|
-
except:
|
|
47
|
-
pass
|
|
48
|
-
|
|
49
|
-
# 最后的默认值
|
|
50
|
-
return 'utf-8'
|
|
51
|
-
|
|
52
|
-
def _get_fallback_encodings(self) -> List[str]:
|
|
53
|
-
"""获取备用编码列表"""
|
|
54
|
-
encodings = ['utf-8']
|
|
55
|
-
|
|
56
|
-
if self.system == 'windows':
|
|
57
|
-
encodings.extend(['gbk', 'gb2312', 'cp936', 'utf-8-sig'])
|
|
58
|
-
elif self.system == 'linux':
|
|
59
|
-
encodings.extend(['utf-8-sig', 'latin-1'])
|
|
60
|
-
elif self.system == 'darwin':
|
|
61
|
-
encodings.extend(['utf-8-sig', 'mac-roman'])
|
|
62
|
-
|
|
63
|
-
return encodings
|
|
64
|
-
|
|
65
|
-
def setup_environment(self):
|
|
66
|
-
"""设置环境编码"""
|
|
67
|
-
if self.system == 'windows':
|
|
68
|
-
# Windows特殊处理
|
|
69
|
-
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
|
70
|
-
os.environ['PYTHONLEGACYWINDOWSSTDIO'] = 'utf-8'
|
|
71
|
-
|
|
72
|
-
# 重配置标准流
|
|
73
|
-
try:
|
|
74
|
-
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
75
|
-
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
76
|
-
except:
|
|
77
|
-
pass
|
|
78
|
-
|
|
79
|
-
# 通用设置
|
|
80
|
-
if 'PYTHONIOENCODING' not in os.environ:
|
|
81
|
-
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
|
82
|
-
|
|
83
|
-
class SafeFileWriter:
|
|
84
|
-
"""安全文件写入器"""
|
|
85
|
-
|
|
86
|
-
def __init__(self, encoding_handler: CrossPlatformEncoding):
|
|
87
|
-
self.encoding_handler = encoding_handler
|
|
88
|
-
|
|
89
|
-
def write_json(self, file_path: Union[str, Path], data: Dict[str, Any],
|
|
90
|
-
backup: bool = True, indent: int = 2) -> bool:
|
|
91
|
-
"""安全写入JSON文件"""
|
|
92
|
-
file_path = Path(file_path)
|
|
93
|
-
|
|
94
|
-
try:
|
|
95
|
-
# 备份现有文件
|
|
96
|
-
if backup and file_path.exists():
|
|
97
|
-
backup_path = self._create_backup(file_path)
|
|
98
|
-
shutil.copy2(file_path, backup_path)
|
|
99
|
-
|
|
100
|
-
# 确保目录存在
|
|
101
|
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
-
|
|
103
|
-
# 使用临时文件确保原子性写入
|
|
104
|
-
temp_path = file_path.with_suffix('.tmp')
|
|
105
|
-
|
|
106
|
-
# 尝试多种编码
|
|
107
|
-
for encoding in self.encoding_handler.fallback_encodings:
|
|
108
|
-
try:
|
|
109
|
-
with open(temp_path, 'w', encoding=encoding, errors='replace') as f:
|
|
110
|
-
json.dump(data, f, indent=indent, ensure_ascii=False)
|
|
111
|
-
|
|
112
|
-
# 验证写入的文件
|
|
113
|
-
if self._verify_file(temp_path, encoding):
|
|
114
|
-
temp_path.replace(file_path)
|
|
115
|
-
return True
|
|
116
|
-
|
|
117
|
-
except (UnicodeEncodeError, UnicodeDecodeError) as e:
|
|
118
|
-
continue
|
|
119
|
-
except Exception as e:
|
|
120
|
-
break
|
|
121
|
-
|
|
122
|
-
# 所有编码都失败,尝试基本ASCII写入
|
|
123
|
-
try:
|
|
124
|
-
with open(temp_path, 'w', encoding='ascii', errors='replace') as f:
|
|
125
|
-
json.dump(data, f, indent=indent, ensure_ascii=True)
|
|
126
|
-
temp_path.replace(file_path)
|
|
127
|
-
return True
|
|
128
|
-
|
|
129
|
-
except Exception as e:
|
|
130
|
-
self._cleanup_temp_file(temp_path)
|
|
131
|
-
raise EncodingError(f"所有编码尝试都失败: {e}")
|
|
132
|
-
|
|
133
|
-
except Exception as e:
|
|
134
|
-
return False
|
|
135
|
-
|
|
136
|
-
def write_yaml(self, file_path: Union[str, Path], data: Dict[str, Any],
|
|
137
|
-
backup: bool = True) -> bool:
|
|
138
|
-
"""安全写入YAML文件"""
|
|
139
|
-
file_path = Path(file_path)
|
|
140
|
-
|
|
141
|
-
try:
|
|
142
|
-
# 备份现有文件
|
|
143
|
-
if backup and file_path.exists():
|
|
144
|
-
backup_path = self._create_backup(file_path)
|
|
145
|
-
shutil.copy2(file_path, backup_path)
|
|
146
|
-
|
|
147
|
-
# 确保目录存在
|
|
148
|
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
-
|
|
150
|
-
# 使用临时文件确保原子性写入
|
|
151
|
-
temp_path = file_path.with_suffix('.tmp')
|
|
152
|
-
|
|
153
|
-
# 尝试多种编码
|
|
154
|
-
for encoding in self.encoding_handler.fallback_encodings:
|
|
155
|
-
try:
|
|
156
|
-
with open(temp_path, 'w', encoding=encoding, errors='replace') as f:
|
|
157
|
-
yaml.dump(data, f, default_flow_style=False,
|
|
158
|
-
allow_unicode=True, encoding=encoding)
|
|
159
|
-
|
|
160
|
-
# 验证写入的文件
|
|
161
|
-
if self._verify_file(temp_path, encoding):
|
|
162
|
-
temp_path.replace(file_path)
|
|
163
|
-
return True
|
|
164
|
-
|
|
165
|
-
except (UnicodeEncodeError, UnicodeDecodeError) as e:
|
|
166
|
-
continue
|
|
167
|
-
except Exception as e:
|
|
168
|
-
break
|
|
169
|
-
|
|
170
|
-
# 所有编码都失败,尝试ASCII安全模式
|
|
171
|
-
try:
|
|
172
|
-
with open(temp_path, 'w', encoding='ascii', errors='replace') as f:
|
|
173
|
-
yaml.dump(data, f, default_flow_style=False,
|
|
174
|
-
allow_unicode=False)
|
|
175
|
-
temp_path.replace(file_path)
|
|
176
|
-
return True
|
|
177
|
-
|
|
178
|
-
except Exception as e:
|
|
179
|
-
self._cleanup_temp_file(temp_path)
|
|
180
|
-
raise EncodingError(f"YAML写入失败: {e}")
|
|
181
|
-
|
|
182
|
-
except Exception as e:
|
|
183
|
-
return False
|
|
184
|
-
|
|
185
|
-
def copy_file(self, src: Union[str, Path], dst: Union[str, Path]) -> bool:
|
|
186
|
-
"""安全复制文件"""
|
|
187
|
-
try:
|
|
188
|
-
src = Path(src)
|
|
189
|
-
dst = Path(dst)
|
|
190
|
-
|
|
191
|
-
if not src.exists():
|
|
192
|
-
return False
|
|
193
|
-
|
|
194
|
-
# 确保目标目录存在
|
|
195
|
-
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
196
|
-
|
|
197
|
-
# 直接复制二进制文件,避免编码转换
|
|
198
|
-
shutil.copy2(src, dst)
|
|
199
|
-
return True
|
|
200
|
-
|
|
201
|
-
except Exception as e:
|
|
202
|
-
return False
|
|
203
|
-
|
|
204
|
-
def _create_backup(self, file_path: Path) -> Path:
|
|
205
|
-
"""创建备份文件"""
|
|
206
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
207
|
-
backup_path = file_path.with_suffix(f'.backup_{timestamp}')
|
|
208
|
-
return backup_path
|
|
209
|
-
|
|
210
|
-
def _verify_file(self, file_path: Path, encoding: str) -> bool:
|
|
211
|
-
"""验证文件是否可读"""
|
|
212
|
-
try:
|
|
213
|
-
with open(file_path, 'r', encoding=encoding) as f:
|
|
214
|
-
f.read(1024) # 读取前1024字符验证
|
|
215
|
-
return True
|
|
216
|
-
except:
|
|
217
|
-
return False
|
|
218
|
-
|
|
219
|
-
def _cleanup_temp_file(self, temp_path: Path):
|
|
220
|
-
"""清理临时文件"""
|
|
221
|
-
try:
|
|
222
|
-
if temp_path.exists():
|
|
223
|
-
temp_path.unlink()
|
|
224
|
-
except:
|
|
225
|
-
pass
|
|
226
|
-
|
|
227
|
-
class SafeFileReader:
|
|
228
|
-
"""安全文件读取器"""
|
|
229
|
-
|
|
230
|
-
def __init__(self, encoding_handler: CrossPlatformEncoding):
|
|
231
|
-
self.encoding_handler = encoding_handler
|
|
232
|
-
|
|
233
|
-
def read_json(self, file_path: Union[str, Path]) -> Dict[str, Any]:
|
|
234
|
-
"""安全读取JSON文件"""
|
|
235
|
-
file_path = Path(file_path)
|
|
236
|
-
|
|
237
|
-
if not file_path.exists():
|
|
238
|
-
return {}
|
|
239
|
-
|
|
240
|
-
# 尝试多种编码
|
|
241
|
-
for encoding in self.encoding_handler.fallback_encodings:
|
|
242
|
-
try:
|
|
243
|
-
with open(file_path, 'r', encoding=encoding, errors='replace') as f:
|
|
244
|
-
return json.load(f) or {}
|
|
245
|
-
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
246
|
-
continue
|
|
247
|
-
except Exception:
|
|
248
|
-
break
|
|
249
|
-
|
|
250
|
-
# 最后尝试带错误处理的读取
|
|
251
|
-
try:
|
|
252
|
-
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
253
|
-
return json.load(f) or {}
|
|
254
|
-
except:
|
|
255
|
-
return {}
|
|
256
|
-
|
|
257
|
-
def read_yaml(self, file_path: Union[str, Path]) -> Dict[str, Any]:
|
|
258
|
-
"""安全读取YAML文件"""
|
|
259
|
-
file_path = Path(file_path)
|
|
260
|
-
|
|
261
|
-
if not file_path.exists():
|
|
262
|
-
return {}
|
|
263
|
-
|
|
264
|
-
# 尝试多种编码
|
|
265
|
-
for encoding in self.encoding_handler.fallback_encodings:
|
|
266
|
-
try:
|
|
267
|
-
with open(file_path, 'r', encoding=encoding, errors='replace') as f:
|
|
268
|
-
return yaml.safe_load(f) or {}
|
|
269
|
-
except (UnicodeDecodeError, yaml.YAMLError):
|
|
270
|
-
continue
|
|
271
|
-
except Exception:
|
|
272
|
-
break
|
|
273
|
-
|
|
274
|
-
# 最后尝试带错误处理的读取
|
|
275
|
-
try:
|
|
276
|
-
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
277
|
-
return yaml.safe_load(f) or {}
|
|
278
|
-
except:
|
|
279
|
-
return {}
|
|
280
|
-
|
|
281
|
-
class CrossPlatformInstaller:
|
|
282
|
-
"""跨平台安装器基类"""
|
|
283
|
-
|
|
284
|
-
def __init__(self):
|
|
285
|
-
self.encoding_handler = CrossPlatformEncoding()
|
|
286
|
-
self.writer = SafeFileWriter(self.encoding_handler)
|
|
287
|
-
self.reader = SafeFileReader(self.encoding_handler)
|
|
288
|
-
|
|
289
|
-
# 设置环境
|
|
290
|
-
self.encoding_handler.setup_environment()
|
|
291
|
-
|
|
292
|
-
def print_system_info(self):
|
|
293
|
-
"""打印系统信息"""
|
|
294
|
-
print(f"[INFO] 系统信息:")
|
|
295
|
-
print(f" 操作系统: {self.encoding_handler.system}")
|
|
296
|
-
print(f" 默认编码: {self.encoding_handler.default_encoding}")
|
|
297
|
-
print(f" 备用编码: {', '.join(self.encoding_handler.fallback_encodings)}")
|
|
298
|
-
print()
|
|
299
|
-
|
|
300
|
-
def create_directory(self, dir_path: Union[str, Path]) -> bool:
|
|
301
|
-
"""创建目录"""
|
|
302
|
-
try:
|
|
303
|
-
Path(dir_path).mkdir(parents=True, exist_ok=True)
|
|
304
|
-
return True
|
|
305
|
-
except Exception as e:
|
|
306
|
-
print(f"❌ 创建目录失败: {dir_path} - {e}")
|
|
307
|
-
return False
|
|
308
|
-
|
|
309
|
-
def copy_adapter_files(self, src_dir: Union[str, Path],
|
|
310
|
-
dst_dir: Union[str, Path],
|
|
311
|
-
file_patterns: List[str]) -> bool:
|
|
312
|
-
"""复制适配器文件"""
|
|
313
|
-
success = True
|
|
314
|
-
src_dir = Path(src_dir)
|
|
315
|
-
dst_dir = Path(dst_dir)
|
|
316
|
-
|
|
317
|
-
# 确保目标目录存在
|
|
318
|
-
dst_dir.mkdir(parents=True, exist_ok=True)
|
|
319
|
-
|
|
320
|
-
for pattern in file_patterns:
|
|
321
|
-
for src_file in src_dir.glob(pattern):
|
|
322
|
-
dst_file = dst_dir / src_file.name
|
|
323
|
-
if not self.writer.copy_file(src_file, dst_file):
|
|
324
|
-
print(f"❌ 复制文件失败: {src_file.name}")
|
|
325
|
-
success = False
|
|
326
|
-
else:
|
|
327
|
-
print(f"[OK] 复制文件: {src_file.name}")
|
|
328
|
-
|
|
329
|
-
return success
|
|
330
|
-
|
|
331
|
-
# 全局实例
|
|
332
|
-
_encoding_installer = None
|
|
333
|
-
|
|
334
|
-
def get_cross_platform_installer() -> CrossPlatformInstaller:
|
|
335
|
-
"""获取跨平台安装器实例"""
|
|
336
|
-
global _encoding_installer
|
|
337
|
-
if _encoding_installer is None:
|
|
338
|
-
_encoding_installer = CrossPlatformInstaller()
|
|
339
|
-
return _encoding_installer
|
|
340
|
-
|
|
341
|
-
def setup_cross_platform_encoding():
|
|
342
|
-
"""设置跨平台编码环境"""
|
|
343
|
-
installer = get_cross_platform_installer()
|
|
344
|
-
installer.print_system_info()
|
|
345
|
-
return installer
|
|
346
|
-
|
|
347
|
-
# 装饰器:为函数添加编码安全
|
|
348
|
-
def encoding_safe(func):
|
|
349
|
-
"""编码安全装饰器"""
|
|
350
|
-
def wrapper(*args, **kwargs):
|
|
351
|
-
# 设置编码环境
|
|
352
|
-
installer = get_cross_platform_installer()
|
|
353
|
-
installer.encoding_handler.setup_environment()
|
|
354
|
-
|
|
355
|
-
try:
|
|
356
|
-
return func(*args, **kwargs)
|
|
357
|
-
except UnicodeError as e:
|
|
358
|
-
print(f"❌ 编码错误: {e}")
|
|
359
|
-
return False
|
|
360
|
-
except Exception as e:
|
|
361
|
-
print(f"❌ 未知错误: {e}")
|
|
362
|
-
print(f"📋 详细错误: {traceback.format_exc()}")
|
|
363
|
-
return False
|
|
364
|
-
|
|
365
|
-
return wrapper
|