htmlgen-mcp 0.2.2__py3-none-any.whl → 0.3.1__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.
Potentially problematic release.
This version of htmlgen-mcp might be problematic. Click here for more details.
- htmlgen_mcp/__init__.py +7 -0
- htmlgen_mcp/agents/ai_content_generator.py +328 -0
- htmlgen_mcp/agents/quick_generator.py +270 -0
- {agents → htmlgen_mcp/agents}/smart_web_agent.py +118 -12
- {agents → htmlgen_mcp/agents}/web_tools/__init__.py +16 -6
- htmlgen_mcp/agents/web_tools/html_templates_improved.py +696 -0
- {agents → htmlgen_mcp/agents}/web_tools/navigation.py +29 -0
- htmlgen_mcp/config.py +326 -0
- htmlgen_mcp/sse_optimizations.py +195 -0
- {MCP → htmlgen_mcp}/web_agent_server.py +39 -19
- {htmlgen_mcp-0.2.2.dist-info → htmlgen_mcp-0.3.1.dist-info}/METADATA +1 -1
- htmlgen_mcp-0.3.1.dist-info/RECORD +31 -0
- htmlgen_mcp-0.3.1.dist-info/entry_points.txt +2 -0
- htmlgen_mcp-0.3.1.dist-info/top_level.txt +1 -0
- MCP/__init__.py +0 -5
- htmlgen_mcp-0.2.2.dist-info/RECORD +0 -26
- htmlgen_mcp-0.2.2.dist-info/entry_points.txt +0 -2
- htmlgen_mcp-0.2.2.dist-info/top_level.txt +0 -2
- {agents → htmlgen_mcp/agents}/__init__.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/bootstrap.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/browser.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/colors.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/css.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/edgeone_deploy.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/html_templates.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/images.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/images_fixed.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/js.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/project.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/simple_builder.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/simple_css.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/simple_js.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/simple_templates.py +0 -0
- {agents → htmlgen_mcp/agents}/web_tools/validation.py +0 -0
- {htmlgen_mcp-0.2.2.dist-info → htmlgen_mcp-0.3.1.dist-info}/WHEEL +0 -0
|
@@ -333,6 +333,35 @@ def create_responsive_navbar(file_path: str, brand_name: str = "公司名称", n
|
|
|
333
333
|
# 没找到 body,兜底:前置插入
|
|
334
334
|
content = navbar_html + "\n" + content
|
|
335
335
|
|
|
336
|
+
# 确保 Bootstrap 依赖存在(导航栏需要)
|
|
337
|
+
bootstrap_css = '<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">'
|
|
338
|
+
bootstrap_js = '<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>'
|
|
339
|
+
fontawesome_css = '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">'
|
|
340
|
+
|
|
341
|
+
# 添加 Bootstrap CSS(如果不存在)
|
|
342
|
+
if 'bootstrap' not in content.lower():
|
|
343
|
+
if '</head>' in content:
|
|
344
|
+
# 在 </head> 前插入,但要在自定义 CSS 之前
|
|
345
|
+
if '<link rel="stylesheet" href="assets/css/style.css">' in content:
|
|
346
|
+
content = content.replace(
|
|
347
|
+
'<link rel="stylesheet" href="assets/css/style.css">',
|
|
348
|
+
f'{bootstrap_css}\n {fontawesome_css}\n <link rel="stylesheet" href="assets/css/style.css">'
|
|
349
|
+
)
|
|
350
|
+
else:
|
|
351
|
+
content = content.replace('</head>', f' {bootstrap_css}\n {fontawesome_css}\n</head>')
|
|
352
|
+
|
|
353
|
+
# 添加 Bootstrap JS(如果不存在)
|
|
354
|
+
if 'bootstrap.bundle' not in content.lower():
|
|
355
|
+
if '</body>' in content:
|
|
356
|
+
# 在 </body> 前插入
|
|
357
|
+
if '<script src="assets/js/main.js"></script>' in content:
|
|
358
|
+
content = content.replace(
|
|
359
|
+
'<script src="assets/js/main.js"></script>',
|
|
360
|
+
f'{bootstrap_js}\n <script src="assets/js/main.js"></script>'
|
|
361
|
+
)
|
|
362
|
+
else:
|
|
363
|
+
content = content.replace('</body>', f' {bootstrap_js}\n</body>')
|
|
364
|
+
|
|
336
365
|
# 确保导航锚点对应的区块存在
|
|
337
366
|
id_pattern = re.compile(r'id\s*=\s*["\']([^"\']+)["\']', re.I)
|
|
338
367
|
existing_ids = {m.group(1).strip().lower() for m in id_pattern.finditer(content)}
|
htmlgen_mcp/config.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""项目配置管理 - 跨平台支持"""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import platform
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProjectConfig:
|
|
11
|
+
"""项目配置管理器 - 支持 Windows/macOS/Linux"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def get_system_info() -> dict:
|
|
15
|
+
"""获取系统信息"""
|
|
16
|
+
return {
|
|
17
|
+
'system': platform.system(), # 'Windows', 'Darwin' (macOS), 'Linux'
|
|
18
|
+
'platform': sys.platform, # 'win32', 'darwin', 'linux'
|
|
19
|
+
'home': Path.home(),
|
|
20
|
+
'temp': Path(tempfile.gettempdir())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def get_default_output_dir() -> Path:
|
|
25
|
+
"""获取默认的项目输出目录(跨平台)
|
|
26
|
+
|
|
27
|
+
优先级:
|
|
28
|
+
1. 环境变量 WEB_AGENT_OUTPUT_DIR
|
|
29
|
+
2. 系统特定的文档目录
|
|
30
|
+
3. 用户主目录下的隐藏目录
|
|
31
|
+
4. 系统临时目录
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# 1. 检查环境变量(所有平台通用)
|
|
35
|
+
env_dir = os.environ.get('WEB_AGENT_OUTPUT_DIR')
|
|
36
|
+
if env_dir:
|
|
37
|
+
output_dir = Path(env_dir)
|
|
38
|
+
try:
|
|
39
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
return output_dir
|
|
41
|
+
except Exception as e:
|
|
42
|
+
print(f"⚠️ 无法创建环境变量指定的目录: {e}")
|
|
43
|
+
|
|
44
|
+
system_info = ProjectConfig.get_system_info()
|
|
45
|
+
home = system_info['home']
|
|
46
|
+
system = system_info['system']
|
|
47
|
+
|
|
48
|
+
# 2. 系统特定的文档目录
|
|
49
|
+
if system == 'Windows':
|
|
50
|
+
# Windows: 使用 Documents 文件夹
|
|
51
|
+
docs_candidates = [
|
|
52
|
+
home / 'Documents' / 'WebProjects',
|
|
53
|
+
home / 'My Documents' / 'WebProjects', # 旧版 Windows
|
|
54
|
+
Path(os.environ.get('USERPROFILE', home)) / 'Documents' / 'WebProjects'
|
|
55
|
+
]
|
|
56
|
+
elif system == 'Darwin': # macOS
|
|
57
|
+
# macOS: Documents 文件夹
|
|
58
|
+
docs_candidates = [
|
|
59
|
+
home / 'Documents' / 'WebProjects',
|
|
60
|
+
home / 'Projects' / 'WebProjects' # 有些用户喜欢用 Projects 文件夹
|
|
61
|
+
]
|
|
62
|
+
else: # Linux 及其他 Unix-like 系统
|
|
63
|
+
# Linux: 遵循 XDG 标准
|
|
64
|
+
xdg_documents = os.environ.get('XDG_DOCUMENTS_DIR')
|
|
65
|
+
docs_candidates = []
|
|
66
|
+
if xdg_documents:
|
|
67
|
+
docs_candidates.append(Path(xdg_documents) / 'WebProjects')
|
|
68
|
+
docs_candidates.extend([
|
|
69
|
+
home / 'Documents' / 'WebProjects',
|
|
70
|
+
home / 'projects' / 'web', # Linux 用户常用小写
|
|
71
|
+
home / 'Projects' / 'Web'
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
# 尝试创建文档目录
|
|
75
|
+
for doc_dir in docs_candidates:
|
|
76
|
+
try:
|
|
77
|
+
if doc_dir.parent.exists():
|
|
78
|
+
doc_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
return doc_dir
|
|
80
|
+
except Exception:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# 3. 用户主目录下的隐藏目录(所有平台)
|
|
84
|
+
if system == 'Windows':
|
|
85
|
+
# Windows 使用 AppData
|
|
86
|
+
app_data = os.environ.get('APPDATA')
|
|
87
|
+
if app_data:
|
|
88
|
+
hidden_dirs = [
|
|
89
|
+
Path(app_data) / 'WebAgent' / 'projects',
|
|
90
|
+
home / 'AppData' / 'Roaming' / 'WebAgent' / 'projects'
|
|
91
|
+
]
|
|
92
|
+
else:
|
|
93
|
+
hidden_dirs = [home / '.web-agent' / 'projects']
|
|
94
|
+
else:
|
|
95
|
+
# macOS 和 Linux 使用点开头的隐藏目录
|
|
96
|
+
hidden_dirs = [
|
|
97
|
+
home / '.web-agent' / 'projects',
|
|
98
|
+
home / '.local' / 'share' / 'web-agent' / 'projects' # XDG 标准
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
for hidden_dir in hidden_dirs:
|
|
102
|
+
try:
|
|
103
|
+
hidden_dir.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
return hidden_dir
|
|
105
|
+
except Exception:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# 4. 系统临时目录(最后的选择)
|
|
109
|
+
temp_base = system_info['temp']
|
|
110
|
+
temp_dir = temp_base / 'web-agent-projects'
|
|
111
|
+
try:
|
|
112
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
print(f"⚠️ 使用临时目录: {temp_dir}")
|
|
114
|
+
print(f"💡 建议设置环境变量 WEB_AGENT_OUTPUT_DIR 到更合适的位置")
|
|
115
|
+
return temp_dir
|
|
116
|
+
except Exception as e:
|
|
117
|
+
# 如果连临时目录都无法创建,使用当前工作目录
|
|
118
|
+
print(f"⚠️ 无法创建临时目录: {e}")
|
|
119
|
+
fallback = Path.cwd() / 'web-projects'
|
|
120
|
+
fallback.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
return fallback
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def create_project_directory(
|
|
125
|
+
project_name: str,
|
|
126
|
+
base_dir: Path = None,
|
|
127
|
+
use_timestamp: bool = True
|
|
128
|
+
) -> Path:
|
|
129
|
+
"""创建项目目录
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
project_name: 项目名称
|
|
133
|
+
base_dir: 基础目录,如果不提供则使用默认目录
|
|
134
|
+
use_timestamp: 是否在目录名中添加时间戳(避免冲突)
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
创建的项目目录路径
|
|
138
|
+
"""
|
|
139
|
+
if base_dir is None:
|
|
140
|
+
base_dir = ProjectConfig.get_default_output_dir()
|
|
141
|
+
|
|
142
|
+
# 清理项目名称,移除特殊字符
|
|
143
|
+
safe_name = "".join(c for c in project_name if c.isalnum() or c in (' ', '-', '_'))
|
|
144
|
+
safe_name = safe_name.strip().replace(' ', '-').lower()
|
|
145
|
+
|
|
146
|
+
if use_timestamp:
|
|
147
|
+
# 添加时间戳避免冲突
|
|
148
|
+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
149
|
+
dir_name = f"{safe_name}-{timestamp}"
|
|
150
|
+
else:
|
|
151
|
+
dir_name = safe_name
|
|
152
|
+
|
|
153
|
+
project_dir = base_dir / dir_name
|
|
154
|
+
|
|
155
|
+
# 如果目录已存在且不使用时间戳,添加序号
|
|
156
|
+
if project_dir.exists() and not use_timestamp:
|
|
157
|
+
counter = 1
|
|
158
|
+
while (base_dir / f"{dir_name}-{counter}").exists():
|
|
159
|
+
counter += 1
|
|
160
|
+
project_dir = base_dir / f"{dir_name}-{counter}"
|
|
161
|
+
|
|
162
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
|
|
164
|
+
# 创建标准子目录结构
|
|
165
|
+
(project_dir / "assets" / "css").mkdir(parents=True, exist_ok=True)
|
|
166
|
+
(project_dir / "assets" / "js").mkdir(parents=True, exist_ok=True)
|
|
167
|
+
(project_dir / "assets" / "images").mkdir(parents=True, exist_ok=True)
|
|
168
|
+
|
|
169
|
+
# 创建项目信息文件
|
|
170
|
+
info_file = project_dir / ".project-info.json"
|
|
171
|
+
import json
|
|
172
|
+
project_info = {
|
|
173
|
+
"name": project_name,
|
|
174
|
+
"created_at": datetime.now().isoformat(),
|
|
175
|
+
"generator": "htmlgen-mcp",
|
|
176
|
+
"version": "0.3.0"
|
|
177
|
+
}
|
|
178
|
+
info_file.write_text(json.dumps(project_info, ensure_ascii=False, indent=2))
|
|
179
|
+
|
|
180
|
+
return project_dir
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def get_user_projects_list(base_dir: Path = None) -> list:
|
|
184
|
+
"""获取用户已创建的项目列表
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
项目信息列表
|
|
188
|
+
"""
|
|
189
|
+
if base_dir is None:
|
|
190
|
+
base_dir = ProjectConfig.get_default_output_dir()
|
|
191
|
+
|
|
192
|
+
projects = []
|
|
193
|
+
if not base_dir.exists():
|
|
194
|
+
return projects
|
|
195
|
+
|
|
196
|
+
for item in base_dir.iterdir():
|
|
197
|
+
if item.is_dir():
|
|
198
|
+
info_file = item / ".project-info.json"
|
|
199
|
+
if info_file.exists():
|
|
200
|
+
try:
|
|
201
|
+
import json
|
|
202
|
+
info = json.loads(info_file.read_text())
|
|
203
|
+
info['path'] = str(item)
|
|
204
|
+
projects.append(info)
|
|
205
|
+
except:
|
|
206
|
+
# 如果没有info文件,仍然添加基本信息
|
|
207
|
+
projects.append({
|
|
208
|
+
'name': item.name,
|
|
209
|
+
'path': str(item),
|
|
210
|
+
'created_at': datetime.fromtimestamp(item.stat().st_mtime).isoformat()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
# 按创建时间倒序排序
|
|
214
|
+
projects.sort(key=lambda x: x.get('created_at', ''), reverse=True)
|
|
215
|
+
return projects
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def clean_old_projects(
|
|
219
|
+
base_dir: Path = None,
|
|
220
|
+
days_to_keep: int = 7,
|
|
221
|
+
max_projects: int = 20
|
|
222
|
+
) -> int:
|
|
223
|
+
"""清理旧项目
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
base_dir: 基础目录
|
|
227
|
+
days_to_keep: 保留最近几天的项目
|
|
228
|
+
max_projects: 最多保留多少个项目
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
删除的项目数量
|
|
232
|
+
"""
|
|
233
|
+
if base_dir is None:
|
|
234
|
+
base_dir = ProjectConfig.get_default_output_dir()
|
|
235
|
+
|
|
236
|
+
projects = ProjectConfig.get_user_projects_list(base_dir)
|
|
237
|
+
deleted = 0
|
|
238
|
+
|
|
239
|
+
# 如果项目数超过限制,删除最旧的
|
|
240
|
+
if len(projects) > max_projects:
|
|
241
|
+
for project in projects[max_projects:]:
|
|
242
|
+
try:
|
|
243
|
+
import shutil
|
|
244
|
+
shutil.rmtree(project['path'])
|
|
245
|
+
deleted += 1
|
|
246
|
+
except:
|
|
247
|
+
pass
|
|
248
|
+
|
|
249
|
+
# 删除超过指定天数的项目
|
|
250
|
+
from datetime import timedelta
|
|
251
|
+
cutoff_date = datetime.now() - timedelta(days=days_to_keep)
|
|
252
|
+
|
|
253
|
+
for project in projects:
|
|
254
|
+
try:
|
|
255
|
+
created_at = datetime.fromisoformat(project.get('created_at', ''))
|
|
256
|
+
if created_at < cutoff_date:
|
|
257
|
+
import shutil
|
|
258
|
+
shutil.rmtree(project['path'])
|
|
259
|
+
deleted += 1
|
|
260
|
+
except:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
return deleted
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# 便捷函数
|
|
267
|
+
def get_project_directory(project_name: str = None) -> str:
|
|
268
|
+
"""获取项目目录(供 MCP 工具使用)
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
project_name: 项目名称,如果不提供则生成默认名称
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
项目目录路径字符串
|
|
275
|
+
"""
|
|
276
|
+
if not project_name:
|
|
277
|
+
project_name = f"web-project-{datetime.now().strftime('%Y%m%d')}"
|
|
278
|
+
|
|
279
|
+
config = ProjectConfig()
|
|
280
|
+
project_dir = config.create_project_directory(project_name, use_timestamp=True)
|
|
281
|
+
|
|
282
|
+
print(f"📁 项目将生成在: {project_dir}")
|
|
283
|
+
print(f"💡 提示: 可通过设置环境变量 WEB_AGENT_OUTPUT_DIR 来自定义输出目录")
|
|
284
|
+
|
|
285
|
+
return str(project_dir)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def list_recent_projects(limit: int = 10) -> list:
|
|
289
|
+
"""列出最近的项目
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
limit: 返回的项目数量
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
项目列表
|
|
296
|
+
"""
|
|
297
|
+
config = ProjectConfig()
|
|
298
|
+
projects = config.get_user_projects_list()
|
|
299
|
+
return projects[:limit]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def clean_temp_projects() -> int:
|
|
303
|
+
"""清理临时项目
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
删除的项目数量
|
|
307
|
+
"""
|
|
308
|
+
config = ProjectConfig()
|
|
309
|
+
|
|
310
|
+
# 如果使用的是 /tmp 目录,更积极地清理
|
|
311
|
+
output_dir = config.get_default_output_dir()
|
|
312
|
+
if str(output_dir).startswith('/tmp'):
|
|
313
|
+
# 临时目录只保留1天,最多10个项目
|
|
314
|
+
return config.clean_old_projects(days_to_keep=1, max_projects=10)
|
|
315
|
+
else:
|
|
316
|
+
# 其他目录保留7天,最多20个项目
|
|
317
|
+
return config.clean_old_projects(days_to_keep=7, max_projects=20)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# 导出的功能
|
|
321
|
+
__all__ = [
|
|
322
|
+
'ProjectConfig',
|
|
323
|
+
'get_project_directory',
|
|
324
|
+
'list_recent_projects',
|
|
325
|
+
'clean_temp_projects'
|
|
326
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""SSE 模式优化补丁 - 解决超时问题"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from typing import Dict, Any, List, Optional
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
async def read_progress_file_async(
|
|
12
|
+
file_path: str,
|
|
13
|
+
limit: int = 20,
|
|
14
|
+
chunk_size: int = 8192
|
|
15
|
+
) -> tuple[List[Dict[str, Any]], int]:
|
|
16
|
+
"""异步读取进度日志文件,避免阻塞"""
|
|
17
|
+
events: List[Dict[str, Any]] = []
|
|
18
|
+
total_lines = 0
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
# 使用异步方式读取文件
|
|
22
|
+
loop = asyncio.get_event_loop()
|
|
23
|
+
|
|
24
|
+
# 异步读取文件内容
|
|
25
|
+
def read_file():
|
|
26
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
27
|
+
lines = f.readlines()
|
|
28
|
+
return lines
|
|
29
|
+
|
|
30
|
+
lines = await loop.run_in_executor(None, read_file)
|
|
31
|
+
total_lines = len(lines)
|
|
32
|
+
|
|
33
|
+
# 处理最后的 limit 行
|
|
34
|
+
for line in lines[-limit:]:
|
|
35
|
+
line = line.strip()
|
|
36
|
+
if not line:
|
|
37
|
+
continue
|
|
38
|
+
try:
|
|
39
|
+
events.append(json.loads(line))
|
|
40
|
+
except Exception:
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
return events, total_lines
|
|
44
|
+
|
|
45
|
+
except Exception as e:
|
|
46
|
+
raise Exception(f"读取进度文件失败: {str(e)}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def check_file_exists_async(file_path: str) -> bool:
|
|
50
|
+
"""异步检查文件是否存在"""
|
|
51
|
+
loop = asyncio.get_event_loop()
|
|
52
|
+
return await loop.run_in_executor(None, os.path.exists, file_path)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_streaming_response(data: Dict[str, Any], chunk_size: int = 100) -> List[Dict[str, Any]]:
|
|
56
|
+
"""
|
|
57
|
+
将大响应分块,适用于 SSE 流式传输
|
|
58
|
+
避免一次性发送大量数据导致超时
|
|
59
|
+
"""
|
|
60
|
+
if 'events' in data and len(data['events']) > chunk_size:
|
|
61
|
+
# 分块发送事件
|
|
62
|
+
chunks = []
|
|
63
|
+
events = data['events']
|
|
64
|
+
|
|
65
|
+
for i in range(0, len(events), chunk_size):
|
|
66
|
+
chunk_data = data.copy()
|
|
67
|
+
chunk_data['events'] = events[i:i + chunk_size]
|
|
68
|
+
chunk_data['chunk_index'] = i // chunk_size
|
|
69
|
+
chunk_data['total_chunks'] = (len(events) + chunk_size - 1) // chunk_size
|
|
70
|
+
chunks.append(chunk_data)
|
|
71
|
+
|
|
72
|
+
return chunks
|
|
73
|
+
|
|
74
|
+
return [data]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
async def get_progress_optimized(
|
|
78
|
+
plan_id: Optional[str] = None,
|
|
79
|
+
job_id: Optional[str] = None,
|
|
80
|
+
log_path: Optional[str] = None,
|
|
81
|
+
limit: int = 20,
|
|
82
|
+
_job_registry: Dict = None,
|
|
83
|
+
_progress_log_by_job: Dict = None,
|
|
84
|
+
_progress_log_by_id: Dict = None,
|
|
85
|
+
project_root: str = None
|
|
86
|
+
) -> Dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
SSE 优化版的 get_progress 函数
|
|
89
|
+
- 使用异步 I/O 避免阻塞
|
|
90
|
+
- 添加超时控制
|
|
91
|
+
- 支持流式响应
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
# 设置操作超时(SSE 环境下更短的超时)
|
|
95
|
+
timeout = 10 # 10秒超时
|
|
96
|
+
|
|
97
|
+
async def _get_progress():
|
|
98
|
+
if limit <= 0:
|
|
99
|
+
limit = 20
|
|
100
|
+
|
|
101
|
+
job_info = None
|
|
102
|
+
resolved_path = None
|
|
103
|
+
|
|
104
|
+
# 查找日志文件路径
|
|
105
|
+
if job_id and _job_registry:
|
|
106
|
+
job_info = _job_registry.get(job_id)
|
|
107
|
+
if job_info and not plan_id:
|
|
108
|
+
plan_id = job_info.get("plan_id")
|
|
109
|
+
if _progress_log_by_job and job_id in _progress_log_by_job:
|
|
110
|
+
resolved_path = _progress_log_by_job[job_id]
|
|
111
|
+
elif job_info and job_info.get("progress_log"):
|
|
112
|
+
resolved_path = job_info.get("progress_log")
|
|
113
|
+
|
|
114
|
+
if not resolved_path and plan_id and _progress_log_by_id and plan_id in _progress_log_by_id:
|
|
115
|
+
resolved_path = _progress_log_by_id[plan_id]
|
|
116
|
+
|
|
117
|
+
if log_path:
|
|
118
|
+
resolved_path = log_path
|
|
119
|
+
|
|
120
|
+
# 解析路径
|
|
121
|
+
if resolved_path:
|
|
122
|
+
if not os.path.isabs(resolved_path):
|
|
123
|
+
candidate = os.path.join(project_root or os.getcwd(), resolved_path)
|
|
124
|
+
if await check_file_exists_async(candidate):
|
|
125
|
+
resolved_path = candidate
|
|
126
|
+
else:
|
|
127
|
+
alt = os.path.sep + resolved_path.lstrip(os.path.sep)
|
|
128
|
+
if await check_file_exists_async(alt):
|
|
129
|
+
resolved_path = alt
|
|
130
|
+
|
|
131
|
+
# 检查文件是否存在
|
|
132
|
+
if not resolved_path or not await check_file_exists_async(resolved_path):
|
|
133
|
+
return {
|
|
134
|
+
"status": "error",
|
|
135
|
+
"message": "未找到进度日志,请确认 job_id/plan_id 或提供 log_path"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# 异步读取文件
|
|
139
|
+
events, total_lines = await read_progress_file_async(resolved_path, limit)
|
|
140
|
+
|
|
141
|
+
response = {
|
|
142
|
+
"status": "success",
|
|
143
|
+
"plan_id": plan_id,
|
|
144
|
+
"job_id": job_id,
|
|
145
|
+
"log_path": resolved_path,
|
|
146
|
+
"events": events,
|
|
147
|
+
"total_records": total_lines,
|
|
148
|
+
"returned": len(events),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# 添加任务信息
|
|
152
|
+
if job_info:
|
|
153
|
+
snapshot_keys = [
|
|
154
|
+
"job_id", "status", "plan_id", "progress_log",
|
|
155
|
+
"started_at", "updated_at", "completed_at",
|
|
156
|
+
"project_directory", "model", "upload_status",
|
|
157
|
+
"website_url", "upload_completed_at"
|
|
158
|
+
]
|
|
159
|
+
job_snapshot = {
|
|
160
|
+
k: job_info.get(k) for k in snapshot_keys
|
|
161
|
+
if job_info.get(k) is not None
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if job_info.get("status") == "completed":
|
|
165
|
+
job_snapshot["result_summary"] = {
|
|
166
|
+
"report": job_info.get("result", {}).get("report"),
|
|
167
|
+
"created_files": job_info.get("result", {}).get("created_files"),
|
|
168
|
+
}
|
|
169
|
+
if job_info.get("upload_result"):
|
|
170
|
+
job_snapshot["upload_result"] = job_info.get("upload_result")
|
|
171
|
+
|
|
172
|
+
if job_info.get("status") == "failed":
|
|
173
|
+
job_snapshot["error"] = job_info.get("error")
|
|
174
|
+
|
|
175
|
+
if job_info.get("upload_error"):
|
|
176
|
+
job_snapshot["upload_error"] = job_info.get("upload_error")
|
|
177
|
+
|
|
178
|
+
response["job"] = job_snapshot
|
|
179
|
+
|
|
180
|
+
return response
|
|
181
|
+
|
|
182
|
+
# 使用超时控制
|
|
183
|
+
result = await asyncio.wait_for(_get_progress(), timeout=timeout)
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
except asyncio.TimeoutError:
|
|
187
|
+
return {
|
|
188
|
+
"status": "error",
|
|
189
|
+
"message": f"操作超时({timeout}秒)- SSE 模式下请减少 limit 参数值"
|
|
190
|
+
}
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
return {
|
|
193
|
+
"status": "error",
|
|
194
|
+
"message": str(exc)
|
|
195
|
+
}
|
|
@@ -20,7 +20,8 @@ from typing import Any, Dict, Optional
|
|
|
20
20
|
|
|
21
21
|
# 确保项目根目录在模块搜索路径中
|
|
22
22
|
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
23
|
-
|
|
23
|
+
# 当前文件在 src/htmlgen_mcp/ 下,所以需要向上两级到项目根目录
|
|
24
|
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(CURRENT_DIR)))
|
|
24
25
|
if PROJECT_ROOT not in sys.path:
|
|
25
26
|
sys.path.insert(0, PROJECT_ROOT)
|
|
26
27
|
|
|
@@ -29,7 +30,7 @@ from fastmcp import FastMCP # type: ignore
|
|
|
29
30
|
import uuid
|
|
30
31
|
from pathlib import Path
|
|
31
32
|
|
|
32
|
-
from agents.smart_web_agent import SmartWebAgent
|
|
33
|
+
from htmlgen_mcp.agents.smart_web_agent import SmartWebAgent
|
|
33
34
|
|
|
34
35
|
DEFAULT_PROJECT_ROOT = os.path.abspath(
|
|
35
36
|
os.environ.get("WEB_AGENT_PROJECT_ROOT", os.path.join(PROJECT_ROOT, "projects"))
|
|
@@ -911,6 +912,9 @@ async def get_progress(
|
|
|
911
912
|
"""
|
|
912
913
|
|
|
913
914
|
try:
|
|
915
|
+
# SSE 模式优化:使用异步 I/O
|
|
916
|
+
loop = asyncio.get_event_loop()
|
|
917
|
+
|
|
914
918
|
if limit <= 0:
|
|
915
919
|
limit = 20
|
|
916
920
|
|
|
@@ -935,32 +939,48 @@ async def get_progress(
|
|
|
935
939
|
if resolved_path:
|
|
936
940
|
if not os.path.isabs(resolved_path):
|
|
937
941
|
candidate = os.path.join(PROJECT_ROOT, resolved_path)
|
|
938
|
-
|
|
942
|
+
# 异步检查文件存在
|
|
943
|
+
exists = await loop.run_in_executor(None, os.path.exists, candidate)
|
|
944
|
+
if exists:
|
|
939
945
|
resolved_path = candidate
|
|
940
946
|
else:
|
|
941
947
|
alt = os.path.sep + resolved_path.lstrip(os.path.sep)
|
|
942
|
-
|
|
948
|
+
exists_alt = await loop.run_in_executor(None, os.path.exists, alt)
|
|
949
|
+
if exists_alt:
|
|
943
950
|
resolved_path = alt
|
|
944
951
|
|
|
945
|
-
|
|
952
|
+
# 异步检查最终路径
|
|
953
|
+
path_exists = await loop.run_in_executor(
|
|
954
|
+
None, lambda: resolved_path and os.path.exists(resolved_path)
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
if not path_exists:
|
|
946
958
|
return {
|
|
947
959
|
"status": "error",
|
|
948
960
|
"message": "未找到进度日志,请确认 job_id/plan_id 或提供 log_path(注意绝对路径需以/开头,扩展名为 .jsonl)",
|
|
949
961
|
}
|
|
950
962
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
963
|
+
# 异步读取文件
|
|
964
|
+
def read_file():
|
|
965
|
+
events = []
|
|
966
|
+
total = 0
|
|
967
|
+
try:
|
|
968
|
+
with open(resolved_path, "r", encoding="utf-8") as f:
|
|
969
|
+
lines = f.readlines()
|
|
970
|
+
total = len(lines)
|
|
971
|
+
for line in lines[-limit:]:
|
|
972
|
+
line = line.strip()
|
|
973
|
+
if not line:
|
|
974
|
+
continue
|
|
975
|
+
try:
|
|
976
|
+
events.append(json.loads(line))
|
|
977
|
+
except Exception:
|
|
978
|
+
continue
|
|
979
|
+
except Exception:
|
|
980
|
+
pass
|
|
981
|
+
return events, total
|
|
982
|
+
|
|
983
|
+
events, total_lines = await loop.run_in_executor(None, read_file)
|
|
964
984
|
|
|
965
985
|
response: Dict[str, Any] = {
|
|
966
986
|
"status": "success",
|
|
@@ -1183,7 +1203,7 @@ async def deploy_folder_or_zip(
|
|
|
1183
1203
|
"""
|
|
1184
1204
|
try:
|
|
1185
1205
|
# 导入EdgeOne部署工具
|
|
1186
|
-
from agents.web_tools.edgeone_deploy import deploy_folder_or_zip_to_edgeone
|
|
1206
|
+
from htmlgen_mcp.agents.web_tools.edgeone_deploy import deploy_folder_or_zip_to_edgeone
|
|
1187
1207
|
|
|
1188
1208
|
# 验证环境变量
|
|
1189
1209
|
api_token = os.getenv("EDGEONE_PAGES_API_TOKEN")
|