htmlgen-mcp 0.3.1__tar.gz → 0.3.3__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.

Potentially problematic release.


This version of htmlgen-mcp might be problematic. Click here for more details.

Files changed (38) hide show
  1. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/PKG-INFO +1 -1
  2. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/pyproject.toml +1 -1
  3. htmlgen_mcp-0.3.3/src/htmlgen_mcp/agents/cluster_state.py +414 -0
  4. htmlgen_mcp-0.3.3/src/htmlgen_mcp/agents/cluster_storage.py +341 -0
  5. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/config.py +30 -9
  6. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp.egg-info/PKG-INFO +1 -1
  7. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp.egg-info/SOURCES.txt +2 -0
  8. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/README.md +0 -0
  9. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/setup.cfg +0 -0
  10. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/__init__.py +0 -0
  11. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/__init__.py +0 -0
  12. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/ai_content_generator.py +0 -0
  13. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/quick_generator.py +0 -0
  14. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/smart_web_agent.py +0 -0
  15. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/__init__.py +0 -0
  16. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/bootstrap.py +0 -0
  17. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/browser.py +0 -0
  18. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/colors.py +0 -0
  19. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/css.py +0 -0
  20. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/edgeone_deploy.py +0 -0
  21. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/html_templates.py +0 -0
  22. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/html_templates_improved.py +0 -0
  23. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/images.py +0 -0
  24. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/images_fixed.py +0 -0
  25. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/js.py +0 -0
  26. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/navigation.py +0 -0
  27. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/project.py +0 -0
  28. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/simple_builder.py +0 -0
  29. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/simple_css.py +0 -0
  30. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/simple_js.py +0 -0
  31. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/simple_templates.py +0 -0
  32. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/agents/web_tools/validation.py +0 -0
  33. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/sse_optimizations.py +0 -0
  34. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp/web_agent_server.py +0 -0
  35. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp.egg-info/dependency_links.txt +0 -0
  36. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp.egg-info/entry_points.txt +0 -0
  37. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp.egg-info/requires.txt +0 -0
  38. {htmlgen_mcp-0.3.1 → htmlgen_mcp-0.3.3}/src/htmlgen_mcp.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htmlgen-mcp
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: AI-powered HTML website generator with auto-upload functionality via Model Context Protocol
5
5
  Author-email: HTML Generator Team <contact@htmlgen-mcp.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "htmlgen-mcp"
7
- version = "0.3.1"
7
+ version = "0.3.3"
8
8
  description = "AI-powered HTML website generator with auto-upload functionality via Model Context Protocol"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 集群状态管理模块 - 支持分布式部署
5
+ """
6
+
7
+ import json
8
+ import time
9
+ from typing import Optional, List, Dict, Any
10
+ from datetime import datetime
11
+ from enum import Enum
12
+
13
+
14
+ class TaskStatus(Enum):
15
+ """任务状态枚举"""
16
+ PENDING = "pending"
17
+ RUNNING = "running"
18
+ SUCCESS = "success"
19
+ FAILED = "failed"
20
+ CANCELLED = "cancelled"
21
+
22
+
23
+ class StateManager:
24
+ """
25
+ 抽象状态管理器基类
26
+ 可以有多种实现:RedisStateManager, PostgreSQLStateManager, MongoDBStateManager
27
+ """
28
+
29
+ def create_task(self, task_id: str, user_input: str, project_directory: str, metadata: dict = None) -> bool:
30
+ """创建新任务"""
31
+ raise NotImplementedError
32
+
33
+ def update_task_status(self, task_id: str, status: TaskStatus, message: str = None) -> bool:
34
+ """更新任务状态"""
35
+ raise NotImplementedError
36
+
37
+ def get_task_status(self, task_id: str) -> Optional[dict]:
38
+ """获取任务状态"""
39
+ raise NotImplementedError
40
+
41
+ def add_created_file(self, task_id: str, file_path: str, size: int = 0) -> bool:
42
+ """记录创建的文件"""
43
+ raise NotImplementedError
44
+
45
+ def get_created_files(self, task_id: str) -> List[str]:
46
+ """获取任务创建的所有文件"""
47
+ raise NotImplementedError
48
+
49
+ def add_execution_step(self, task_id: str, step: dict) -> bool:
50
+ """添加执行步骤记录"""
51
+ raise NotImplementedError
52
+
53
+ def get_execution_history(self, task_id: str) -> List[dict]:
54
+ """获取执行历史"""
55
+ raise NotImplementedError
56
+
57
+ def acquire_lock(self, resource_id: str, timeout: int = 10) -> bool:
58
+ """获取分布式锁"""
59
+ raise NotImplementedError
60
+
61
+ def release_lock(self, resource_id: str) -> bool:
62
+ """释放分布式锁"""
63
+ raise NotImplementedError
64
+
65
+
66
+ class RedisStateManager(StateManager):
67
+ """基于 Redis 的状态管理器"""
68
+
69
+ def __init__(self, redis_url: str = "redis://localhost:6379/0"):
70
+ try:
71
+ import redis
72
+ from redis.lock import Lock
73
+ except ImportError:
74
+ raise ImportError("请安装 redis: pip install redis")
75
+
76
+ self.redis = redis.from_url(redis_url, decode_responses=True)
77
+ self.Lock = Lock
78
+
79
+ def create_task(self, task_id: str, user_input: str, project_directory: str, metadata: dict = None) -> bool:
80
+ """创建新任务"""
81
+ task_data = {
82
+ "task_id": task_id,
83
+ "user_input": user_input,
84
+ "project_directory": project_directory,
85
+ "status": TaskStatus.PENDING.value,
86
+ "created_at": datetime.now().isoformat(),
87
+ "updated_at": datetime.now().isoformat(),
88
+ "metadata": metadata or {}
89
+ }
90
+
91
+ # 使用 Hash 存储任务基本信息
92
+ self.redis.hset(f"task:{task_id}", mapping={
93
+ k: json.dumps(v) if isinstance(v, (dict, list)) else str(v)
94
+ for k, v in task_data.items()
95
+ })
96
+
97
+ # 设置过期时间(7天)
98
+ self.redis.expire(f"task:{task_id}", 7 * 24 * 3600)
99
+
100
+ return True
101
+
102
+ def update_task_status(self, task_id: str, status: TaskStatus, message: str = None) -> bool:
103
+ """更新任务状态"""
104
+ updates = {
105
+ "status": status.value,
106
+ "updated_at": datetime.now().isoformat()
107
+ }
108
+ if message:
109
+ updates["message"] = message
110
+
111
+ self.redis.hset(f"task:{task_id}", mapping=updates)
112
+ return True
113
+
114
+ def get_task_status(self, task_id: str) -> Optional[dict]:
115
+ """获取任务状态"""
116
+ data = self.redis.hgetall(f"task:{task_id}")
117
+ if not data:
118
+ return None
119
+
120
+ # 反序列化 JSON 字段
121
+ for key in ["metadata"]:
122
+ if key in data:
123
+ try:
124
+ data[key] = json.loads(data[key])
125
+ except (json.JSONDecodeError, TypeError):
126
+ pass
127
+
128
+ return data
129
+
130
+ def add_created_file(self, task_id: str, file_path: str, size: int = 0) -> bool:
131
+ """记录创建的文件"""
132
+ file_info = {
133
+ "path": file_path,
134
+ "size": size,
135
+ "created_at": datetime.now().isoformat()
136
+ }
137
+
138
+ # 使用 List 存储文件列表
139
+ self.redis.rpush(f"task:{task_id}:files", json.dumps(file_info))
140
+ self.redis.expire(f"task:{task_id}:files", 7 * 24 * 3600)
141
+
142
+ return True
143
+
144
+ def get_created_files(self, task_id: str) -> List[str]:
145
+ """获取任务创建的所有文件"""
146
+ files_json = self.redis.lrange(f"task:{task_id}:files", 0, -1)
147
+ files = []
148
+ for file_json in files_json:
149
+ try:
150
+ file_info = json.loads(file_json)
151
+ files.append(file_info["path"])
152
+ except (json.JSONDecodeError, KeyError):
153
+ continue
154
+ return files
155
+
156
+ def add_execution_step(self, task_id: str, step: dict) -> bool:
157
+ """添加执行步骤记录"""
158
+ step_data = {
159
+ **step,
160
+ "timestamp": datetime.now().isoformat()
161
+ }
162
+
163
+ # 使用 List 存储执行历史
164
+ self.redis.rpush(f"task:{task_id}:history", json.dumps(step_data))
165
+ self.redis.expire(f"task:{task_id}:history", 7 * 24 * 3600)
166
+
167
+ return True
168
+
169
+ def get_execution_history(self, task_id: str) -> List[dict]:
170
+ """获取执行历史"""
171
+ history_json = self.redis.lrange(f"task:{task_id}:history", 0, -1)
172
+ history = []
173
+ for step_json in history_json:
174
+ try:
175
+ history.append(json.loads(step_json))
176
+ except json.JSONDecodeError:
177
+ continue
178
+ return history
179
+
180
+ def acquire_lock(self, resource_id: str, timeout: int = 10) -> bool:
181
+ """获取分布式锁"""
182
+ lock = self.redis.lock(f"lock:{resource_id}", timeout=timeout)
183
+ return lock.acquire(blocking=True, blocking_timeout=timeout)
184
+
185
+ def release_lock(self, resource_id: str) -> bool:
186
+ """释放分布式锁"""
187
+ try:
188
+ lock = self.redis.lock(f"lock:{resource_id}")
189
+ lock.release()
190
+ return True
191
+ except Exception:
192
+ return False
193
+
194
+
195
+ class DatabaseStateManager(StateManager):
196
+ """基于 PostgreSQL/MySQL 的状态管理器"""
197
+
198
+ def __init__(self, db_url: str):
199
+ try:
200
+ from sqlalchemy import create_engine, Column, String, Integer, Text, DateTime, JSON
201
+ from sqlalchemy.ext.declarative import declarative_base
202
+ from sqlalchemy.orm import sessionmaker
203
+ except ImportError:
204
+ raise ImportError("请安装 sqlalchemy: pip install sqlalchemy psycopg2-binary")
205
+
206
+ self.engine = create_engine(db_url)
207
+ Base = declarative_base()
208
+
209
+ # 定义数据模型
210
+ class Task(Base):
211
+ __tablename__ = 'agent_tasks'
212
+
213
+ task_id = Column(String(64), primary_key=True)
214
+ user_input = Column(Text)
215
+ project_directory = Column(String(512))
216
+ status = Column(String(20))
217
+ message = Column(Text, nullable=True)
218
+ metadata = Column(JSON, nullable=True)
219
+ created_at = Column(DateTime, default=datetime.now)
220
+ updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
221
+
222
+ class CreatedFile(Base):
223
+ __tablename__ = 'agent_files'
224
+
225
+ id = Column(Integer, primary_key=True, autoincrement=True)
226
+ task_id = Column(String(64), index=True)
227
+ file_path = Column(String(1024))
228
+ size = Column(Integer, default=0)
229
+ created_at = Column(DateTime, default=datetime.now)
230
+
231
+ class ExecutionStep(Base):
232
+ __tablename__ = 'agent_execution_history'
233
+
234
+ id = Column(Integer, primary_key=True, autoincrement=True)
235
+ task_id = Column(String(64), index=True)
236
+ step = Column(Integer)
237
+ tool = Column(String(100))
238
+ status = Column(String(20))
239
+ result = Column(Text)
240
+ duration = Column(Integer, default=0)
241
+ created_at = Column(DateTime, default=datetime.now)
242
+
243
+ Base.metadata.create_all(self.engine)
244
+
245
+ self.Session = sessionmaker(bind=self.engine)
246
+ self.Task = Task
247
+ self.CreatedFile = CreatedFile
248
+ self.ExecutionStep = ExecutionStep
249
+
250
+ def create_task(self, task_id: str, user_input: str, project_directory: str, metadata: dict = None) -> bool:
251
+ """创建新任务"""
252
+ session = self.Session()
253
+ try:
254
+ task = self.Task(
255
+ task_id=task_id,
256
+ user_input=user_input,
257
+ project_directory=project_directory,
258
+ status=TaskStatus.PENDING.value,
259
+ metadata=metadata
260
+ )
261
+ session.add(task)
262
+ session.commit()
263
+ return True
264
+ except Exception as e:
265
+ session.rollback()
266
+ print(f"创建任务失败: {e}")
267
+ return False
268
+ finally:
269
+ session.close()
270
+
271
+ def update_task_status(self, task_id: str, status: TaskStatus, message: str = None) -> bool:
272
+ """更新任务状态"""
273
+ session = self.Session()
274
+ try:
275
+ task = session.query(self.Task).filter_by(task_id=task_id).first()
276
+ if task:
277
+ task.status = status.value
278
+ task.updated_at = datetime.now()
279
+ if message:
280
+ task.message = message
281
+ session.commit()
282
+ return True
283
+ return False
284
+ except Exception as e:
285
+ session.rollback()
286
+ print(f"更新任务状态失败: {e}")
287
+ return False
288
+ finally:
289
+ session.close()
290
+
291
+ def get_task_status(self, task_id: str) -> Optional[dict]:
292
+ """获取任务状态"""
293
+ session = self.Session()
294
+ try:
295
+ task = session.query(self.Task).filter_by(task_id=task_id).first()
296
+ if not task:
297
+ return None
298
+
299
+ return {
300
+ "task_id": task.task_id,
301
+ "user_input": task.user_input,
302
+ "project_directory": task.project_directory,
303
+ "status": task.status,
304
+ "message": task.message,
305
+ "metadata": task.metadata,
306
+ "created_at": task.created_at.isoformat() if task.created_at else None,
307
+ "updated_at": task.updated_at.isoformat() if task.updated_at else None
308
+ }
309
+ finally:
310
+ session.close()
311
+
312
+ def add_created_file(self, task_id: str, file_path: str, size: int = 0) -> bool:
313
+ """记录创建的文件"""
314
+ session = self.Session()
315
+ try:
316
+ file_record = self.CreatedFile(
317
+ task_id=task_id,
318
+ file_path=file_path,
319
+ size=size
320
+ )
321
+ session.add(file_record)
322
+ session.commit()
323
+ return True
324
+ except Exception as e:
325
+ session.rollback()
326
+ print(f"记录文件失败: {e}")
327
+ return False
328
+ finally:
329
+ session.close()
330
+
331
+ def get_created_files(self, task_id: str) -> List[str]:
332
+ """获取任务创建的所有文件"""
333
+ session = self.Session()
334
+ try:
335
+ files = session.query(self.CreatedFile).filter_by(task_id=task_id).all()
336
+ return [f.file_path for f in files]
337
+ finally:
338
+ session.close()
339
+
340
+ def add_execution_step(self, task_id: str, step: dict) -> bool:
341
+ """添加执行步骤记录"""
342
+ session = self.Session()
343
+ try:
344
+ step_record = self.ExecutionStep(
345
+ task_id=task_id,
346
+ step=step.get("step", 0),
347
+ tool=step.get("tool", ""),
348
+ status=step.get("status", ""),
349
+ result=json.dumps(step.get("result", "")),
350
+ duration=step.get("duration", 0)
351
+ )
352
+ session.add(step_record)
353
+ session.commit()
354
+ return True
355
+ except Exception as e:
356
+ session.rollback()
357
+ print(f"记录执行步骤失败: {e}")
358
+ return False
359
+ finally:
360
+ session.close()
361
+
362
+ def get_execution_history(self, task_id: str) -> List[dict]:
363
+ """获取执行历史"""
364
+ session = self.Session()
365
+ try:
366
+ steps = session.query(self.ExecutionStep).filter_by(task_id=task_id).order_by(self.ExecutionStep.step).all()
367
+ return [
368
+ {
369
+ "step": s.step,
370
+ "tool": s.tool,
371
+ "status": s.status,
372
+ "result": s.result,
373
+ "duration": s.duration,
374
+ "created_at": s.created_at.isoformat() if s.created_at else None
375
+ }
376
+ for s in steps
377
+ ]
378
+ finally:
379
+ session.close()
380
+
381
+ def acquire_lock(self, resource_id: str, timeout: int = 10) -> bool:
382
+ """获取分布式锁(需要配合数据库锁实现)"""
383
+ # PostgreSQL 可以使用 advisory lock
384
+ # MySQL 可以使用 GET_LOCK
385
+ # 这里简化实现,实际应使用数据库特定的锁机制
386
+ return True
387
+
388
+ def release_lock(self, resource_id: str) -> bool:
389
+ """释放分布式锁"""
390
+ return True
391
+
392
+
393
+ def create_state_manager(backend: str = "redis", connection_string: str = None) -> StateManager:
394
+ """
395
+ 工厂函数:根据配置创建状态管理器
396
+
397
+ Args:
398
+ backend: 后端类型,支持 'redis', 'postgresql', 'mysql'
399
+ connection_string: 连接字符串
400
+ - Redis: redis://localhost:6379/0
401
+ - PostgreSQL: postgresql://user:pass@localhost/dbname
402
+ - MySQL: mysql://user:pass@localhost/dbname
403
+
404
+ Returns:
405
+ StateManager 实例
406
+ """
407
+ if backend == "redis":
408
+ return RedisStateManager(connection_string or "redis://localhost:6379/0")
409
+ elif backend in ["postgresql", "mysql"]:
410
+ if not connection_string:
411
+ raise ValueError(f"{backend} 需要提供 connection_string")
412
+ return DatabaseStateManager(connection_string)
413
+ else:
414
+ raise ValueError(f"不支持的后端类型: {backend}")
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 集群文件存储模块 - 支持对象存储
5
+ """
6
+
7
+ import os
8
+ import mimetypes
9
+ from pathlib import Path
10
+ from typing import Optional, List
11
+
12
+
13
+ class StorageBackend:
14
+ """存储后端抽象基类"""
15
+
16
+ def upload_file(self, local_path: str, remote_path: str) -> str:
17
+ """
18
+ 上传文件到存储
19
+
20
+ Args:
21
+ local_path: 本地文件路径
22
+ remote_path: 远程存储路径
23
+
24
+ Returns:
25
+ 可访问的 URL
26
+ """
27
+ raise NotImplementedError
28
+
29
+ def download_file(self, remote_path: str, local_path: str) -> bool:
30
+ """下载文件到本地"""
31
+ raise NotImplementedError
32
+
33
+ def delete_file(self, remote_path: str) -> bool:
34
+ """删除文件"""
35
+ raise NotImplementedError
36
+
37
+ def list_files(self, prefix: str) -> List[str]:
38
+ """列出指定前缀的所有文件"""
39
+ raise NotImplementedError
40
+
41
+ def get_url(self, remote_path: str, expires: int = 3600) -> str:
42
+ """获取文件访问URL"""
43
+ raise NotImplementedError
44
+
45
+
46
+ class LocalStorageBackend(StorageBackend):
47
+ """
48
+ 本地文件系统存储(开发/单机环境)
49
+ 适用于开发测试,生产环境请使用对象存储
50
+ """
51
+
52
+ def __init__(self, base_dir: str = "/tmp/agent_storage"):
53
+ self.base_dir = Path(base_dir)
54
+ self.base_dir.mkdir(parents=True, exist_ok=True)
55
+
56
+ def upload_file(self, local_path: str, remote_path: str) -> str:
57
+ """复制文件到存储目录"""
58
+ target_path = self.base_dir / remote_path
59
+ target_path.parent.mkdir(parents=True, exist_ok=True)
60
+
61
+ import shutil
62
+ shutil.copy2(local_path, target_path)
63
+
64
+ return f"file://{target_path}"
65
+
66
+ def download_file(self, remote_path: str, local_path: str) -> bool:
67
+ """从存储目录复制文件"""
68
+ source_path = self.base_dir / remote_path
69
+ if not source_path.exists():
70
+ return False
71
+
72
+ import shutil
73
+ Path(local_path).parent.mkdir(parents=True, exist_ok=True)
74
+ shutil.copy2(source_path, local_path)
75
+ return True
76
+
77
+ def delete_file(self, remote_path: str) -> bool:
78
+ """删除文件"""
79
+ target_path = self.base_dir / remote_path
80
+ if target_path.exists():
81
+ target_path.unlink()
82
+ return True
83
+ return False
84
+
85
+ def list_files(self, prefix: str) -> List[str]:
86
+ """列出文件"""
87
+ prefix_path = self.base_dir / prefix
88
+ if not prefix_path.exists():
89
+ return []
90
+
91
+ files = []
92
+ for file_path in prefix_path.rglob("*"):
93
+ if file_path.is_file():
94
+ rel_path = file_path.relative_to(self.base_dir)
95
+ files.append(str(rel_path))
96
+ return files
97
+
98
+ def get_url(self, remote_path: str, expires: int = 3600) -> str:
99
+ """获取文件URL"""
100
+ target_path = self.base_dir / remote_path
101
+ return f"file://{target_path}"
102
+
103
+
104
+ class OSSStorageBackend(StorageBackend):
105
+ """阿里云 OSS 存储"""
106
+
107
+ def __init__(self, access_key: str, secret_key: str, endpoint: str, bucket: str):
108
+ try:
109
+ import oss2
110
+ except ImportError:
111
+ raise ImportError("请安装 oss2: pip install oss2")
112
+
113
+ auth = oss2.Auth(access_key, secret_key)
114
+ self.bucket = oss2.Bucket(auth, endpoint, bucket)
115
+ self.bucket_name = bucket
116
+ self.endpoint = endpoint
117
+
118
+ def upload_file(self, local_path: str, remote_path: str) -> str:
119
+ """上传文件到 OSS"""
120
+ # 自动检测 Content-Type
121
+ content_type, _ = mimetypes.guess_type(local_path)
122
+
123
+ headers = {}
124
+ if content_type:
125
+ headers['Content-Type'] = content_type
126
+
127
+ self.bucket.put_object_from_file(remote_path, local_path, headers=headers)
128
+
129
+ return f"https://{self.bucket_name}.{self.endpoint}/{remote_path}"
130
+
131
+ def download_file(self, remote_path: str, local_path: str) -> bool:
132
+ """从 OSS 下载文件"""
133
+ try:
134
+ Path(local_path).parent.mkdir(parents=True, exist_ok=True)
135
+ self.bucket.get_object_to_file(remote_path, local_path)
136
+ return True
137
+ except Exception:
138
+ return False
139
+
140
+ def delete_file(self, remote_path: str) -> bool:
141
+ """删除 OSS 文件"""
142
+ try:
143
+ self.bucket.delete_object(remote_path)
144
+ return True
145
+ except Exception:
146
+ return False
147
+
148
+ def list_files(self, prefix: str) -> List[str]:
149
+ """列出 OSS 文件"""
150
+ files = []
151
+ for obj in oss2.ObjectIterator(self.bucket, prefix=prefix):
152
+ files.append(obj.key)
153
+ return files
154
+
155
+ def get_url(self, remote_path: str, expires: int = 3600) -> str:
156
+ """获取 OSS 签名 URL"""
157
+ return self.bucket.sign_url('GET', remote_path, expires)
158
+
159
+
160
+ class S3StorageBackend(StorageBackend):
161
+ """AWS S3 / MinIO 存储"""
162
+
163
+ def __init__(self, access_key: str, secret_key: str, endpoint: str, bucket: str, region: str = "us-east-1"):
164
+ try:
165
+ import boto3
166
+ except ImportError:
167
+ raise ImportError("请安装 boto3: pip install boto3")
168
+
169
+ self.s3_client = boto3.client(
170
+ 's3',
171
+ aws_access_key_id=access_key,
172
+ aws_secret_access_key=secret_key,
173
+ endpoint_url=endpoint if endpoint else None,
174
+ region_name=region
175
+ )
176
+ self.bucket = bucket
177
+ self.endpoint = endpoint
178
+
179
+ def upload_file(self, local_path: str, remote_path: str) -> str:
180
+ """上传文件到 S3"""
181
+ content_type, _ = mimetypes.guess_type(local_path)
182
+
183
+ extra_args = {}
184
+ if content_type:
185
+ extra_args['ContentType'] = content_type
186
+
187
+ self.s3_client.upload_file(local_path, self.bucket, remote_path, ExtraArgs=extra_args)
188
+
189
+ if self.endpoint:
190
+ return f"{self.endpoint}/{self.bucket}/{remote_path}"
191
+ return f"https://{self.bucket}.s3.amazonaws.com/{remote_path}"
192
+
193
+ def download_file(self, remote_path: str, local_path: str) -> bool:
194
+ """从 S3 下载文件"""
195
+ try:
196
+ Path(local_path).parent.mkdir(parents=True, exist_ok=True)
197
+ self.s3_client.download_file(self.bucket, remote_path, local_path)
198
+ return True
199
+ except Exception:
200
+ return False
201
+
202
+ def delete_file(self, remote_path: str) -> bool:
203
+ """删除 S3 文件"""
204
+ try:
205
+ self.s3_client.delete_object(Bucket=self.bucket, Key=remote_path)
206
+ return True
207
+ except Exception:
208
+ return False
209
+
210
+ def list_files(self, prefix: str) -> List[str]:
211
+ """列出 S3 文件"""
212
+ files = []
213
+ paginator = self.s3_client.get_paginator('list_objects_v2')
214
+ for page in paginator.paginate(Bucket=self.bucket, Prefix=prefix):
215
+ if 'Contents' in page:
216
+ for obj in page['Contents']:
217
+ files.append(obj['Key'])
218
+ return files
219
+
220
+ def get_url(self, remote_path: str, expires: int = 3600) -> str:
221
+ """获取 S3 签名 URL"""
222
+ return self.s3_client.generate_presigned_url(
223
+ 'get_object',
224
+ Params={'Bucket': self.bucket, 'Key': remote_path},
225
+ ExpiresIn=expires
226
+ )
227
+
228
+
229
+ class COSStorageBackend(StorageBackend):
230
+ """腾讯云 COS 存储"""
231
+
232
+ def __init__(self, secret_id: str, secret_key: str, region: str, bucket: str):
233
+ try:
234
+ from qcloud_cos import CosConfig, CosS3Client
235
+ except ImportError:
236
+ raise ImportError("请安装 cos-python-sdk-v5: pip install cos-python-sdk-v5")
237
+
238
+ config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key)
239
+ self.client = CosS3Client(config)
240
+ self.bucket = bucket
241
+ self.region = region
242
+
243
+ def upload_file(self, local_path: str, remote_path: str) -> str:
244
+ """上传文件到 COS"""
245
+ content_type, _ = mimetypes.guess_type(local_path)
246
+
247
+ with open(local_path, 'rb') as f:
248
+ self.client.put_object(
249
+ Bucket=self.bucket,
250
+ Body=f,
251
+ Key=remote_path,
252
+ ContentType=content_type or 'application/octet-stream'
253
+ )
254
+
255
+ return f"https://{self.bucket}.cos.{self.region}.myqcloud.com/{remote_path}"
256
+
257
+ def download_file(self, remote_path: str, local_path: str) -> bool:
258
+ """从 COS 下载文件"""
259
+ try:
260
+ Path(local_path).parent.mkdir(parents=True, exist_ok=True)
261
+ response = self.client.get_object(Bucket=self.bucket, Key=remote_path)
262
+ response['Body'].get_stream_to_file(local_path)
263
+ return True
264
+ except Exception:
265
+ return False
266
+
267
+ def delete_file(self, remote_path: str) -> bool:
268
+ """删除 COS 文件"""
269
+ try:
270
+ self.client.delete_object(Bucket=self.bucket, Key=remote_path)
271
+ return True
272
+ except Exception:
273
+ return False
274
+
275
+ def list_files(self, prefix: str) -> List[str]:
276
+ """列出 COS 文件"""
277
+ files = []
278
+ marker = ""
279
+ while True:
280
+ response = self.client.list_objects(Bucket=self.bucket, Prefix=prefix, Marker=marker)
281
+ if 'Contents' in response:
282
+ for obj in response['Contents']:
283
+ files.append(obj['Key'])
284
+ if response['IsTruncated'] == 'false':
285
+ break
286
+ marker = response['NextMarker']
287
+ return files
288
+
289
+ def get_url(self, remote_path: str, expires: int = 3600) -> str:
290
+ """获取 COS 签名 URL"""
291
+ return self.client.get_presigned_download_url(
292
+ Bucket=self.bucket,
293
+ Key=remote_path,
294
+ Expired=expires
295
+ )
296
+
297
+
298
+ def create_storage_backend(
299
+ backend_type: str = "local",
300
+ **kwargs
301
+ ) -> StorageBackend:
302
+ """
303
+ 工厂函数:创建存储后端
304
+
305
+ Args:
306
+ backend_type: 后端类型 'local', 'oss', 's3', 'cos'
307
+ **kwargs: 各后端的配置参数
308
+ - local: base_dir
309
+ - oss: access_key, secret_key, endpoint, bucket
310
+ - s3: access_key, secret_key, endpoint, bucket, region
311
+ - cos: secret_id, secret_key, region, bucket
312
+
313
+ Returns:
314
+ StorageBackend 实例
315
+ """
316
+ if backend_type == "local":
317
+ return LocalStorageBackend(kwargs.get("base_dir", "/tmp/agent_storage"))
318
+ elif backend_type == "oss":
319
+ return OSSStorageBackend(
320
+ access_key=kwargs["access_key"],
321
+ secret_key=kwargs["secret_key"],
322
+ endpoint=kwargs["endpoint"],
323
+ bucket=kwargs["bucket"]
324
+ )
325
+ elif backend_type == "s3":
326
+ return S3StorageBackend(
327
+ access_key=kwargs["access_key"],
328
+ secret_key=kwargs["secret_key"],
329
+ endpoint=kwargs.get("endpoint"),
330
+ bucket=kwargs["bucket"],
331
+ region=kwargs.get("region", "us-east-1")
332
+ )
333
+ elif backend_type == "cos":
334
+ return COSStorageBackend(
335
+ secret_id=kwargs["secret_id"],
336
+ secret_key=kwargs["secret_key"],
337
+ region=kwargs["region"],
338
+ bucket=kwargs["bucket"]
339
+ )
340
+ else:
341
+ raise ValueError(f"不支持的存储后端: {backend_type}")
@@ -1,4 +1,4 @@
1
- """项目配置管理 - 跨平台支持"""
1
+ """项目配置管理 - 跨平台支持与集群部署优化"""
2
2
  import os
3
3
  import sys
4
4
  import platform
@@ -8,7 +8,7 @@ from datetime import datetime
8
8
 
9
9
 
10
10
  class ProjectConfig:
11
- """项目配置管理器 - 支持 Windows/macOS/Linux"""
11
+ """项目配置管理器 - 支持 Windows/macOS/Linux/集群部署"""
12
12
 
13
13
  @staticmethod
14
14
  def get_system_info() -> dict:
@@ -22,16 +22,37 @@ class ProjectConfig:
22
22
 
23
23
  @staticmethod
24
24
  def get_default_output_dir() -> Path:
25
- """获取默认的项目输出目录(跨平台)
25
+ """获取默认的项目输出目录(跨平台 + 集群部署)
26
26
 
27
- 优先级:
28
- 1. 环境变量 WEB_AGENT_OUTPUT_DIR
29
- 2. 系统特定的文档目录
30
- 3. 用户主目录下的隐藏目录
31
- 4. 系统临时目录
27
+ 优先级(集群部署优化):
28
+ 1. 环境变量 WEB_AGENT_PROJECT_ROOT(集群共享存储,最高优先级)
29
+ 2. 环境变量 WEB_AGENT_OUTPUT_DIR(兼容旧配置)
30
+ 3. 系统特定的文档目录
31
+ 4. 用户主目录下的隐藏目录
32
+ 5. 系统临时目录
33
+
34
+ 集群部署说明:
35
+ - 在 SSE 集群中,建议配置 WEB_AGENT_PROJECT_ROOT 为 NAS 共享路径
36
+ - 示例:WEB_AGENT_PROJECT_ROOT=/app/mcp-servers/mcp-servers/html_agent/projects
32
37
  """
33
38
 
34
- # 1. 检查环境变量(所有平台通用)
39
+ # 1a. 检查集群共享存储环境变量(最高优先级)
40
+ project_root = os.environ.get('WEB_AGENT_PROJECT_ROOT')
41
+ if project_root:
42
+ output_dir = Path(project_root)
43
+ try:
44
+ output_dir.mkdir(parents=True, exist_ok=True)
45
+ # 验证目录可写
46
+ test_file = output_dir / '.write_test'
47
+ test_file.touch()
48
+ test_file.unlink()
49
+ print(f"✅ 使用集群共享存储: {output_dir}")
50
+ return output_dir
51
+ except Exception as e:
52
+ print(f"⚠️ 集群共享存储不可用 ({output_dir}): {e}")
53
+ print(f"💡 请检查 NAS 挂载状态和权限")
54
+
55
+ # 1b. 检查兼容性环境变量
35
56
  env_dir = os.environ.get('WEB_AGENT_OUTPUT_DIR')
36
57
  if env_dir:
37
58
  output_dir = Path(env_dir)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htmlgen-mcp
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: AI-powered HTML website generator with auto-upload functionality via Model Context Protocol
5
5
  Author-email: HTML Generator Team <contact@htmlgen-mcp.com>
6
6
  License: MIT
@@ -12,6 +12,8 @@ src/htmlgen_mcp.egg-info/requires.txt
12
12
  src/htmlgen_mcp.egg-info/top_level.txt
13
13
  src/htmlgen_mcp/agents/__init__.py
14
14
  src/htmlgen_mcp/agents/ai_content_generator.py
15
+ src/htmlgen_mcp/agents/cluster_state.py
16
+ src/htmlgen_mcp/agents/cluster_storage.py
15
17
  src/htmlgen_mcp/agents/quick_generator.py
16
18
  src/htmlgen_mcp/agents/smart_web_agent.py
17
19
  src/htmlgen_mcp/agents/web_tools/__init__.py
File without changes
File without changes