htmlgen-mcp 0.3.4__py3-none-any.whl → 0.3.6__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.

@@ -0,0 +1,262 @@
1
+ """上下文感知的执行器 - 确保 context_content 被正确使用"""
2
+ import json
3
+ from typing import Dict, Any, Optional
4
+
5
+
6
+ class ContextAwareExecutor:
7
+ """确保上下文内容在执行过程中被正确传递和使用"""
8
+
9
+ @staticmethod
10
+ def inject_context_to_tools(plan: Dict[str, Any], context_content: str) -> Dict[str, Any]:
11
+ """
12
+ 将上下文内容注入到工具调用序列中
13
+
14
+ Args:
15
+ plan: 执行计划
16
+ context_content: 上下文内容(如咖啡馆列表)
17
+
18
+ Returns:
19
+ 修改后的计划
20
+ """
21
+ if not context_content:
22
+ return plan
23
+
24
+ # 获取工具序列
25
+ tools_sequence = plan.get("tools_sequence", [])
26
+
27
+ # 遍历每个工具调用,注入上下文
28
+ for tool in tools_sequence:
29
+ tool_name = tool.get("tool", "")
30
+
31
+ # 对于创建 HTML 文件的工具,注入具体内容
32
+ if tool_name == "create_html_file":
33
+ # 修改内容参数,确保包含实际数据
34
+ original_content = tool.get("parameters", {}).get("content", "")
35
+
36
+ # 如果是主页面,替换内容
37
+ if "index.html" in tool.get("parameters", {}).get("file_name", ""):
38
+ tool["parameters"]["content"] = generate_html_with_context(
39
+ context_content,
40
+ tool.get("parameters", {}).get("title", "网站")
41
+ )
42
+
43
+ # 对于创建内容的工具,也要注入上下文
44
+ elif tool_name in ["add_content_section", "create_content", "add_text_content"]:
45
+ # 确保内容参数包含实际数据
46
+ tool["parameters"]["context_data"] = context_content
47
+
48
+ return plan
49
+
50
+ @staticmethod
51
+ def enhance_prompt_with_context(prompt: str, context_content: str) -> str:
52
+ """
53
+ 增强提示词,明确要求使用上下文内容
54
+
55
+ Args:
56
+ prompt: 原始提示词
57
+ context_content: 上下文内容
58
+
59
+ Returns:
60
+ 增强后的提示词
61
+ """
62
+ if not context_content:
63
+ return prompt
64
+
65
+ enhanced = f"""
66
+ {prompt}
67
+
68
+ 【重要:必须使用以下具体数据】
69
+ 以下是必须在网页中展示的实际内容,请完整地将这些信息整合到网页中:
70
+
71
+ {context_content}
72
+
73
+ 要求:
74
+ 1. 必须将上述所有咖啡馆信息完整展示在网页中
75
+ 2. 使用卡片或列表形式展示每个咖啡馆
76
+ 3. 包含咖啡馆名称和地址
77
+ 4. 可以添加适当的样式和布局,但内容必须准确
78
+ 5. 不要生成虚构的内容,只使用提供的实际数据
79
+ """
80
+ return enhanced
81
+
82
+
83
+ def generate_html_with_context(context_content: str, title: str = "咖啡馆指南") -> str:
84
+ """
85
+ 根据上下文内容生成 HTML
86
+
87
+ Args:
88
+ context_content: 咖啡馆列表等具体内容
89
+ title: 网站标题
90
+
91
+ Returns:
92
+ 完整的 HTML 内容
93
+ """
94
+ # 解析咖啡馆信息
95
+ cafes = []
96
+ lines = context_content.split('\n')
97
+ current_cafe = {}
98
+
99
+ for line in lines:
100
+ line = line.strip()
101
+ if not line:
102
+ continue
103
+
104
+ # 识别咖啡馆名称(带序号的行)
105
+ if line[0].isdigit() and '. ' in line:
106
+ if current_cafe:
107
+ cafes.append(current_cafe)
108
+ # 提取咖啡馆名称
109
+ name = line.split('. ', 1)[1] if '. ' in line else line
110
+ current_cafe = {'name': name, 'address': ''}
111
+
112
+ # 识别地址(- 开头的行)
113
+ elif line.startswith('- 地址:'):
114
+ if current_cafe:
115
+ current_cafe['address'] = line.replace('- 地址:', '').strip()
116
+
117
+ # 添加最后一个咖啡馆
118
+ if current_cafe:
119
+ cafes.append(current_cafe)
120
+
121
+ # 生成 HTML
122
+ cafe_cards = []
123
+ for cafe in cafes:
124
+ card = f'''
125
+ <div class="col-md-6 col-lg-4 mb-4">
126
+ <div class="card h-100 shadow-sm">
127
+ <div class="card-body">
128
+ <h5 class="card-title">
129
+ <i class="bi bi-cup-hot-fill text-brown"></i> {cafe['name']}
130
+ </h5>
131
+ <p class="card-text">
132
+ <i class="bi bi-geo-alt-fill text-muted"></i>
133
+ <span class="text-muted">{cafe['address']}</span>
134
+ </p>
135
+ <a href="#" class="btn btn-outline-primary btn-sm">
136
+ <i class="bi bi-map"></i> 查看地图
137
+ </a>
138
+ </div>
139
+ </div>
140
+ </div>'''
141
+ cafe_cards.append(card)
142
+
143
+ html = f'''<!DOCTYPE html>
144
+ <html lang="zh-CN">
145
+ <head>
146
+ <meta charset="UTF-8">
147
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
148
+ <title>{title}</title>
149
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
150
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
151
+ <link href="assets/css/style.css" rel="stylesheet">
152
+ <style>
153
+ .text-brown {{ color: #6f4e37; }}
154
+ .hero-section {{
155
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
156
+ color: white;
157
+ padding: 80px 0;
158
+ }}
159
+ .card {{
160
+ transition: transform 0.3s;
161
+ border-radius: 15px;
162
+ }}
163
+ .card:hover {{
164
+ transform: translateY(-5px);
165
+ }}
166
+ </style>
167
+ </head>
168
+ <body>
169
+ <!-- 导航栏 -->
170
+ <nav class="navbar navbar-expand-lg navbar-light bg-light shadow-sm">
171
+ <div class="container">
172
+ <a class="navbar-brand" href="#">
173
+ <i class="bi bi-cup-hot-fill text-brown"></i> {title}
174
+ </a>
175
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
176
+ <span class="navbar-toggler-icon"></span>
177
+ </button>
178
+ <div class="collapse navbar-collapse" id="navbarNav">
179
+ <ul class="navbar-nav ms-auto">
180
+ <li class="nav-item">
181
+ <a class="nav-link" href="#cafes">咖啡馆列表</a>
182
+ </li>
183
+ <li class="nav-item">
184
+ <a class="nav-link" href="#about">关于</a>
185
+ </li>
186
+ </ul>
187
+ </div>
188
+ </div>
189
+ </nav>
190
+
191
+ <!-- Hero 区域 -->
192
+ <section class="hero-section text-center">
193
+ <div class="container">
194
+ <h1 class="display-4 fw-bold mb-4">{title}</h1>
195
+ <p class="lead mb-4">探索大族广场周边的精品咖啡馆</p>
196
+ <p class="mb-4">共收录 {len(cafes)} 家咖啡馆,均在步行范围内</p>
197
+ <a href="#cafes" class="btn btn-light btn-lg">
198
+ <i class="bi bi-arrow-down-circle"></i> 浏览咖啡馆
199
+ </a>
200
+ </div>
201
+ </section>
202
+
203
+ <!-- 咖啡馆列表 -->
204
+ <section id="cafes" class="py-5">
205
+ <div class="container">
206
+ <h2 class="text-center mb-5">咖啡馆列表</h2>
207
+ <div class="row">
208
+ {"".join(cafe_cards)}
209
+ </div>
210
+ </div>
211
+ </section>
212
+
213
+ <!-- 关于区域 -->
214
+ <section id="about" class="py-5 bg-light">
215
+ <div class="container">
216
+ <div class="row align-items-center">
217
+ <div class="col-md-6">
218
+ <h2>关于本指南</h2>
219
+ <p class="lead">为您精选大族广场附近的咖啡馆</p>
220
+ <p>本指南收录了北京市大兴区大族广场周边1公里范围内的所有知名咖啡馆,包括星巴克、Peet's Coffee、M Stand等品牌,以及特色独立咖啡馆。</p>
221
+ <p>无论您是需要商务洽谈的安静环境,还是休闲放松的舒适空间,都能在这里找到合适的选择。</p>
222
+ </div>
223
+ <div class="col-md-6">
224
+ <div class="p-4 bg-white rounded shadow">
225
+ <h4><i class="bi bi-info-circle-fill text-primary"></i> 实用信息</h4>
226
+ <ul class="list-unstyled mt-3">
227
+ <li class="mb-2">
228
+ <i class="bi bi-geo-alt text-muted"></i>
229
+ 位置:北京市大兴区荣华南路
230
+ </li>
231
+ <li class="mb-2">
232
+ <i class="bi bi-clock text-muted"></i>
233
+ 营业时间:大部分 7:00-22:00
234
+ </li>
235
+ <li class="mb-2">
236
+ <i class="bi bi-wifi text-muted"></i>
237
+ 设施:WiFi、充电插座
238
+ </li>
239
+ <li class="mb-2">
240
+ <i class="bi bi-car-front text-muted"></i>
241
+ 交通:地铁亦庄线荣昌东街站
242
+ </li>
243
+ </ul>
244
+ </div>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ </section>
249
+
250
+ <!-- 页脚 -->
251
+ <footer class="bg-dark text-white py-4 mt-5">
252
+ <div class="container text-center">
253
+ <p class="mb-0">© 2024 {title}. 信息仅供参考,请以实际为准。</p>
254
+ </div>
255
+ </footer>
256
+
257
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
258
+ <script src="assets/js/main.js"></script>
259
+ </body>
260
+ </html>'''
261
+
262
+ return html
@@ -0,0 +1,392 @@
1
+ """改进的进度日志管理 - 解决集群环境下的查询问题"""
2
+ import json
3
+ import time
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Dict, Optional, Any, List
7
+ from datetime import datetime
8
+ import threading
9
+ import fcntl
10
+
11
+
12
+ class ImprovedProgressManager:
13
+ """改进的进度管理器 - 支持 job_id 和 plan_id 查询"""
14
+
15
+ def __init__(self, nas_base_path: str = "/app/mcp-servers/mcp-servers/html_agent"):
16
+ self.nas_base = Path(nas_base_path)
17
+ self.progress_base = self.nas_base / "mcp_data" / "make_web"
18
+
19
+ # 分离不同类型的存储
20
+ self.jobs_dir = self.progress_base / "jobs"
21
+ self.plans_dir = self.progress_base / "plans"
22
+ self.logs_dir = self.progress_base / "logs"
23
+ self.mappings_dir = self.progress_base / "mappings"
24
+
25
+ # 创建所有必要的目录
26
+ for dir_path in [self.jobs_dir, self.plans_dir, self.logs_dir, self.mappings_dir]:
27
+ dir_path.mkdir(parents=True, exist_ok=True)
28
+
29
+ # 内存缓存,减少文件 I/O
30
+ self._cache = {}
31
+ self._cache_lock = threading.Lock()
32
+
33
+ def register_job(self, job_id: str, plan_id: Optional[str] = None,
34
+ description: str = "", project_path: str = "") -> str:
35
+ """
36
+ 注册新任务
37
+
38
+ Args:
39
+ job_id: 任务 ID
40
+ plan_id: 关联的计划 ID(可选)
41
+ description: 任务描述
42
+ project_path: 项目路径
43
+
44
+ Returns:
45
+ 进度日志文件路径
46
+ """
47
+ # 生成日志文件路径
48
+ log_file = self.logs_dir / f"{job_id}.jsonl"
49
+
50
+ # 创建任务信息
51
+ job_info = {
52
+ "job_id": job_id,
53
+ "plan_id": plan_id,
54
+ "description": description,
55
+ "project_path": project_path,
56
+ "log_file": str(log_file),
57
+ "status": "pending",
58
+ "created_at": datetime.now().isoformat(),
59
+ "node_id": os.environ.get("NODE_ID", "unknown"),
60
+ "updated_at": datetime.now().isoformat()
61
+ }
62
+
63
+ # 保存任务信息
64
+ job_file = self.jobs_dir / f"{job_id}.json"
65
+ self._safe_write_json(job_file, job_info)
66
+
67
+ # 如果有 plan_id,创建映射
68
+ if plan_id:
69
+ self._create_mapping(plan_id, job_id, "plan_to_job")
70
+ self._create_mapping(job_id, plan_id, "job_to_plan")
71
+
72
+ # 创建日志文件映射
73
+ self._create_mapping(job_id, str(log_file), "job_to_log")
74
+ if plan_id:
75
+ self._create_mapping(plan_id, str(log_file), "plan_to_log")
76
+
77
+ # 更新缓存
78
+ with self._cache_lock:
79
+ self._cache[f"job:{job_id}"] = job_info
80
+ if plan_id:
81
+ self._cache[f"plan:{plan_id}:job"] = job_id
82
+ self._cache[f"plan:{plan_id}:log"] = str(log_file)
83
+
84
+ return str(log_file)
85
+
86
+ def find_log_path(self, identifier: str) -> Optional[str]:
87
+ """
88
+ 根据 job_id 或 plan_id 查找日志文件路径
89
+
90
+ Args:
91
+ identifier: job_id 或 plan_id
92
+
93
+ Returns:
94
+ 日志文件路径,如果找不到返回 None
95
+ """
96
+ # 先检查缓存
97
+ with self._cache_lock:
98
+ # 尝试作为 job_id 查找
99
+ cached_job = self._cache.get(f"job:{identifier}")
100
+ if cached_job:
101
+ return cached_job.get("log_file")
102
+
103
+ # 尝试作为 plan_id 查找
104
+ cached_log = self._cache.get(f"plan:{identifier}:log")
105
+ if cached_log:
106
+ return cached_log
107
+
108
+ # 方法1: 直接查找日志文件
109
+ direct_log = self.logs_dir / f"{identifier}.jsonl"
110
+ if direct_log.exists():
111
+ return str(direct_log)
112
+
113
+ # 方法2: 从任务信息中查找
114
+ job_file = self.jobs_dir / f"{identifier}.json"
115
+ if job_file.exists():
116
+ try:
117
+ job_info = self._safe_read_json(job_file)
118
+ if job_info and "log_file" in job_info:
119
+ # 更新缓存
120
+ with self._cache_lock:
121
+ self._cache[f"job:{identifier}"] = job_info
122
+ return job_info["log_file"]
123
+ except Exception:
124
+ pass
125
+
126
+ # 方法3: 从映射中查找
127
+ mapping = self._load_mapping(identifier, "job_to_log")
128
+ if mapping:
129
+ return mapping
130
+
131
+ mapping = self._load_mapping(identifier, "plan_to_log")
132
+ if mapping:
133
+ return mapping
134
+
135
+ # 方法4: 扫描所有任务文件(最后的手段)
136
+ for job_file in self.jobs_dir.glob("*.json"):
137
+ try:
138
+ job_info = self._safe_read_json(job_file)
139
+ if job_info:
140
+ # 检查 job_id
141
+ if job_info.get("job_id") == identifier:
142
+ log_file = job_info.get("log_file")
143
+ if log_file:
144
+ # 更新缓存
145
+ with self._cache_lock:
146
+ self._cache[f"job:{identifier}"] = job_info
147
+ return log_file
148
+
149
+ # 检查 plan_id
150
+ if job_info.get("plan_id") == identifier:
151
+ log_file = job_info.get("log_file")
152
+ if log_file:
153
+ # 更新缓存
154
+ with self._cache_lock:
155
+ self._cache[f"plan:{identifier}:log"] = log_file
156
+ return log_file
157
+ except Exception:
158
+ continue
159
+
160
+ return None
161
+
162
+ def write_progress(self, job_id: str, event: Dict[str, Any]) -> bool:
163
+ """
164
+ 写入进度事件
165
+
166
+ Args:
167
+ job_id: 任务 ID
168
+ event: 进度事件
169
+
170
+ Returns:
171
+ 是否写入成功
172
+ """
173
+ log_path = self.find_log_path(job_id)
174
+ if not log_path:
175
+ # 如果找不到日志文件,自动注册任务
176
+ log_path = self.register_job(job_id)
177
+
178
+ try:
179
+ # 添加时间戳
180
+ if "timestamp" not in event:
181
+ event["timestamp"] = time.time()
182
+
183
+ # 原子写入(追加模式)
184
+ log_file = Path(log_path)
185
+ temp_file = log_file.parent / f".{log_file.name}.tmp"
186
+
187
+ # 使用文件锁
188
+ with open(log_path, 'a', encoding='utf-8') as f:
189
+ fcntl.flock(f.fileno(), fcntl.LOCK_EX)
190
+ try:
191
+ f.write(json.dumps(event, ensure_ascii=False))
192
+ f.write('\n')
193
+ f.flush()
194
+ os.fsync(f.fileno()) # 强制刷新到磁盘
195
+ finally:
196
+ fcntl.flock(f.fileno(), fcntl.LOCK_UN)
197
+
198
+ # 更新任务状态
199
+ self._update_job_status(job_id, event)
200
+
201
+ return True
202
+
203
+ except Exception as e:
204
+ print(f"写入进度失败: {e}")
205
+ return False
206
+
207
+ def read_progress(self, identifier: str, limit: int = 100,
208
+ since_timestamp: Optional[float] = None) -> List[Dict[str, Any]]:
209
+ """
210
+ 读取进度事件
211
+
212
+ Args:
213
+ identifier: job_id 或 plan_id
214
+ limit: 返回事件数量限制
215
+ since_timestamp: 从此时间戳之后的事件
216
+
217
+ Returns:
218
+ 进度事件列表
219
+ """
220
+ log_path = self.find_log_path(identifier)
221
+ if not log_path or not Path(log_path).exists():
222
+ return []
223
+
224
+ events = []
225
+ try:
226
+ with open(log_path, 'r', encoding='utf-8') as f:
227
+ for line in f:
228
+ if line.strip():
229
+ try:
230
+ event = json.loads(line)
231
+ # 过滤时间戳
232
+ if since_timestamp and event.get("timestamp", 0) <= since_timestamp:
233
+ continue
234
+ events.append(event)
235
+ # 限制数量
236
+ if len(events) >= limit:
237
+ break
238
+ except json.JSONDecodeError:
239
+ continue
240
+ except Exception as e:
241
+ print(f"读取进度失败: {e}")
242
+
243
+ return events
244
+
245
+ def get_job_status(self, job_id: str) -> Optional[Dict[str, Any]]:
246
+ """
247
+ 获取任务状态
248
+
249
+ Args:
250
+ job_id: 任务 ID
251
+
252
+ Returns:
253
+ 任务状态信息
254
+ """
255
+ # 先检查缓存
256
+ with self._cache_lock:
257
+ cached = self._cache.get(f"job:{job_id}")
258
+ if cached and time.time() - cached.get("_cache_time", 0) < 5: # 5秒缓存
259
+ return cached
260
+
261
+ # 从文件读取
262
+ job_file = self.jobs_dir / f"{job_id}.json"
263
+ if job_file.exists():
264
+ try:
265
+ job_info = self._safe_read_json(job_file)
266
+ if job_info:
267
+ # 更新缓存
268
+ job_info["_cache_time"] = time.time()
269
+ with self._cache_lock:
270
+ self._cache[f"job:{job_id}"] = job_info
271
+ return job_info
272
+ except Exception:
273
+ pass
274
+
275
+ return None
276
+
277
+ def _create_mapping(self, key: str, value: str, mapping_type: str):
278
+ """创建映射关系"""
279
+ mapping_file = self.mappings_dir / f"{mapping_type}.json"
280
+
281
+ # 读取现有映射
282
+ mappings = {}
283
+ if mapping_file.exists():
284
+ try:
285
+ mappings = self._safe_read_json(mapping_file) or {}
286
+ except Exception:
287
+ mappings = {}
288
+
289
+ # 更新映射
290
+ mappings[key] = value
291
+
292
+ # 保存映射
293
+ self._safe_write_json(mapping_file, mappings)
294
+
295
+ def _load_mapping(self, key: str, mapping_type: str) -> Optional[str]:
296
+ """加载映射关系"""
297
+ mapping_file = self.mappings_dir / f"{mapping_type}.json"
298
+
299
+ if mapping_file.exists():
300
+ try:
301
+ mappings = self._safe_read_json(mapping_file)
302
+ if mappings:
303
+ return mappings.get(key)
304
+ except Exception:
305
+ pass
306
+
307
+ return None
308
+
309
+ def _update_job_status(self, job_id: str, event: Dict[str, Any]):
310
+ """更新任务状态"""
311
+ job_file = self.jobs_dir / f"{job_id}.json"
312
+
313
+ # 读取现有信息
314
+ job_info = {}
315
+ if job_file.exists():
316
+ job_info = self._safe_read_json(job_file) or {}
317
+
318
+ # 更新状态
319
+ if "status" in event:
320
+ job_info["status"] = event["status"]
321
+ if "progress" in event:
322
+ job_info["progress"] = event["progress"]
323
+
324
+ job_info["updated_at"] = datetime.now().isoformat()
325
+ job_info["last_event"] = event
326
+
327
+ # 保存更新
328
+ self._safe_write_json(job_file, job_info)
329
+
330
+ # 更新缓存
331
+ job_info["_cache_time"] = time.time()
332
+ with self._cache_lock:
333
+ self._cache[f"job:{job_id}"] = job_info
334
+
335
+ def _safe_write_json(self, file_path: Path, data: Dict):
336
+ """安全写入 JSON 文件(原子操作)"""
337
+ temp_file = file_path.parent / f".{file_path.name}.tmp"
338
+
339
+ try:
340
+ # 先写入临时文件
341
+ with open(temp_file, 'w', encoding='utf-8') as f:
342
+ json.dump(data, f, ensure_ascii=False, indent=2)
343
+ f.flush()
344
+ os.fsync(f.fileno())
345
+
346
+ # 原子重命名
347
+ temp_file.replace(file_path)
348
+
349
+ except Exception as e:
350
+ # 清理临时文件
351
+ if temp_file.exists():
352
+ temp_file.unlink()
353
+ raise e
354
+
355
+ def _safe_read_json(self, file_path: Path) -> Optional[Dict]:
356
+ """安全读取 JSON 文件"""
357
+ try:
358
+ with open(file_path, 'r', encoding='utf-8') as f:
359
+ return json.load(f)
360
+ except Exception:
361
+ return None
362
+
363
+ def cleanup_old_logs(self, days_to_keep: int = 7) -> int:
364
+ """清理旧的日志文件"""
365
+ cleaned = 0
366
+ cutoff_time = time.time() - (days_to_keep * 24 * 3600)
367
+
368
+ for log_file in self.logs_dir.glob("*.jsonl"):
369
+ try:
370
+ if log_file.stat().st_mtime < cutoff_time:
371
+ log_file.unlink()
372
+ cleaned += 1
373
+ except Exception:
374
+ continue
375
+
376
+ return cleaned
377
+
378
+
379
+ # 全局实例
380
+ _progress_manager: Optional[ImprovedProgressManager] = None
381
+
382
+
383
+ def get_progress_manager() -> ImprovedProgressManager:
384
+ """获取进度管理器实例(单例)"""
385
+ global _progress_manager
386
+ if _progress_manager is None:
387
+ nas_path = os.environ.get(
388
+ "NAS_STORAGE_PATH",
389
+ "/app/mcp-servers/mcp-servers/html_agent"
390
+ )
391
+ _progress_manager = ImprovedProgressManager(nas_path)
392
+ return _progress_manager