mobile-mcp-ai 2.1.2__py3-none-any.whl → 2.5.8__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.
- mobile_mcp/__init__.py +34 -0
- mobile_mcp/config.py +142 -0
- mobile_mcp/core/basic_tools_lite.py +3266 -0
- {core → mobile_mcp/core}/device_manager.py +2 -2
- mobile_mcp/core/dynamic_config.py +272 -0
- mobile_mcp/core/ios_client_wda.py +569 -0
- mobile_mcp/core/ios_device_manager_wda.py +306 -0
- {core → mobile_mcp/core}/mobile_client.py +279 -39
- mobile_mcp/core/template_matcher.py +429 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_151217.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_152037.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_152840.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_153256.png +0 -0
- mobile_mcp/core/templates/close_buttons/auto_x_0112_154847.png +0 -0
- mobile_mcp/core/templates/close_buttons/gray_x_stock_ad.png +0 -0
- {core → mobile_mcp/core}/utils/smart_wait.py +3 -3
- mobile_mcp/mcp_tools/__init__.py +10 -0
- mobile_mcp/mcp_tools/mcp_server.py +1071 -0
- mobile_mcp_ai-2.5.8.dist-info/METADATA +469 -0
- mobile_mcp_ai-2.5.8.dist-info/RECORD +32 -0
- mobile_mcp_ai-2.5.8.dist-info/entry_points.txt +2 -0
- mobile_mcp_ai-2.5.8.dist-info/licenses/LICENSE +201 -0
- mobile_mcp_ai-2.5.8.dist-info/top_level.txt +1 -0
- core/ai/__init__.py +0 -11
- core/ai/ai_analyzer.py +0 -197
- core/ai/ai_config.py +0 -116
- core/ai/ai_platform_adapter.py +0 -399
- core/ai/smart_test_executor.py +0 -520
- core/ai/test_generator.py +0 -365
- core/ai/test_generator_from_history.py +0 -391
- core/ai/test_generator_standalone.py +0 -293
- core/assertion/__init__.py +0 -9
- core/assertion/smart_assertion.py +0 -341
- core/basic_tools.py +0 -377
- core/h5/__init__.py +0 -10
- core/h5/h5_handler.py +0 -548
- core/ios_client.py +0 -219
- core/ios_device_manager.py +0 -252
- core/locator/__init__.py +0 -10
- core/locator/cursor_ai_auto_analyzer.py +0 -119
- core/locator/cursor_vision_helper.py +0 -414
- core/locator/mobile_smart_locator.py +0 -1640
- core/locator/position_analyzer.py +0 -813
- core/locator/script_updater.py +0 -157
- core/nl_test_runner.py +0 -585
- core/smart_app_launcher.py +0 -334
- core/smart_tools.py +0 -311
- mcp/__init__.py +0 -8
- mcp/mcp_server.py +0 -1919
- mcp/mcp_server_simple.py +0 -476
- mobile_mcp_ai-2.1.2.dist-info/METADATA +0 -567
- mobile_mcp_ai-2.1.2.dist-info/RECORD +0 -45
- mobile_mcp_ai-2.1.2.dist-info/entry_points.txt +0 -2
- mobile_mcp_ai-2.1.2.dist-info/top_level.txt +0 -4
- vision/__init__.py +0 -10
- vision/vision_locator.py +0 -404
- {core → mobile_mcp/core}/__init__.py +0 -0
- {core → mobile_mcp/core}/utils/__init__.py +0 -0
- {core → mobile_mcp/core}/utils/logger.py +0 -0
- {core → mobile_mcp/core}/utils/operation_history_manager.py +0 -0
- {utils → mobile_mcp/utils}/__init__.py +0 -0
- {utils → mobile_mcp/utils}/logger.py +0 -0
- {utils → mobile_mcp/utils}/xml_formatter.py +0 -0
- {utils → mobile_mcp/utils}/xml_parser.py +0 -0
- {mobile_mcp_ai-2.1.2.dist-info → mobile_mcp_ai-2.5.8.dist-info}/WHEEL +0 -0
core/ai/ai_platform_adapter.py
DELETED
|
@@ -1,399 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
AI平台适配器 - 支持多种AI平台的可选增强功能
|
|
5
|
-
|
|
6
|
-
支持的平台:
|
|
7
|
-
1. Cursor AI - 多模态视觉识别
|
|
8
|
-
2. Claude (Anthropic) - 通用AI能力
|
|
9
|
-
3. OpenAI GPT-4V - 视觉识别
|
|
10
|
-
4. 其他支持MCP的AI平台
|
|
11
|
-
|
|
12
|
-
设计理念:
|
|
13
|
-
- 基础功能不依赖AI平台(通用)
|
|
14
|
-
- AI增强功能作为可选插件
|
|
15
|
-
- 自动检测可用的AI平台
|
|
16
|
-
- 优雅降级(AI不可用时使用基础功能)
|
|
17
|
-
"""
|
|
18
|
-
import os
|
|
19
|
-
from typing import Optional, Dict, Any, List
|
|
20
|
-
from enum import Enum
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class AIPlatform(Enum):
|
|
25
|
-
"""支持的AI平台"""
|
|
26
|
-
CURSOR = "cursor"
|
|
27
|
-
CLAUDE = "claude"
|
|
28
|
-
OPENAI = "openai"
|
|
29
|
-
GEMINI = "gemini"
|
|
30
|
-
NONE = "none" # 无AI平台(仅基础功能)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class AIPlatformAdapter:
|
|
34
|
-
"""
|
|
35
|
-
AI平台适配器
|
|
36
|
-
|
|
37
|
-
功能:
|
|
38
|
-
1. 自动检测可用的AI平台
|
|
39
|
-
2. 提供统一的AI能力接口
|
|
40
|
-
3. 支持多平台切换
|
|
41
|
-
4. 优雅降级
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
def __init__(self):
|
|
45
|
-
"""初始化AI平台适配器"""
|
|
46
|
-
self.detected_platform: AIPlatform = self._detect_platform()
|
|
47
|
-
self.platform_config: Dict[str, Any] = {}
|
|
48
|
-
self._initialize_platform()
|
|
49
|
-
|
|
50
|
-
def _detect_platform(self) -> AIPlatform:
|
|
51
|
-
"""
|
|
52
|
-
自动检测可用的AI平台
|
|
53
|
-
|
|
54
|
-
检测顺序:
|
|
55
|
-
1. Cursor AI (通过环境变量或MCP上下文)
|
|
56
|
-
2. Claude (通过环境变量)
|
|
57
|
-
3. OpenAI (通过环境变量)
|
|
58
|
-
4. 其他平台
|
|
59
|
-
"""
|
|
60
|
-
# 检测 Cursor AI
|
|
61
|
-
if self._is_cursor_available():
|
|
62
|
-
return AIPlatform.CURSOR
|
|
63
|
-
|
|
64
|
-
# 检测 Claude
|
|
65
|
-
if os.getenv("ANTHROPIC_API_KEY"):
|
|
66
|
-
return AIPlatform.CLAUDE
|
|
67
|
-
|
|
68
|
-
# 检测 OpenAI
|
|
69
|
-
if os.getenv("OPENAI_API_KEY"):
|
|
70
|
-
return AIPlatform.OPENAI
|
|
71
|
-
|
|
72
|
-
# 检测 Gemini
|
|
73
|
-
if os.getenv("GOOGLE_API_KEY"):
|
|
74
|
-
return AIPlatform.GEMINI
|
|
75
|
-
|
|
76
|
-
return AIPlatform.NONE
|
|
77
|
-
|
|
78
|
-
def _is_cursor_available(self) -> bool:
|
|
79
|
-
"""检测 Cursor AI 是否可用"""
|
|
80
|
-
# 方法1: 检查环境变量
|
|
81
|
-
if os.getenv("CURSOR_AI_ENABLED", "").lower() == "true":
|
|
82
|
-
return True
|
|
83
|
-
|
|
84
|
-
# 方法2: 检查MCP上下文(在MCP Server中)
|
|
85
|
-
# 如果是在MCP Server中运行,Cursor AI通常可用
|
|
86
|
-
try:
|
|
87
|
-
# 检查是否有MCP相关的环境
|
|
88
|
-
mcp_server = os.getenv("MCP_SERVER_NAME", "")
|
|
89
|
-
if "cursor" in mcp_server.lower():
|
|
90
|
-
return True
|
|
91
|
-
except:
|
|
92
|
-
pass
|
|
93
|
-
|
|
94
|
-
# 方法3: 🎯 在 MCP Server 环境中默认启用 Cursor AI
|
|
95
|
-
# 如果没有配置其他 AI 平台,且在 MCP 环境中,默认使用 Cursor
|
|
96
|
-
if self._is_running_in_mcp() and not self._has_other_ai_platform():
|
|
97
|
-
return True
|
|
98
|
-
|
|
99
|
-
return False
|
|
100
|
-
|
|
101
|
-
def _is_running_in_mcp(self) -> bool:
|
|
102
|
-
"""检测是否在 MCP Server 环境中运行"""
|
|
103
|
-
# 检查是否通过 MCP 协议运行(stdin/stdout)
|
|
104
|
-
import sys
|
|
105
|
-
return not sys.stdin.isatty() or os.getenv("MCP_MODE") == "1"
|
|
106
|
-
|
|
107
|
-
def _has_other_ai_platform(self) -> bool:
|
|
108
|
-
"""检测是否配置了其他 AI 平台"""
|
|
109
|
-
return bool(
|
|
110
|
-
os.getenv("AI_PROVIDER") or
|
|
111
|
-
os.getenv("ANTHROPIC_API_KEY") or
|
|
112
|
-
os.getenv("OPENAI_API_KEY") or
|
|
113
|
-
os.getenv("GOOGLE_API_KEY") or
|
|
114
|
-
os.getenv("QWEN_API_KEY")
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
def _initialize_platform(self):
|
|
118
|
-
"""初始化检测到的平台"""
|
|
119
|
-
if self.detected_platform == AIPlatform.CURSOR:
|
|
120
|
-
self.platform_config = {
|
|
121
|
-
"name": "Cursor AI",
|
|
122
|
-
"multimodal": True, # 支持多模态
|
|
123
|
-
"vision": True, # 支持视觉识别
|
|
124
|
-
"free": True, # Cursor AI免费使用
|
|
125
|
-
}
|
|
126
|
-
elif self.detected_platform == AIPlatform.CLAUDE:
|
|
127
|
-
self.platform_config = {
|
|
128
|
-
"name": "Claude (Anthropic)",
|
|
129
|
-
"multimodal": True,
|
|
130
|
-
"vision": True,
|
|
131
|
-
"free": False,
|
|
132
|
-
}
|
|
133
|
-
elif self.detected_platform == AIPlatform.OPENAI:
|
|
134
|
-
self.platform_config = {
|
|
135
|
-
"name": "OpenAI GPT-4V",
|
|
136
|
-
"multimodal": True,
|
|
137
|
-
"vision": True,
|
|
138
|
-
"free": False,
|
|
139
|
-
}
|
|
140
|
-
elif self.detected_platform == AIPlatform.GEMINI:
|
|
141
|
-
self.platform_config = {
|
|
142
|
-
"name": "Google Gemini",
|
|
143
|
-
"multimodal": True,
|
|
144
|
-
"vision": True,
|
|
145
|
-
"free": True, # Gemini有免费额度
|
|
146
|
-
}
|
|
147
|
-
else:
|
|
148
|
-
self.platform_config = {
|
|
149
|
-
"name": "None (基础模式)",
|
|
150
|
-
"multimodal": False,
|
|
151
|
-
"vision": False,
|
|
152
|
-
"free": True,
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
def is_vision_available(self) -> bool:
|
|
156
|
-
"""检查是否支持视觉识别"""
|
|
157
|
-
return self.platform_config.get("vision", False)
|
|
158
|
-
|
|
159
|
-
def is_multimodal_available(self) -> bool:
|
|
160
|
-
"""检查是否支持多模态"""
|
|
161
|
-
return self.platform_config.get("multimodal", False)
|
|
162
|
-
|
|
163
|
-
def get_platform_name(self) -> str:
|
|
164
|
-
"""获取平台名称"""
|
|
165
|
-
return self.platform_config.get("name", "Unknown")
|
|
166
|
-
|
|
167
|
-
async def analyze_screenshot(
|
|
168
|
-
self,
|
|
169
|
-
screenshot_path: str,
|
|
170
|
-
element_desc: str,
|
|
171
|
-
**kwargs
|
|
172
|
-
) -> Optional[Dict[str, Any]]:
|
|
173
|
-
"""
|
|
174
|
-
分析截图(统一接口)
|
|
175
|
-
|
|
176
|
-
Args:
|
|
177
|
-
screenshot_path: 截图路径
|
|
178
|
-
element_desc: 元素描述
|
|
179
|
-
**kwargs: 平台特定参数
|
|
180
|
-
|
|
181
|
-
Returns:
|
|
182
|
-
坐标信息或None
|
|
183
|
-
"""
|
|
184
|
-
if not self.is_vision_available():
|
|
185
|
-
return None
|
|
186
|
-
|
|
187
|
-
if self.detected_platform == AIPlatform.CURSOR:
|
|
188
|
-
return await self._analyze_with_cursor(screenshot_path, element_desc, **kwargs)
|
|
189
|
-
elif self.detected_platform == AIPlatform.CLAUDE:
|
|
190
|
-
return await self._analyze_with_claude(screenshot_path, element_desc, **kwargs)
|
|
191
|
-
elif self.detected_platform == AIPlatform.OPENAI:
|
|
192
|
-
return await self._analyze_with_openai(screenshot_path, element_desc, **kwargs)
|
|
193
|
-
elif self.detected_platform == AIPlatform.GEMINI:
|
|
194
|
-
return await self._analyze_with_gemini(screenshot_path, element_desc, **kwargs)
|
|
195
|
-
|
|
196
|
-
return None
|
|
197
|
-
|
|
198
|
-
async def _analyze_with_cursor(
|
|
199
|
-
self,
|
|
200
|
-
screenshot_path: str,
|
|
201
|
-
element_desc: str,
|
|
202
|
-
**kwargs
|
|
203
|
-
) -> Optional[Dict[str, Any]]:
|
|
204
|
-
"""
|
|
205
|
-
使用 Cursor AI 分析截图
|
|
206
|
-
|
|
207
|
-
Cursor AI 通过 MCP 工具调用,返回结果文件路径
|
|
208
|
-
"""
|
|
209
|
-
# Cursor AI 的特殊处理:
|
|
210
|
-
# 1. 创建请求文件
|
|
211
|
-
# 2. 返回提示信息,让 Cursor AI 通过 MCP 工具分析
|
|
212
|
-
# 3. 轮询结果文件
|
|
213
|
-
|
|
214
|
-
request_id = kwargs.get("request_id")
|
|
215
|
-
if request_id:
|
|
216
|
-
# 自动模式:等待 Cursor AI 写入结果文件
|
|
217
|
-
result_file = kwargs.get("result_file")
|
|
218
|
-
if result_file and Path(result_file).exists():
|
|
219
|
-
import json
|
|
220
|
-
with open(result_file, 'r', encoding='utf-8') as f:
|
|
221
|
-
result_data = json.load(f)
|
|
222
|
-
if result_data.get("status") == "completed":
|
|
223
|
-
coord = result_data.get("coordinate")
|
|
224
|
-
if coord:
|
|
225
|
-
return {
|
|
226
|
-
"x": coord.get("x"),
|
|
227
|
-
"y": coord.get("y"),
|
|
228
|
-
"confidence": coord.get("confidence", 90),
|
|
229
|
-
"platform": "cursor"
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
# 手动模式:返回提示信息
|
|
233
|
-
return {
|
|
234
|
-
"platform": "cursor",
|
|
235
|
-
"instruction": f"请使用多模态能力分析截图 {screenshot_path},找到元素 '{element_desc}' 并返回坐标",
|
|
236
|
-
"screenshot_path": screenshot_path,
|
|
237
|
-
"element_desc": element_desc
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
async def _analyze_with_claude(
|
|
241
|
-
self,
|
|
242
|
-
screenshot_path: str,
|
|
243
|
-
element_desc: str,
|
|
244
|
-
**kwargs
|
|
245
|
-
) -> Optional[Dict[str, Any]]:
|
|
246
|
-
"""使用 Claude API 分析截图"""
|
|
247
|
-
# TODO: 实现 Claude API 调用
|
|
248
|
-
# 需要安装 anthropic SDK
|
|
249
|
-
try:
|
|
250
|
-
from anthropic import Anthropic
|
|
251
|
-
|
|
252
|
-
client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
|
|
253
|
-
|
|
254
|
-
# 读取截图
|
|
255
|
-
with open(screenshot_path, 'rb') as f:
|
|
256
|
-
image_data = f.read()
|
|
257
|
-
|
|
258
|
-
# 调用 Claude Vision API
|
|
259
|
-
message = client.messages.create(
|
|
260
|
-
model="claude-3-5-sonnet-20241022",
|
|
261
|
-
max_tokens=1024,
|
|
262
|
-
messages=[{
|
|
263
|
-
"role": "user",
|
|
264
|
-
"content": [
|
|
265
|
-
{
|
|
266
|
-
"type": "image",
|
|
267
|
-
"source": {
|
|
268
|
-
"type": "base64",
|
|
269
|
-
"media_type": "image/png",
|
|
270
|
-
"data": image_data.hex() # 需要base64编码
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
"type": "text",
|
|
275
|
-
"text": f"分析这个移动端截图,找到元素 '{element_desc}' 并返回其中心点坐标,格式:{{\"x\": 100, \"y\": 200}}"
|
|
276
|
-
}
|
|
277
|
-
]
|
|
278
|
-
}]
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
# 解析响应
|
|
282
|
-
# TODO: 解析 Claude 返回的坐标
|
|
283
|
-
return None
|
|
284
|
-
|
|
285
|
-
except ImportError:
|
|
286
|
-
return None
|
|
287
|
-
except Exception as e:
|
|
288
|
-
print(f"⚠️ Claude API 调用失败: {e}")
|
|
289
|
-
return None
|
|
290
|
-
|
|
291
|
-
async def _analyze_with_openai(
|
|
292
|
-
self,
|
|
293
|
-
screenshot_path: str,
|
|
294
|
-
element_desc: str,
|
|
295
|
-
**kwargs
|
|
296
|
-
) -> Optional[Dict[str, Any]]:
|
|
297
|
-
"""使用 OpenAI GPT-4V 分析截图"""
|
|
298
|
-
# TODO: 实现 OpenAI Vision API 调用
|
|
299
|
-
try:
|
|
300
|
-
import base64
|
|
301
|
-
from openai import OpenAI
|
|
302
|
-
|
|
303
|
-
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
|
304
|
-
|
|
305
|
-
# 读取并编码截图
|
|
306
|
-
with open(screenshot_path, 'rb') as f:
|
|
307
|
-
image_data = base64.b64encode(f.read()).decode('utf-8')
|
|
308
|
-
|
|
309
|
-
# 调用 GPT-4V
|
|
310
|
-
response = client.chat.completions.create(
|
|
311
|
-
model="gpt-4-vision-preview",
|
|
312
|
-
messages=[{
|
|
313
|
-
"role": "user",
|
|
314
|
-
"content": [
|
|
315
|
-
{
|
|
316
|
-
"type": "text",
|
|
317
|
-
"text": f"分析这个移动端截图,找到元素 '{element_desc}' 并返回其中心点坐标,格式:{{\"x\": 100, \"y\": 200}}"
|
|
318
|
-
},
|
|
319
|
-
{
|
|
320
|
-
"type": "image_url",
|
|
321
|
-
"image_url": {
|
|
322
|
-
"url": f"data:image/png;base64,{image_data}"
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
]
|
|
326
|
-
}],
|
|
327
|
-
max_tokens=300
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
# 解析响应
|
|
331
|
-
# TODO: 解析 OpenAI 返回的坐标
|
|
332
|
-
return None
|
|
333
|
-
|
|
334
|
-
except ImportError:
|
|
335
|
-
return None
|
|
336
|
-
except Exception as e:
|
|
337
|
-
print(f"⚠️ OpenAI API 调用失败: {e}")
|
|
338
|
-
return None
|
|
339
|
-
|
|
340
|
-
async def _analyze_with_gemini(
|
|
341
|
-
self,
|
|
342
|
-
screenshot_path: str,
|
|
343
|
-
element_desc: str,
|
|
344
|
-
**kwargs
|
|
345
|
-
) -> Optional[Dict[str, Any]]:
|
|
346
|
-
"""使用 Google Gemini 分析截图"""
|
|
347
|
-
# TODO: 实现 Gemini Vision API 调用
|
|
348
|
-
return None
|
|
349
|
-
|
|
350
|
-
def get_enhanced_tools(self) -> List[Dict[str, Any]]:
|
|
351
|
-
"""
|
|
352
|
-
获取AI增强的工具列表
|
|
353
|
-
|
|
354
|
-
Returns:
|
|
355
|
-
AI增强工具的定义列表
|
|
356
|
-
"""
|
|
357
|
-
tools = []
|
|
358
|
-
|
|
359
|
-
if self.is_vision_available():
|
|
360
|
-
# 视觉识别工具(根据平台调整描述)
|
|
361
|
-
platform_name = self.get_platform_name()
|
|
362
|
-
tools.append({
|
|
363
|
-
"name": "mobile_analyze_screenshot",
|
|
364
|
-
"description": f"分析截图并返回元素坐标。使用{platform_name}的多模态能力分析截图,找到指定元素并返回坐标。",
|
|
365
|
-
"platform": self.detected_platform.value,
|
|
366
|
-
"enhanced": True
|
|
367
|
-
})
|
|
368
|
-
|
|
369
|
-
return tools
|
|
370
|
-
|
|
371
|
-
def get_capabilities(self) -> Dict[str, Any]:
|
|
372
|
-
"""获取当前平台的AI能力"""
|
|
373
|
-
return {
|
|
374
|
-
"platform": self.detected_platform.value,
|
|
375
|
-
"platform_name": self.get_platform_name(),
|
|
376
|
-
"vision": self.is_vision_available(),
|
|
377
|
-
"multimodal": self.is_multimodal_available(),
|
|
378
|
-
"free": self.platform_config.get("free", False),
|
|
379
|
-
"enhanced_tools": [t["name"] for t in self.get_enhanced_tools()]
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
# 全局实例
|
|
384
|
-
_ai_adapter: Optional[AIPlatformAdapter] = None
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
def get_ai_adapter() -> AIPlatformAdapter:
|
|
388
|
-
"""获取全局AI适配器实例"""
|
|
389
|
-
global _ai_adapter
|
|
390
|
-
if _ai_adapter is None:
|
|
391
|
-
_ai_adapter = AIPlatformAdapter()
|
|
392
|
-
return _ai_adapter
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
def reset_ai_adapter():
|
|
396
|
-
"""重置AI适配器(用于测试)"""
|
|
397
|
-
global _ai_adapter
|
|
398
|
-
_ai_adapter = None
|
|
399
|
-
|